diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dd84ea78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..48d5f81f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..bbcbbe7d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ea5b62f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +/*/target/ +/*/bin/ +/*/.settings/ +/*/.classpath +/*/.project +/*/.idea/ +/*/package-lock.json +/*/node_modules/ +/*/.vuepress/dist/* +/*/*.iml +**.versionsBackup +/target/ +*.iml +/.idea/ +.settings/ +.classpath +.project \ No newline at end of file diff --git a/README.md b/README.md index eeec7cf0..617031ef 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@

- + logo

- Maven Central - License - Troy.Zhou + Maven Central + License + Troy.Zhou

## 文档 -网å�€ï¼šhttps://ok.zhxu.cn +官网 http://okhttps.ejlchina.com/ ## 为什么用 @@ -25,8 +25,6 @@ * å�•方法回调,充分利用 Lambda 表达å¼�,让你代ç �超级简æ´�ï¼� * 超级轻é‡�,但性能å�“è¶Šï¼� -* **阿里云最低 1 折**:https://www.aliyun.com/minisite/goods?userCode=zugtbi5w - ## 如何使用 ### 如艺术一般优雅,åƒ� 1ã€�2ã€�3 一样简å�• @@ -92,35 +90,23 @@ Websocket 方法: 项目 | 最新版本 | æ��è¿° -|-|- -[okhttps](https://gitee.com/troyzhxu/okhttps/tree/dev/okhttps) | 4.1.0 | OkHttps 核心模å�— -[okhttps-fastjson](https://gitee.com/troyzhxu/okhttps/tree/dev/okhttps-fastjson) | 4.1.0 | 与 fastjson 集æˆ� -[okhttps-fastjson](https://gitee.com/troyzhxu/okhttps/tree/dev/okhttps-fastjson2) | 4.1.0 | 与 fastjson2 集æˆ� -[okhttps-fastjson](https://gitee.com/troyzhxu/okhttps/tree/dev/okhttps-fastjson2-jsonb) | 4.1.0 | 与 fastjson2-jsonb 集æˆ� -[okhttps-gson](https://gitee.com/troyzhxu/okhttps/tree/dev/okhttps-gson) | 4.1.0 | 与 gson 集æˆ� -[okhttps-jackson](https://gitee.com/troyzhxu/okhttps/tree/dev/okhttps-jackson) | 4.1.0 | 与 jackson 集æˆ� -[okhttps-jackson](https://gitee.com/troyzhxu/okhttps/tree/dev/okhttps-jackson-xml) | 4.1.0 | 与 jackson-xml 集æˆ� -[okhttps-jackson](https://gitee.com/troyzhxu/okhttps/tree/dev/okhttps-jackson-yaml) | 4.1.0 | 与 jackson-yaml 集æˆ� -[okhttps-jackson](https://gitee.com/troyzhxu/okhttps/tree/dev/okhttps-snack3) | 4.1.0 | 与 jackson-yaml 集æˆ� -[okhttps-stomp](https://gitee.com/troyzhxu/okhttps/tree/dev/okhttps-stomp) | 4.1.0 | 超级轻é‡�çš„ Stomp 客户端 -[okhttps-xml](https://gitee.com/troyzhxu/okhttps/tree/dev/okhttps-xml) | 3.5.3 | XML è§£æž�扩展 - -## 超详细教程,请查看:https://ok.zhxu.cn +okhttps | 2.3.0 | OkHttps 核心模å�— +[okhttps-fastjson](https://gitee.com/ejlchina-zhxu/okhttps/tree/master/okhttps-fastjson) | 2.3.0 | 与 fastjson 集æˆ� +[okhttps-gson](https://gitee.com/ejlchina-zhxu/okhttps/tree/master/okhttps-gson) | 2.3.0 | 与 gson 集æˆ� +[okhttps-jackson](https://gitee.com/ejlchina-zhxu/okhttps/tree/master/okhttps-jackson) | 2.3.0 | 与 jackson 集æˆ� +[okhttps-stomp](https://gitee.com/ejlchina-zhxu/okhttps-stomp) | 1.0.1 | 超级轻é‡�çš„ Stomp 客户端 + +## 超详细教程,请查看:http://okhttps.ejlchina.com/ ## è�”系方å¼� * 微信: -* 由于近期交æµ�群的二维ç �被爬,扫ç �入群方å¼�已被关闭 -库的使用上若有疑问,å�¯å…ˆåŠ å¾®ä¿¡ã€�18556739726】(请备注 OkHttps)å†�入群交æµ� - -## å�‹æƒ…链接 +* 邮箱:zhou.xu@ejlchina.com -[**[ Bean Searcher ]** 比 MyBatis 效率快 100 å€�çš„æ�¡ä»¶æ£€ç´¢å¼•擎,天生支æŒ�è�”表,使一行代ç �实现å¤�æ�‚列表检索æˆ�为å�¯èƒ½ï¼�](https://github.com/ejlchina/bean-searcher) +## å�Žç»­è®¡åˆ’ -[**[ SA Token ]** 一个轻é‡�级 Java æ�ƒé™�认è¯�框架,让鉴æ�ƒå�˜å¾—简å�•ã€�优雅ï¼�](https://github.com/dromara/Sa-Token) - -[**[ hrun4j ]** 接å�£è‡ªåŠ¨åŒ–æµ‹è¯•è§£å†³æ–¹æ¡ˆ --工具选得好,下ç�­å›žå®¶æ—©ï¼›æµ‹è¯•用得对,å�Šå¤œå®‰å¿ƒç�¡ ](https://github.com/lematechvip/hrun4j) - -[**[ Json Kit ]** è¶…è½»é‡�级 JSON é—¨é�¢å·¥å…·ï¼Œç”¨æ³•简å�•,ä¸�ä¾�赖具体实现,让业务代ç �与 Jacksonã€�Gsonã€�Fastjson 等解耦ï¼�](https://gitee.com/troyzhxu/jsonkit) +* 在 WebSocket 基础上扩展支æŒ� Stomp å��è®®ã€�å·²å�‘布 v1.0.1】 +* 在 Android 上通过 Kotlin 扩展使异步请求与 LiveData 集æˆ�(å�³å°†å�‘布ã€�敬请期待) ## å�‚与贡献 @@ -128,4 +114,3 @@ Websocket 方法: 2. 新建 Feat_xxx 分支 3. æ��交代ç � 4. 新建 Pull Request - diff --git a/okhttps-doc/.gitignore b/okhttps-doc/.gitignore new file mode 100644 index 00000000..e2a59199 --- /dev/null +++ b/okhttps-doc/.gitignore @@ -0,0 +1,5 @@ +/package-lock.json +/node_modules/ +/docs/.vuepress/dist/ +/docs/.vuepress/dist/* +docs/.vuepress/dist/* \ No newline at end of file diff --git a/okhttps-doc/OkHttps.logo.ai b/okhttps-doc/OkHttps.logo.ai new file mode 100644 index 00000000..e9a96afc --- /dev/null +++ b/okhttps-doc/OkHttps.logo.ai @@ -0,0 +1,1327 @@ +%PDF-1.5 %âãÏÓ +1 0 obj <>/OCGs[28 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + OKhttps. logo + + + Adobe Illustrator 24.0 (Macintosh) + 2020-04-28T11:13:07+08:00 + 2020-04-28T11:13:09+08:00 + 2020-04-28T11:13:09+08:00 + + + + 256 + 160 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAoAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYqlHmzzb5f8p6Hca3r14lnp9sPikf7TtQlY416u7U2Ub4q+GPzb/Obzd+bHmCHTbGGaHR TMsek6DDV3llY8VeUJ/eSsTsOi9B3JVfZn5OaF5m0H8s9B0nzPK02t2sDC6LyeqyBpXeKIvU1MUT KmxptttirMsVdirsVdirsVdirsVdirsVdirsVdiqldXVvaWs11cyLFbW6NLNKxoqIgLMxPgAK4q8 /wDI35//AJY+ddZk0bRtSZNQBP1aG7ja3+sAdTBz+1/qmje3XFXouKuxV2KuxV2KuxV2KuxV2Kux V2KuxV2KuxV2KvkH/nNqLzH/AIp0KVxOfLQseNud/q4vvWlM3t6hi9Pr2G3fFUX/AM4by/lgt3cx zrTz+3L6u93xKG27iy8Hp/eD7VOnw8sVfWeKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KobU9OtNT02 7029T1LO+hktrmOtOUUqFHWo8VbFXwv+cv8Azjv5p/Lm5bWdJeXUvLKuHi1GIET2hBqouAn2aHpI vw/6poMVehfkj/zllJEbfy9+Ykpkj2itfMVKsvZVvAPtD/i0b/zA7tir6rt7i3uYI7i2lSa3mUPD NGwdHRhVWVhUEEdCMVVMVdirsVdirsVdirsVdirsVdirsVdirsVSvzN5Y0LzPotzouu2aXum3S0l hkHcfZZWG6sp3VhuMVfDX5xfkb5q/K7W49T0ySe70AyiTTdZhBWW3kB5JHMU+xItPhcUDdRQ1UKv sL8ltc8167+Weiap5rhaLWriNzMXT0mkRZWWKVo6DiZIwrdN+vfFWb4q7FXYq7FXYq7FXYq7FXYq 7FXYq7FXYqtliilieKVFkikUrJGwDKysKEEHYgjFXy3+en/OKdsY7nzN5BSO2KBpr7QWYJER1ZrR moqf8Yyafy02UqvKvyd/5yA82flteLpt0JNS8teoRc6RMSHgJPxtbM392wO5Q/C3eh+IKvtvyR58 8r+dtEj1jy7erd2rUEqfZlhelTHNH1Rh+PUVGKsgxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kux V2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvPPzn/OTRPy08vrdXCi81q95LpWmA0MjLTlJIR9mNKi p79B7KvivWvNn5r/AJueYBaySXms3UrF7fSbRW+rQrXqsK/AirXd3/2TYqyEf84p/ncbP6x+hYhJ /wAspvLX1aUr/vzh/wANirEtP1H8yfyo81iWNbvy/rUQHqW8yFUmir0dG+CaJqbHcdwajFX23+Sf 50aR+ZmgvMsa2Wu2PFdU03lULy+zLETu0b0+anY9iVXoN9f2Fhbtc31zFaWyfbnndY0HzZiAMVVI J4J4lmgkWWFxVJEIZWHiCNjiq/FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYq7FX55/nB5m1T8xfzhvvqpM4lvV0jQ4Qfh9JJfRhC/8AGRyXPuxxV9t/lZ+WOgfl55Xg 0fTI1e6ZVfU9Q40kuZ6fE7HrxHRF/ZHvUlVmOKsS/M78ttA/MDyvcaLqsSibizaffcQZbaenwyI3 WlQOa/tDbFXw7+V3mLV/y0/OCz+tkwPZ3zaVrcAJ4mFpfRnBp9rgRzX3UYqzL/nMXVPME35pJp18 8i6TaWUEmkwEkRFZQfWlVa0LNKrIW8FA7Yq88/LD81/NX5e67Df6Tcu1gZAdQ0pnP1e4j6MGToHp 9lwKj5VBVforo+qWer6TZarZNzstQt4rq2c7FopkEiH6VYYqi8VdirsVdirsVdirsVdirsVdirsV dirsVdirsVdirsVdirsVdirsVdirsVdir85/ygkj0385/LX6VPEw6vFFOz9pjJ6YLFqdJCCScVfo xirsVdir86fzxlt7/wDOXzV9QHNX1KSFQm/KVKRvT5yKcVfe/m7yD5N84W0Vt5l0m31OOAkwNKpE kdacuEqFZFDcRyAbfvir4/8A+cl/yX0Pyd5p0FPKEEgi8yerHDo6s8zJcQtEgERcs/GX1xRSTuD2 2Cr7D8i6DN5f8laDoU7B59M0+2tJmBJBkhiVHIJ7cgaYqnmKuxV2KuxV2KuxV2KuxV2KuxV2KuxV 2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV8Gf85Mfl3feTPzMutUtUaPSdelfUdOuI6qEnZg1xECKcWS VuQp+yy4q+jPyK/5yG8v+dtItdK1y7isPN8CLFNDMwjW8KgD1oCaKWfq0Y3BrQccVez4q8d/PD/n IXy35G0m603SLuLUPN8qtFb2sREi2rnb1bkioXh1EZ+IntTfFXzR/wA44/l7qHnj8zrW/ukabS9G mXUtWuZKsHkVucMbE/aaWUbg/shjir75xVjcHkDQB5oHmm+Emp69FH6FneXhVvq0RrVLeNFjijry NWC8z3bFWSYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqkHnj yN5d87eXp9B1+39ezm+JHU8ZIpQCElib9l1rt27GoqMVfHX5gf8AOJn5j+X7qWXy/EPMek1Jikty qXSr2EkDEEt/xjLfR0xVhg8vfnqsf6IGm+Z1iI4/UBDqASlK09KlKU9sVZh+X/8Azid+ZPmK5jl1 yEeW9JqDJLdUa5Ze4jt1PIN/xkK/Tir7F8h+QvLXkby9DoWgW/o2sfxyyvRpp5T9qWZwByY0+QGw oBTFWRYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYqgtI1rR9Zs/ruk3sN/Zl3jFxbSLLHzj Yq68lJFVYUxVG4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7 FXYq7FXYqkPk7z35U85aa+oeXL9L23ikMU60aOWKQEjjLFIFkQ7bcl3xVLPzi1i90b8rfNGo2TFL uHTpxBIpoyNIvphwfFOXIYq+bP8AnDj8yf0Z5iu/I9/LSy1mtzpnI7LeRL8aD/jLEv3oB3xV9iYq 7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FVO5nEFvLOVZxEj OVUVY8RWgHc4q+WvKP8AzmrLP5kaHzVo8Nn5enfjBc2XqPPbgnZpgzMJh/NwVT4A9MVfT2kavpes abb6npV1Fe6fdKHt7mBg6Op8CPuPhir4R85S+c/yV/ObVJdDnazV52u7JWBNvdWM7l0jkStHQboe 4ZTShxV9P/l1+bfkv85fKd/oNzSx1a7tJbbVdHdgX9OVDG8tu5A9RPi69VPUdCVXxr5v8p+bPyv8 9CzuuVtqWmTpdaZfoCElWN+UNxET1BK9OxqDuMVfoB+XHnax87eS9K8y2lFF9CDcQg19K4T4JovH 4ZAQK9RQ98VZJirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVa5LyC1HIgkL3IFKn8cV bxV2KuxV2KuxV2KuxV89/nf/AM4saX5lNz5g8lpHp3mBuUtzpxolrdv1JXtDK3j9hj1pUtir57/L 780vzC/J/wAx3GnvDKlvHLTVvL16GRGYUqygiscnHo69dq8hir6guYfyn/5yL8nL6crQarYglCOK 39hJJSoZDVXicrv+y3Yhhsq8I1r/AJxd/Ojyhq8ep+WCmqmzkEtpf6dMsFwhXoxilZGDeyFsVeze WtN1X83PLEnlb83PKF7putadGXtPMHoGBHNVQvDIRxSU7Fo90cCtOwVTr8jfyq84/llqmtaHPfQ6 p5PvaXmnXQJjniugRG6PAagepHSrBiPgHjir1/FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXY q7FUp8zXWqWemm90+W3RrZhJPHdEJHJGNinqEgIx7E4C0aiUox4o1t3sL8pXmo67rFtqFtqMcv6P MiPBMzfWPq0zDnFKACr8KVSVTuR8XtCO7rtLOWWYkJfT86PQ/oPzelZY7h2KuxV2KuxV2KuxV2Kv Df8AnJKD8jL7TTb+c9SisPMsUf8AuOuLNPW1BKiqq8UdS0Rr9mUgdeJB3xV8Z6H5j1nyxryar5d1 GW0vLVz9WvIvgLJX9pDUFXH2kaoPQ4q+zPyT/wCcndA85rBonmUxaP5oPGOIk8bW8Y7D0mb7EhO3 psdz9knoFXuWKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KsZ8+2lzNp9lPHbteW1 lexXN9ZRjk0sKV5AL+1QkHj3yMnD1sSYg1YEgSPJJtGKap5r0y+sBJMlktz+kNQa2NqpjkTjDb0I XmY2+7IjcuNh9eWMo71dmq9w+DP8sdq7FXYq7FXYq7FXYq8Q/wCcl/zyn8haVDoOguo80arGXE5o fqlsSU9ah6yOwIjrtsT2AKr5j/LT8mvP35qajcX1s/Cx9U/pDXr5nZTK3xMAd3ml3qQP9kRXFXu0 H/OEHlcWfCfzNfPe/wC/kghSKv8AxiJdv+SmKvHvza/5xr85+QLeXVYpF1vy7Gf3moQKUkhBIAM8 BLFBU/aVmXxIxV6v/wA4tfn5qGqXUXkPzVcm4uih/QepSmsj+mKtbSuftNxFY2O+3H+XFX0/irsV dirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir87vzS1DUPPP52 6vHG/Ka+1b9F2FTVRHHKLSClNgCFBNO5xV99+UvLGl+VvLeneX9LjEdjp0Kwx0ABYjd5Gp+3IxLM e5OKvP8AzF/zkz+V3l/zo/lO/nujdQSi3vNQjiVrO3lJoyyuXEnwH7RVGA+g4q9Ruba2u7WW2uY1 ntrhGjmicBkeNxxZWB2IYGhxV+df5h6Fcflr+buoWWlyFG0O/iu9LkJJKxnhdW9T3Kq6g4q/Q/St Qi1HS7PUIhSK8gjuIxWvwyoHG49jiqKxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Ku xV2KuxV2KuxV2KuxV2Kvzo8xNL5L/PC8uJkP+4PzCbxV6lo4bv14yK/zIAcVfojY3tpf2VvfWcqz 2l1Gk9vMm6vHIoZGX2ZTXFXhHm7/AJxE8t+YvP8Ac+Z21qa107ULk3mo6QkIZpJJG5zenceovpiR jX+7alT9Cr3xESNFjQBUQBVUdABsAMVfnn+fnmCHzV+cuv3emH6zA1xHY2nDfmbaNLc8KfaDyIxU ++Kvv3y7pp0vy/pmmGlbG0gtjQ1H7mNU2J/1cVTDFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FXYq7FXYq7FXYq+W/+cu/ycvbyUfmHoduZzHEsXmCCMVcJEKR3dBuQqAJJ4AKe nIhVhv5E/wDOT0/kuwi8teaYZdQ8vQ1FjdQ0a5tQTX0+LFRJFU7CtV7VFAFX0ZB/zkj+Sc1n9bHm iBIwKtG8Vwso9vSMfM/QMVeM/nH/AM5c219ptzoP5fpNGLlWiuNfmUxOEbZhaxn41LDbm9CvYVoQ qxb/AJxX/Jy98yeaLfzhqluU8u6LKJbVnG11eRmsapXqkTfEzeIC+NFX2zirsVdirsVdirsVdirs VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirTKrKVYBlYUZTuCD2OKvCPzH/wCc RvJHmW6m1Hy/cN5b1CYl5IYoxNZOx3qIaoY6/wCQ1P8AJxV5VL/zhN+YgueMWt6Q9tv+8Z7lX70+ AQMPD9rFWe+Rf+cMPLenXMd55u1V9aZCGGnWyG2tyQekj8mlkX5cMVfRNhYWOn2cNjYW8drZ26CO 3toVCRoi7BVVaAAYqr4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX/2Q== + + + + proof:pdf + uuid:65E6390686CF11DBA6E2D887CEACB407 + xmp.did:e162cb60-21aa-4ff8-8217-8d09729c2dd3 + uuid:6f336833-9404-b643-9a8f-9e47e02abffc + + xmp.iid:77657c28-e3ae-4323-aeac-9c0f28920502 + xmp.did:77657c28-e3ae-4323-aeac-9c0f28920502 + uuid:65E6390686CF11DBA6E2D887CEACB407 + proof:pdf + + + + + saved + xmp.iid:2afc754f-ab50-44e3-9c34-d29c32560605 + 2020-04-28T10:58:27+08:00 + Adobe Illustrator 24.0 (Macintosh) + / + + + saved + xmp.iid:e162cb60-21aa-4ff8-8217-8d09729c2dd3 + 2020-04-28T11:13:05+08:00 + Adobe Illustrator 24.0 (Macintosh) + / + + + + Web + Document + AIRobin + Adobe PDF library 15.00 + 21.0.0 + 1 + False + False + + 256.000000 + 256.000000 + Pixels + + + + Cyan + Magenta + Yellow + + + + + + 默认色æ�¿ç»„ + 0 + + + + 白色 + RGB + PROCESS + 255 + 255 + 255 + + + 黑色 + RGB + PROCESS + 0 + 0 + 0 + + + RGB 红 + RGB + PROCESS + 255 + 0 + 0 + + + RGB 黄 + RGB + PROCESS + 255 + 255 + 0 + + + RGB 绿 + RGB + PROCESS + 0 + 255 + 0 + + + RGB é�’ + RGB + PROCESS + 0 + 255 + 255 + + + RGB è“� + RGB + PROCESS + 0 + 0 + 255 + + + RGB 洋红 + RGB + PROCESS + 255 + 0 + 255 + + + R=193 G=39 B=45 + RGB + PROCESS + 193 + 39 + 45 + + + R=237 G=28 B=36 + RGB + PROCESS + 237 + 28 + 36 + + + R=241 G=90 B=36 + RGB + PROCESS + 241 + 90 + 36 + + + R=247 G=147 B=30 + RGB + PROCESS + 247 + 147 + 30 + + + R=251 G=176 B=59 + RGB + PROCESS + 251 + 176 + 59 + + + R=252 G=238 B=33 + RGB + PROCESS + 252 + 238 + 33 + + + R=217 G=224 B=33 + RGB + PROCESS + 217 + 224 + 33 + + + R=140 G=198 B=63 + RGB + PROCESS + 140 + 198 + 63 + + + R=57 G=181 B=74 + RGB + PROCESS + 57 + 181 + 74 + + + R=0 G=146 B=69 + RGB + PROCESS + 0 + 146 + 69 + + + R=0 G=104 B=55 + RGB + PROCESS + 0 + 104 + 55 + + + R=34 G=181 B=115 + RGB + PROCESS + 34 + 181 + 115 + + + R=0 G=169 B=157 + RGB + PROCESS + 0 + 169 + 157 + + + R=41 G=171 B=226 + RGB + PROCESS + 41 + 171 + 226 + + + R=0 G=113 B=188 + RGB + PROCESS + 0 + 113 + 188 + + + R=46 G=49 B=146 + RGB + PROCESS + 46 + 49 + 146 + + + R=27 G=20 B=100 + RGB + PROCESS + 27 + 20 + 100 + + + R=102 G=45 B=145 + RGB + PROCESS + 102 + 45 + 145 + + + R=147 G=39 B=143 + RGB + PROCESS + 147 + 39 + 143 + + + R=158 G=0 B=93 + RGB + PROCESS + 158 + 0 + 93 + + + R=212 G=20 B=90 + RGB + PROCESS + 212 + 20 + 90 + + + R=237 G=30 B=121 + RGB + PROCESS + 237 + 30 + 121 + + + R=199 G=178 B=153 + RGB + PROCESS + 199 + 178 + 153 + + + R=153 G=134 B=117 + RGB + PROCESS + 153 + 134 + 117 + + + R=115 G=99 B=87 + RGB + PROCESS + 115 + 99 + 87 + + + R=83 G=71 B=65 + RGB + PROCESS + 83 + 71 + 65 + + + R=198 G=156 B=109 + RGB + PROCESS + 198 + 156 + 109 + + + R=166 G=124 B=82 + RGB + PROCESS + 166 + 124 + 82 + + + R=140 G=98 B=57 + RGB + PROCESS + 140 + 98 + 57 + + + R=117 G=76 B=36 + RGB + PROCESS + 117 + 76 + 36 + + + R=96 G=56 B=19 + RGB + PROCESS + 96 + 56 + 19 + + + R=66 G=33 B=11 + RGB + PROCESS + 66 + 33 + 11 + + + + + + ç�°è‰² + 1 + + + + R=0 G=0 B=0 + RGB + PROCESS + 0 + 0 + 0 + + + R=26 G=26 B=26 + RGB + PROCESS + 26 + 26 + 26 + + + R=51 G=51 B=51 + RGB + PROCESS + 51 + 51 + 51 + + + R=77 G=77 B=77 + RGB + PROCESS + 77 + 77 + 77 + + + R=102 G=102 B=102 + RGB + PROCESS + 102 + 102 + 102 + + + R=128 G=128 B=128 + RGB + PROCESS + 128 + 128 + 128 + + + R=153 G=153 B=153 + RGB + PROCESS + 153 + 153 + 153 + + + R=179 G=179 B=179 + RGB + PROCESS + 179 + 179 + 179 + + + R=204 G=204 B=204 + RGB + PROCESS + 204 + 204 + 204 + + + R=230 G=230 B=230 + RGB + PROCESS + 230 + 230 + 230 + + + R=242 G=242 B=242 + RGB + PROCESS + 242 + 242 + 242 + + + + + + Web 颜色组 + 1 + + + + R=63 G=169 B=245 + RGB + PROCESS + 63 + 169 + 245 + + + R=122 G=201 B=67 + RGB + PROCESS + 122 + 201 + 67 + + + R=255 G=147 B=30 + RGB + PROCESS + 255 + 147 + 30 + + + R=255 G=29 B=37 + RGB + PROCESS + 255 + 29 + 37 + + + R=255 G=123 B=172 + RGB + PROCESS + 255 + 123 + 172 + + + R=189 G=204 B=212 + RGB + PROCESS + 189 + 204 + 212 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + endstream endobj 3 0 obj <> endobj 5 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/Thumb 33 0 R/TrimBox[0.0 0.0 256.0 256.0]/Type/Page>> endobj 30 0 obj <>stream +H‰l–Ín%E …÷ýõ·RvýzK@¬F0bÁ]„“ÀûóÙÕ¹¹š‰¢t_×�Ë>>>Õ?=¦‡O�%}÷ýc:¾%iññÇËÓñkúûxxü¥¤ëk*Y[å9ÖàÙx¦×+Ó??½\Ÿþù÷¿ßžÓËŸÇÃ�¬þãõø’$þ$I“\´µ4[îUjºþuøŒ?/–­hº,VÌt‘™­Ê7ïëqÑ™Wmï3µç5ô¶ÿ4‹/=ûLMocŸÞöŸ&«÷ɧ]Î]%<ý~|¾OCk.�4¤‘ÇöžÇEjžE>:pÐ (Û"¡º²TL‘,‘«H ôJxÕraÚàÝ +ëO + k. Z»÷ÂFÑ×{^Ñ5â<W&_8CNÀɃâ Õâú­Yß í®+,p^‚°x?Sü6ÖS°Dö@÷^BF¯a«jØbAI¬Æ¨â+[Œ mêHoÊ:�õÍøŠÝÏ_+ÕBžG¥zÓAï÷JEgz¯Á>©çÃ1ê¨%Ðñ&ÄAÙƒ�²MÔÀMÍFÙ¦„ÁWs½Êq)¨K2mLÇñ +%T1 +â�ÞøP^µ@ˆNÈ®œÀð2Å(ÆíñGÈ­¤‰lºsËd¹9œ)Íi¯¾Ôv¸0A‰Œ0£J¨eßmà¹"b–}€šD¼Ã‰�FÀ÷Q¼ikÔ‡"B.ôá°uqö¡�½}[�K� ÞsÐó>U—‡‡®™Å‹>+Y�<én’Í“Âå0‡_§.ôÏÇÙäíéyÔe…F­¾¹'¥EýŒ O.7’µúía€®˜:YÈv#�åÈŒÐI:¸àä9ôïˆq¿Á߈hrm“ÄåY±mà á; 6ì&ÎŽ±„`¬`“ñ5q‡-6 ô?úl îîõ&M—CÐ’ýÒ7˳D˜‡Ü¦Qœæ·Î¹÷4ã“áüí3΋½ïÎ�žF虹ÍrîxOê‡O|H}>þ`Ÿ*ÉÅ endstream endobj 33 0 obj <>stream +8;V_XJJ#jD#Xd4d5+1SX4k?U7kXQc'G=>.H6q"@BLur1a^ul6G:$$B0#_4l*GRFZb +ec9[[Yo*>D!^mYY!'TMP[%[P_FB]Hl=l#A^/CJ)-5eZb/6L[pm(T]sucI\gipAofZ +ni2^*Z"gR?)ci72n@(8#s#g=>fBS![-X30TkI(+/ +m;(X,!3nPEQi~> endstream endobj 34 0 obj [/Indexed/DeviceRGB 255 35 0 R] endobj 35 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 28 0 obj <> endobj 36 0 obj [/View/Design] endobj 37 0 obj <>>> endobj 32 0 obj <> endobj 31 0 obj [/ICCBased 38 0 R] endobj 38 0 obj <>stream +H‰œ–yTSwÇoÉž�•°Ãc [€°�5la‘QIBHØADED„ª•2ÖmtFOE�.®c­Ö}êÒõ0êè8´׎�8G�Ng¦Óïï÷9÷wïïÝß½÷�ó '¥ªµÕ0 �Ö ÏJŒÅb¤  + 2y­.-;!à’ÆK°ZÜ ü‹ž^�i½"LÊÀ0ðÿ‰-×é @8(”µrœ;q®ª7èLöœy¥•&†Qëñq¶4±jž½ç|æ9ÚÄ +�V�³)g�B£0ñiœWו8#©8wÕ©•õ8_Å٥ʨQãüÜ«QÊj@é&»A)/ÇÙgº>'K‚óÈtÕ;\ú” Ó¥$ÕºF½ZUnÀÜå˜(4TŒ%)ë«”ƒ0C&¯”阤Z£“i˜¿óœ8¦Úbx‘ƒE¡ÁÁBÑ;…ú¯›¿P¦ÞÎӓ̹žAü om?çW= +€x¯Íú·¶Ò-Œ¯Àòæ[›Ëû0ñ¾¾øÎ}ø¦y)7ta¾¾õõõ>j¥ÜÇTÐ7úŸ¿@ï¼ÏÇtÜ›ò`qÊ2™±Ê€™ê&¯®ª6ê±Z�L®Ä„?â_øóyxg)Ë”z¥�ÈçL­UáíÖ*ÔuµSkÿSeØO4?׸¸c¯¯Ø°.òò· åÒR´ ß�Þô-•’2ð5ßáÞüÜÏ ú÷Sá>Ó£V­š‹“då`r£¾n~ÏôY &à+`œ�;ÂA4ˆÉ 䀰ÈA9Ð=¨- t�°lÃ`;»Á~pŒƒ�Á ðGp| ®�[`Lƒ‡`<¯ "A ˆ YA�+äùCb(ЇR¡,¨*�T�2B-Ð +¨ꇆ¡Ðnè÷ÐQètº}MA ï —0Óal»Á¾°Ž�Sàx ¬‚kà&¸^Á£ð>ø0|>_ƒ'á‡ð,ÂG!"F$H:Rˆ”!z¤éF‘Qd?r 9‹\A&‘GÈ ”ˆrQ ¢áhš‹ÊÑ´íE‡Ñ]èaô4z�BgÐ×Á–àE#H ‹*B=¡‹0HØIøˆp†p�0MxJ$ùD1„˜D, V›‰½Ä­ÄÄãÄKÄ»ÄY‰dEò"E�ÒI2’�ÔEÚBÚGúŒt™4MzN¦‘Èþär!YKî ’÷�?%_&ß#¿¢°(®”0J:EAi¤ôQÆ(Ç()Ó”WT6U@� æP+¨íÔ!ê~êêmê�æD ¥eÒÔ´å´!ÚïhŸÓ¦h/èº']B/¢éëèÒ�Ó¿¢?a0nŒhF!ÃÀXÇØÍ8ÅøšñÜŒkæc&5S˜µ™�˜6»lö˜Iaº2c˜K™MÌAæ!æEæ#…寒°d¬VÖë(ëk–Íe‹Øél »—½‡}Ž}ŸCâ¸qâ9 +N'çÎ)Î].ÂuæJ¸rî +î÷ wšGä xR^¯‡÷[ÞoÆœchžgÞ`>bþ‰ù$á»ñ¥ü*~ÿ ÿ:ÿ¥…�EŒ…Òb�Å~‹ËÏ,m,£-•–Ý–,¯Y¾´Â¬â­*­6X�[ݱF­=­3­ë­·YŸ±~dó ·‘ÛtÛ´¹i ÛzÚfÙ6Û~`{ÁvÖÎÞ.ÑNg·Åî”Ý#{¾}´}…ý€ý§ö¸‘j‡‡ÏþŠ™c1X6„�Æfm“�Ž;'_9 œr�:œ8Ýq¦:‹�ËœœO:ϸ8¸¤¹´¸ìu¹éJq»–»nv=ëúÌMà–ï¶ÊmÜí¾ÀR 4 ö +n»3Ü£ÜkÜGݯz=Ä•[=¾ô„=ƒ<Ë=GTB(É/ÙSòƒ,]6*›-•–¾W:#—È7Ë*¢ŠÊe¿ò^YDYÙ}U„j£êAyTù`ù#µD=¬þ¶"©b{ųÊôÊ+¬Ê¯: !kJ4Gµm¥ötµ}uCõ%�—®K7YV³©fFŸ¢ßY Õ.©=bàá?SŒîƕƩºÈº‘ºçõyõ‡Ø Ú† �ž�kï5%4ý¦m–7Ÿlqlio™Z³lG+ÔZÚz²Í¹­³mzyâò]íÔöÊö?uøuôw|¿"űN»Îå�wW&®ÜÛe֥ﺱ*|ÕöÕèjõê‰5k¶¬yÝ­èþ¢Ç¯g°ç‡^yïkEk‡Öþ¸®lÝD_pß¶õÄõÚõ×7DmØÕÏîoê¿»1mãál {àûMśΠnßLÝlÜ<9”úO¤[þ˜¸™$™�™üšhšÕ›B›¯œœ‰œ÷�d�Òž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª�««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½�¾ +¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ€ÜÜŠÝÝ–ÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿ ÷„óû endstream endobj 7 0 obj <> endobj 22 0 obj <> endobj 23 0 obj <>stream +%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 24.0 %%AI8_CreatorVersion: 24.0.0 %%For: (Geoffrey) () %%Title: (未标题-1) %%CreationDate: 2020/4/28 上å�ˆ11:13 %%Canvassize: 16383 %%BoundingBox: 346 -1086 4373 1381 %%HiResBoundingBox: 346.440002441406 -1085.90124511719 4372.73776087041 1380.27267906984 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 14.0 %AI12_BuildNumber: 332 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([套版色]) %AI3_Cropmarks: 372 -628 628 -372 %AI3_TemplateBox: 500.5 -500.5 500.5 -500.5 %AI3_TileBox: 220.5 -880 779.5 -97 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI9_OpenToView: 34.7825975116702 -216.213424585018 2.27083790911365 2548 1324 18 1 0 6 43 0 0 0 1 1 0 1 1 0 0 %AI5_OpenViewLayers: 7 %%PageOrigin:100 -800 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 24 0 obj <>stream +%AI24_ZStandard_Data(µ/ýXÜ©.!Fð 9BkUUUU-Ö�0×Oãñª�ãüMß$µ<þ§hŠÅkŒd眭N)¥”´‰ì”˜ PpX¨ Ý öÒ)�ägÓÑÁËÒ§åçãU’óQHRX ú ¬ö¾åM&}äm’F¬xŸ_‘Ûg¡:lWD‰*—>.åu ÍTúx.¡ôñ±Ûáj�¾}$–Æp‘áHzÅì Ë*úØ$ÉôQ½ Ú�wÒûøüäv”O‡–¿8$iž¹êÏÑHK)là0t!ÞÍmNmmì×6ËÙÛìw�6`½b¸€@Á0\@ Ã*€A6” C + + �À¡B‡ +Åpq€ƒ+¤À¡Ã†2l`´°BÂ. 8`¸€€ p`¸€0Á�á¢Á1èÀ�cz¨àèA‚c°`Ç,$ŽÂp� *Ø@â ¯²­n锫ü%ý‘*tW’xÜwù�$ì  6ÂpÀ—£¥P'JAư�4Æÿ¦ì|Tû_t-§sfô~’F(MfˆèŽ°È¨éÚšt…µrºCâ¬Ì�U»vãá«p0 +Ï2ˆc ‰G„`@&à L¸ ž€ˆi‘ˆCdAˆT ÂFÑH0ŠÄ˜@0‘Ô/ áÄ{@ Ž(Ä”À…™!ˆ#�@B,ˆW!(‚ œp$<á @dà„$ì  c&¿ÌaÌtõ²HYìeöv“ãÉÎñòIn¡œÑÉ”]úG†ÄÎý‹ðîh™£Ìe5yþ˜a^M.�TòçÇHfêsÞm²¤c%ÞN渲�‹Lõ^ÑL‚ј"¨ìù„Áh úPÓTˆÅ˜_0j‡˜šÊ3¡X0 +DAWFÆaDX>¡¨:„a¹] ŠEb s�ÔÄÓ¨�i$ﱉ¸žÝ‘@C(ž ‡D4��ŒD2Ô0Fñ H,j".Æ1œsÆ$ˆ�‘ŒF# C'èÄH”AH˜Ff$Š#¡ A°‘ÌaT¾ßå_u)sœ½ ;‡Y–F ‚Le‡¸N(EØe¢�¨ÆX Qs^±hŒë +o "þл +a‡O �‡O�(;LÄ‚Ñ(„)ì† +ƒÑ0V±P$O+rŒ…"ñÄ3D­@�¢Š>|�#o$®ÈD`ˆÅÕƒ1,�D0Ÿ# +DâFóÐ`,Žh‚nqC”•û�È:D=0OdO¿gã“p¢M¸.�Eâ‰Db.S\×W|ñИX0†Qk´|‰†íþTZ¢}¹Ž9�I=Ê's UÙÇn«O©Lí™^žÕ§É�ÊV2wCÕÛTžoî®ù9Ó4æD¤B×_Æ8†‡‡x.“¥¤¬DXZ^.ã»n¿ßñøL, @ÌôaQC“ø€Œcšq·@à¡QÖX$Òΰ<Œd$1Ìpc�H-U Ï¶#Ó(ä±�6úUþ6+’9Ézö™ò1eŽòåÐç2•8÷ÎËF渤V“Lh+Ù æ‘ =c…”ö¢”9�® 1g:š››œOb³Æ4‹Ä"¸¸…-ÜP(Œ㌜Æã�Ñ`0 †‚qF2�qŒc4 E#Ñx#ذF5¨áFÓX¡±PLç‹YÌ¢”g$ÉS£ÈFÄ›d|œm¤XÖ“¢£m¥wÜa'Ï�ÔêqÇ\e¶u9Ó¦UÅ,×¼NæpOshÛÕ%ÊXé¼Ó•G™£J‰wdˆo¥ ¢�‡„±@0EÔT†À|B±`4<%„Éd¢¤¼‚Œ5è�hjªÕr!\TTUV†…¯D%�z¨‡«Õldde†·ba k؃=Ø\ÏGvwxy¿Äâ׸‡, � Tàø€“M„“²Ò3ó™HE.²‘9ïéu;>ÿ'¾8ãŸD”¨„%0‘‰DB1Šž�Š+ŠEO±`8g( Æ£ jXo(ŒÆC£ô`øðÅ�(„ˆD= Šcy²±H "Œð!/Ë÷`·ÛÙ¶±1ƱªªÜ %Îä2��ÛbŸ´±)�@$¢ÁP ÐÌH, +Kõá�E"�Èc©HÅ1 ÆÁ‡{¸Å).�t>£YˆhÔƒîË@,ˆÄ‰ˆ"÷ÁXàY)H�PÌãÊ“@<¼ÁPüÿÝ|¿Ÿ3R‘‰<5�� L ⣰«cØ"ÍÕx%¢Œ¤ë 'œsçØÐ�Ç@œ‚\Ä0Œ…b�Hà˜ÀDÄCCÂâÃ5Žq‰ûåÝÕùº0E¢¬öP 4ƒFDC 3œpúL®²Wjû0ŒI N�í˜C33 àÐ`(,(,(0\@X@�áÂ1Óc츣‡ñî.¯ès×[æ°mfgf’9謎3{•$ƒèb† G%ŠÄ"ÁH4ˆ‡Ä(ª°‚¥0Å)P‘Š+ +…b¡`(ŠÅ,ºð‚µ°Å-p‘‹/ Åb±`,‹‡Å0Ê0ƒÅ82É8#ÁP0 £Áx`L£ 7¨Q k\Ùx#ÑP4 F£ÑxhÌCü@õ`÷€ùð‡ÄƒâañÀxh<<< €„‰ÂBÐT!Úh,JQˆL¹a†V8a6—et‘°Å(&‘ƒcUÂ×›{°†!¶0…%, í¬¬Œ¬6öP�b”¢õºº²ªrµ¦¨A Z�‚NIHGFE¥¡#fXá„ï4úl&Ûõø¡�.ª¨¢o{­RÝvú0�aŒbóÁ¹©¡1"ˆ @ 8|Ì�@ Ð`‚8pÀ@Ž´ÀAä°Â8(Žk + f�:£€%æŒÂhha +,p¬ 6„`-¤@ )Ø�– +„f4¨ÀÂ6@`ZHAX¡C @Ð8Ì5BÁͰa'HalPA…vdBÁna…Ä Q˜óZH�&À�*tèp›B£@U*tèp�c, `ű‚†ZH!ƒ 4´À , A… ZX! +à€ ¡`4 +„&`aƒ +>`X ZÈ á,¬ a-hà�‚ + 3 +mK(ÔÎ(¸t¬�‡T @  +‡F¡Ê(�–Q +w„B™P@ +UBÁE(à¬Ð!8phX!p‚ )H´ŒB ¡0F(Ð +=BK(+¡€C-dÐ@‚Là�†lX¡C 4d ˆ!àЀ  Ã + à°A(P�RpØÐ‡ +P€C `8 +ZÐ`tP¡Ã +à°A. Xh�ƒ˜ Ã. ,™‰ÀƒËAC -¤ Âƒ«i=üýñÛIÉ +,à˜é8lÁ†& A)Pàª@Æ pÐp‚ ”ê¸ìQ¦YæÃŽÚÏ‘rÐâ#;–V.ªåê¼ÖðæD)Ê@‡ pÐp‚ `8 +h`A +0 +$�- @…,h¨@ D°B +h +*<ô8€ +@Ð!o@â⎰ÐÂXAƒ +(¨ÀL•ZX!¡�C P�† )Ѐ/ÀarÄc2m6\ ‡ÉŒih!N` @*À‘¾#}8Œt€…¬�‚ NÐax 2pB dЄa ÀP, . æ8 "à°¡…4X@ƒR0A0`Ç4ˆ 0€��¸€0Ñq€4|À(�TØ€·‰-¤`‚: ¡…:l@  <=`‚xÀ„РB .páB8À +„ 4, ÇVèðp6°°‚† `È$.\@Œ”T +th!…Z`*¨Ð€ã8À +ZHAà +‹ Ç@Ã:€ Q P®J«ò%å^O÷鼎§”Ž«*É¥‹e(´IaÏX#‡‰=K×;M¦ôªÌ7X»ŸáP�¦ìW»4K5Í¡ÑÍ�à 摚û”UfZa•ÍCuIT÷_eÞ|X•u¥CÛÑòr/²šXCÁúêF´\[;­~ä*»QË&)ëj›²»2¥Fc»«´6×Ê{ºx¥'R™ÈÌî?ëd¹,‹°¥7oSWMjÖÝÕôÝgdEÿ‰Ò“®ÏÎÖ8›u{íÖ¤ü%Ö]ð°pZciûYvW¬õöÖèT›ò¬½DÙØÌȪ&?X�È–HfX»6…%ex®Oõröaøîv’”åԳع[™�Äf{•HêjGǼ¥G²£±;zýPÆ|¾|ö±¬]s’^4sgÐküaáäGÐWT?Oâ•n½u„XuSO²•´è©9«™,ëU[Ö½†èz—õ³-4•s¿þ7ïØóWA·÷*L3_Ó‡ãûe›veÛ¯4òüø¦ûä�Xy\¯ŸÝéŽbÔ)Ó�ïþ˜LUeû‘tíNUs¶¬O}Yij»]kÍ©»#»K³TmFxŸ²åõªg}»šlöj4M£šè¿¡:¡‘˜í¿»ã›mÚí´Ò}•­—|ø�¦¬wý/æ2KÁšº>¾Êí²¿»•]†^Ly†Ãª»Õ'Sµz\w³AiåÕ%Ý«5äVÏF'¼Ä;Þ+øûLfi3YeÀœü{cDÆw~·K™YyK›¼ç[×›B)"3ëT²Í›–•‰_œÌ�}(8½ž,¯Ü'á=Ÿ•»wžî^w„¨Y>Û!uèŽe•«"]ÕkOUÝ2ÜÉI$¶oÔV}ΖÚùÕ])»Ð“3…d§�HO2Þí¬ÌHX¾ÕžjSÝ<É&ëE5âñ¤±rü_YYmæ;Õ%Y‡ï•¡üA¼£�‚î•Þm(%mÔ*Þqpdè¨÷žJIåëé½±W„vwÌ’˜Šá‚q\�‘Ñ`oòŒìv&­öœº¼rzÖãvß|8v/›I¢Brùèt�³ß4ï•e9÷ ÖÑíy–ƒÍ«ûÊk±ò¾§mrecœö¨2Ë»›ÇRêa+y°-Ý=ç¢y6xWj¹)W'Ïd2-TWféÊuçìêÕ2im÷ñ‹žÚ3-…2Ôýô·¿|=]%Ù¬š{ŸD7ö²–Ø[‡Ô×iœˆWSW�ý»îzÞRÌÆ�åIÙFO£"ûvB¬›1åÆ×4ie´ú¤)ë÷ŸÜ˜p³žx#4Fjëcd'«‘ʺlÈì&_‹SìÙí^BXê&ÚíÆ™ëïîæÌêCä;º•WOsº÷2ÅU¤©©Õ«««Ë©ZÖ{æœ�••3çìv¥6‘æÊ~3(Çfbšì÷¹T¶dŸÎ‰e)é¶ÝrÙ9#ÙmX¡¬�Æì‡Mêo°5?¥½ˆ'u¤¦�ª(v­}ü”»šýˆÛ²Wù¤Ùœ_Ùžc•Ìv®°öRGúÞhZ{ÏLè½ú‘Iî5Ãûë%É—Š¥±ÓïäÞÎX~æ$ÖÒG0Ã]|Κ*чî¤Ît«Ã7møÿïòNuK�X¯UÖ•íÉ,ú)‰IV–g¼<áÝ=œó<:aS^x72®9åÏ<üiáÕ!%½Ž.T”wttëSjžn\©cõ$ŸU#¼[)5=jò*-s^až ÖÇ»’â$ÕÙóTb¬�„°—3sv¬}¼MK®ÅÛüäY²×^÷¼�-Å®Ù^y§–¬jöA3AìïÊ&ÖÉœ%$sÙér¤Ùc‰a_Yˆ†WÄzçŽy&h}§ˆÎª˜þܽµ¥¨Ë9R÷ʪx«�Yòž<鱃‚é_ɼŽHü¡{³Hî—r¯Ÿ,t§/ô™¾l6ʧdçÅ® NÓb·°éß'oR÷¢Á#V ¶wÍ»kéȇ¿£ýäí�Õ™„\¬¹òŽ™—ro‹¹W¼Bs­ðƒ7”/›ÝûUÝJ¢;R]ªŠwj›­¦º§ÉÒªÑ�l–ªæ É«X³:�íws¹³}Ä·3¡ ff˜WšÊ¢/ËÎŒgw‡¤ØÃðN²Žã��°GÚ+X“$VÇÙžVXµŸËâ·v³´Ð᬴ñ4E9MVÞIÞ˜ÆÛI‚Í—ðµ�ÜôHLóØiærïÙ¶±ZŠÝzEøÚæW9v¿veñä+iñCtqÏuÄ×óiÇ̺O9í¥!±ë1ø+›ю׃ÎäÈ�â ¥ÚB;ešäu~Ôž•ÊX&é�7‘jï¯Ä§NŠX?˜(ùöB©è +NZáozU}õN/nщ\N²¼z&’L} �Žäøp&Yž_÷8eÆ7~ël�üÑÆžé{#¿Õ¹¼Úî)’gã{¼¼fËnE’f�Ïâ­ÊÒöÀÛ˜Ùé®öè{S¼k&Õw®‘ä×Ó“y?²¼×ÙÙ“LD–f§2”ÂÖ&Ïd�cºÐõÈ;'UËßæn»¢ÑNŒ�l¼[Êm"B"´±ÞÉzXÙ™ð–=p<‡)tõKý¸cQÚé˜RWxÔéí®ÇùNòÎFt|Lu֛ʯU½J™ìñÏ ùHë-6cbvÜù½úÜ/®áåw´c™u;,ñø(cñ°;ö©²à{TæÈ¡eó7ÙÑhRYwBÐÛÈ�ãS0CçZ¼ù*ek� ÞgØÌJ¶šFÌŽî•¿ÏÉ{‚ÄÑ«^4™ ”“V7¬IÙjV?Ä+Y­î¶‚õAÓ²·.¦“æáÄWÕÇå=úJ9;ŽÆ;žÝÑÂ2×Ñ\‚.²)×�yAû¨™Îìú$š¤}´òfÜ:¢¡˜ü æqK¢Ösj1ù £ÿ(|M4™Ü�<º^H%g#‹ý°æ¯êã^²>,ÇR?þ_ÝX×ÇÝÑö‡õªÁñ;Ò<¨j&ë-§Øø(¼åwXÕ*ëW6RãÃtö‰Õ63RvÔMÕ§^BvyÒ•WxGbY¥mf~û�ìÏàiæ ,J3Pšñè>·º×]eŠJÒjíNÝ ­ÄÕƒ¦#•t)ÎÃé^Lª>O,Ÿé”|Îáo½JN�bå�Ù͵¾¼ž%Û�{síáÂ2¼]I:ÉxK“|zÃ£Š„ÑO›e9µWHwX=Ü<¡;\›{7% Y�K“ÌÕK·-ßÉŽ’…‰sy¾åš*m©Uî �C®˜6p ÇCÐ NÐa��Æ"€�…¬�À -¤ á*€�…`Àá"¯’-[œÁVWã*¹?�ïê‰=ãûgr¥s.…ÇíßSÙKåc…ð(Îí=þ–,9é„ì¸{‰e(“9;]$záI|iD%:�{e™™NÍŸ ÐŽZŽé8•’Œ`¸€Èp˜lí¸aöuyRÓÑÃÃ’±?¤4a¸€È`<ì� {¦#)�*±G«ÅLÇáK0êœßSÎÉK6ÉåQ”Dmé8]*ûÍŸtè ©IÞQ3ôóËòÄ&ÊÝatÞ}#Iô‘ö“º£²3DÏÖiYrxœ+hH$yÇï‡IÏ#‰ËÇÙ ÔYwMÝ>)F>^²œeŸRJ‰òÄêháT‘�žIÒQ¤³+9òAE£ã.§'Iú@�!–Lù°:©�ÎÛÓ™ÂèÚSG¢“#C™棙zïH�|ÐÉ&uÜzyº¡�$ù¸¬UZ€ª¹Òèˆw!ó�UiÒ#ëTD¯c=ûÊ�£™>ÔËÝ•ÅzÐïgzfKî|xnÅ4½Ð(vÆŸèƒhÒ££’HZG­¶¢û^¹ù(,“«ó碓_™K²`V½æ:.]°Ž¾¨LŸ³<Ô”ªÃcÓ›­üjèätU¶*ùï.bM±:W¥âón¡ö°Cª%ÖÑ­‡ƒ¯ ÅŒÌ7~~)Ö,¡™ÁÁ“ówùbñ‡–dY4<õU9øNËøœ,Õ1æQg0ÝÎíΔCxw‡­Ê<)¶Ò ïJÅÖXâ]i°5Ö<’Ò`ëçCÊ«±%C5‡­OÓ¨¥ÄæØ¸.Ë�Œ\Y7åsK´œ�¨^hKÙ7:“^³Œm\bÌš¹~–$˜’=k}n’Rš%#§©÷”œ¤C××,,U§ŽçËÑŒ�¤©?^ºS/Oˈ…'é©'¹PÒ5SÇ)$×ðÒý” æYÙà)éSˆw2«Ç>µw“gxu«šáݾ×ÏI›$š¤§ÝX4“%VX„’ä!£ÞÑð$©Žtª]éÎ$í£á–ê¡“¤>¡sÒ6¤“Ýdå8¹ÊU©HD,:×o˜‚s{‚‡‰äf¹óÕ¤®gW8s&ïQªAc�¤TÕt®{Ò•´°GÝa-SÛ2ªk•`Q�‘ v|r™‚¶Ë–Ñ¿¡ÒY ÙìûËw·›4�%Dtvßy +¥>\Jº{Ù©>ˆ~ Q[u/[Ê”ùtE=»¹r ³T÷Av£ÙÝGËQÏY]‰)¥£>‡‰R_Që$HG9‹n}›¢ÏýÙk½ò9ìÉø12b?u_i� i8¦4U†Ó™ënï²ØÞw•î�®$i$É:fINÙÙ­²$XGÕdΫ¯Š3é^\Gæ-¥�:~Ë,“Öq÷"¹òQõ—]}GC2wy)T;®jyR.ú“wN[RIÒI3nÑÔ>f%ilI:1ÏðÒwÃóœmÍ:LöÿN„(™Eeƒ/š“ë¯Lê4Ýüì’±ÃhIELÌ?ª#Ou¼ä`Í�æð¿ÕáŒQ²%©:�fÐê(V_GQ–|:Ç2æqh½)ÉiæÜ¥œ†cv9)ÅÛMÚÝRˆ~ªšr]n‡«ªèî žZ×:*$ì[4]Þdk—dÓ*i6Ç!Ëq¤H­ëؘÄî8:Ë—Gï„s2æ’Ö�“Uóôà ˜4Ù{ÿJ[Ÿ¤‰Wõ½†HÊSþFæ§é&¾içyâcæ%¦øòJ¢´‹�iÆ<;]å/Ù‘«ãvôÛqnE9oi6%):°FSG¬Õi.„Óñ¥;)D½²ºÉK"Lóßf½LÒL\¥e¹)i« ÑѲÍKê|kš:½«”ùÖ÷š¾Ý(‹Ï‘�è6u¥|xèè’z)ç£4ã9ÏAhÄÃ^ŠŽìëÞ�jB#æÃžªž­8G·Ó§R’WÚq†¤ˆ${ �›vO¢ØQïî\­ƒG‰§—MöÆR¦ÛGš®ÒžvI–ÔRR¥Õ‘åý´å‡õâ‘|F�ës°äSª Xò9I<°ÌæêònŠÎ¬ ÖHIâQ4„Vï9¥žWù2Ì«ÛËêI†bY,«Û+ÅzT¾iÙ¹œdõ¡±Ü©Gè¤Ë£õ¤3SÞ¸•–ÌÄ[íöÛ–©Ç~G{[wY4ëvÕkC¹5X“eú4³L^îÚÔ?5®×1k™qUðU¦R~žþð\•íuY²òæ&ÓòøÎ Ì\¢a;˜'³²}l,™ÆI3~N&9“NÁ¬Í†Ìl§l +ÚpÍ6cΜ 'kF,2ã�Œ’s•gþ Ïv<‘`RÕ†ìg•UEM*²iÙ‘ð¤yF§Ç�Ýö±–ð¦ÌNg“ÃvJ¦ðΞ“bUiÄÐè–�ÅGU�´íOŠŸ©«ì™ôŠfŸìO‡(S¬ã´¾Õp�Ìï&ÇxËšLûë!Ö8µ¥ÝøVÚŒ>yÒiO%”›9Ú Ÿf(4‚·2™©´ŸkRw&ÿ‘/´Þ§˜bË·…÷U‰§WfâÁëq +)Mz”Sx»š©:ÏaIjÍ^]µH<ػٙ¤v£¹Ùå,_â«í¼Ì¡¹·,”5Ë—dk÷DÊ<š¿¢ Õ™H(�ÊÌÚc¶ÃOY­'˜uC3£ UŽÇeRM©>Å¢o]x'—F{Ú,o±h·bZ �_f%Ë8æ´«±Šáqg–vï\­Ž²™6bT7fÚµ,ã{ŽíŽ3“wUDiO7ï¹ô¹þ± 0™5g»ÞŸè!£Í!ð‚07`84-pÐ�a À`‚ Œ±   A…¤ÐÂ4ê. ‚`A… -ph€ãÐa… 6`h! 0‚@°A6lÐÀ TØp�4TÀ‡ñjìN>’¤²@Ã0° aà :,0˜`C.Éä¯'=† ˆ øJM«Ÿ2ÞY˧ÒD—8+Kt±4L“²[Ì*g#h5EA³ I;RT9¢{ÊõÅDŸdIÆ7>!¬Šñ#x*<ÖO:Å,¬‰¯gD)i‰çAq^¾Ð’h¥:ê"�TÕ‘ML>Êg†¤\Dbt9Âp‘á®’u;©³VŒ(…—Ê¿oöú/m1\@d°$ÖÝrœLDiš Qœ. ‡¹Fæ…ug|ÝXrJxô»!ü4û*œé‘Oާ,),É«é#·!“¬¹T!¢X%Ž Vö%vä$´ÊàQÓær(M¡ÌâÈeJ3ïÆi>²Œ3Ð`õ…>‘Ub Kv¤#±:ö.ooø’Ì].kž©³ÓÓÄhYvæ vu÷¹�Ê7hwÞk¬?÷g¯™ÉP ]žùÝh,ÑŽuÊ5í‰ubÐòæ÷l!æ�,fgo·š4³Ìô½–öû+u‚î$"’±ÈnÄjªšR[•�$oˆÊ/–üèòæ4dþ}ÂJÛó°0ídÈt•Õ:š®39%�‹ §¤È•uVÊl|gíüU¡¬*q´)ï·zÑŒÌ++ß:(chÒ”xsÄãÏò%ÙdΡ bÙñ˜Ê#ÓcO+WÊb9cÃ{¹L2Ñ=Þ;‚nV~íHUâû(õÜdt™w"³P&åÝd=K¥1CqŒ2”x=ºOùRjÝç•Y?¼‰ÉS÷ñIlITÓV(KìÉêßq.3ØìªÔÏн钫¦›†¿“‹rŸýED2èœú½ø‘úÊYõäQæ­Ók·gÀϵã3ËNõòˆ.­îüC·—lNm·£É³l,¶æY½:#V”ÁÈRr=D­ºNë¾,!›Ænœz¥ÑS“ºÌ†YY5(FÎÒœYÞÐ*–ÁˆJ°æI6’î½®Ó5ÉCgħÝYõìÅ›�Ùqޤ0C†s«lZSÖ d»G0;å4ËþœF(”-ÒÑF,Áß{$«rR•“nÕ*gæÑ¯œòª5ÞjæŒnó¥ÌGÏO…ÌiµºÒΓÛ4t•’[ǶBäæ%É9—„T6›ÎÌYd6:óçuHRæ–Oõ;y9§G7òYJÏä" ¡ 3Ý4âÐ1=RhÓó¦’‡3½‘ùu)©Ê¯)‘ñçë ŽÛÁ}WìMÓxéÕƒuÕŠL¬+©ºi;ñÄjCyòf�¡ª©ÏŠÑ�.“Ž8cYG,5�–,Ϋٽ:&G®ÖáÝýüùý°HÈܱÂzÎrlIdq]ço´ÛZ=-X'•d»Û—ô©zkHjwΪª¶¼±ØKõú,5>IkÖteFrv­®u#ÛSŠÜÌOkÐÄdÚw´ª»IòÚpÊìÇh/óɳ86U;úHü¢Ø3¸TGû¿³33¹n9DuxE7Ë7uFs?™:±ŒÇªÓÙÖ:9KJeóÛñˆâ*¼�ïu$—ü®©t:Ïií-/HI™b›«]VÊ©\—39±(gºŸœPˆ7W«Ž{U54¶ï�=SÆ�9˜ýÝLk°ÙZqн¢õ~œÕàYN¡UÓŒölZí6çZÞ çÐ:EJšÎYyŠž*æ1;X³)þz‚fFŸçΊk™Ê~6“I£«¬|n Ñó3”¤ôáž ÍðÒÎ2I¼<ú’N,ŸõZöú»©»ÄJ+¬â%9›ƒføÎëâÍ©æÿ”™x>ÌÌú¤éšx>?Ë“Ç;׳œ„dˆžú©E<´�,x­¬$;^ÑõñR®UYO,3=Ë–Te#˜YWS3¯N]Ѽ:¦³yuŠi6¯ŽïhŽƒž”{_6wõ4‰ðS$±¿øÊ›×^¶f×°&ËÅ[¦{<ì~…^|M�.t:ªÂq©fìyÌÐy9�ìS÷œ#û%õÈ-±G—72ÊÉ32JÃÌñkGÃʼt>²ÝFOÉdÇD_¶¬*{C˜ˆ—g¿šyos|÷ØÜëÇíõ9Ió°úÛ¬½éÿ…·¹¿f‡Tfº‡jgtuWúYÚùV»òÉîG¿ßkÝa5D·9›Î펓‘>WCÊãî¨æä¬Þ«¬9cN³g¼1G^fÌùÐQ‘ÚìqŸil.Î¥dI«ÿ²¦ñ/õ/m#¦ÍF¼$ü˜ñSw6~*�\CÓÁ[Ó•gÓ‘RúÕ§²Á³ByNbin®wCer}f¶�&K’޼~ä|9:æè$B:Ž6–4&T$«kšMÅ'Þ¥ƒî\|Û¤Ãj4¿×E:£ Ý݉ШÏÝ‹úß®ìN-Užíª®†|Jªº)ļSžŒ¡ï®¦ ——Ö*�çêì(Ýwe‡ÕõjS3:|¯Ì»=¬,ê´.ÉFÒ£Éñ-gHNÇ­ÇžŽ´boW™tÜz-4);¿¾×ÂGÞáÍ㞈¨ùtþóg4YrI:o9rÄ´×Ý�—¢ˆ}¦ËdÒÄé²ÉªISÙ¤1‹2YHe“REgÆ»nÑçõ¦y¾v”h$´¼'¡åõ@“¤¹V¡¯G�¸3gGZ"Ι•ŒU™¢Q%«ì~ë Ëu7Ú¤®Ë›�\WeãºVŸóË9)GHD&vàÑœï%Éê�ÂÉ{ÉÍ}WRU<ÕMu¨v™=‘Ða$FõQÚõ,Í•Y3BÒõvÏWèð‘5óèÔá]É„há±Êª¼‡e‰�ŽÚdÕþæãë.ç–™VÚ[Ëã‘h�”³³;Ž5ÊÞrqTyØÁ¢TbÓ*­ø©A<_þÞ~êzFè±õW˜'»Ú^…Tö]ѧ{GÓô×zâkT5I#¥›bÕ×jµ×zaUJtÕk•ªÅ*��3C”BS$«–Óu­bRRÙŸTC–Á;}.Qrêç´#ÓºÛ•‰‰v?§¼y§L´½®’•|îêV¢©ëÔ´y;é“dReBBK|ëŠÊF²²©½y»,�gŸëI)>×Ýs½¦¤[I� +?FÃÚŸ¢—=e®÷�MD–GgYG…ôÞ Ý¹H2�¢9Ï �žG$U?¶.‡zËå¹Ò&Ivb&½ãbR¡Ï,‘*«ò¸ËS~*ég“Bev?%e +•)?D9µÑHåa¤»� ñŽB{¢¼sX�øÃuf¬×%ÍÄ¿jåYï«Êd¡´™hluE²góþ]}i?�ÆœC#ÙNïZÒx4�àÜ®Z󱊽¥¥É!¥ÜÞÔ®iwÕÞý¾¨øIJ4›Þ…Õ£¶FÌÕ8-¬©q0Ó5âàÔV:lh!…:TÀ(È�!¡Pà‡ +´` *@ éœãÄiqɉι´œg®ê5Ö)±-‡hTõ*C£"×=i!;mGu|��}Ê#ç�ʶ¥UÕ·ãìl².Xw«Ã&Ͷûò)6k±Ž-’Ïažo²l*§Ä©nÖ«ÊÖ,WfÙJ¥!Z)±ðþÛö9Š–Ýx™ûÙ�òîê•G;ÃNÚ]±&íÎ'­1ó®sÇëðz¿ˆ¶£Ô41;ì¤I{ÙT®ÐjhÒ0­t™~�ÓëÍ4ßá¥ÏƒÆ´g¡½WL{æQ�-UUiϽŒJÐJs5g5wÄ19+ÝœOBwVY6wFhsr.ÖÒæŽäªw3¿¨‡K•h•rtôsT'“Fˆ¿¬3ã™±¾±ìCfgˆEÇÙ)o®‘UÞ”­¶gçC–uäæ‘ÉNT%•¥$²»dR™—2"«¼”‰Œîre+MV‰r¾ìa;+²¬=1¾³[Úlì>B±W(m(3ß Vúdü!”¹ññ$ì% Ëbéä™L}äÜþ‘ë �rÖ”CÁ±sèeõ:ÈÐ~¥ºk„¯qÖ\56¢“Ž2íe,÷Q/¶jl¼V’YN‡!åØê ¿ŠˆUxÄ©§íVNUqêxxD&98UvF¶:)×�LŠl»Á¼km9x;lÐZu¢ªuJnŒÈæì0N� kæ’9æƒ?‘¯G9Æ£~ŽŽó¹[KF¶:ꧪ_�f†l +ËϺ©’Ú(™c’Úm-5"V«ŽsºVݲ#µÅΊÄ4›Jé¸Ì[{Jk%}P ölð¬x,ZÝx”ª1Ѥê£ÐG�‹l2ƒ.¡³Z©&_ï_$©ž_¦e�ÝѳHòF§»5òQDB?<‡­Ý„:ìx%D¡ã.³sO8ö92“ëaEy2±ã‡He29sTó¬L:gŸiY×â Ýˆ.?ÃAìÔ/582Ø;Ð.u/ñ\<¨Ó2;æ˜Öf¦”³’GtTë&GÎç¶æê‘TZ]e?{ ºeÖVç´´o4¹;®Z8•sV·É:¨¶Y�Æè©liCD—Ut]|ÓPhšDÕ)+º-¢ÏÑ„Ž¬xêOÜ2ʼZÑ©sÚ(Ô^ŠKmëèJXó ‡.}e„·›J¥oÄ9•¥…Gcž�qMâ�qZ"K+«ñtÔfñ̌ǜ‘ñ�èuWÉt–oÑEK£²`ù’~éçÍœŒ0ov…Dev•^ΔÝó «’ŒŠiFF7zŠÙ”�ìbæÉ(ÅÌdT:ª�ß*Ò[F<5±rå¹´â‘�ÑRæš•¡™ìŠ|d&<Ã&M«?º"£f{Wt£:I›úQô¯(Íô½.�ìÀÊñÀžæ'LëOGW +’ó&© �ôÌ»»I!<ŽäaŠe½vkB?n¿zRr¾M8¦zÙiú”þ—¡§š»ÎW1Ÿòêª2_V7›ð(³ªÛ=1ÛuTÝL%ã;yMû‘XÕÓX!ûY•hUãM«d•‡Fy.4ñÄæÔaâ�µÿá¡LÍ9…7I8c%Ø^á�YwHu›9±M3„7ª£o¯íG-)¯^5W×|¾"4¿1´Pê—ù†Y¾Š«€‹[ø�"&–Yè%­ðQó†Ä^ê¿Àšår1�ëÎyõõÝ>XÏ"FBœÎ‹•å_¼q›¶ëÈMŽ z„… LM¨i:ªh!ߢË1b.çÕÛ¯n�‹å•œÕ½¬~çÌtLAXHZ¦¾6Í2C’FÅ,õ¢V +ÁoVúâÓD!Ô%Rgc5Ö—(½h’5çS9„]‘¥µ¦¬KC>Ý"ƒ…ï<|1M�ÐZô3¥²½¿Ÿ,èAÙZ@¾á—ÒKÒr«‹"GÛ@fTÉ(ÓÎ#Æãiö_ýÌyT¿ÜJ¾»ieWÐHM‰t<Êyç8²ÔÂþEVøÌƒŠ~ Ü¢‰z_1Ьœ’s•fKo×ðVe%£ z{€Û†ËýfRÀöcòlBB_¤ÞÝ™v=QQù×LE Ù Ä€Í ºÉ­88æò‚dUÛbkeWþtF7óÚ‰ï"p®€Ôˆ•²øýédøW y6[îtH&\À åmóZ|‰çòضڂ2Nù¨}¥Æ€ç¼›•�Õ¦ëxŠeÞE‹=œè Û¢° u#:­¼\ˆL =zÇÔ½®Wƒ6¡�r¬&åËæW¾�ƒ ?Rb>{waú¦ä·¨7Þ§Õ½Žuo…d èõ;Ž i“7…_J©OÜ©°Fòl&)mjlçÂqòcdZÄ t ëhó®5¹ðæð}C¶ µÉ•ïAš¤S´@ª¯ÜþΧ Åš%Ä"P?=”iÅÎÐá�ÁÒ&;xd8p†¸»ù>�‹F¹ž‹¦ÿ£¢O€�eÜ%!È«òõe<þEÝÜÞ‰欰�cw$�:‚R¯�ù6D‚@ë$PìßgME¤/Î@Tï°Á£\6Þkq©&‹1÷Sͼšý¨þ îjàó<›taD9eis‚6¤½2šÚÐ%Çc5ëBà‚þŸ(QH0*R‚ ~6v#XøbF�P ›Áktðd +¾%êË1Ðã›ÕEB]×ĉ®|Eƒ-›ðsízrÍÍC‘ô†Rì§õ�ÜâŸæWø¤k–¤?Ñl· 5¥Ú÷¡>LŒ&EÁôö+aŽ5z�ZR‰�—$©ô«ëà 2ÌaBù<^ØÔ,Ñxà›kšø·^›[kЉãê×úÊÍ:œ¤5»üQ?ð&Ï')0$¡�z Ü¿ºfé afÀâ6@�ó¸¶f\ .I0„™€O›°¹P¦Ý<ö úÃi‰ñþ&…rïÂ_Ylõ×õr�^¾c<�>ç@D×’»q° R�‚A=€Äjæ@Ò=²_LêÅÝÏ*=º”EP¦÷ˆ¢ÄƒïÅ'y¤Öž¶ŸŠ …Ë=Z4N’âDz)''dRA|�*Þ;åâN§Êõb³1?¬9¤— +^@|PiAÐî­É'·_g +{�Ú#@'RG´^æÛ\×¾¬—”Sáq =iûP‚Tßa b59Ú¸?«+Oý¼¤9ª‡Ò8TH®1:ìY—êpW_ “s?@˵’ ¨†SŸsȪ^H{³ÞQ~^ÍÊ|u˜EYtÞŸÿ!qW& Xô‡ Û�*˵2BRA!“sÃpÂ+’È×¢ÓªÕöšcÜäFDº›ž÷_Äû·Š9gÿÀâ›zÐñžÛ)NÊÇ)å§¢–ÕèB°yÜXêèÝ4ð æˆ€d´®â#NŒc•,&Ä68kp‚˜ñ—šöpד5™q‹Ù÷j@iU­aE?]Íú¥J¬˜ÿ}±¹úT�%�ßâÖšï?½ü‘%Di°? øúM‘\_à!K3@^Ög!VAÈ$3<*‡žÞVT°îÎýµbŒ%,¥Ë(ߟ"5§=[6"ZŸ#vü'}q4¡ žO!  +Œ"ñ6�ç˾ÛX¥2þ1PŸÏ Q\²ã¾’žŒör|Ó”+ÊÞ0/Jr 71~28¯¢åZÏLùszÒ¦µÞ9aOÏŒyø›ôËç9Gžø —h8œ£•—a“à�a‘û” 7 Œs¶œŠÖœQÌiVe +¿’¡3jÌ aã cR@ïáÖÅDf £ÿ;ªS�½”LøÊ¿ì ´b ÆÜÁ/ª¤êŒéóþ÷¯€1¥æ„i»‰“²µt÷ÁÕùc¶éd8²VÂXÅÛÚ–ƒ†À¦“ʪ‚MF~#CãõZl1ÚYI¿{7‚„¼Š“vûUõ÷¾/èI²{ditÞE.�_hDoŠƒ”ˆãƒIŽ,¢€ÓQßæ�TB€�š\ù�…[LøœñØŽ¦ð�Æü+¤¯?A¹„,Q\êÛâð¹°Ýr"7ÚE4ÙøÑ'¬`¦$ ¢¿*¤M’û0ùÍ¡©ñØ[¢ÀæÑ+9hŸ·×³Ý:Ÿ�3�QÞ¡ŸÁ7¼/]ާ]LàÔQ%nò8iÂÑg{(ÐîÆ +k–¿¬rà¶Mзӹ‹LY y ÛÊOòhÝæðnâç:o<¤m„¯É‰€˜1Àíã1 GHý~9sØùwŠ”LÜ|(D·^ù˜=Pø# ¢^¬ñ_5P†Áxuµ˜�Vôk€bõ9¼†Ô¸V�Ö¶z+M# ¾†qúÚ^‰§"X2rÄ÷—Åúodñk x&b¶ðOà¨Ä¬B¸2÷i\o–üÛ#Œžt’Ò‡WŽm’c¯^cÖ¬ º,Ýñ ‚k˜@Hç•­`lc[í8¢@»†]ðLÀB ­S ïl?­7ÿRßní’ø—¥_Hå+�² V¿]°À0ëª]oHÂS†Wñ¼JÅvwà•$ê¹n_~�Ò¹¿O}9ð×kÆdԇñØk¶–—`�:ˆË{[K¤ß’ìH°‘íh Œ)œ@›ï—-Šž�äÙ&«¿è0–Ïüp0DL2™"ÙÎ{RãÅûvLnÜTP­p\‰6�a*4Ád¶=qW %®X¯lñ¬Æé19-Ö‡èt +Ôú ÖÇY=„y¢m&5B;¤þ%'ä½,€�Ê3ÔÖ=L£íYh†#‰³HH'‘JY!í£ñ­L®@16œçš�­¶B×1W¾¬ˆ“ D(ù(Ê ç€¯ª¼>Ü*pI+ Ï]½²@Kÿ¥ëÃJÈûNxæ‰ED{'RhØ.©ç"sZ¸¦ø#PÕ¸Ä'0-Cq,–Ÿéôµƒ“F4–÷ªLyÎ~=4ðà¯·Þ 2ÈY?ƒkÊ8çvë«lKéÍœÍE¢ص¾²j â[b"ÞJVIíoÎ2e®Ño/k¸M\+‚½�EÅWÃ߸�âБòV{/sÊPº (È¿ c +R’ç!l§f[IJ•–æ8q͈dL‹:@]íÊŠü\`½¯eIîi(ƒ(toö)|g§üÖ¼¸À²¿÷éW„`A»Ã*Ö°Â^'m¥SÙ=³¡³QuÙÆí$½­KR�<9JýfiAÞg×È“ø¥å£$ÐEŽrÀojQQÇr€[pÓafè�šØÅ>ИgÛ1/^áJ‘hÞ‡ÿ›7¨é ’$ éáž°üôrá³Èª+“>†4± Ü´?�ÁA†hÁö'b_�|ÓÉDè_c]Lˆþè9˜²*¨‘ü’pð¾3œÈqGÁFHˆg�ärä~¼ØHš�»®�*}iä—]^3gsRg-C>†ß`c9g†°ñE’ÿê_]ù,�ÅIƒà—+öðnQ©¾‰±;FíÔË™ÊÞXdSØr#Ki¸š<ž�„Èøñ{oâ€F°‘)ÀŒÃ`©P=¬ñÿ/@ÚͺÏCÐIºäK¾aN•So*áÄ ²ºLKÞ”Ò8’bêP;VÝý…tEa:�V0�‘0“Ä•tv[š´£ÍI‘¸&w�Yº¬~Pb÷;ZPÁð3þ…P[^A +XÌeÞÚ©éöxLÉ=‘’ º’v¥×�(p„]é3w•™ÈÊt9>¦’±‚”¡CÚ¬j…Ô„íCÒÐjžÜ7š–/+ ï·Vt¤~Qè:]³¸×&ñ¬N›ïøâÞç¸ÝžRv¤<–*~ åÎ9@%`£�ÞQ^ÍÌ�ÁÔõ�o4óG‰!)¤&³¤¸­Ø^Üd È'`"âá¯x“¾4†Ü7BÔ=[¤½~ë�CvÇ´èõÓgŽDÚEt™è î9“�~'ÃÊá™m$tïá8#«¬õ–b']�Qlª#£ý>Ë’2ÛÌ�ʼn¤¯{½4ÏOc&ü8ž:, ’µƒ˜ÏÚâ>Tô(Á‰‹³*܉OoUÿ‘?l M*L2ƒê PuÔŠ1©¡†1Ù^yA½—�ö+üÚLÔFªC™¶ŠÌ]©…R5‘—דJΗ:‰[Ìß ;ÐÐE×CÒYâƒs—c€ÂÄígf†;Sm° +_‰€[ªüÍ%#•3m´M`˜¡t‰mçDâøCúoM`æ…@LŽ Š_¸Î¸‰ 8rr/ß®SÂL{\n¤—Ÿ2À;J÷KŒ)~ªÐƒ‚:©bîMï–ÔgqµEjÇPcÜS¿‘' 3hyÿ#Ä£ó¨C,�Pæ“’Kð-Û`•„ÖØËÚùéÌØ¬®"—¼ÓV_ÁÅxå?š6ÔÛ�Sy ñ™x‡úÍÖÊzzÆZ] Ò)*òòyÊíƒÛ%¤ªˆçö÷ÉÿŠ2̬V\ +®›•ÌMñ5Ã0ËŠ°­¨r�N)w›s1©1ß™‘‘‘˜²1ƒhÐÒk U¡‘Ü=?ü¡ú¯÷ ö³I®ée¡OPÜý4ƒæw`Z9Á¨8ÄÑìêá";3 ž‘‚„1}–s[Ój+›Ã�æ,N'Ã$™ö¾¦ÚlÜÖvÊþe§^(á^°é<)Dý ËIä‘fã ÝÙš0-kxØ“é¦Iæ8Îz!�Àïs ÊÑŒCó¹1žÆªÀPÊãòp0?p»È¿:¢Ï@èßÒ`A™nvÞg3ÚX†Ñš_R4¶èÓáõ"áÆÝ\ µ 3̘5˜-¿_½¦ YþLPöäMáŸÞ÷wdÜê¨Ïz¥>ÐÔÙ_CgJ­QТ�©êq +G¨¢‰`xŒGØk¿6XˎβòËø€Ô†Sدö¢“þ*ñKƒ]©5°‰CºÂvkò„B¸aÐ$Ë, ¡á±ð–¶á�¹Í”Ž_ëÏDõ�üfKüÿVÐÉÆÚVÕ¦PyÁ2àÑÎ…ùsT +ì>ﺋFhWa®[„j‡Í |ºì˜¿ªfTœãÊ™Ìg¯�¦7¤'(»+J„Ú&“ú¸ÑOdÄD;šU˜ÁT=UÄ¢¤äjG“R[§˜R¶ÒSRþÏ*pŠäåÌÁÑúOâÜûÅ6¢‚R¶×_Ó#Ã\aÌS4G`ök¹&©Ô|h}>!{¶“�‘!fÚð–ÑŒ‰ÙþG�±Z »q³©Ç*“l¨—Ïð ­‘Jý 3™såY�A“Í7¶ÂƒÈ~ÑKöD¯ý‚BÞ.4`’"(¿Ñƒ­Ísv‰p“¼ §è«øH†6×Rƒ,lýVôíÛ7@2�“à Ñý3Æ/°Ý¢…uS|"Èån‰ó„.=;8�Ka ‚ÒÏ‚Q%°® +=tÎ%5\Tÿ˜=ÂÓÉÄ¥(åÆE~®‹¤²iº�ˆbB&&|;·„&Eš>îÕ�‹Õ�Cc`±ZrzÌ�רRdz٩€ ’ž–_ „ŽÌ|?ÚHö-3cÉX�Ó«ÒÀÊ1^QtÏ/j �8ó«'¿™b®K�¡uè»ÁCÛåü;ì+.* g¾f²Ž?A‘MZµFxFPKÁ«9\š µÙaÏ#;8P|@Z¥c×èœá¹¥%ñÞ1� C€Õkma瓚ŒOá ¿Š\ï>]OuÎçÜjÊ´t’�Db™_´Oœmö_Éùò—‚Ѓ)$çiÉr1IÒÂ�/ +|ŸÁñ `ûË$ ˜i߀,SK [ÎÊ+á¢k•8ÐIcˆÐ;È7ò®•,Õ­ ZÆŠªq5dHZçüæçœ’xá¸~nóí?A¯£´›g«XûY `ÔœLqèänŽÜçѶšf˜aNYÕSiÈk÷‘¬ý°R�7¤ûë˯Ù‘î&äÚÂÙÂò¸VÞ>‚�G{;^y¨„ 9dïÑ1"��Å· ç?TØs£’B‘8±`I_Š·‚�Éi»þÐ7¸ÏãŠÖ¶,dSö!X5BUµ X%˜}¢kˆ¡¬’¸€”õ·¢þL6*Kœ¢3ºþ¥…�/Œb¡ÈkdnÖ_’á=­¹¦Ó¬Ì±Y™©ð¢3@ûÿóøI'a b8Îó�^�(°†:0Î:ð‚ðC<\å™Ï—I=ÿæ»MV1Ç5CàºÉ¤’1ßÚt¶k!,LFz²”~�: )µ@æ<šty¹dÅ´“Q‡F~á�ž±çœ�kFvt)”VaÁ ·„^ ,ŠoË'ô_X§Ú¸½ËJkó8¼`’€ü> ©°T“:Õî½Ëν[8ÏL‰d ì³~\»’Ö<.z6ÝO?>‰”Ÿ¥¦ã8j¥±¨XŸãù‚Ë<%«Ã=ìÕÊ…]<Ç^�eb›©Ò=ßp虃®¸«¢ æ¼zÊ—ý.°ÍTHŠ&I^+QA�eß-fQÍ"äc‚*8:ƒ|â�€_úK +™¢††/´°½�ˆ�ö¥C +lŒèwØzXÂõsI y¯øïÎAœ€<‹ØŒ¥‹�ÐÕ=qH†1u�UŠzáÂô‘3`‚5l’‡ +� ¢R‰ Ò1µÉƒŸK¦(€KBièn!ð€+·€’8Gùžé­N†Ø7ÌÌo:A©UK$sU,£B$Õµ3ˆ›`”}¬Ïíi,%BùsSüe@c-h[4Œ×"D.Â刦Óä]�A&¥l£°’¡7OÚÑìØƒ¶(Ê$,;CZïŽõæ�ÿÙUŒü*çb¾‚�IvNή8T”¼×áœñƒGv¶‹ ú2Ò�£.E¸‡‰áv?8yt1ч(�Í1!Îj¿»X‰µ:äϵ‰æåŒú³³+¯¤Åsé1†¨¡õ+ûTSè] £Ñp¹`A“õ[Õ]‚Àõr1$ŠüFȪl™…Yï,ƒ‚u‘þeÅ‘1MN"0·?Moü²F¼/ 2�´�lÇwVÂX;È€’Ègªè.;}ÝB°ÃÔÛìP7p?átQ³!}¹ì—ÞßqZ‰—à�î,óãOåÑ^�;X!úNA~ð6în»}òi,ÓyS1‡%S�JÉ$<»^¢zcØÈßÒ¡zŸ|në}èèÖQY€20¿ïå¯étôËE†ò@¶;ÝÖn—³Œ +¦q0QÛ(x²ô7ƒöÐ)@Åû¨±>ïU…'g�®ZƒW PhÇÆ�`Ppï§Ã&`€Î†W®®QA†¤. +À6ÀéO1µÁ +šï¸¡ö$ËD§¦‹Z±Õ©ô%Cl®è`ƒÐ)M .¢nèû V›L¾Õ ­1 �4ÙÀøû¬×uÚtdmã)Æ�’ôÈ„8A¤ À‡�ñEÚLZF)a»ïV…‰è –QÅl¾U ³P_oh.ñrù~¸¸´üR¶$5d¶5éÀPp⹊äœÑ.3u¾�ã +Ï;f«øO�™ž`8e„§ÙµÌË@Z¯(Ó6¨Ž§v¨)Ÿ>Ð�¼¡çrŒ1!€¡Èósi2->2â$Ã̲àFV{�!|¼*Zån…g¶EåD@‘0vODê³Ø,=�€\ƒ™Vf€€m¾æºlo�×à›Õ2®µ»xÀw%”�F Ï'ˆ,zÖz Êж^ÜüïlQ¬¬­nR0`ë½²�-Ää¸q{Í]R;qÎ.øŠ}tY�u +�þˆ|Þözï ñ´}VQ°X♄Á„a‰—bsS`"÷Þi�Šƒ²rš›e?ü ŠØai¯Óª»ŒmâÙ+AÝaåÆ^~wOž5Ž‹V°b*ÈÛ°ûn(í¦dŠÝ nT—ø.壚GÉk1(ëØYäP$�Œ�öRö9³ÕbI)0@‹çdÚ¸p&jë:z$”�4¬¨é5µÑš�?ëßúY®ê¬'0‰‘È\ò^ÜVd‚À~}Œ¤9�FÙ9¿ü +tã�¶ßä¶T¿ºV·©z~�y´´ø-GôÁC"©B¤ÑÏš'Ÿ›e�‘:,%ª¤âÓ±«êo®“‚"k,Ù´�òo+œèÒ¯˜vÅ!°)”)9¶ÛÊgë°±d¨´FI†Äò´�6U §§®}¨<•Ì»RÙøDf·s¥`°9uO!I#·6ÕðõZ¨Ü +’g5 ÊwúXÝ*¯�4þ@M×Xü½ã +›k¶éÌ–§¤¼ó„¢ù<7ˆÓœ4Ã_ꢳ”åèºÿÆ• u §¸++Ø çê=#•$ð�P¨ +?Žeè6‚Táisµ9/Ý é›<é›é3j½¨xÙ)PÒâW«sÙr™ÅózX–æ}xÞ ´Ï/Ëev*À¡[�rŠú~±t¨"*ambH(¸¡þÙt½b±M–ûŽ|þ®î?¿É¶aTKSç K\@v.µ]Øš:0àÏXâï|-ÜÀv˜i$(×’W¡47µx¿¦Ð^5 úï8“NÛ/&³Á¼.£§;‹×Êùr[º"ç�™ä&¢·Cw~bË4aBÓý‹^=i "�þ× Çhà ÜâÑ}¸%õ‰HøHxYäщg÷òò2Æ·2þi¤† g‚Ó’¯n%«�¯êPØë<‰†F €WB­½5/-3—Å�m~Ž<@y±7ÚNÙfCm©êƒjœ¯A ¦Ø—îr¹ý‘ªÎ 4* aðI i´»£uf,!N¥™:B«Š¡OñE_)!^ f¿GŠËL”ƒÎ<ŒRmgleŒl"rù�þ¾f;W¼î$ùYL~”®„�ÀŠæ-\Ä "Z‡€¾5$¢vÁ7Ü 9v؃h"?â!äY2/8ùĵ—ÿfl ?†e‰§¼-8×IÁpe�ÊN¯«=Çä_ å̸%uÊJFz€×N�¢‚'ª€Ó”+Ú _xý< ã³TUŽÏËhHX„ð'%4ëC +ÅåàGuB%LOw*é`ÙÔc%®æ 6Sz}~õVIÄXѪT™T|z²<ÓåY¡îÔRÐΓkóX1?þ Zù6![Ù§©sWøEôWו½Rš×C­ÉçWš{I~¢`Ùär –ù²Ôea�â,ÉüZÌ»ãY–½€*³Ü£<½–LÕ–2çítËåØÙ¾Åˆf\¶m.fœ®PžrÉÏÜ ‹YÎ1Ìþº÷.&fèo„žøù2¡�¨„_hYÛË_ LˆØÿ/ 1 �É ¬Âˆ`Î8Z¿nÀQ³ü[ZÜ?0שÔÓlÀ£Cšeƒ¯¨5QC°ùU/ß2S…ÖÝ®Â.™ø‡å9Iëü â#EVwu·˜ã2öQ^ô„=­Q°ÏçGõ Äý^Ì»šÇdÆ$ÀÀÛiÆÈl_õúÄBoe 6ÿÙŒaˆ6º@ÚØ³6ñº� æY£DþVa’ Õ&Smú LÊÔöÙÊ~Áó,[fqÿ24Bfp»s4s‹h3�ÇË“Æ3vÆ[(Lz江¡µB@zÐ~2ÀѨÉhŠ_• M5Ð?/žÒœ¦@ÓftŒM'ö$É^wÈú €Übšì‡3”>¼& IàëŒþÝH§LE1I’\œ–— y‘ݘe“m'ù»ˆ§Un,‘*Îòy!ž7Ú^Æ<‡Þ�æ!Ðç0ˆ%"肌ûÃݵ´Ìõ²e昆=Mc¿©~^0MXµºCc·‚ļ¢4• +Ô2ƒ +†ÿðefKšãÕ 3F{Ùΰ˜Œ Šã§bùëÛþ“ ¼†uÝ3�8tcD»~ô¼ DCñfžÄ\›þOÊ–vÓðË~–á„_vD�³JϘîIYd¥œÍË@V-)³ÚE¹g’ÎÞ\°í[OËþŸ¿¥L+ŒÅñxløþ g Às _c»¿Ló«Q§¹ xЖW‚ ‰– Á×^Àz©Í"ŸãNñéh%8¶üöœì¬Ñ|AtPÂîÌcýqyÞ9?�Eõsp*%ÕÎ)NØFKRþ\ELÁK?5‡Ï•iØvVìñ`'nåO%X’¯l™'QN#ÖÑa _8ÑÁÓ!`SX”Â5´8h¥Ó–£®„EP«ÅÔPÓï¾U¥Xت�èÕ°4´G©eØÃNúÉ$c6ÿ´L‡¬�¯µÜÌ¡¤:Ù²tÔêè)²Rì_z1?:½†’ +žÚ`^–Àã�VŒãåœOuÒ5žÑιLÖÞ6à¾I³;½”ê˜pà=ž·Š1ëw¸ Z?¡í£DC!µªXõ–Vcr]Zfî@‹4ˆMüi…aµÖ^96÷C +nûíLA7j˜^õQæé¸÷$&sf™ÙæNa—&§à! [‚0À܆¦@m{Z’gb.©Q`‡Õjü‡­o_ã¿ÖRÏš„‘Šñõ^éýËŒù0QŠ›¾@& ß0;†!H{p3èýÝ…I*€ÇƒÖñÜÏ¡@5áÅÌæ<Cc=Ãå *MŒ¶©0tÕ³‰Hô¶žÎ€œJyŒCj%m1éM¶o|‚ç´—¨$=¾E†²¯¤»ÿ,ã˜W£ÙG­ÝØ‚³°•ñ%A§5x”®3Œò‡œ %RµßrM¨Å>ŒY+“<ɱ¬›ñU]ßú‰¡&Z9~$§ÎBÆ¢d½çwœåXN}-Ø0æ¯[QpsP[\(Y•‹¥v”ûrÐ�îøº|1Ró„YÕ¹´ñ7ö+¡ÂÇ}„lG�Ùoh±‘–EÇ‘&4: ¢�ÞŠ�9b“ ¡DB›™×®£¾†™¿¦°®·e»ªXi§cœ’—,Š ? ¥¨Ššª+xô®óB3£Šž!kAC"µhÌlJi#UQ©ÊÛHƒ�"VøÏI§'¯þÃ8ldš–Ah´(ª€µQ0–ð½®V�m©—\ÿ“Á #颾NûKÈ5ÈZƒÒcéõc4Ä (..Î Û{h¦À’S¸'ï= +pKĵÕâ^Í3ûÎQÔ^ò;µÐáÿ+ë€.¢ g DúÍôŒùr*+j�h9HÂ’Àä$\ÈÉ:ð!J¤�y ˆË+½å +œ5 ÏBê-8™ìD•2ÐØ�Ë ¡)‘€§pÞ¡n."¶‘� Ò¦ádóˆÞÍ€i ½6Ý–‡(›ÈxXÝÿå¼!¹Â üÕ�óPÑ÷±@»†ð „ÿu¡Á\,4ÐÁ-*´ì'Ä A ZB0ÞÔMX„L”Á:=ˆ°ð8¨\„P* ¢Aèƒø#„Ö+ˆMC¨t‚p:Š >¡½±×•2.i„Žˆ¿Gh@lŒ„ ß'¡´bÇŠ@aBñâð?P¦’©¨êT' �Žc?–<Á…b ¬úä¬î>VaÂ;ñ‰_M8Dýyðú'¿|âš@9>w>t¾G§oçïk[µ§6÷°ç›†v?ŸOONN£‡§ªéyÚÅ„î4[–еòàÃTmàÁýyÜ}3ÖûÒ€gÐA<�@ªj‰Tê½TÙt€Ù’.<¹%“u§šOv •áÒ퀌êÒæäŽ@¨$ô_½ti7‹stiÄëÌü=Ü5˜U] ’Õ®‡�¨u ØƒûÆÑÚ/ �–ÊAëè–hƒúÆ[èŸß u–5½Á ›¸r'XÊË{>î„K•tØ„~SBáCŒ(¢ñbÅ�£ÊwÐÃ&½…ÖÑi’i¢É<Ãi‰v2툰»s™¤FÕI Àšu°ý“éöã|3" )vêÈëÀšO‰D¥QUdSöÑ—`'ëlß»¯Ÿê#µ]:ºƒƒ:Ç¡ævépÔ:/Ýt¼Zƒ:L£Ã�‰/f´t´Z›Û-�?c‘@Œ/�â€ÄK§í ×%½tôêèi©�ré,ŽNƵ8囘ã‘eØÝ4òœzÐ3 >Ê÷É}Á;rxGP}ÁMÎÉ`£fãä_|c¬°8í�!yå‹AìÁ¼HÕ'˜LD,óÄ ¨ÀÌ奣ßÅ-è]`q›Ô@WðfµìöÒlrˆÝóFKùÅWc€E¯êyjkjRn91JLAðõKD9#ivbÍ™! b.SvPg³Œ0îÑQÕ¦‹¿º¿86MfÂЩ,œà‚Ž£À$VÍÌ7 +¦âÖüUçà¾'/¼%[2wýOx–A�wú’¦B8é€^ ŽºŒ ó%�#=LÉú„ ]d•À¿¼È\ /𶬿Ö7Üí¢ìr]lV{o—%Ú‹ä©9ïíOcïß5Ëäâ“Ég¦jq—˜¾(Ñ“.pá®YYÚHqq{Þê\;Fâ–×g‚èúÖ -2֕įÅÄOFêâƒÕb�¥òE0-÷Ì*ÂZÞ]»ëŽé,° Q>Zò  潪èx²)¹\E‡p(™°k2™d<3 +Çg0³´IÃhšC˜Vp•Á(Єsi\Hp†ÆŒ¨†Ä6EØøA¸ƒï½Þ‰¼16ã0a‹ 3ß÷·¿kõ»ù>äglñ©‰>p i âÕM?L»üv�'3‹yôž<µ(¸ªÀ?j60z7ZŠÀѽ~)Â[Z@»"”}žq•QÇ·ôIöU‰)d 3s"mSñ*M|÷Y‚©Š††úÙ�ž!­:O3ëC@?vÜ1€ý›¸$·j6í*+ ªJ’ÓÜe5Àî)~Vñ5£gÉ 7^ðg?Xyê­}Òuw²¯Î§SÌÚl5¹„ÐaBa—šÛÁh +¡çXÕ¤ .x@߸ Ë­„䇕~æ]�í»²ºIÌÙnhÙÀ,`È÷`A*LYqÒÆù¡’{�ËM:-pK¹t•_bª·KæÔÅrÆ*Á2À†deQ9IQ h�7„·–aâ±ÏWAhÚÊå@ðt |F­ z/;*_ÓU}&Î`ù)Ԁ؄BuŽhbQJ°Á;'-º„^àaÌÍhÈ›»@чY™6&ª-Î*g–þe…3 6Udhí]3®Zƒe&úÄñI’ìö3¼—/êÄ?�VëxžnýÌ.ZM¿X] Åuíó† o©;î!mž-‡ÚH×ÞøÕ†[S´€ð%ü\zŒ:R‚Jççn°ÚbÐãà.U©¼Ót%È–,†ê�9©Ñáb·[0E›y;¯Lþ¨\Ò=·tO’²÷i x9™óvƒ–�‚èïÀ¡�z¡'»Òƒ¬U[ýž*âÉAÈÙU~ Â"Ö´!²©…`�]Áx\¸ŸJǨBò¦rŠ˜Ë«dk¢Tƒ`ÀDáœ�¨¤Ï�vþüøÅ[Ÿ<¡(oöI#5òFë ±ÊèøBic¸~‘ó4ˆ¶— ŒŠæd)ÌpsYP5é¾_6‹r jÿîP¹»£•ƒ_Œmvƒ™2s™Àׯ¢ÎVBŸß_ÂB�S pg;zqW&›'WCQ¯5OorJC̯E˜4'Å“:œa$XÄYã¼x€&mN÷rÿ·Ä~…œPŸˆ/ĸ +ryÔ›€T¾>ûVÈEøfÃnå $¨éšÐõ®X8j+X+ÓÒÞÆl`4ÊØ4üòíµGŠï¿Jö; £%ñû{O·�¤#EHä ,KŸ%5É­Ðg qÕ¢«ZÊÿRæq!ÎkJ ¦,¦¬ÅŽ@Ô: zœj¹|ÌK•Váò^²­²8Ê"·5Üd „”<¤ aÛ ¶æÏuüUE°%^7Φà,1~©TÈ9:oëX1¨-.”p£:Iˆ…,T«÷ÌÓ¥T…ÑÊœŒ >ÉfTS™PH,ʼnÐ_Z‹�t²S‰5ÎØz† !ÁÕ_ïœìV…ÖŠ-Y^o^®PõЕDª“ü¦íjÄúÍòˆöôú”6€’ë]ÊðæÌ'~´S#¥çv_Ü%€+«Í‡ç&µÒè&G‰�Ãå¢Mј³šò®Ù8§Q Eùé{¿ +÷"œ?JãÝà!† ƒìæD.yƒ” þ$Ó¢ÁßB4Â�ÇB�°^Ö°wéàl§ Ó3X¹=Q`][qä îÐęҲ˜á_ Ûa¾ÂÌømg<ꋃû|5|9ÿV•S‰Ô$œ +_½¢ _¬=.%¤ÂñŸØ­ÑìM�OÄ…èÉï¼ëz ØfvN‚ÑJÄ«²ýŽ& ×ñÓŽšs’`<´± +·FbKæã¹§ÄŒñˆãõg¬…5/*�ÕQX„(jBqðì��qéïn{™ÛÁóꮤ »‹Ê·RnwCzyö_ÛU?øQÈ?Yzh&•Xl`üSR2OG¶ªM+y…h•Úˆ„B d™ÐN�¤i÷‹øð¥{µÑ�k”ŒØW†”žu¸:oép»„gO–çÀ! ®®žîÌWÍ+ÒŸ ¹ùÝÅ@jk±C¸"NUeJÚDʃ*O .�Š3XâXœPii�ª7Z1O¨ø~V2g¶KJÈpÄF©ð$ÁSÕ–lU­zŒ$¬#͉‚js§’+§ÆÓÄ£›x僄¨ul’-4ÖwQ²fÈAÓÇs›ö—ÉáŽIô6ë +“>ÂV­ø’/7¹dçié€Ã¤¹O–ÖZ%µú$rßmh©)ù+!JW;I �oÐ=>“¸9Ä~fÒ[A W%ÑâOIzùÆ…$b<,$#y¾«EA’�ºjÎÑÏtÀûkE 4uã imëðÁ—6²Œ32ó,ÚXåa¾Öy/J˜E ЧqŠªÈÞ•EY3Ï‹ÆDQ‘bÑW QÆ+}ˆÙÒ¡;Ú¦«±5Ô�"W:.­b˜¤I”ô›Z9#ù‡¡­]• €Ô˜Bz^Nﱜœžâˆ–êØÞ�Þ&€®rš¦,¨*?þ³p•�nX¢£ WM~9Iºª¸½W¾àã +? +U0R¸1�û…‹7�‹‹4|ëÒ+ZÒ‹ïO¶èW8”0“­‚š ªúè�/:¦JÁÉÈHå=Xõ PÁ¡…¦�ÁÒRã^ N?Ýé�Æ{¡'÷Æè¾Ã÷Áê÷àC£‰ÈBh‡i'ÚúmÑÙÓiO:FCؼ"Â(T€+; m)†XVU«öm½ ¢TÂÉ]â"O7Øn6 �p­üÚņ…aZ/óh „jÃø5ÁP Á[±P#ƒà +m|Üš –ëŽ]‰ZÀ®“bJZ1o³æb×Ö¬lPC=öu\; +Æ!ÕK”-ѨG;<»¯g!Û·€ðÙ$—̥㭟=m#me †Ë€9¤Ý*V?úi†Þ:¤ÝïLÎñg+3Ò–ÛQˬhm´Æ²íçÚöt»2Hè´oë7Üb�aÏ�[sk¢ —†„�[™)×fÛšgý¸Ú Òý¸XŸ¯ ]¼Çd΂¹¤”¦Ý„¹L Ò8C·Fˆ.Ë\È«Á¥¢;ÁÕE¶tDÒÕµË>éœ(R}IJ ÑícU ýb¨¤™»rçRæ‚XYרe@ÅÿÈÜù’ísr‰·27ruAsÝçôùEÝ!!`‘0ëÇ$¨™·¼ªÜb։Ш­‚uqm`uaCÁcƒkÙÜ}£ø¹QÆ<Âùû…D—‘5[ܵ©ñø­vÓðP‡w-Ù+ÏåLÆ[c$Oÿ¼ÈlUIÈ[U;´UÍÃÌJ-æÌa°ˆ*ýé¬ÔÕƒg¡óSçØ©–�€m†Ó1R(&h­KHaêæ`��‚>l2]q|l¢@¤Hj¤|î¶H§+Å ´¼f•ªÿ´°îŸ÷!Ÿ ¨‰pˆ&+5{uYßÝOãnZÒˆ—žÃØY£¯}„l*¬âÞàp¦”êqÈDkz1÷\¡+±;€½°½ô††YîN˜]Z"0YIÇSmeO1ÎØ“$ÝV$Ê(8BÌ #Ý~rYD 7êL•�öÌbAßJ¢œþ¥ý;Z¢r>�'fkÿmŽíŒÕ%«6ÎÚ>BÓÛ4 ÿ¸ýú›°g ²6§aewXGl8™­\4ÌæF§B±,ÆÇ©tÏM5úÍg"Ý¢»5Ð6Dy^1çÐ+3JPÂb)i ÷îj€ÈœïÄV Ô×EšÃ9¾°6?VŒv¼ÃC‚ˆþ0ãš3.rŠÿ[3OØŸbõ₾d¢ækTŸ¢¥ +n0­Øä”è”ü‹¨Z�°hã¡c¼:2½“¤m¯7ÖS�¾Ç2qw‰¶Ic�`¯¡™ÜO+v9ê=°¶+Àhà#ú–rŸbÄzÑ[ràkb«9à)Кô+ ¸}樴‡`>�%V…®lˆptc-k!¥<óU `íá*ze³ñ „zLº?êl1 +7¯x!S€Nò¢- « +]âï*Ù�@u¬‹Ñg™y¼”ÚÂDóþ/ø­Õ1�9‰»ŠilHAP8CÛKq”÷Ús쌢Uí�cŠ~„©ú6>«®:“ÕÆmp£Œ¬;ƒçíž\'YM‹•“kQ¿UÀS͵]¬AU¦y6‚�y(ý̌ա Ì“]A€ÄB… a�̵z�Úp¨[,õ½òõøÚ8àé!lÄŸ‚¦ƒ ù ÒeÍ[#UÄPìÕ&鹸`fØqk‚RÒûÞjã¢íß®�œxCíPß¡Ðþ5…†Æ^ù<›¦–òÝrüë¤êóÂHöÖà�å'‡N 5ûwìêÃ’Ñ}ƒ˜Ôøÿli‹ ®ÒëR>ô±…ýãð»¡J±¯gFª)®ÐþªÖÿo+{øJnèþª)j¸ý ¹~ö^*êýAC3ƒ§ ý¼„÷=�5ëÛ¬€pZ·’‡;_¬õP òäÑ ëÏìdKŽ‘Ê"âsæËa $‡rª$¾.‚ì´¹ÅYgu—Í“D#KTê6‘Ò.Ñ8V Œ¢°¬%Ý~«¨Ç ^Š˜ÞýMÔì‡rjú¨›Ÿ•ÁõÀ´ü~Jö¡ÝÙpÔZçôcÔ ´il³¡¶•í#€^ T,/×›á#V)zµ„ûÃ*îYã²]ØotB]¶£>t JAª‹Œ^¯�§HeÃG߃ê�%Ûã;À¦ô�Wl‰®NÌ’ý»’òˆI£)=\eYQŠîÒ¿¢Pü´ô0=F“A�O,€—Òivà +?Xõmö8´é2û§¶À‰—ÎfÈj:°Ž>œJJzÂDAH¢¼‰¢RZC¤H4L“Fn�HªJ!ªOØþÏ>JˆåQyt9˜•zŒó‡lÓ&ìb2¡+~#?Dfs}k¶T�3[*áóFæ.­”éÆÅÄÈcU×Í*¢IÖ7¿6fl«†¾3‹w£õ3¾& “‘•2ô×¢dšgÇy*€Þ‹:Àìo¬&\¬@eÖVöô"æùVŸFðR¦að²L ‚*µ��H0=ï-ÚîÄ%á�„,éØ F1CS10ˆ1Î=w'ŸN²â7?�?Nw"´ÄH!e¨ë˜À,<—’ɸdH´´Ô!⇩b†§.Ñã/£6s474á¥IŒoo rµÈ,YЬ+„:¶Wíg„"/2ï/,¯Û�‘6 ªGëŒm>aQ_Fµehê´§>¬êzÔ)ýŠ)Èê¼zõwG¬�”u½Š†9ͳ>w9eØ:@¡É­G—ÓKP4D(bA߮ט½ËæHöX‡�ê¿ë˜u} +aÒ^ßµ,‘¡oÊ·ž1€ò®ÛøÓï¥D˜€8”Ãë›» �Ÿ Œã'Œ™W°d³ýc­¤BàcAÛò÷ü’34•1ÝÙ‰:4@G/ïýO´>ûT:'!†É¶¾H¦“`ÊtÒZŸcì’È»fºr²½—›A =ý7@ +íbÖõ˜)>èŠÑïa ¾�í×6º¶Ï×­:.P]Î7mB}º!>ÛrA‚b|ä¨âRƒf—û€�…ùfr¸ô�Þ^_ïXäþnóËÝŽß“ciÂåí£vS¡>T]ÉDÞã-o–]'°ßñZÍ¿á8ÅÀï+·hZ�±ö½ ¯*‘îÒ-Ãi;=ñ­ì–-K‘¿ÔUO�Ó.ZíŠy:Ä„)–f»Øò¶�à„@J·6ì?ä˜yqü‚ÿ36Ä6 ü©�ó°žw±˜OZTLÞŠÏ%ä¹ÈÀ#èö¹ysKáXˆ·“·à–�&åQ4¹¨Ú�/�+Lz¨~C-2è-™©|‡6? )šoŸÈñ?4³‰8t[è~ÊJ„'á¨J˜jm©Wh÷\£ä��(!Ù5ó^3[x¹ßÈ›ž4æf�§!ÒïÆ0=±²^«½W«©“]BãŽ×ˆë#âñ¶ë’íÐß0ˆ.~ø„@Hï9i¯zÈ´ßÎ2 ªV$w¸ï â=ûŠ+ßiC»åh*Öý8ß{øŠ¹|‰Ãôÿ -/–h'ÿX{Ùkʳ+ãOW?#œy]óqðhκæi›€#\Z!ˆ3�W±›>J&]Ûô +óùL¢‘vçÉ»ÑÕnÈe¾ž =9â†r +^¶}Ðq?›,Ã` z¿P64jyw“k)‘`) �bæ¢cës"Øa:4U�åa´ª¢-JúwKóbÈ£Ãx‰˜ŸÂ´±Ý$£Bá^Îuyš_y„zÈÿŠ�º‘þ1Fr*�c{›ÏöÇÕ�·Tƒlt;2× ™©#Få>E¡¿�vptŒžæ Ë9Âûs}tÜ2�ôŒBŒ^‡Æü °Ñt$‚OïL�R½k ¿ã:jËlÚüɯ½•:©ù^ÞûÖl›ßc­gö^Wªfcûê½\l;‘}Ü»‡eëÐRéÚY£Õ¥i§oS�‘P·¢JðÔ¸øÓ8U oê`síÉðÁæÔ\¸2¶l„½Pßœ6Ž¢RbéÂC3ŽYa3•’Oô’ÝÞÐàµ(Íi&£3^3¡Zñ“ŠXÞþùpFÈZ3±à3VVv—wpÅ39À¨ÝåÚUUѾA]ÕèÏuÛ`êÄGñiéGÃè»í…9¶á î�HâˆýIJ_¤ë�Žun7ý]C>)@:˜“´&$ñdÐÏ ‡[:´m‚nùr˼3  ¯‹K¬¿|îI/ ïÓpR_,ïaþle•Q¨‘nûa6 ²Ç•’„ËÓ aC‚¤»f¯pFå .äÉáZ†Š}¿ ùEèªþ­K—gIÓ$ìLιÆMqxT£2e?û˜b¶ø [w–ÆñŸ"ö5Â�aŠý­)Ë1%«¥Ä¼šLIù/¤lÙˆæÔ2+Êu-º‹Í×Íä˜åOIÕ?`/+…µA~Ï(h5-µOŠY1Ûƒ3_áÅ4ǘ°Ç��…Eb§±'v@rT˺§–¿Ñý·ñ�HXþD!{§�×¥Dúô +%‹B®Sma `åáè£FéµGþ~: U�È âBóÇV$ÚÛsᯔ‡–Žïˆ§|7>þ¢œB”S >Y¼¥SóéšGwÎ;RtDç_ŒðÄ¥Úw#y éòQUq:md^èµÿõ›…†%wâ$d®»—iù¯ÞôÖIÔ݈^DµpYùkÀ ËŠºç �«HÝnláG§¡lRêOèR›‡RDsÞ;¹á37¸O½IÀ#×(t²=”«ø*÷Y9^ì`­‡T.ý²øWéYx—ÐÄúåî„üÕΉ×,=ÿt• I¸¬¶RäTm$T[5%fVÔ•DÈ/°ûØZrÊ-FÃÇcÕ‹êÌpß}*‹À°�Tߤ)JÂhjÒnVÔAB]=_2JéÀÎÏŒ=+�ðãXµèJ(F—s5ËTfè ¯[ÈܤÎG�щ±à³wóËØ�­ qÏ8\,óMÙY2lƒ\¿\}Z©a8•ªœ½§³vâeÆ¡‚#HÉÕŠ¬PH¨ÝZ¾QHŸú,ÿ8¿fÌ_YcÌžRÝ÷üП<ÁeF8T;Üw*ôØbÁsÃ<›%,rhû':�| +„ õ’|†à3ãõ¸Ä‰ü[1ÛÔnñ%£éÔ9[”βpLWæ�‘¯ºRæÌ)öù›1eåvn†+�{~Ò»}ÏVη&šh¬3XH·BŠ4Fë4ˆe7óÿÌQ·?¸P›ÇêÝw?R{\Ž‘‚€Rš¿8¹ÎTâJyt‘UDÒgBn¥¨½SþD/Z¼<|ª7P1· @)䦸ä ÖÁ-NÁDv2µ³›;÷�eTœäÔ¹¶Ôßß‚‚ƒÑAZèž@ô鹟·R3)*cbo+*b Y¦ÉH˜ìJY`¾\R]9ó'Нtt;–Œ«¸G&aiºÔ �ézùã§±`{ �“ÍÀÇØXÅ=Å2.º@ÜO—ܰþìÅ%ü\z×å2î’˜^-¤V>×*âàˆâ0)âÃJŸ¬èF¯)¬„ŒŸŽ´¢¾?´A/œð`#<„0ùâÏi³Ãk!ÜXvQ'Dü xHðO +ËÍé ¨’@‰Ї՟¹ð]åŸD}Ü�뼩g~º¿1ƒ¿±kâÌDcãVƒR])ÿ{nŒîðàš*êLiW0²Í€ƒÇGvF∠DBr_‡«MÌØÉUJ+‘Plwd¾)ø†ö'C=ú=nÚ*ÙŒA(¬©Ó³ØiÝÿ™ì©6f=ÌÅ¥²/UùææÚuOÍ}2A Ÿ 謮å6ÂH<,(Âa¢/ÉéjD´ÿ¨Y4ÿò¾†ÁþôªÂlJ××Å– Ltñvÿ‹%æ?_ôR„?Téh:ªÀ1¼¤ŠRt*å0û´í�.ú” ¿|ŠBÔ?Ik'¶V ÚWom�³w9«n’/,__W”vY| ê]Vm•sˆ’­×~}jƒ^�¬·ß~³ Ì÷-Ln}£û·�Q¯ð�¢Uº&ñL6C„áŽàÁd­¦‹ÂŠ‹K}ÞrÉoT«�€fþ$¡ß0b{[¯�õ„HÓq½- hSPŒŠ£Uqýãñ,]v.ª�kÁ´Å ·PÞ‚((£ßµ¨õÏ'1Ðkvâ'eaPªSæ©ÞIƒ^]„òÿ§›¡‰oTPúvû•˜¤nQ• �òSØ0 æ¤õ_ÂOP^ +²¨2AÇ­Êpwæ«-Ÿ)V6 ‘¤ùN“Ôn©öPË- ø¼¢n./iâwª„µrX0ðñŽåÃe‰¿æËTj@KS ‘n!ØwQC/éŒñΞ(õãhð½ê³ˆÔÊž>ˆ§W§UÃò”Z«õ|Î~t ª +ô£V¡‰ýù�‡÷wÂ0Yáݧ{�ýÜ£q¹rÝœ,g6oæ½<ÀÌŒÞí‹¢c¼� t°-¿Ùò]SšNkç‘ÑR×, F¢Ö¯ýXã…å/çþSLàµ`Þ”ÊûEï¤s�üÇÉÄ7Y3 æËùË$hz˾¤™ü[cOw‚N âÅ•€âG°¸.^Á˜18åT œ-ИÒÀБ¡Ì¶ZÝ=‚?ï¬f}ÕÌof°wß(Ѻ‰h£I‰Dô½é—È´Í$¦Éòž¬á`ÅÕöŽtä Ó»&¾‘$¾ÇB)¤ÿ¶ ý6®·V(õÓÓ7 ^lsIÑbíó¨éX¶åâ.ypwµ0" �”km—¤ŽÖtËq–AÀŽ�ÁÒˆWVq† G8| ;-ç¹é (¹±Üí<8J�~H�’%« ;Óñ�;ùÛм#!¥—r÷ bî®Cî®ÿ�9‰E«QnÎW·ðQ\/írË;y­áŒ¼4Fæ™t +¯ q/Ï%{žÊÉ€×]Ø)Bä BˆùÎM‘âî!iœûñÎíñƒTU ¸¥˜ñJmæ™pÂü3k¨#%,÷·�9£u6ñ ÜþîqQO]ßí{T¬R†QÆ?»Vj]¿Bˆ"L{}”zÝè—‘þ‡¾P΢nzûo$g$3æ×�XjrnðãmnV™d0º4+Ui»£4]~|ú¬ †Y†êî¡KþßTðæÂ�ŒF6ðµ×žw(�Íü…Á¦ú�ðÆÏU!Ìsn½ù>Vù¥cTHhTÿFä%¾#d=6¤Nù³RíuÖ¯ÃçYþTßÑ>Æþ‘p³1}û¹ÒfÆ:0T~ê\—#Ûu…Ý_¸)fßîE‡*HŠÃ�2Ë`_Ǥ«÷ñ`QíW)7zºip†ÐÌ ;蘙àz `ñ�\!¬•«Ãá4OÚ­Jõf�‚‚7By�(=æÌ@!5]%J5Žà‰]Ó × +�Z¯Qc€µ),ûÍr7.¿µzWól…w®„Â|,±£dv +¤Ã±Q�é>réŽ[àh�“¨ a GàþFyD)¸gÜ0.ý@x6¬Â- Ó?:šåyÚ‡áÚˆYŒ¢V0Â<½¢ÜŽg¡3E8\�jÁiAѰÅ�Ø&ð&"7ÜÑ"øÃß“†wÄ�%Ö}Ä-jô".ý�{ó±@ÿIÿúO ;jûãPÜ>Ñ]Û\æeœø3°lì¯Ç6ÓÉ5ýÅÍ•ñµ êŸïË69V0½X#*šÄŒ@IÈ$)ÖɈ’Dä†h4É€œ÷°ÍËÏ�jˆã@Ù𫥃†ã?ãÊeXçûbÎÅÑAµéˆ ñÔ"—Å ÑTq—SV¼4as{�’0O�\B]¤Úëœo…×ï(}©Ó?»±‘àÞ!èÛéyµ­Ï±?R��"DR ÃÇHL““ ¹0nt€Ll´>e%*áJIuÝóe ØÊ„át+vônÁâ{ÛÊOm8ÑÙŽ4kmŽ=•ñÅõJ·9¸@éI%õ¡Ýö\«í»h}µ³Cö4ØÈ•féNv['¿'³óý—Ç&+´.]eÏI¨¤KAzP®óÂ4ÉØ�§QB]@+Ÿ†3Ïbœ¾Úl»R4-��̘({»´à´? Þ¨¦¸oY®¢í%q´šz�Ë^…dëbŒm¦Ípœ�ÔZ?:Áü¿w°fd{`Íøà]òÚü�-‚b>—U\‚•‘ ë y©Íö ·OéNeÓ{aåhÇâ!åàObR®‰ú^zML`aa¹<¤cRªãb:‡9">,€Ü +Ö:cWëgYqwó/YDæ8g?Ì~3Ð&!dŸè,*,É´¸�a„Ï™Œ„rNP8µ˜€€6Ö%sE§ñû2¸õÐH­†ËìÙæ�ïú.�߯“&[²ç‹-�f*º©'}ZÌDŸÐF0JRBºCñ�þÄ-¢\JF5’“&r*|o‡r� N[6  —&–?�\…€3v"·F"&,u¹+u”Ìô.�JIPHðßÙB÷¢•”z¼FànáéO,,©�G�…z�KL{ · +̆Q` X>ƒ†¾åŒ<#<‡�2¥$e’Z’ü%¼Ä ó¯áVŽIgBžB¸E?%R3T5Ó*šaÍD†ÅÏkQ¡ê” J<Α‡!‚J¢ô8W�"©Žƒ”ƒTɘ‚ÔDXŒlçÅK""F¢ $AhaªF^ŒM̨ZõB+´B«5¡55¥¬¥}ÛC7Y™\¬¢õàå�6™MgJ¡6©ÖLË8å¢O„ÚZ#—³ƒ*ë©JúùÜE HäDipjËä㜂ƒe—q“úb¡b½¾Æ2\²¢©@²¸ÄçávÓP•Õˆ¤CŸNØÔH„À¡P(Ä@bF&^ ”%E()]((%ž0ÂGiT L…“„$TŒñrRpHS!B„B‘Äì%(8‚!!‚ž2Er¬j1øã+½…`p"CydB»!è¿8D¬!^’ á á1‡1X‚ÙP¡‹EŠÄQ(¶(9!$„&3p6‘@•‡ÍD‘« +�šš2LcÚ0åç0�†Ã4mݶ�°„ÖB†VZ†(͇†­+°ìP`Ë–l`k>ó…N`;¶mÛ +­kÒ‚K©(3P.3ö6ìbܳqˆñx +1›� ^ïRa>SÔbE Ô¶s•ʆª˜&Œ{‹ÔËaŠÊÃë{…Õ˜¨ÊRpÕ«LR“À +¯‘äU�¢b",&4T�˜‹T˜*™†"ÆCU…ÌîÆ&<Æ1Ö¤þ®7…óaÕƒP0­Jêae) +«UI*¬V3X•¸B)Ôç3a ›B½hBýêžõõ‰†?ÈÅ í8aZ2HËRâлYpW¹ÉL� 2‹ CfEŽÌMh…þ@súB ë—PìÖa(†â°ÃÑx�EƒJs ©W”ÐŒ^W¨ 9•8—�»Ï;M3/Ï´BSñ÷‘  z\§ixêA4 v$– +uh©¹“øèŸ…ÑZÝ4¬©q‹[†xq�ßEJ£¡[´MÕ>ùi‚UDÒ» Q‡ÈAšjpå7¤cº£)§Ì8¬ñ¦KÖ2�=Êí��Þ>S'i”©¨³ãÑ�‰[¦†ŒÊ”Ozˆƒ$”œ„ É·‘ÃXIé$,"ÙðE#QR j”‡%FØÐrWC“+¡¸F³›µ)ò§�£˜ÈHS¡PI}ÈZ½H,ų́¤¢ÃïR\£ œ"Y¢‘èÓK ! £B!rL^8DžC +å2$GæÄò�ñoJWp²ŠåU½V²îòè>¿pšdnè'?ò?zoŠId&•)m™P„=scA’ƒ¸Îh2~\2ÖItMgá³?”Ìœ˜qIX®„çâi•5\ëhrê«eªÝŠ,&ªŸ˜ªœâª¶æ)^#*.ÆØÄÑêêžÙÑ\¡·VxÚêf¦A›ùTeF¦­UWÔMJš×ÍÃïb¤¼OÖ’é•ɧ¡¡�­øBÌÝáŽG,2³…‚Ì$.–™Xf%ô¬ž‰pу²7ÂÈÆ�-2š-² +"Ñ$™?2$® œÌ$ÝBd×c&»(¾0›ìš·0ÙÇÓü¢àlßÛ¢Z‘£¸ ¢Ò”uM.Ÿ’«�+jA¶hÜ*µZåDó0:Ó’OLÐ…T’³b:Z…PÌ'­EâºìØ«GÁ]ù‚´Ó.$&št{ê[§%ò˜Hm}‘DEÇr¾F´Ø™D‹*ÑEKßô[�7}¼ +ÍY!õqЭÑ«a* +M9K*üOуƒôxìüÄd +“‚•ÒtE&N Q,44þ�…!šNIÁã�›ñ¦LÎÙ—Sê�AŽ.2äg¦F¡™@£Ç +OÍcˆ(ú˜ +r<‹®©ß¥’L±ñpÜø!MÍX2tiª>kè+k¤¬%±†°E£˜p­†%šÈÈTdfÎN‡Ã¡xŸ÷CCÃ55Ó1ð¶ã!^D qy_6ÍGBì…dJJªQ$Ñz…Ww¹¤¤‚”‡ˆj 5J%¡z/íɾÊBaÄõ“ãu"ªšÚjµ ++FÊÆY�Ú¶S¶2�™±lyi,D0„<8|„F™ $N)"Á‚ÐÄÁ<E TmDíÃg%¡o(™¡ì ƒi¢Ye‘2a¤J,D±PRU"$$U231BlèÈ©TŠ*Q»T<™a´Â\šçiô¡(LË<ÏÏ­Nàß�Ýw­z&„Ãá0'#¡¦h<3%1A„""""B1D¢b‘)‡’˜‰Å«7œ¦åLs¦išg–Vщ¢�hg ²ý’ �qQ}Î׫6“#÷f©ŠP›ãEU÷E•äEE­Â· Qì®�ýñÑ,N!sÈŠjÁÒÌ÷¦OeªAqÇn�Eñ¢K˜ÈT¤0]lÓ9LUÁNMÜY}Qc{¸/ëåpËF9\âpË„v®õ .È‘b»À®è"w×d'Žˆ¡düŒ¸ûuýï„ÊF~_‰É§QjSóGßÔˆdLäíX‰¸³RM¤²b…¤kFœÙ�ŒöíÑ®¶°^}\²áX¾PBúÆã<”#q†\Ñ\ç*6)ܼ‹.‹ž"Ti>ájè '¡h†xœ¡x,0P@®y8šNMYª­UІj.ƒ;Ô£ù%ÐYU…ŽÈ›@fˆ! 1ž<‘Ã� +þÊÛó÷¢‚̳'®f®à)£,Q«ðÃ'ROhÒ:�„`æ2X`@�‡ øX+jr„û%fxÑT�„/ \ˆQ­Tµ¨Í´ŒÕ©šQˆÈ®À/ƒ à�  �‚ L 1 ‚À�D  (Ͷr„Ÿ>ž±IØÚs‰ƒ8¦ü8½hV3u)·©ŠÅ™=4úªj.3ènQ£ñòx‡D£§cHkg#"$Êl™Ë43c ƒí«&£S¡bˆ•TÉòÂ) 8ªXC{á—c!ƒ1,Ï`Œ…Raaeш�Å‚%ô~H^¼²¨GÌ,>ÅàÇQÉŒ4÷rG?ReŸEk«é¤›µVÖD*œÅk©‰àX¯˜¢™Œ¾ŒÄ׊Ú|ý=ZÔ„SÚMà µÄ¡Ö©§¶.£šµoˆ†.ܘÄ{^C'ÍïSº…Ë]D&“É ²_5TŒ¡e³’¯EyrÆ�é¦.s”(*7DŸ8?ç$såPÅïþíß·÷ ?kWSõ|�ŽùõÃo�¬ÊãýOGBãÈ(Œá$fÂ3ôÄ©š’ö/qyæˆñ+1#âoõ\�Yö:ì͆åvøus|âh*ë/É$•Ÿ�I®sÆB®��\V¬HÅø¶‡D.b˜ r>ãñkœö�[Œ)¶•0Ê|ç³Ó¦JÇËrͳ�ÒÍãZ;‘‡l”ä!‰Jò:q,1“©ág¡‹¦t‘áF"‹È�¤!gâðÔ‰~üUFg:Ãuj‚ 5ÎÔhUGÆëªÚ¦Á¨.Ì83²Óéø¸ M°Šjˆî}$EaÙ¬${]õª‡ÄžQê1%fE[Ѩ˜š§¢j"Q>ZT• OŸ!‰sF+ÍRÔˆ‹Mmj%‹š\«Ô<1�R5Ó­ +Ê&¬3ʨkäsÎÜ•Ùþ{»—73 Y_Ñ“?ŸS¶}"ª5j$ô­µ/'IKhõGeÇåcܙ٥Q}ÿ…êK˜.Ÿ¸ôǬšØ_g­4Þ+Äd�Ù�fEçã¶7b’JðHÊ+Clé™_Ž#<3¬q¬÷A2™‘—<¡í-o„hÝÖUB t©ê°‹G�d!$S‹?ŠN-L-¤4âІ$®ìª™—Ðk‰ÆâR q�ñìžlÛ®˜ø&¨â¹!¦kˆ:!®H©«t¤cÑ3‘ŸddÖYÑÈ*žñSqÑ+%uÆéŒ‹3açk›bYK4�O]¢:²…�×~vé"dñ²¸�¡1U'E¶6ãò+¾Í8‹sNEc’�˜5A¬aÔ˜Nã’‘:¬ÊEJ ¢ˆqïbë?�pQ‹TöDÑQŠöS}f¯3w\žY‚ã�"^ÑR¤´5®xUV›&Iêþ·ž„Ä.Ö/šâ’»ˆ´ØÐ$îgß.«Îƒt7Dgð±¥²Uêõ‰-VcÉCÎeî¸yÝVÛ¼ÅPDdÅP켉ÉPVŸÍ+ ©�Õ&ä[Þ½èWËa†êÚ¨W+‘T­ŽF/Z¸ºN4<O‹‘zôO„:žKþ–ÛÚ°U¯fâv´ë„Héjí4Ó¤bÞ6s +3mÑ/Ó¿Ê´þb³ —.AB +U8$C‘Gž ÒÄzžQb.ì”iqÌŒ,©–Plœ-qYdˆ‘›<…�8@(XÅSAí0¨j�� ÿÄ "ùé€` +ø(^ôi ¡í-n#¢y8p‘pÛé3š¢ J+¢·F‰êè$R2iI>5 —Óžå¡£Î3’•ŒFŸ;ÄhÁ ª@ª +vÐ#øÁ®9èE�F5è�M…È6ƒÄ‚‰„èT#AdP`É Òɳ ¢K!BH´5ˆŠ ¨ª²ªT®ª¢â/ƒRÉ뻄º\Ÿ¾ÁU—A•„ÖKSSSõ*)–YŽjQG%3�ˆB©ôpZpPè¦*PÚFÍØá¨u$T ¥Ód­�\îðɧ¿ÁŸC}²fE�û…¾ÁÓ~¤<ïÐ×ë!þ8 1.sÄ+”IZÃPÁågzK1d\îûn{‚Z·ó‚;2¶+&�ƒ*ŽO0ý®±Çƒ]Z»uwr(z…j«üP V(µk û +mtêJƒçPZʵnÅ -Uë¿m;.‡°‚´ÔÖîÍ %ŒÖÂöl¥CëÏF`ß–e×OæF+ZÜ*ÔT¨‡¶Æ•šÆGË9…Òt¥‰Ò“ÔSkÁOé¡G”½J]®03C�(†ÊX"¸Df†A†CžÞЃCL1b³6ê¡æP²à’a( �¼;�˜„Ä$ž@b¢Ðh $þ4˜„þ²$&7‚,ËÜp(…9 iWŠA$ü@_L¡ø_)Ê#Ÿ`ñæ…ì !Uƒe:U@ç´™¥ø òX|�çiˆ DD¤@,±È +4êLQB¾UR +‘Q(„üatÍ<: ”S ÉŠ< Ró}<ˆ|<#J!úa +æR”1T¤*¯à1 …‰V¤Â¿2ÕÊeàF m¦£ÉMCÁò +–sŠ–¹L—ÁÈe0†‹ð¡DbbëŒ33.gÄ<'3S‡™ágún&LQ˜ðvVvÞñpc‡{~÷ ‹Ù¿-ñ@ÀÿCásËLs’)jZxg$Ôˆ ;5œ¸ ‚BTÂÂg:¬ž —{0°�½;�Ò¿&X‰ +Ž™KÉÔ 7H/µ0�Žÿ í{TÚ¹îé÷dê„ê[ /|ÓÉóePaŸ©Š¨ñe Q†1ÕHŸAÆ¡êË€"¸däû2bòep›ûE�ˤõ?P„þ3ã·Á¿½ „Ë9Ђ¨A2ùжS] ™@:�ø"údúD F ? ]-/ÐéUJ0‘ I*\êy0�NE +.iT„?L—Áe°y‘Ñ#B‘«è­¯èžÉ*<Š(]Q¯ºÃ„3~sQxY üL©~¤ ÕqŠ,aª&LÉepSXWp}EÁ£Õ$|Uÿê¯;üÅ +'ª²$4*H4‚hA 4"΀°Àй¯à˜ ôcê‹À©—ЦnføãE^B2�èðpy,Pœ&4M th¹hb…ZBCtk­_};„H¦�"�ú†Vê¹@�†~b„üQAë€W+T2¡wCÏ +íÏÿ4¡ÿØCga`ï*/� ½L«€P”(Q 4"µ~Q…o@£…Ž:ü¥I9ÔL +ÕÂaäßÂHYˆ.ƒ(*'S(É–U¨ÔÜèìµ³Š¨Ô4ØÛ:£p@ …C‚Á¨tû€›x8žF‹CBA(Ó*²"€èT¯'¸)$õˆ£Žžà!Ç]Õ\L\Ì›ds)J`EÁ–‰†ëQâ^±õáb¢ç¡»Þ!Ñã!épüÆ­€×?ØC̨4“¥‹&xJšT#Âû8õÞ‡mÒÕî Â–ØÞ*poPŽr6†€wÇ 1ÿôn·!ã;ÄúÙ~|ìÚiîéL¶Läù)ûŠ‘–[.ÒÀôbS/9³ó ä½ƒ,!kaeÄ@°rÐ KŸÑ61L•JáŠx±PÞp„[çÞb±äw“…kÈÛMèyu�5�#��Ñßt' so$fÙm(¥A„Ïáô–æ sÇF‘ŽÄ„¨ž'§Û\˜M?h&¶.hnzOÁÙüØP�!Æaæ¨?^ “Þdñ­6{ƒEgn’ƒ·I}þòçšÓ�d!õ=w�cŠOõ–­pz2Ii›{ƒæ#�-¯*h¡v‚ë”�߬âd�¼Rç×ÁJbļƒÃÇèîNס‡ÐI6C§w�ÉCåæ8ƒ\G>¹A8Â~e˜…"ŽpÌç§­b+ݯÆÇA¶G’P9'Z-°'ò_LLš+P›d‰÷%AeĉJòð™ —ò»n/˜‘l†D§€Å«ÎÚ§�f©§X8YÉ56Ð;pB²Áx~£s¸›¼#tíÛ\)ÉËÐ_HÅFi6"0¦”\D)Q‘xê•ð›^Úãwy1,@§œÞ¼í²H8êñqcYlI˜ìÊ~åŒ#è‡ °%|Ë)*’'†šŸI쭗󨿑ÑGj:^TÁÑŽ—Á†Ëo/B…KÉ€-�ø7‚¥§šÉkÇeE ¹˜b3¢åŠ›G*¸ˆxµ´·u�> €Û0ùŒÊ ¢×Ýùφg_¯c)òN =!ÄÕ‡rå^¤ =’ª•ØUv®™/Ôìp˜£Šƒä&æN³ÌP˜m ÚZ>—|+ˆ(“§Ÿ KL­Ïf8QFK$íóuueŠåâ¸?")ô;ÅKl+aoŒ‰Ljûª^€û|¿lt’ 4ÍK}lº8ͦ »ð§õ º1ËüË2ͦ˜4„±äk¦`¸Ÿº÷³Ñü.Þ ¤5À#qÓs“£ª{4s±Î¼íÐèœn\>aòh—˜™»õÊ�Ì!1$²ñ\¯wwQpþóUŽ™,Fu§rƒÒZ¸Çȶ¹0!µ©›¾1+«›e4ýÍM¥Ø‚…¥ŽíHx+ìÝ2ÙY}”H¤Fu|ÉON¤›/úÒ©`µ“ê£îšGÆúá…{r�ç cm·<8ƒXt@p²!Œ«’²ªÇdJw±ñ/TÌ–�C«z‚£STÆ[ôø|a�LÞšÈ Š²ŒWðE?ävŒáÉÉ̓(ø@ç¤IHüÆûРÅÍöuI!ä‰# T‰8;‚¤.ç'Q¹¯¼14òó6L žÐ’%@?µès˜,žÜ¾±)NÊHƒ�Š}Zö)úÏ0Ó Zi'²ÓĤ™¬óG`X›¨ÆOª>LªÄ³4éO @?D‰6òsƒ‹B %:ô£ÿ”cÇ¢ñ&§¡Š5„ ó%†·½†K¿1°3x¦å×rÓÛpNÈb§HÍé�ÌVÉ(jLViOÑUèϼ.‹Tštcùø*Aõ:iDãŠm5Ã!8�³�$T©hn�*?MJQÄšÎiê±t…åÖ|¤�Ëd±�à ½4-˜#¤›>ÂÞh^ˆjà’õ§+#ÝõVua_Ý�à}ñÞlx×eõA²rÄU]HT­Kvìhò›�:}ƒV3ÉRýœmc GŠfÖy·MûãCl[ÓS m€»{Ù•„ØQÆ�Ó€5º +nx!…F¡V•ã�Īã"Ü­¥"ʺðá´þšrÞÁSE+—R‰_çñ�Þ±N¶´‰Ì} ½Á¥8…43,ùi2—Þ"9�êm÷¤©bI™wœª@}^šº68C ‡¨c*޲�ð4ë Ò>Üä=±ãºÁëÓÆùNüýp‹å©…›ª+º4ÃÃv=ÈÁëDaÉjX 5 +`ì¢f—1œ3Y7»p†]— 2ÍaþâE뽊æÅ‡Nä ôD€Ò¼!efˆVÒ®œ¾‰±‘‡ö''—vJ1I[ƒÄÔé:®¡1¨ÈïzôÇkû•®7d´5OöÍ:€¢ ª2MZw?Aq¡Ÿûš4ñmfÎn$&|gÏÿÖpf’²O­µ×�á‘…öCÒ°0pÙ4ÌZ&NzjŽùäBÂ|Ô$àL†ðcÜò ¼À Øðý5f€«Ä+Â2À}×è†ÙG`p6àÍø +Ç"œˆdb¦»|¥g27¾Ê\{F”ÜÊ„‘’ƒî~]�›H¹ð2F›úI±ƒ9JG‚¶ú�Ch D×t4üš¹Ä_êÀUf¹ôTn±ÓøIÁ zøúË]E›TÇ乡½ LÊ’ùÍK%,‰…,e—¨„"•AGŒ‰Ôí‚ÎÙ=Ìûp¦B*ßgL"šµŽ4±'Ó0ÚMàÉ/ÍLÛ2¡1,÷ï̧ÿ‚þ.Ó�ïCÜŠt™qkýŽ!ê~ѹƒš,ETsÿüHd]ܳ#ÀÑŒÑÄÍ:‡½”I@ãa¤ ]¥U‡HõÓà}¼¦/dVfP¢luÒ9Ò/ÞêOÛ0ZZäÆ[@“`�JÆ^eEÆzÐ_DuŒxØ=Þ¿f†¥8Zº�³†t]*�¼êNZ°ýèb  µTµ0¨n¯Y1¹bÄ…w9P2–Âw×Nç¸òŠí=Mˆ·"«ùœ¢R#ìƒ]ÝîÇ9×ç|bƒ�Qq\otô�dãØԻϢÙpZáB8ƈ—M®D� +ó‰È Ï]&�¾f&FA³¶ßy*olWb.� 7Ò'Ïœ­!Å- j(êŒÄ{¹eCD8"ãòôÎ2+}¥dìì‚K +7uÊ[£N‰‹$îð¤Íîð„Ø|*î^uÐ=&”ë‡ÑEò¦YÅšjn÷�ð²5ïGÚóŠ4Á 54DZžpdáôåc×'ùÃò\ˆ® 0 À²gŒ&’(/K,£‘3IùwŒ‡Ø© “³©”+A%ÖÉ_jÆJÁz 󾜬ÞoP/�r÷Ÿ ¸•7]+Þ{ð�+“f]«Ÿ+vÕO^" ?åqÈÚ'‡KŸ€fÄÞö�"^ÛÒó¥iHþÆP&Œlˆ½h~1>ðIÞ5·C¤Ûå&ÍÜç0ƒÑœð¦¦j*z•rÀè[êt53ŽTÍ$)ÚÛôøó~Ün‡X$³ 5ÓyÓóÞîÊrþÆxtª<ÄkÝì‘EjHÙ\Àgˆ=+����p•â$©ÅTÏ•X™Ò|7H›¢§¨t¯ /??0°„êÝ4éÕPÜSGZy§A„¢ÔRT�`«ö ‚$…·ät|úq¼`­‰Kïqÿ9†ƒè6¿ [%Z¨Û®ÏÈH]”¾1_IœӀ˜ œÝ—kà¿…íI;^ç·Ù¼ÍW¢ËB†ÏiáF3*;ª–­fbÁ!:hÓM7”ȼt\6ÈÇ‘,Lo< +v:`€zO€ŸÃ˜�ì“@�Žç¡ï�ЛTDЃ½¶�°þ½&ðV©›7;tܧ!nCª4Häyެ¼«às<á^áÐShplUÛWÌ0ÈÔ34-Ô�sY­Ð ›¸ÈHB·Ÿ—‡Eõ Èúò§.ú 'QeûÃöp�ðª:¶xHxî¶¼Å:´ÆBÃ7¹æµNa+Õ¾øÇ%™c"JðâFH`…o³m†¸Y+[ºêÚ˜$>.HS‘ÂÂl'7ñ\ïî3% +@üÉ{eµð0ÈÚç³ +¸…eÀéƒu'DtYŒ‹IFÅSk#öAþ–è¦��^�4# Û©TÝlY˜…èÆË@9e#¦í‡F?°^¶êk“¾¹0W×k…ðhd¼W7™¶•¤Å‘Èô¸M“ím+‚„jå'¶dÔ«‹SùŒélŽcdÀÔPÑ�’s1> b†Ìߤø΀ º㈧õ—yÝð›�vn†:¬;c�'8\hDD£œ,åþ¯aÜ7óƒ¾0DÈäpü“÷%|Ó;¶<­ âì²ÔÄvéÓ*ÏO�8}:Nàj‡È#o%Ô£À‡!´Á’fcB¼<þjz÷×bážšìI×£„|—•š.Æ­Ò°ÙŠ´€V„Õe@º¨¦|ÈU÷ Æ8zq+?´Ü~GëÊH97¡aÑÕN˜ÐD`É 6â|ƒÃž/@U^Vvy}Í!è4H>Ù³Nó=UÑ‚h&)wM`œj‘ìZ½¦â0ÌÀ ?írîôõÚ5"ÉËåõŒÞf"Û2µ5ãJ¦ß]6Ù"ij¶˜M«yã“ÐýcL!DtíÅqS„æfﯘ¾?±f•¼«WXªúWteŒÂ®{’# dÖ{ÝK¤WÌ1µø4f…MŒuÜPyE¨"†NçâwKУ=ÃùÇ –è�:{âU©IžÌ8™l/ +°H…m(�Øþ]éz³–åj“‘2ÓŠÿQ4Ÿ&ßÓçkü†‚_a¼aV· =©cè¸lU‘ãöѶÛPµ¸†‚q,(èÑÔF²7Ø âgê;�+3-PyGÁß”ï:5§óOêâ‚&¦!·š‚r#–eØ¡Àˆ²ðH¸y¹ õM)À�ð{ÁªKÉ2o “÷Ñ㪓욣•ÀÆç=4—mF\×}ÀQ)•V�ýÌ¡&lYÌýºßW}Ãz�_.0aaAù¡`ꮨª0~fnã�”�X„±”»–?­ð1‘ɼÙÞ/`ÝÞÑF}ÙÙ:ì!ü +Õâ%·-Ó>:{Ëh0µ$‚‚Þa�¡1;£68eÅãÌ�¹¾y�fŠØàVJ¥÷8N§AÉbi<$â$xÈw£½l¾>�d ¥ãˆ’‘Û&èq|IÏï"m6Ô^äç‚>¥iJ±—ýíø%@ç¿24œŸV8îÀl|E”ª£<á"îCüåRAQby5²LTWú¼å�ƒ÷GŒut¹Ä2æ|Ø›ÞÈvÇQ(½&!gï3Ì�¹«”5Jβ»Ùý­¼’w+© s ‡.¥¿Û¬1ºž.�Þ±!Âv‡¢ú!ÂûøÎ�š;Tø|F �›©xùmi¼Q€É×fܽ±QYÉŽ=&‚™<ÏØ +Ieù„}öO‰,@Ç ±3 I+ >¿†�Ñe—|; +¸®ðº¶Üì„×Õød*&ÂʺǸ(Æ“LPÑ®”£ê/¨6ՉDZ9ݸB¶ÇFá.dp3~N”ñA¥)’ç°’o¦e{ªH÷ÒŠË BKœ”•h0¨+ #ãG�¤•™}dоj�­_�pL»ä×°G¥p¿çi»ŒÌ´Ð�h)¬­>W©9mnV—Íö õû…^®¢ÜI¡‚æ0=þ8{9;šb‚nZ_È êNÌyƒyüIëÔ®}¾€#Š&Ö •ûãÁøÑé"_ç‰Z%Þšß“rn(}×Ôb€GÍ¡Bü¾«1´‡šÐ$ ˜£=rÓÕêÿŸÞ6zr¨Ý=ÍVžäbo’EþèAk(˜©œ +Ò4WÖæ""ùq{„Šš~œ£Ë÷B$åLXð, þàáºâ<—Ú…ó2¢BÔë3¦n1˜úŠAûý$ù•/sƒî ©w¹d—»zXC)Nïžð7¢Dö}P�Hû‚0Äã¹›«Á +ñÍåÚ¤P>¸Ç™¼ï¥@#ŦÉ\˜°%Xë±ÌÙ1,½­(%‚kt-,y0‘—\ó’ȈZ´Î—*VÂ/ž¼ÝÔRí�Z%Åë}×~Þû]±![çl'DD&Örü oæµ�©¯ØÈGæ,ÞÌZT”2ãð O�tS4Ê™9’‰Ý×ýÁfÃFk¦Y+ê¸ ÄP™¸¹:Z¡mÉ5¡NM½ìð… -ÓÄz "R˜÷�Þ¥¯n)= –ð쮈ƒ„?„Í{æ©f„K%–CK/áÏÊöÂHÐÙÚ 0G`A{ÜïÏTy®ýT§D˜Ú~"0Ùu÷zî|bï1¦–³;òº­Ž³ Œ#»›zûâE °”É A'äú�Ú¡h–ÌéJÒ©$Ř[ИYAøy¾IS +ùý ,±t8äøËJxTs’êÞ¾yI\ÇA1ö_÷òeÍ]-µ•Ðéùk]Ƭ´sL¯Þ_÷fÛÀÎDaVë6ÇÜe“.!KBÚ%²9«?a›o5A4ÌòÀ*û¦ãiíè*øßó¡÷©Ãùä#bu6(Å»øÝJààea0�ã¥eI \pî\”¦�ò´’÷´Çø$˜ã‰9Š –Ÿs§fÇjf7�•G:$$•ÑêKË}£z<ÕÆAH§~‡¾æµ,¡‹ÏHˆO)ø½èÎ<ž˜·àPê˜�jÈú�š�9Ü*€}JD¥ZéGìpY › æŠo¤ƒJëЛ`Áà¢ÕS^+•¹Åÿ«‘Ø8;Ù»kEç<¿R”Iµ7ÃçxÙË“vD3¼T«»@¹£þ‘ ú°ÿiB’i‹'vÔ·ÔK^:K A SŒ�(] ûϬêáŠ2pdj9ÉFÇqA&l®å‹lH=Ì¢ê‰aY~²Î·åü( ÈÎA�Î9Ì&�Ü6ê3ˆ.UX€/„Ç”“ã�8mAÈó.öhz٦Ƽ$0P¤Z ÓñÞú1ëÑ>³.^˜Ð­åA…à^1Qå|2äT‡6¨¿@α+¿èp¢ó • ñFÊóÞT„C�&%¬Iœmîê?µ[~Ã"{E¸ô<ªCþ�"†9³BPù'ùa‹ãɼš–™n"¤èè» +=ýwÈ!ŽWr)¼. šøW»ªÚù¶AO‹ÇãhMFù¿e^VOAÃÃŽ\ÁÒAF½ªÍ•ص|ÉÕƒ*ýFÉ.’'t—³©Õ4 U+éCÃß×# h~ %t¨·k¯ÏÒ$½³몒œ‹w�<*t…�™0vnÊ0 ++ß8´ƒ×?MWqBæiQI*ïü-]£ ×Y9¶ùÿƒœîÚ¸rœà ++šƒRûi¥ôa}�Ƀ4é.BÅ®ÅÂã$}Ö/]{læVUlÔâ¡ÎüL�jd-9 +4¾Lç0ÓP"šiï“°Ž^í è¶¶tI'­¨ñ ‡†a38ÓI¶Zâ ¤:ŸË\mÅæ:j]²{æ¡Jau“�W�–„’°æ«S&L‚t”ùµ�Ò¥+–¡†öP:Å8›íf¡d°„'(›ƒ`»æ§cíN…Z?p†ÐâÂN-�ü>´8’$t¥©ß£-‘Ê*.Jl4ÿ×½Y.ðNÖÖ›æÒr¡Z"¢‘]6í.çÿWcŽqíûæî�£ŽÐSBKÈ�j*?ÜXoÜ¥›O)�€ÄwÎMa×Ì ŒE•ƒŸú÷ÈtXã÷O×[ƒBaÊÒx«kšœ½ÊöºYîè-Æ�”Üã•ß¡—K8=Õ\`Íü_;Ãúô+¿„º–Ž›’7qškÿxá‹sÙIožÛ´<×/ØIεùŒÿ€N=%2Ù6&슩×<â´ƒa7V⎠3ò­o9-a@…Áèɪ°¬–ñ@Ý<¶Àj‡ã�Æxä£K§ iG®Éœ†î§Bef>;€BÓê¥É§²5¥îOCž Š“à‚ëà%!IXc\vÆ7Áy*(´3ÁzŒ shˆv‹·S†A=~qÌ#/B(äEOÚ9ø¤¦±l�¾ýv÷8Å7=´ñd¨"?ÿjÄD#YN¤²]Ÿr"`ã¹’Oà›TÁ¹6 +áfµ:?ÊXcí!Tp… 2�0d9èrtmóSÌÕ Ûë^ë J-³Oxî€î8÷2æ´ü ]s�ÛÙU{…ã�›"ºh–àI)¼U{³ó·Ü²(˜S@ˆ¶a$ŠÚɬ;“Ýôu7Ïv‡¥¡ë\‚iY‘ô)ãUÛ±„«(³»ƒvFÑRaQP‚œKžÛåÁ¯X*1Y`¥È¦]¿m¡Ðéã\?¡™g×U l-ΚÕKd g÷D_ ŠÇýšŒZÀÄ0²¿ÕOˆºŽÓ‡ü3ØkÖ|Æ ê�¤íT-1"ð nçË7@Ù⪵%änÅÞÝÇíé÷ò]+}ò#-�»%aµ­Ü“¸>_æ¹MñIRÜO d æö÷I--SÀoËÝQ81ëò ßLõ6¿‡*fpÉÜØ¨‚f“E{¶F–cŠŽulµ§jê|�@íãØž¹®ÂÕ}ä”ò„�7�ZìXdœ“ìúVa¢)Ù¶®ÄWаªR[9}‰K±cUT%²3Æ¡ÝyK8;T¾%�D9Ìr¯‹öHþò�ŒÆÿAqä]ê^‰/™vÂ�2ÔÞNå´°ß9`“ !Ï3D¢3Â}?�¹·;œ+U¤ÎâsÞ¶-â�ñ—ah¯…ÓÕkiìžÜ]=X‡ZáuabVÛL[F”¸$µŠ²¶0Uºª(6)ººV3ž’?�šO,_l5F£ZKX¥ëK•&‚"èÕ-á\D䨤»¹§ç‰¶_š&VßE µØÂU‚1Î¦Š´z'Å–d)HaY*BhÎ5òµÝIFÚ.=FH©Í×(ÞîÀ }tb (%TìM?>äm‘¹„?73L)üUÒ"3x“½`ø—ª²¬‘§z{$ÂY}­/¤š Z‡ùz�‡åüÞVi|ê $Í�,Ï`¡x"¥EB¯¨D`L Åçmˆ}5;cKR=¸FÙ~DwÈÝÒ#ú�í-÷÷"-j2 LúYCeÝ 1ýŸPŸ?4¡\ ¶aáÈÅfq@ƒß¥"9³,uÆ€içÕ�<˜K{€d¡0ê^Ù‚ŽH±pïP«—¦lë_óœ¥ës +%Ž`X´øç÷¶:ËckÆ ¥nT+�a&¼S°$:ê+$»µ¶C§9ëæÜzÅçûžïzØx'ŒÊaÿ$®)ÌÀùA +³Î’Zož!ìH?µÎ éüB×ãÀò„+–â«I85° jÞK`"å$âz’-Mª&.¢Yù?t·Cç±f—•3íó#<õßKgF‚7qíÀmCÏG¡M*…ý-UÜŒ>_ÓDQT0‚ÔÕG›"¡¸íë]ìPµêònp^`¯0ª GZÔ°íY ”ý C%ѰøMº£D^“<¹p^¨¶Ô=Hûµ‹�sõ7…ù=zª©�ÓK�ìwç©u±RNOÁ¿Hy&*˜„ VÏ©±KÍÜX»|ñâ¡�=ƒ] éÞ†û¯ˆÃ3ö—Eé´æ~œ«2wÈý»Iv£ˆ®þTÇ eõGv“—À5¦�¡e•¨k•ýšn]óØrn$ÕŒj^Çbã½óçé�áSºD.�¢ä%¡˜SY™“µŠåEï%€ÓQàÔämÀ䆳†m€!ˆï z¨GJ:©ËÂ|~8H’å¼ÛFŽç)%ͨa⪌÷òý¿RïdÙ‘]ùi²Õ£}*É7nÃÅð·'ÉÅ&áê¯�Àm¼‰÷$'«^ÐM�`ÉɈHu3¿F9ÐzNJá@;ÁÞ•iD˯ +=àÛ0燺ľB¬•$Í(üz�ýPët�Ô^T$¦R2 �_.*V5gßaÞ\¿T +�RÚÝ#®-,Hù ˆ^>”C/\ÿàöRúúÿÚ4¯ÊŸ¡ÝŠø×É^ÀžÊþü? U´tjU‘'Ú{Þ+¡ÎTÏÕ•òƒÑ­Z {;4_sv±'b»¡‹95®i\KˆX»ËŸ•.Ì(ìR¹:Ld*Ëè‹ï +6¸w¡´£±à!WØ +˜�•“p1_Ìõ­Njàîõ·‘´•2…,·2H lÉ„¡‘‘Šœºyïa¥^¿B—0DJd+Nq rk§Cò6“æ +…¼K6‘ *%7¯¥õØ×Ù“抴íëØ=ê¨mpJ²HG ^Êlàß[´­ôi›6kç½å¶ïöËË c ÑáGü¥Úø‡\Ò  µ²š0ttù{Ô=Þú"Yô—ŠAaì‹¥,!ÆŠY÷Þ³V¤¸~ÉÔCY´ü?m z{öãÊbšgN:òÙ”ö�4f·1XÿTň>¥=èüŸ¸]ÜW. ԶĈùéŸ0¤]¸�S ›0bÃ^P‚MáúŠ0Ržá0q@E`k/2 Îvrí{‘;„Áò3 J0´]1HŸ:½4(Ûc@QŽ x†Pè.²Í2Xø•A‘Ì€¼Z»µœ!dÁÆ8”ÁDåFao Ï�í¶ˆUÑ•q' i[Dç4$ÁJ @rÓÕpÊèV(5‚´-‚Ÿ6À—�’ã5#ê°H°Üð¥ºD÷†w+Âp8¨VEÈ„C%2VâO×�ÒÆá½Š¾ìÊaˆµæÕø(­ÞÑ�­Š€„: Q̬ÁÁ/v€¼Í�Êr];ð º5éí;Ì>ô†‡E'‚ýx k"HÌ–'ôi«HLùì45_ðècñá|aœG‘aëÃOû÷áÏ#¢ý`gD4ã¶gþÁ:"¢@X5D\G& D#Dî£!Š ÒŸ�ÿC@iÛ‚ÛC�m@üƒ`¶C°‘¤t8 +A“C€h!à_ã1~Ü�#É;•!€@Ø�°³< iÿ ŒgýWd"dÍQ ±Î@ð`¾ÄPéˆè–é´ˆDZˆ\ IÖ‹ÈW!f®SHõuVîúª¿Hã )Ž8V>¢)¤„ª#<��à<âãs‚<Ia#€½ÑCZ„ø@GCÓ�žN™’È™˜ 哸ìƒ4‰O¤ß”ø ƒD¬×o�æ+ñZƒôÐ_3Hd\âJ Òè%N{ÍL¸² H^` #câ¦%XüO™˜ +ÒÍ™øt â*i¶#ˆk4ác!ˆ—j“ÄÕ² Ûs .Ý„Oˆt8aó5é%i,s"C�üª8p¿è#�=qû€Å'Žz~âÀÎë³M�DÝ‚"~D±Ô4@º(!q‚ÕC1I¡w¢xJ˜uzn£`¾eŽbâþƒ½ÓKP‚�B˜Çl-)håçžJ£Â¤—¢\ù#DSœÁAâ'îG­§ø_? + *.¨FÅÒmCÙ¨PY~à¶T¼?P*hš 1CUà ± +`ïcùRîÃÒUøi…”ÌûZV„¯>(µ"ïôAìV„¢ æŠ0Ï¥»"ü>QÝ+¢”+‹䣃°øB>*0Œ�Ÿˆ…Åó±¸|@’ů߃¡eq7½=žz�ågá®{„FZœâvÔÂÏö0d-Lh!±…éØCÕ÷z¨-,TpߪG7·8‰zP¾Å#ÓƒáâCzp'CJ¹xöy°ÎÅm« ë§§‹›G »8Æ<”Ë]8[ná…)å!,/™:€lÆÛPBqƃ§ƒe�qœé ¤g\«tp¥Ÿñ™ë%4BŒ ä¡–ŒÌ FHƒéçˆJƒ«ž€™cç€Î"4Îá&j×(95à3tÌѸj_@_ ø—DÝrAkø°ã •—×€?9Ð(ö‹ ´%ÇlÙ°4rÈφñ ‡ÜS6=Áv‡‚eŽ¢qÛm¼ƒqè*X&qÃF¿ŸKsC�âèA�8xëÆÇ‡#ò»qÕp4Ðo,E¾qgÛoÈF8$ÙyƒC—PGuƒ#CàŽˆ&Ž<È�cŠ"w¿Ñ¤CE«^EŠò ²Ë8Þ¹7ºÙqœlèZRÔ!rÐ)oorülX9Zq7Â/ÇñºÍqšnTqŽçF«žãˆr£2è¸üK§ÕL2A +ã«$ôã†SJÇ—€a�Ͳ˜:• « ñ3øðMÅ´ÞNÞÁuœ·!h–aÐN®ãž 뉼:Pô¹ðp¬¢P ·1TvW)Ô�mÔðÄ u‘뀻6`�jÿ§­¤Ð·Æ¹c§ÿ­´á&p— ic~uÄ�6¤×Ýl¦Žàÿ²8Ó]—�°Ôá‘dƒmê¸nl´˜¼Äôè×äÆF¼xL¢ÄÏNGH´ °9y{�1¥4€ÓÕ®ÑO¨£.é +ãp:<¶„äœý›=kä6�5xtæ2 f^�³¤# ¬†|Ò±S åÒBÕ $ÑŸ'9K …¶Q£ŸèÈ50$!~ñ½Ëi8#:N6AC ш_ÍÎЀÈiŽ3„† šÃî�†¨¡ý3|Œ«Ï``Ù3þi/ã\)óŒÒܧxbèŒå²ãŒÀsŽPn¤`j30èY3"âÑ ü²f:ÊÈ ØçÈ…˜q_ì—�¥Q—ÁªsÄl7'®Æ2ú¯¢2ÿV «¢2°j¤ 쌟 öš#n“qé´dRs„ŸpR÷•µ…œ"2¼‘2àœã÷£#ÞãçðhÇà“clèmc(ÊÒùâdŒ9Šƒ1¢ƒ¸ð4Gø+Ƶ0ÈãÆr �ë"k| J ¸‰¶ˆ�FÒŠBA µ:£b9Œp„ÃËqŸaÀa@/ Àû*ŒŠ� Ã?\" r Œi© ÆUÌa5Ã%•5$ã1FÁƒdäGæ¾7 Kto9®FC§Þï™Cèµ9à +Œ^ÙßyÌñ—rš;© h9&xÌÍæÀ¸:Xíç@X:0ÆÒa¦™….`\òs)߀„^Ág€+«c!ÿBU†¿j?¿À ½/ðË«/Ð)™/à¨#wøâqQN�º{ê£^›†^€Èt„0/®Í�H�áŽ~ßFGwÁ‡täb~tÀj!¦ãvAj°.æ†:üÓ…WÕaj7SÄž ’ƒ5Ó¬Ãg¹€ó:î‘ âbÇ;ã‚;;žð׎�pÁËJúÀ¸Ãü[4ÓŸŸòŽkoÚw|í2ÁÃAn!×ðð·-¼�@)maY<d –�üׂCy„m- e«K˜G&µøæ<@›Wè�”´xYzà‰ªhqw=�wÏì�àfA #çe,ïä²p|äSÞ�G¨daS>A.8É ·ô†±`Á‰…áÜGÞÃB?†æG´ÁÂÓúX8¿i¿ÂþÈÊWˆHÓ+ð�^ÁÏd×��ˆs³I‹+øS@B,°<³ÞÑ´M œµè&­hþ"oV à@ b¿3á VLÊ1¢ Ŭ ?»Š†ÒV!ô b&«¨Dƒ<¼YÝõ¡ASqã /U…Mb—*|!†¡ +#Ä Sa®„˜˜ +‘'Äc{ +QN*®°üGÅQ³¨ “ I5TÜoTpaH0?Å3;O±VCpuб7ƒsÁ†StÛ×#æ£Ïre˜)7¦hÐ 1ûR@½!×R@ÃÕÀJÑ)2)õ†«†(lRÈÖ�D’Âý†Ä)TˆI-fã<¤°£ˆíCnEã¨d»>ÄV. +’ÕÍÓS˜Z•(ÐÌ +d‡‚m>$ï…âÊmB�©ÉY4n½Ÿ¨ä‡ì +˜>h &ü÷‹�û„þó‰ø>ÄØ=Á,SOä2OŒž�r'0\Ø þ‘ÐéÄ•B4þ²rBæ@dß{-Ñà%C$¤hˆá%Îx ‹0Š›¬Ð&ȆHèkâ.®Õ’‘“·jM"CÄœh¢negBZˆø² +‘Gf�HÊD•3dÂBBä cG"qŽ˜ˆ"L œ ‘ô½Ä/AÜ%Žõr ‹6—@/Û[1ÊnD®j uŽˆ³„/‰HàY㔩%W"†•x9"ù©„Ã’Hô”кD¢•z7‘dÓŠ aRDù%áKEyÊ[‘NBHÉÎ$TŠ—D­m‘¤•ll’²‹Ä¶‹Ý=¤¿È”íAña$TGÀ`ŒD˜±D‘hºÑH€X#�„0rO­»6ì# }#Çöˆêqä›Å +:nG`dG Oñ*±p=b˜Ñë@TŒ>bG ÝáÛœ $6ã O#^H¸g A$ÄË<Š8F@#á… –„$@½ˆÉK! ’xÏ"l—Äáak&ñ©"¼os)“8B.%CBC'BW”ø1É&%%BÈ)ñ|D¨M‹Û¤ßÑi•œ¶­ÄÉò+îC\d Râ-�é?µ%(pˆÏ¸œÒ‰]ž!ÆÖK°ÆË_‚ë…8l´8&Ù*Ä +1Á‹B64!ø“è�7ì!42ÉÛ`Ë%–k&Ú(u ÉâÙŠAÔ&J4y¡;‹PDÂrj‰MâÅðfM’“Ø$³€ðqmr€PÌ}¸oÓþƒê3 â�“èüÁÇœ„»d¡“õƒ;u’”TÁ@ÒAÓ¸'¾Iòú ”2}¨O"œ¬äIš|€Ô(>à/ | Ò“d¼‡9z‚{{¸‘oö€¡ñ;»d¡'ŽT=eÛ€|UÐCªÄrÁ(U{h%G7u}ï•Åy^J¯ +„í¥S[pLq„f¢'N!(íª}uFöäË_«�=¹²×õ ë ´N´·®ù=Ý[·»÷ä#â{"ç<œ¬žÀ…y˜Ö  ¿°sÇòä¡gõD¿KítO¸­Þ“uáÔ=9 +<®ËwhTOÀ;ñ9­žˆ�; þ{;xĨøá˜ÆIÖ¼aàDסÌõĈu€�¡:T¾ŸY:°�ŒãÞA‡Gç‰_ÏAÑÕâÌO4ä»ËÁé<™¦rÀö¦à-oñÒR°î¾Åy‘Q‚_�\ìH +ø†Ë•¤€sq�€ëÇ…f£Ÿ\0- +�ü°$ +´ð‡ö` ¾Å#¡ —.FäTAáéºÿ G³ XŸ0äÛ;Ãô;ïÒÉôl˜ƒD?ìr€ÉxÁÏ|JÊ‹™œPB'l;/ÆÛ²ôrº&€]/_iÛíåÉ™_žÉ0ùrUL€—¾\� Lܾ̘!ùÅpK˜Ñ~ñÊûË—+¡þ¿XÙgyPfÎ||Tg\|9¹ÿmÞ™îÞI‰î çŸ%B “ˆ¼ù™ëàütP  qà4¼ç_…†™9&Út»ÿDS¨L\ø!¼2ÆAÅÑ8Xi¾ÁAWIsê7P7ý |Ð)�›köŸì—æ3n@U¦¹´ nšmÀ»Ó|� šk¯ƒœ°´ÿH ¦j©©‰‚0£šôÒ€gÕd k5Q¡ÃW“òðǚ̜�´&×jp«àd&׸0ƒ®Øö6øÇ2ˆ ÐÍ2è«z� ~+úkzGŠòHúÝ�A~"6«r‹A¬×äI Àøk¢†Øa€.B0@Öÿ/ d°Éˆ‡itù7’x©1àÔü¨)õ4G&•ûç:£`WûUQBP4�FXB°gö±EYü ‚íø�v‡ãy¥ª1d4TN�� ¦<¨rGbÉ-ä0“�Qȉ0ÙuŠÅJ‰¼xÁ]ýÂOŘù<!È Z JûD¤vÝàÀX¢Ü%BàmH~T¢?ƒ åF_óHePé@Œ;Cœ½øn@í]Œbþ=ùê08ŠQ5Œôä4ĹéøªëÍ£ˆd fL§C}ÖÎaåE-:•·/ Žm·æÈ …À…8ÚX™Cô»FBÜ�z`ÚG· øÈƒ¨`Wã_'§ ”$™ŠÔŸàa”¹w€ìJ +•Þ�Xó#.àJŸ8—îJÏÖªòŠ`Të©Ó­ž«�£ª1q‡Y΄qŒgCÚÇÞmþ*d¾3Jq ñìÅ9`ñ?:ÔœÐ$â€=Ü ÕÝNÓ(þdÉ¢¿�X@3»vé¥wV8ê*¥¶�œàÄfv'�|g2óA1öm ¬ÅH·“4(¤¢®A]’PâC%~iÅ�À®™ØŽ¥oà‡Î{:RÔŸ%&d|}#nùk2PÀ—x�š)«ÇÀÂjÅ€aõÁçÜ«ä0 l2fNSŒ16Ô”3Є&”Ï:<×\Xlè«òÏ+@/Ðpìvï_© óe˜#ßÉ^M ’\ ø±p¥ 6; ý‚ Jê½áÚV,Pü2Ã+Ð,î>ÉH\»úgôLzÑhQ¥ªÀâR&ã°Â£ŠŒ 0943£ñÕP°W�T+CÒQÀKaÑóû¨»0Èw+5¡@[nMr¤[BÇ™å{°-ËT÷Á ž-,—Ë o½˜“Øè>@~W®ia±K?òÁC¡%/€ó0b$O á,€0¡ò|ÄöyŒÆ©˜GE.4¤�è*’´ÍåÜ›¾y à4‚ÉîIAZ€ G("¦>ø|Õ¾3«ð†îºý’N¬á}ˆ<ü†ÿmj`:€ù•ÚpO“5te›l¦f‰,[Æb `Іß4È. aY;û€ ˜íô.û‚Xãë0æâ¯ü,=8HꜾ.@AÀøÌ’9é+•Ö?°X�ÍÄÌÈd`Šp¡URR ´¬[°6K®M”[uC¶Ô€`"8î8jKï€çƒÀ]�’øÿ-t‚±pÿðH�þß�*@(žÖÿk­ G×ÿ)`)þ¹1‹“]Œ +hÿGQê¢eïsLôßsXªÐÇržü—^lféZÁíZ vÀ$ï%±$¼ãöO’·Ëý¿e†0\ÿTù^Û½�Œf¦sõÒ–…ZàSé%{½òtSú/©ÞKÙGMsË ëcó¯Ëø4ª½dTò¿ Ó˜¹'O¶oâ_WC‹^&’GÂ…ÂçD#ïßš#٠ؾÑ´^“†1: œP€YÚA®¿Fˆ”ÇÓóäAõGñ +2µ”ÖÒ?°7þu×)¹ ýÕ×%þ½u$<›œ?Å�:ã#H–EwwÁfò“réɾ“5"¥Ä„]|'•ÇXâü}Üë®ÿÐÜy +±gí ª^ÆLeEÉÃ0ðßÌBO”k¶«ïG&dÓp€—ï"Þ_“KšöèìÀ\Žîçü³Ä£û»¨RÁ´@÷‹”mÕÜ¹í›ø£Ë™÷íg2i þì¬ýá;ÐEýeÞA³Ÿ +,¤?AB ¹gÃ~™,ö +¡ëçó¶I½™ghY�õû+ÔTÒÏ H'2xUÿæV”M¨¿º�RyŸÂ¼Žcú‰ÂÊL±Rø +½zô§ï³FM*¤úÏ^FOÉζöù�+03sH éüE¼5wƒÅ?Öü%Ž„(Ü1#÷¶ÐçÚ´üÅæïÙ(®8í“-òóy";¤ûĹÖãw̳‡Iì +³i�kFX$ÎB±„ÅßV`CŒ�w°Åïu~J‰=°�â„…0Íë_Ú{øÅ(î�~O;ÂÂO¥w!'‹ü2Ñ]ƒ¿˜*Q@—õÉ +h{ˆÀ?¿‰»"ß÷û+CR)0RÛ†â߯,‡ï +¨ÔªË£÷ Qm ÎA¾û8û%ÍeR­ûG�‰ÚÇϨœûw£Ó¸0£/Ç};A`;ú²þÜ}ûû\x¸BwíÖäÂlª¯ú+вöçJ~ºßòÍÓgíIyn€­R¡Ù×ôcjMÇ‘}^N<Œ¾žÙõ?œ1ŸWÚ‰}Ûÿõ +K›Ò—¿>Ã!/ +Ÿœ3j×_'yµ»~¼yÛ¼ÕtëË•¥Þ-rJб²&Þ®~(oiNõ§¯wxl �”·+õà�X° Rfõë$îsˆS#§?œ+B%ËjúÒ‰0”f^fÙ0ë&ýò=J0>UwGŸ£þÆ*­`êê¤è—ãÃß ·k¡_�^ÈTš‹@ÿóY:÷|1o5‚ùZi•Ï�‹gEn ôù©eLªÏ„~-j&g�­Ï�B%<ÁæS™Øâùs,úF!Cç�™‹ª%ý«o¾…I„¡z õǼ¾ùICåö¼ÖôQS¬ ö柈z_Zž�7ß—íoï7V=]Л_,5¤Âró"yóU þﬡÞ±)¥Âª^bæÍ'5&xƒÐÊÀzóËv¦‰ð~oþ6‹¡C6“Ïj>•ôò}4:VóI…-S™;ð©æ/?¸—Õü6+Cªv#5aåËü|˜-KðóŠ{™Ÿ Àö +ó·æõ9 .=¶ÅåcBU«Ÿ²ãò‡»^)Qb€.Ÿ¢±˜Tø¾àòç�ކž@Šð¦]å“�âòŸï¼VJ¸T0·ü*:Ãö-?‚ú«‰«î¯³�aÂäSÉ-;¿¥ü­dÈ�í`'N~‘„ÀB!N>‘YÔŠ"µkY˜“¿2šþ�KŒ“O®ô\M�ÿ�ÞŽ$Ÿª5˜ÑÖóa‰|�‹Uùì½ÐÒQó€EÈA“ì›ÜᎯ¼.Ƭn|õbž¯¤ôdü3°ˆR³N²[ü9’ÐË´çÝ+Î(êì^¾›ÛAuÏŠº40eu–È€ï£.¿ÃÒí©/Â9k¾ÇýÀ0ÅP£¼ÚYÉíÃ=äeã…š0ÇÜíÞ~û´|~`Ï\@Ý>ÐzÙj//c¶ý<l{èÁÇ�ü6¶_µvÌüˆwSk¿Íè7j{:xßê©åÕ×ì�s‹K7gÙ;Mù玉°-8²wh=£u•§Æ>ç>C ž©…GÄ~ˆz +� ÁEØk[S6µ{ÕT±ÿúÖ ÃbD3Ds±×ò~ÝûF�]ÿŽò¾#{wKIÇõí€E�u¾€¶¾ãGëÕïTæ-ÙÁà²7º–ð«opîêù2³ÕÏ!Cÿî£yÚØª4ü!û¦†5R}¬mú™0…#™ŽœnêýÆV$G^¶MRŸúxŽ»jUõ*(m<èQaEéÖòr{ú Ì,^=&Xª�ž oäUßɳM�‹b謑“ßS¸2ý!>-2a4wI¸5²“Ù¦”>É…:¸”Jú[’>lÀëq—ªHOIð1º”ÊëF¯°CÏ÷3rý=3x�“P}3¶5�]hô}©y¢f6ßHÂQœç“³Yæ#•½‰¯�Œ*¯¶&]@/Ùå³wX/¾ªßÅçç9¨'vÓ×Kaø<¨Áã“¶&øü ’iãy~˜héÑEÀ{ç% Úc^ÞM½Õ>/?fÿdêu¶NËË[äÅNÙ¡0³IÆükcí¼7ISr$^ƒBÄî•owÿ9Òˆ0Vå Ælådâ=gÊ3û7|œ‰À¡üdrl>nöPwÕÉ“Bö@L>×Âséb;þÊN¿ÐNý~äɦ¿nåU¨ª†yÝ%ÿð‰@6ú�‡ÌlA¶œwéxû™µN2Î1tg2úñë[¼4]Òž"¾K¤Ç'£i8nýÐÀú×ñh Ccq<ÿ0WO½oWØøB–BzÀ›ºüv‚NØžwÏø»©‹"}lDZòfŒ¯€Áð¤ÿ°°öâ;RÄõÃ3¶‚ž0ØéW°ƒ¬É~¦ƒUü”ªÌ?°!ßÈã@‚…:ý"@¯OãlÑ#Ÿñl?Ô‡x™- Špè<š×Ÿ‡í:;ûtðr–ÞvøP6»U/4X>%ÉÙ�y˜Éðÿå°lÔÞ‡=¸ Ç_Z°t Ÿ¯†¹“òXK,…¥óZã°§IøWrËžw‘`½Ë Øà;$*é¸ße·>-ø÷y�Ïüa„%�߉–x˜ÐG‘eNÆÈ¢`¨N?PŸ¥ƒ˜T  ø(]ð¹äÌc$ž7ÞÕ)ñÛ»¿/‚�9hŸÌN#î÷Mx™,9¶Ÿç@ùýî]ÆüÄÚ×&÷],ZèÓ÷Á3pÁÙ4)ßu¾äžÎ�?} +¾ó›‚Ù¶ø8#îýÜÒ‚]$–ïâõŽe^8"¬¥‹Ö€Ó;|tJ¨<¡§ yÞ)ƒƒJ‡'§«ª¼²¼öÒÖa½)êÀxG.ÆqŸ—ûrƒw +À{÷* ß\³Pî>iŒÒ€j�SÚ½AfÐü|)Ø]Ž+¨sÌ- ÉÁ,æêƒ0¹qqº{\§õã[—¤¢;`Úº¡30P¶'ÄsÁ€•{ÃyxæN÷©þ‘©¬KŸÊÕÁ`¤B¶ëq"wêÓ+’7 �;Äj=Ř~%w²p¾ »ÅÓœp3`îóy&Æl,Íäìí`X ªdÜaªÝÎhl¤ÀG+rû›ŸwF¬ äsЙ¤ñ0+b)i{Nk”í¬º9TÇAv!Øþ>›^æ‡Û]Rp45\æÚ3f¬gdvÚ8ÖžN,Èçۧ°OTû‡á-9äDì§½ÅÀ35¡Fð´ÒŽbWb'„•võ, v!7—¥F;£ u±ÉÅ/|~·T.-MÜ¢{öêO£ˆ\�Ô™³¿•b•-m�3a5{–Ц¥Jß"̾WåŲS5Z ZH°MOöýFfl‘€¼wÅ›°Ù5Þœ ‡ËRp[=ö&ªpó“Ç4võzgõ¨ŸÅ.õïÔ|UÈj# F„òÚ^i“-Gö§¸ÛÞO�°3™xFQ+ß’`_›ŠêJ°«d Xüí¸ÑEvî¸�/YP!.¾¯WûSK–wvn„Üë®Í=[¡"äuZ¡FÄø²õ�7’0cîeéº-¾A̤Aå:/Á t.yÑ ®Ð+Ǥ߀ pë �k-¨×úÆêú¦u~ž-ÖwÌŒÔHiÞY‡@‹a˜šÃSÖ¹ãWJ<$á=jˆ²Ä0%«¯ ;ãUöl¦³¯ü>^e¡’ãBðKfd»x®tEY]mMˆÒtRC6W½�õÜë¼9´Fªºî‚eÞ©([ªëµʱ/ +ÕÑH[\`]Á–ý}#�:”¼A~n¤0õNœnœ¸àIÝç~2†‡°=ê´šÞr`ØûUÔOSõ˶ª'… +Žl +Éh ãû�zµH¢ŽK­=õéLOü^ƒ»7žl¿ÓZa£Ñé^‘ɵØßtAÍòÕÖtã^ŸŸ ¸U�ôfz^ +¶I‡ƒó€‚šÚÁó©BBzAÜ¥{g{ïUq+K/R©¾IvÜ2À_x*�òt‘¢&T:(]É>ËG5Pú(Ýé`ýÈÀrIWz,ýH×BÝ»²úwA/éÇ Ákr*�¶üч�f°‡ íèôB“w4€@µŠäÐ>®_€HÛ%@›ÕÛ³ å2éo�ÌEGŒ‘£ý€)ú Ñ\YD/*zGÝ©“(aC�ÅD‡ÄZ ·~èRíº†è—Ç­_& ëй&B˜»xR?ÍË×>6²Q0ælprBÇkµÖ`DìÃ9žtˆŽèÕÒôí)�TÐÛý…9]'30DØ;@ç¿’4�k,z�³ÈIŽ'îêÜ�Çõõ}7Æ,á?Ÿ�CÌ×;ÂÎÍAá=áÈ(SïÓ«ç�VÕ|Žü�PÍ;Ì!×6ˆçÙÇöò€³íÎ�æ0K +¬Fìœ=ç¹:`™cç- ò�:'%UÛç\ïåÉr�zØBîmãüÁÀ– èˆàœ‹­~ßtá ³K>�l~—1šÏÖü’¥3aØM8|Ró>—$] ˜4§Ò¨_&Üêšk�¿¢-wá/p›sÄ ùey–³áôµ[Œsâq™k¶‰WL©ÇKQ¼KæTáìàQÉ Ìc.Û]S{á¶b þVÌíÍê–çäGT?î¯Âe˜#U1·^Ìo±“co`:Ìdøåç_­äD¿fìv^^$uHô7*ƒ.·vcµÒ]Ž~w°w)Ç«·\ëEHùðƒªå‚?�½Ðšþ,e–{#Ê••)JZ¿ó„åûØ#ˆ*�²¼¢n®üNx�HàÆVÎŽ•_·¬üÄbT9¹ E¡v_œ+ú”ô\™–V¼ÕäPb);#,¬7Êñ›0·$NE�pÈ&+z»ˆÕtÄÚ“ƒß)V…”è󉌜œN‰ÍýJ“#¢ÉB’&·Ì‚{ÇuI˜ü’§  熯Юäø½–Ç–)NrÂ÷?èÓH~Á�§·�ždï‘Ë«’(H wù”cA§_‘£†kX(–œ0>+T¾-\ +·ÿ€øŽåJá©®Z°½¿ïgÙ Gÿ¬G„E'uÐÓ ï¸‘Wh�”¦ðSNm ±wíŒ`S¸YÔY=h +7’ó/EQÁX3…—Ч ïSM[ÇÉEÜ`Ò[dOá¡“yMQýß)jàÎqOÅŒTéöS8’ºëÌNáCú¨6iê)œ^ûeù®-�Â=ÙÛÔt˹·²žÂY!<9yÊ™”œÂ…}–ÑЊOá;r ºbö™ø^aº…>…øÇý c&žp6LØ C*ái¶´/VÂ×£H`ÿË¡.ÚzÏD{C3s»^Øs¿21yYg„—hçmðíá |´%Ý4C ßòàƒDÁ;[Lpƒƒ«O$á�ÅDÑJ8ƒ? +е5 +/8Y endstream endobj 25 0 obj <>stream +Ù©w<+Þc4ÖÆPÔ Ü××x[²¼G~«Wi›Ò�; Oñ/yñ=®8°úlÄ +\\n‚Á¥›»ön­OzæäIXÀ1/+«£a•&Îø!½Ño_Ü_óð†ê( EÿMåÙÓ˜’ãßt)QÇårVñжï7º„*iï÷ýI¯‚.à €ÂÂú=žÜs¥bù ž³éØžyá÷,<üQEªµÀ¬¸fê Œì›÷â,øíô=Ic¸8®Ÿ“çÛN—ü%Qúލ|£PîÀu4  Œo‚àð¶MÒÊuKøÛ/®: +_{÷Þz›@uE,iµ2�uÑÈÇËœöÆ`UŸ;%öÖÿEöY¹«’«7å"Â@~V׿oz“5�T4ôN?¶JæX:o6†EñˆòÔ æ­tMoŒÍBH]”7€±ƒŽ>´?Þ +ȃæ‡2‹·Y#øtähx‹ š¿YÛ¿nCð†¤,û7LÒ3´í»çêDC=Q%bòî•Ç©:É‹´f‘t7Òs©ŠÔT�»›U8Jtï8ànemµG&€$rtPñ)»ÐÀZýçð?¬ÃGz‹³;^vÛò{1g7¹áš6ÂyÙM 3 GºdaTŠ»_úîÈÐ¥4sn×m_f[ FŠàA¿®¨VT·b¾ŽŠZ)=+;‘§;üÔ’º­n¼<`ä˜#SþtGl¾üD™óO÷|ÞqYÌkËøt‹äPÆNePõ§› ˜y¿\ðÓ�fm\üdX%~º—\‚o¬ž®O÷ÈýmŒ]§f\‹}ºArÍpÈjÞòæÓMš³Ùz짽ϺÚyúQÝ.uq°KmRzTw‰ =Æ^’é‡ênr_wù¬êŽþ0 Ÿ&:ªÛ.cPðkÔ‰ê.nóâ†mÁë +gޱØ=R†êæŒpÖ.8�0u{j”%‚ëæ“Ò³^Ý ÛÞ•�j¿“ö�ºm +]Hk°nCd`�ùYä`Ý�5ÍÍ´‹m®Eƒ3†7È^º_ç4�ëê~£‡u{·çmMlX·RZ•LÂy|t›>¬ÛýÏ +ë¾ÀV—C÷]Ö­õL\‘¹nêßh®[ÞÁ¦ @ÙÃ\wöñ[h&ÞçÀ\wY×Ò]Ù5×íjO«4ò™ŠúœëÖ†^´dXw¦+“aМ +¬;hÁ­õ�µaݹ Ú4ø��¯îð´°�Éë«©[ÑÇÌ7Ò}=Sw±7´þW÷$±Š*zuÇ콦îP# e¼›ºÕ“B^݃ÃO„5qÀîÕ ›²ŸŠx…�W÷â•Cº*š“W7 h3ÀÖ¯n´âDu÷«;Ùé!Ô5õMÝÕØ'–M¼©ûÜ›«¥ ©©;²ZÚ[‹©[-ê¨à Œ‰¤löMÝ—ó#VRÖ-`Óðú஘hÀº¡¥Ð\7óí[—åÂsÝŠ¨$Ö‚Ý„Xn|Ïz"XÀuo ±=\·ÃZÃŽ Öý�>ZCtu¬ûŒ×@1wÚS· Vâÿî•B·žÙ¦”¾‰'m¥±—î”nL°ªk©Áúàåz‹”`ÞåußBIõ0XJš»!pÎ�’}gnf�éJ´<t½Ü—l?ÖfT_ϽÊÍäk½$‹3°1¹ÕçÎð²N©ªš3æ#ÚÁ ò:nsÄÁ¬�>ÆÝHçÀ(í-«ÅMNxÒ(” ?Ü÷R{<(6K 7àò˜Ê‚ƒú niøWÙP ð¸ïCcºŸ2òï·Ñuze;žãæ$ü›dÍPpþ“ÞÞe©õ¥ß©’J9x{°ð€ç6€6Ÿä™pE˜ ¢ýæö Ňñõç"k}Äm¡Z|Üqû;!ÔX:áŒî¶m„5y¶Z0…l›7d¿æä¤�ÚÇ–D=È^~|¶IÕÖŸ˶š“Œ$E­±ýy€jùЉ“€a{½b®Hñ L赋RòåÒ`-sɵÑcÞ¦ú/ˆÍÒÚÉlj ?ÔÖ´´È·éñªmºÖ¡+Y{›E§v?â–Ù[ÔNÜ/Óàh<í +¸"5¨#ìZëL^‚~Ñ0ÂÒv„-~#m®ç¦< k´ÁV¨�öH…×ʼZ0]&¢}¡Ýˆˆ0ˆ´þg¿½ãnÑ=8â³Y¡Ù¯Š r,ßÙ� @ FaÎÎÖ`@v՜ư ò²0ã걤S³Ïä(¦¢¸ÓÈÌže¸5³ÁB×+U˜!f#äà�ô¡Åuµ»ì°"øÐî²=Z!–Æ.×�˜ÈE£Þ½qSÙ ËòÊ+nèeóP²¹kYÝ l“nÈ9Œ=Ê‘-¬£�9 …l£)F” +½ge|ì¾A<çósl=ŒÅçÉcȯ{Ç·æX�®øåcYœµ2Œ}«Æ_Rº)g,vñ¬wM‡ppM÷‰­s ªö]i܈ Š`mÏ÷°WC˜ˆkQvÒ<âÚ>fTp… gôyÇQ´‘{Ëhð0áÎ`'�’×�ïlÉC°o8Ñ€qVìlè€M™Ê®|þ½<²ˆ«æ6´0º~-¹õÕ¼K~Y‰}-1•h¤�&ÄòÆ×ç¿ÖÞjÖQh¯k(] è5/AÁñ~)E^ÃÀþ•ÝYaN÷»fñ!l}f�¶kç''þvÍ/{GµŸÑŸÅÒÌ(Ù�¿„t!]W='=ð†ÂæÚ["K®µ8–ŸÄµºs ‹03¢ˆ†®Á''žO78K±[«s3us�“ýmkÚñ‰ß±5Sì˜ÚŠ]ɤ-\âÐZK„ÈÞ&”¢Öɲ öj m(%­‡ºjÖ¹ð +´ÎàŒ@<© »YëÅ~ +Pò`eØéa¥’–X%ë¸;α6’˜qv%'Ö� v;q-ˆ:)¬©’?`=#\ó¡5EùCÁÃWOì~<‹ ¸5˜w5ñ¯Å î/Í\ÍÄh§ +�Ç@s«÷½~¼bz•Æi5Iwí€K ²ÒŸ²7Õ‚Õp<ËXN®­-Œ0‡¸j-ü^s’S¯jŠcØ)ÕXqªvÞ‡ó"ÉE2O5gAzÊ+ÕBp—sUS0›�Q-|†8c”5 +¬AõX`1=5YY$•Ü,NMeÃoVnœúþ¿êª‘ijѸ`îJÍ^`jréA›Š-û•šÅ±ø 'umœ€¡x‘:s7]>B¶”Ç+ä[¼sæ°Œ:äϪëùÆ)jÉRwô%�cN¢& ÔI¸PÓ)%pVBqP�X±K’¾j,Pã³|ãý´8ï•�•ñin£ýÆ12 Äðôt¤Ê9bËÀÓ.užï&gßë4ä9ŠjPêÓšÓתd—8}KiE»iÔ¦dÓÞ$-¡’—�ÔtfI0€š6ðžn ààË4G¼¢¸aµðzÈ1+Å{HÓEaÏòäL†|é!Ô`T~‡t¢�ÁŠ©§î´4N†NCÛH…ü5{Ú‰ÝGtéƒJP%0�±7n�Ò›…&ã:‹H%JOlD €:w~'MáõÀÖûP& ֯Р+�ä)iíù;2%=•oXô…þHhÔPõ~héÓ…“zQ;[€Ò!ű®�ý`Ô…)‚4lX4öÑ œd.0Å–…ĽIAû`dÒÑ@ðlÊ +D¾ûް8º�’¶{ê‰ÁÊšÙèc�¶Ú 4šHºk‚|rÙö»1šeŒ\£ÁÝ«íÔ}îk*Ó¢Ù9Ú*c/îUtR7¿Ç¯sUY>…¢éü½ê>F¨T ¿b¢ís§ÉT�8�ÙG4¾j´?RÁÅùƒç�ÓôÙ‡1 ¼r‡åB‡Vi Pg>5½Þzb¥¡‡£9ìŽ!uSE)4C7`rYã•·ZšÇ_ÿ@™4¥mÚtÙê2¥D•¾EB÷—" c›¤E©ÚfÏldpþw š%ш23üO¶t¢õ8-ß1®ò@"hðu’À@ËÍ< ³�怶tÆþÁ’æ0† ÐÝ�vHxùÏÁÚù|=€;çÏø^¶á”Iý\¥ç)�Ô…Ÿ«µÈ¿µ\ˆû\±(¼,Ïç¾×)­ñY‹Ì×ñK'iù«ºIž>¬ÈN>TÜ9f—M¼ö,Øž�†˜è¹Â¿1¶ž‡U&³%•Mz–1£\£u¼Í³$›´*¾ÿÔJžÃ¹†ÎCŒÖY#že2)–dn\zqNȺEF#çóìÎ5à�Ê©åPÌjk;S«@ÝQ(t²x%;k (N댌º¹6–í¨3uøY•é ’ÓhIß tÎn&™�7ÂioÎÕO!Ò§õX”º]ÎÏh.HÚ—œ‘5£³Ò5@•ž¸ÙoÉ…¾§¹„#iô_Öžf¥×QñªÈ` +ÐÓ,�;›H)ÙžæymÐfŠp?5kk={j¬™¥�:'�X³ìòó±fA#¤ÃŽèÿXsÏRÍŒ^Æ`¬yªF€X×l�O¡Š¯Õ¡TÃW×lLM+/˜]sñƒ’˜‡IkÆšIç­1B™š¡Å®u·Òã”…Jhƒbh§¥f…ÔR3,­îÜÿ—A©YA5LËRâKͧ]SË I(•Í�¹ßí>ï1ý¥f™XXM!†Ö’.²ºì» +J}Ò\[Ó.�nж’ÍŒ?Ž|W7Øù™³@óô«Æ:³a§ç–x¥lf Oö/3$iw3»b¨J?À¡aæÓ2ûÜ»!Á\D™‹‰ˆÅêÃ� ™šéŽi#B?æ>Y3çB†Û˜)båU—s|°?ÄS `‹yG¼(jÍËB#æ”åØ@�P„†Ùà,:„¹¶‰˜D˜í£Ä‘£LúÀRf~¿Ô.?‚™vzÂM_¡m®Ü•m�?)è% QìË–&—´úäçôq¦ù´�Çýäƒt¦ÑGÝÍžJoH­Sêרpm+¶Œ�ž[§ØÎ¹®/û_ã_|¬×�Ej_Üïü‘N¼EM~'b¨²âáY�óX O‡œX$óä' +OáF91¢§ŸXWO×¢’î_¨öü8F9à{ÎJÓP©½Ÿ�kîŸÒ‹#~6£Îç— +$óõ©M•�ŸݾϮ¸’÷ÙàO‘À¾÷îÓ­ò·Îc1uŸËà³3?Û›ô™÷SDðÏj¾k„¨S4MP@eaƒƒî(×ÕÔÄô'~“Öh‚‚.S·=k­` ´˜ÍÓv„)Ah‰âÚáTÅ¢O(�p¡Yã§ í^õ‰·Ëù¶Åwh°$çÅÑ£lÝ#U +¼cB¢ÂÊ™˜‰†™Î8¥QÔíÀŠŠy‹þ¨ªk0 %î‹æÝ¼{Œ†²EÌÝ9@‰DÀ1=„h8qt×Ѭ ×Q£G,7k?Ú UÑ õ Û‡´¼ ÊX�‘ZðÌ\Hš'» [%uË ÏfÒ÷4ÚxÒ8‹ƈÒÞi:8ÖôsTÇ\µ)]rªcc•®†Ó§m˜ý,�8²šÂ¥.u´±[*ÒÆé»Ôº>g`êSŒs-1¥1%.™^á&j3]þ; ijÜ€­©ü™‹Ú6�hÁ`ûMmb5"§LîÂètpœs«„ùNÌËU¥Øå€yN%2Br} +ejãüS4·â@<B� ž<Ù‰ ‚ÏP¡˜7„-¢æ`Ô cóMkE¥‡FÀ@£"JZ¥G•¿ÀC¯µ!TˆÍöIý [ða©Áã§KtƒŒºÝê©q6iƒVªq^S丕¤£Íí“ñ9!½1¨šXV¤MUWÎÆ}æA¨¤=aÞŒ8%¸‰U�€:kVßYÔZMOr|«ø¤Œ3º¾Ò"P¼zª¦§ôUÍ-›º�u£t�…rOÜu2`%V„:ó.¨Ä‚j¯G4ŒuÄr£| «À¿†~ƒ Y–ŠYM¦$£³F–FÍSPDn^™¡õ°´ÐÖ´ön fµ +ÆæÈ×ê‰^Pæ³uß\%ÜÊc’‘@uå­ N‚nè+¡YYYrZ˜rNK%±Ù/Þßkõm8ëTbjÕYv»OOɳj$´ÏŠy§"Ðú5Y +YhÉfÃuU´^z´Ãû}Ì“6X×`ZÙR9­³4³¥Pû={–Zjãá¿'Uûî¥Ñ»Z‘Wä¥P&xlom0ÈÓt˜^kcLPØZ-ß4[Ä{öÉ6-¼äZÎöá¥-bz¦k›?n;ˆSo[ÙQ´Np'ämoÛœ~åÖ§H°ãtë°@(p'ᤕ¼­ÙLýn°½Mi¢ê[[+"å¿eÙFŽb‰w„.p±; wŒrP¶—)¶xdq{±òÕ¸ØÏ“ñIž4hƒ#WíÕÊÕº³Üí‘\Ò^ÜssÍmÈæ•ÎŹhÄûÜÐXù¡›[9ÄEm“½ÂLYVÅt]Ñ·1u¨˜Û6Yð.Éë ”,Yì¾ y·ØEn†6®Ù ©³GÈÈÚ�Ðε|»¹tî›ï*Í ÝÌw͉÷HðŠ�Ýžmñ‹÷z–fƒ¢óA^åŽ[.!éS^I"<è2/@%Ó²ó¦ˆÞ¥k¢R†‹ôMèÑôN:¬ÕkÎ òÀ^yðG ÓÞChȽÇÖÊ-yи{/ÝcÒi—º±P}yGÿç. `ãËÚ?„”-Ž’Çú /’ú­Àä?^¬Cî"Ûæ§ý\‰ßoñù½ù]wÚ Z¿A˜�û¿ÒGw×€ ÿŽ +ñ09Dã㯈w=ßç> À¥p[€]ò¹RÀ“þ©ì;z³²þK€wS}ŸkœÓ¤ |¦eNh¦e[ÈT«)3m¼ÙÞ@�Tc;ÞxN௠]d8v‰ìŒ�ì¿)‹Î:`–l�À’1}“oÅ”°”p’Y¶Šï·É9ìË$BûΗ²SyÓ”°d+� …ÉrJ/gø—éRbJ¿£XéëAÜ";ÏçÀŠì%¤ThMÊ\kw&ì®eó*åë0Á©¤Þ<ªFº•JŸÿ×£$-Ë–Yz[ì@w¯!1¥ÆˆÞvÀÎHP €Õ[ ­{~… +;`üÇÀâÙÑ8fìY0_ß3;`÷[qÙ§¢QòpPš9}±(`“žs(r§€ïðÔ‡™1‡:š"}·±—Y3'hËÏz’+t}'à'††3ͤeú˜R©*¡¢€ `®µŸµr÷ÜÑ©Ú/À¢l:§àìŒû¦^”©» ñÇ<ùsd�^0ØW¬RÂò¼1^äßö亼³&±íøÖÄ ðš˜ Á®h pPs9p¯3F:ô²� £üUöùÝ„¿§ æ`W-W_)�û”ÊfÙqá1°§$_ÁI‡&a°tÿ&8hçÉ÷ï{¥ü¯xÞËþ}&Ö`U ‘ý›"°®þå[éßzó­ò˜k¦ú^²" ¡Ð,—-9t#áš»÷ý9‚ËοH\S¯°…¿q'^‡¥þaØo]G8‚ÒU ¡ëwùk†!Þõë@cMž¬ßá�<„’ö ðÇ�а§ªgtƒ–Ão“ùõ;! ~,,²ubåëW8kŽPyý¶¾ÓÃh^¿›.šá¯2)Èïéß/ì€ð_ÜP†M1üEnVˆ‘ ­„¿�oý-–€âòú�pý ¦¹ÉsX—DKŒù½�*ôóúKOLrÂß (¡´…<¾²eýêPåñ×ÿ„Ê|:’"Çßù7WIጿœ2þ£ïÂ*xÏì€}áoön.¯¡L‡eø+Aö§’‚ý„¿Õè |®´ð—R#âÐjkM¿Î|²ü~˨^ô +5™ï¼ë×sV¤Â®_ ´ˆ¬(ÚU‘e\¿®40©×o!ùùâj±$r\¿ùÓtäë4곕ßõkh™$‹y~+Y€ ØÇ`ü%ªXŒZ¿ÚÇ™y%Gшª'Ü-ÑÖ@6~P +÷¿Ãͨ}iüzâIØ] +K>ò14áþƯ�YäÒó ztÊ9m#lÏïq…rL;…~1DS[»_y³7e~V‡þ§p°Þƒ òâ»P»Š''�xcA¿|%B $Sú•‚6I*Ù?ìA¿tÓ D¿½'‡Äœô»œ¤’jî¤_±ñ£æJJéw8cÚH£ �NÈ¡ØN~9À¾d|f:A]8 ;)sÉï0;T^Òou(&ÒÚÔ-/ýês,Fg­ô»Dºí7:M`Dl¥ö[I)À£ßÇ£ T3 ôõ ?%�ð'’ú eŸƒÑ”&¾á'ˆ5£_ e<Ö~ßž·ñwG—³Àþ²B•ê¨Ú:ú¿ÁÍ„¢XÆ8F8¦Ä%ÿ"áõ¦#Å ¡[ ƒ Nz˜ÑÞÌv ‡‚ˆ­0`m^"à£0åáå!v…wÀ%Þ2FOÿÞý@Kó/!¶ �ïîúÌ"VtûªŒS`™æ…‰ÀÑž^‚e"y{ I–ôŒ#x>ü(£Óþ¯D } íÿ¦_ìÜÿ.æõ€? T¶ižIýßé°½rØÿñÖH à¡*ÒZV?À&ì´®�AYd�Ϫ'^¡Þ¨iF¤xS/,¼·úzÌ–SÖ°‰yÙ Vú&Â^Át›$Ð˃÷i0se›•ƒé\ö`çøØç#’äY¸Âõ!vÖ« ebZÂk­ß�˜v´Fí4¨»ßmÇaýçÃÂÇ«B• sí� †µ€¾ªšÈPð&Ô±qÎáë5R¼ <|*åÛn�X·BlW1¦â‰øéª"!¦“¸´$¶LüÚÜ'æ�ø¹"åZ Y?§õÜÍÕˆë~§†‚¦Ì+Æ×yŽClG#¬vÇGà\üq7wæÅÓW�:P0¾dáW,Æ—›2^ôbQ«uvƨDѦ± ØØÔ 4tcIK°&v±DÂÖ±�ÁÍ1ÄZ2`[Çs“£KE‹ñ;†(‰Kôx8¨��á‡5?ýØP+òÞ|P�i¤?%·rëègòÎxq �I!ÄžónÔ‰œ‚�ñŠŒ|ë+ÕJG>[ÁIô�,`’�ÎÅà§Jö¤Åï=Â)•h‘,Ég6…’KS‹2“\À8�jmõ“Ô��]&9æ^v±“Œ9Í69°>ˆ¹’ç·n:s` áÙ<eé¤MK\YÉ9x�K˜|¾Hqi²2ö³4YL<ÄÑdqYŽ=j¾™£ÅA¶ž×x&{º»·rÎdYEš™&;0:ê3Y®d%&|U2Å´ÕÙFÇŸæ€wùl¤ÉŠkaÒÑþΩÉ㥱æ|rOš¬îU}ô²OiòWq.;»h0Ó[ OÈÓd„>ŽH¸ |ˆŽ¡„œLÁ4S.½Ék£fš¿1¹¨œ\WÓu?!�œ¼bÞIí[N6 ƒ´-Ž¡8ùd3 _ˆ“!ÙV½ÆÉ1[AËa‹4€íy$¬›éÉâ•&‘Ñ“s¨™¡(&'ù©ÒÊòSЂÙE’Á2¥'ߪ¥H‹ÐFO¶®³A~&§pÙý×]á~™ØA¬žìcø²AåɈazj?ž'ÇÝ*à¨*ॣ'¸çó�\�Ün6ƒ;ù~¯r–ÿ©ÜÉk6™!±¹“aD�®èñ=Ç:ô{%$ëØxW²;¹SÙ d@¹ˆÇÈefð†€b1Q6ÿ(�[¹e¤ßéëÕ£L”‹ü+° Ìè›~¶Ç>nI ¥ìjiɤŒ6cô>/EùÌÕæE›ŠV~h�©<'Ü<åŸ�{NÊsNY-SQ+•µ77\嬱Ô[9E×ݶéd¢X’妊KËî ôHh[néåMþ’ GÉåè¼\o—H-y½,.üg cƤ®Ë¼mÙ·u�\�™Ý_ç5ÿ›eÖ ;ý53 ŸvXÅ£&å*¨ùÿ¬™ ¤è›w�¾_›Ùâè.Ý,Ü®¨9-ËØa�ΟŽ_tœ�Ǽ°M6¼ Éä|ÎfÔªÚ2`ç/A›Üù˜qˆ…çH¢¬ýG=+dF�ÔèžË·ùœZïsñRPõ Ëß_\‰¤6ýí–*Ð`�Ã8ů€½ è7hr‰³™“ÐF¶ÐSä§+6´ñTã‡niò*$ZXÙîdí^‰Ï´ès‚C‡2Fãltpòj”::æ]%±Ž¶�RÎ��ZáCÚìÖÀÒ‚8P1N¤0B$=ÍX¢I+ÍÄ¥�>]±õÏ!e¹ÂC¤Ã–ƒG#Gî¼å"$DàKŸ|Lçø]qiËØ àÐ4fLE6›ÆêXFœ¦/ }ë4 +Ÿd±ÒòK»úé©Ò Ï ö¡.Ƥ{¨ý}QLMJÝJG´Lrz~ò…™KM1®ä ‹€K­ÆMý§èÖ¨íJïETò+6q@u~€ŸOÜOÜÈŽç» ••Ÿ­¤’]ó©cƒ‹9¢OmÈñÿ×>õ´?uaꌕ œ´�™ÚO­¾ù· ðþÔuW#ñ•ÖÜNf“j�WÛ ‹ŸX'[ý÷°ö§��ºâÔUÈ‚?u~±ò-S‰ø©Ëÿšõ^Mø~jÒh¨ÚvPMVÿÙfùwî§¶¼íC…y”ŸšˆÜ¥g¸€ ^Y#s¡%Ðû©ù�WüxTg7¯ �j—íúBW¿E�j…šàƒÂêþ‚�êWîãHX¦¨vŽÓè�B¿&QM½ Í4 ¦úFl+^©›jò˜m»©VÈ>S›rÙTÏÆ +|$�W´ÃT¿@(“›»GRuM5:zŠâäd‰?8TÃY—V‰t!ªi{QŒÇÍǬ¡º¥9Ù�@üÃ} C5{?x0†jÐ b(óÙª»~ãRݽNU[Ѿ¢M”pí¼¹•3KõÑ7e¨6#V€[€CõA¥à�Ô˜Úè ,(ÜÕ= ¿Åu‚Th’¨n¦`±®�vÂ)Q­c ¸ˆj�®$mu€¨F÷®Õú‚UãvéEõíT�~ žúšãàÇ�9Oíw@4�ÉÁyjQÄv’pÞÐp+¸ˆøÑ>Òõ‘� è·Ý<µ%x>œ™zçPUeŸ©SÈ9gªÒ´35Öd þoç©3Ê÷bq2Ä€)/²oO½ç/Qb +å4õ·]:õ’',M=à‘Ž&Sƒ$û™mê$S{_ŽâÁ)µð‘˜ƒ‹P ©EÁ—å=vä�:âÞZòŽ@Ñ�ÚE‹ ÿÄÑŸ•X1Öm¯�Ôçâ…cˆó^òÎ¥cîo�y�ÆŒ<êÕþQû�®~à +^×\O¤ŸSOùçµèŃPÀG}ÖÀ ؉â\åAS{‰NÜì°¨¨BßônQ›81ô£6º5Í¥†ƒ©å�NÌÍœÚ^Vñ2ªã”jx ¥Ø%z©¾Ë*iC£×\*¡Ôúš#»PÞSE¨ÎéGRÒäBuhÁ3v[ª)·øîÖœªÎ76TáªKÖnguP1Ø'ÂÕÃék©y§özõ +,£ó`m,˜±~w/g«•5„™wy²QeM¦âú7]ÖV±VòÌF‹É¾$«Ú³&8¸G8­K§ü_z­'kTèpë�Àx1¸^Ñ 7Ôê‚]®ßkNd“]R ˵FêŒ#ÏËÐÀÀÂu4’ k?¬\#ç+®×8˵ÑS¹nžpÎVÙÈH®oñtzÍ-”&7¹>;ìe·GjáB¹[×ë ßÊ53ÔÔêeˆë\{+å:õ�¢ª\ šºx$zGQ(×Ý Ñ÷Àõˆö)×�Q§ÿŠÊ6+×&#»âΆÓʵöõKf7e +åÚ|5§ëç*äC´Áéæ6%rB*¹˜`W*ÿL×oÁ jK�.O×�¶p�\}kºþÀ£«ÞßàU`­#-�ŽEºŽÆO—�+¸†]ºî›ò¾ˆ�K×zbND�ÖXº^C°XüÛ®4›k$êíÌÞVF½Ð­ç—íÚ2Ö[ƒ*»æh‚©L˜/£Ùµ0-܉<ÿÁ¹jשy»ÉùøíÚµm(²C¡­œ½h×õìɆ3ÿ‚v}-•Â5i¼*EÇëX“ÖÄ)ýO‰×xíe»˜ÑGÝy5Ýx:Éóûam 9™š;ò¡Œ×ðë;GEmÆë1f’©�}7ãµdZgž‹85^ƒAÊë`ú#h×pÍ5õŠ®]oƈ÷¢%ßÞkEÔ®·® +á§sç‚ñz¬’Šíõpós]óþ­“._Åãä"×k¡�í]úz0‡Q¤¿.”=”þºaD¯!]&O¢„½A"~â#ceØ;í} bï…Þ„Z + “bŸ›ÆÖ{¶zìÐþݵ‘ݳdq<ÙÐo�è•éì—c¨@ îšÚoF‹ë9š¬ä H$ÏÆlJš?‚6©E;ͅܤ´½ë7í¼Gj,‰Ëõ'ˆ°µa¿¶—p.:�m<²4wh;x‰T²m�Wl7×ñÇ*Ý.ƒ�Y$µoçÿùö.Oµ~ ‰Ðîè+]wšã&ÊhúºLKn½Ëýõmnî7¤T ù?·Ï§{çF7×VUÑÍ~ {Ãtk9b¤ºaZÜö\w†‘ìN�évi»cn&Î3Qªæ÷wO…ì"Þñœ§È;Æñšw4€¬. Òª¬7^Dˆ3ð.Œ)ðÞ;ðé(¾ã�[¸ùTßòÉ®ÍÐõæŽ'œØÖkÇ¥ÁߌØ|šÌ¿#|·ï/qÿˀߢÄf~ï�µÅÿ}‚_íŒ&‚Œá 18¯%3¯ƒÓYDøÎ Çz…ËÚì]e0ܶK×ÃÐ~{¸±Üþâe²”�xpd•8ÍÀNã£áŸŽ:ÿÝL÷Œß�uíB”öba|ÂûÖË8Ý64ÏÙØÔhAÝ8øò÷âÿ>•âñ* +ä¢>¾ñª…u¹Ó*â…<–M/øDŽç«upäåb%9¶@ÙÐ䜃›¼§UÝÊ-û|s’òy©\¼lh]¶rG–»wk…÷K[ŽŒ`y»ÜY ú�yIš\Ñ®æ ÃÀ’3Ï(Ü¥¹õžá5l1àæZ¤"'8—26ý};‹ó�¼¾ìtŽ»·syn´v®”nÒ1ö9,õN²8ẚ,¡Ã<·�hè&…õ\î3#,-úQ£ˆ¦}=:‰ÿDxÑ·¥‹x J÷³tSºkÓ9c— ›¯Ór½M¤ÕÇþ �Õ¤ö¡ŽúP u>–sž©OÜ’{ôbßÞJ>Z’V÷&Tvs�G±aR…íñ&([8 Þ¥%½Ç‹JœZ _þN#ÿar%KÞÍøü'ožàþ.Tþ¥Ãò¸¹üÕú…•…yW™· +ƒ·L󦞛7Ëy´e·�;o›„i½ç•ô¦ÊÊ´a%ôÿ%úfq’ ƒ-�¤—?•üÿEBqò9ÉËP+=A/©Zšž@mLWCÛ—òQ_ƒ !¨¨¾&¹5[½F 9k’õº¾]¹­×Uéìú�›=uÆvå!müõ¶¡=Èn‰Øg›ÂÏ‹ì/f÷(¿Õì!R7"í»û"!×þ2†2[mnû5ÈŠ,p/�0Ur¯J§ò[ºG ÈÊç·{™7]ÏíÞð�Ù«Biäxê*„Çû1mªµì}éó×û%¡%s }Ï~ª¤£fàgh[pð—SDÌœZøîðªa>~£øl}ŒŸeŽßÛt³!_GpSÀg•OõPQáå' •ä<óm*"œo^·.ôHQ">€íàðÿëÿÿ<1×°¾?¯ïû÷ÿûûGYIiîÿÿï�¼ï½{ï.Ì +ðö}eg¿“ûÿÿ{j=6!#é"˜ É<µYPÎïpån¾·“çVÓÌ¥ÑÄ™Ñ2mdsMÞY}³·æsüÜ~Ðaž]¹§½\ë|Þââ´×M`š§2Qø›8½÷LœÙÌ;��«é±šæ_$¾ø‡ #dàìyqMÂKŠì‡)£òƒ‘µ�QÂÞÔéýN›‚6ƒŒþꇸS¡ú¥Ìî� +%üR£†5xgµuÍcqþÚº�¢^‘£Ÿ-ã&FÇ”ÕØ9²Yˆq¯P‘X]öu:6¬Lœ�åuסÁߺ‚Ó"FXÙ~˜Î÷­îQ¤›oþØh™9mö&°MÎl«cÊj,ãòâ_`_ÖµY®Ù3ó:Þ7OuŠè;0 �+|½Ï ^]Ó—6ÏЕÍ7ynõ ßšÿ!´³�$í¿¸Ÿã'WóxñºŒDÒÙ‡¯Mv;"i,,Ÿt-Ÿ2�>hï÷ ÊI¡/a~½�QÂ~ Äá§:%¼�<½5uhõ�ŸÚ7’äÓÖÝ3qcóLÝX½³•³{Às7Rcà”æwL)²k!òÝ>^¼;ið&büû>…t2wÚlÌYÖË +gƒ]þÑ$˜„þK>k$È@{°îûÚÙ;ß¹^“wVÏÌ�ÉA‹{´Ô°Ÿ¤’®0s¦*AøƒñúNw*4lOÈQY+pÑX#Mn{åîœ>8¿Ó•ûAˆ{þi3l'`‘x“÷Fô¸gK�ÞT£…7œbܳ�4Ãö�ÓÃêâ7@öCŽ|~Ç;×y¸w_'Pîãô±}›¼µš'<ǘg÷|í|^šO¸îþ)´³?W¿¿¼jöÐè¦Ð®Úß4rf=flÆÆØ=Ë¿´À8 ^<»t¨÷3éœ5¨†b-Ñ”±VGe F4é¦N®‰2ÐnPCñgH’YW¡8üKšÜ¦NAÿShg×à™Ñ3vb4Oî'MjŸ4µCˆ|7 žmeÓbeâ´s�=CG†}³¯6l3Ï`ÝluóYNgiŤa¿höв>‰uu‘Ø�çP®.ûu!H½Žã÷ÖiàÖ:ÏWÎFªü»£HÁ7†¶q�ut&eÔWø‘9ëlá< ½¤Éý +`$ú+û�^Štð&ÄûCŒ{öä6ê4l3u†k2i H2ç«%—r‰½é“kAâý$LÀï Fd�ÁÈ&]¡È%� Äâ¯2qø ˜‚ÐbžÐb_‚ÄûN„o«S'÷¾Ló5|hôУ�v ³kKyj‘ íîÝÇÙ{û5wl5Î\­Ãuû:[8nƒ�ñ;Œ«hs� ÊiÍ4Ièƒ÷hâÖúŽ÷MûcHgßø±ušº³Y‡ çGc�PC\3i%ìL™Þ�DÉí?ƒvZn\P¢ž‚Š_ F¿¤ÉÍñ‹û;\9®û?a½_Äø§„ÉýO�á/»Ô ¾„Ùõkë:ݸþSˆ×…óýlžoœ�èVç¾õœ¿¸ž³uûZ¯0ëZÇî„áúå�×üc²ü1îi«J$z1S&ø?uö +@<³!VAûŽý‡Ðî-âA¨Ù‰û~ÎßÛßáÒù Ä<;è0Ïj¼³ƒñ Ì`,‚üó6|h]FN;Óô‘É:ß·ú°�®Ù[“…ù:ThØ~ +ýÚ:à8ÚÊ&c­k=lgPn~:õv8kª‡ý€ŠÃ~¢?€ÑjG%�¥ +Úà˜Ú œxz¨OðýÔ9øÄhìfLö¦Í¯&÷c’Ys@ÒY_©¬ÀxüF�}7ŸÛïÏÝOœ‚ZªaŸ°�M f6 Ô,/¦r†%œ2U‰Ã?¹ç_0ú E2gK>k M:ç-½“'ø)âý½´ºfÏŒÖ|«¥H ÿÕN�ÕDó;}roš¼²zænl&’ÜûM¡]U:ø™2½_Çoîópï¾Ñãß tˆ÷yßfèÈh.}YÛä°&Ònÿ`*ö*ˆ¦¼téýG’€ÿf¯­[Ñ´²N÷ÍVÊ ´u¶p¿FmŠÜë f8ÒV=ç,UÐ~ çœB±^Êüú"È¿¯tÉý�|L›\[ªÔÐ7¨ñH[騤È€¬ Œ +î|çjôÔ÷l*TCßø—*¿6‚‰ŸÁ FÞéÎ}¡E½/Ô¸çw¾p¿§PîÿÞùžðÜçùÊÙ?ˆu_hqï!ÞÙ?„v6N`›—©³g™¹/Óà¥É4{gY�0[§B¡H[™%‘¥´–UUx0'§?+ɧŸ:5ôAˆzQ¤…¾ +5Ñ÷ÞÙ7|lR¦„_jtÐZܳoøØ¼ÌYÝž³—,»¶Q¯˜’%àýƒXÆ»u¸nÿ†ð­#iþy(Ò°gâüz-�]îgcȲa>Žr}ÂȾá 'Ý €ÆÞDêíBŽzÞh°+ˆÁXGà!I/ ÁXKÐ1IG™‚sþâ:ÐáÝWªüú(QÂïäùµŸ:Áÿ� H¯F¢jÔ³süà>ŸÛÇñsó<Ý8m�¢�v¨Qïö¬Ë`eôêÁÒ,+¡ò„•s”)a7ÂôK°bÑSèAYSàqIKÀ!Y7i~s¾m^Fn,;S76û„ï<…ÿÃÎïAI¦íúõ4yg³ ��Õ ¿ÕH“ÛŸ€…#]áG&-UZh'Yv½)áíÚO%ºªÄ×ó„ãþL]Y¶æMîaœ£´@¬-¤jÊ€XÎ:ÅlòÔ:M�Ú ¶§H¿MßZMSW6ÿ€ïl§OpW0c±v°ã±¾‚9W‚9/€±è¡@ŠN +-âý D½¾”¹ýT& +m+ûÔ©¡gÊôþ¡Å½ÿƒX÷yŒ"ÿþ“§  õ ¾ƒ +û~Ï�WÂä~§Í¯½„É R´»‘"·6Ô§àýä)x'MÞA‡yöøÎ&Z컑$o#G?[ÉðjÜóAŽv¾ÇPîÏØ™ekøÈènZ·ñ¦eqßè¥MoÁÅtÖFyñ¹¬œ ØÏG“Ü”¨çw¸tÞGðî÷pï¾ žZ×ÙÂý£HÀeJØ©N í£É¾{‡gëŠÞM™]-ŽŸÚ§¡;«­ivvæm¬Æá®y�/œ—‰ÓfgäʲD‘|¾È2ÏbÄëF‘€_ ÓO»”éýG–~>§ëÖmöÒê¢J=Ÿ¢opã‘>°¢Ð ‰ójp�Óuë9€oý1Óïû$ÒÕ5zj´Í^ d˜ç…÷þQäQ$ŸÎî˸‰Ñ2nb3ŽŸ[M_Y=s'FÓÈ•Ñ9[·ãÇÖ1¨~f­ŒTfØhì n0.ezûУÞoýz,}ƒ�uƒ�þè‘ûyçºÎ7®'QÚW?Ä]C“Î9C“M+IæŸDL?…vC<ú¦Û65æÕP¢„�‚�Ì,(Ÿòƒ“5’柊ÜëM£‚u%”u‡+ž´‚Š=ˆ¯ÏЕÉ8]6:éÓÏ?™ +Ö d8ö*‡NŸàŽ Eboòôþn]ÿÔ«‡ùn C=/´Èׇýê*Š=ÑιÁ‹Go”ðW¡8ü ^<ö(Rðí¾û:\9„xg;}~íP …¡§25Rìû<à¹ßCHç�*ý† ñn£F@›çkçmìØ‚ñ>œ¯¹c«süâþøîN¢ ´ Œ +ÚLÞLÞQŸƒKbï}ùgû Ö} E;¨aßÀFc/’üë6i³µÍÆZÝl0¹1,$ ÝÁKiÖ+ŒŠYË€ò•M¹‰3ìwçlÁ»¯”¹ýL–_ûœçsá…wõ‚Žô&Ÿò’¦·[Õþ•k±_2sÚìX®>’Üv«#’3WRÈØ/+ê.*&h 8*ižnÝBÔûR£†BŽÉ>AÇdoêlÒüÚMœÞû‡Íã¶ù Å;›'<#§�müÒj›<¶šFïlÆn,;C'&×ô¥Í4ug³ÌÛ—eê¾l]»¯V¶FsG&×ü¡É8ܵz',Wß®Õ3u·cæÃ¸ŒÛÙ0™9[¾ \£mÑh¾6º¨’Ï;… +z¥L®÷1´³®ý¬ÌšV®ÑCë=ˆs=§ëÖ­i=k-«±V¸K“g•óY™ºKÃ7–µ®u,ß +A‚葯ÉÓk/ir‰ ù|ÐbžÏñÆÑ>ƒw5Ò¶ÿÞyŸCº'NBo@4l?�vm&Î@[¨qÏÿ(Öõ H¼´hgã·6÷ÒÕI–Ý›¿7Zn,{ãç6Qúy§NÁO$©÷qßz�[P£^Í#hGßô±Õ7j݇pî']n=‚ˆþÃÒ9ŒJ‰>"Ëhm¥#rg;÷}Âw>ªtÐ;ˆñØ¥LûPãžï ÏÙ;Þ¸!Æ<Û(�Hrï)ö} Â<{¨QÏ.z䳇ûì¡F½{‡+çiæÒèš»µ~ã×Ö¿s=‡ Wû€ó<ÐàÞ2ô³ƒ õîœ=9{pî&rÔÓ2Q~µK–Ü[I̵‘"·7XîïléüÏ Þ× ”ó4uj3MÝ™Ĉ×#ôxìV<Ä´Ob]c÷°™:´,Ñd_’åöC™Ö¬pb³P"«åŴΰÄS>rÜÚ;ݹ Q8e N6é©„PŸà{©Òû}íþ ßš·áKó7€l]§ÛfóÆy›½µ.3'–�¹#“ɲaÖ3úK&î‡Yß>Ø ÞØ¬Ž£oºjô Þ-ËÐ}¬õí_­p6ÍZ¶†�MöA´«q¸mõ ÝYvf.Mžq#£gèÆdn[ÍCç}ëúPä^OÒ ô b4ö-?Îö­¦¹3£�ï>ÒåßW²ìò€éjëš-[ÛìlŒZ¿ZßnìŒ�Mƒg&Óì�Í1dòWÌØ£– JÄû>ƒv]Ç;Gÿ,Úu"H>¿–«mòÜæ>4'Ð�ÞñÎuœ@¶ŽóçÖyÂuu�!ß'‚ôóBŒ{}Ç+çéÄä�0\�”yÃ×F×ð­Í8�mu�Û,ó&†�±;Ëò ÖÑGâ@¿¤Éý=…rž¹#ë9”«¡HÃ~rÏïtç>Î_›·ÑS«kðÒê/]}@ãŒÁIgÖ‚MÙÀ(aNœò¨ÏAŸDh=úyÓ�ëA‹yþHè‹"û>Ðažï×ý~ÓçVãüµù?·�Ã÷æsúàüÍ^[ã�?’´‹"k¤Ë?O丷ã�³mîÖ<ŽŸ#à7’ô³—0··ÐâÞÏŒë8€oÝÇÐëtã¼Ï ]?ªìMŸ]›ÒÏë|ãz�Ú ´hçŸ>Ã> +•°/ir?S§÷/¨ñ8cXÝ”/0Ù”§LqúذV;ë H6å¡F?¯Ã�û?ƒxCˆ|6P¢ÝßùÎuï›z¼ûAŒvöNX®¶ñK«­lZ¬umnóW«œöZ÷&óÒÕB�z='Ü6[ß:6f­£Ñ;Ëò ÊÍH��sÒ&·NÚäÖC’|5Q¤_mDX]ÖP§à®`c=u¢Ð 9-êõ¡H=ïsˆGûÚõ*†þBMz‚É:A ÞB¹OSWVËĉeeâì¬LÜ}µÂõ0k[}µ¶Ùט4ûËÚV_cÒê°(QB;ïûÒy$1à'‚äó4ziY¹1,N—�Öñ¾užïœßùÆõ E¼n$é÷—4»þKçmöÚè=5ºq®G�†í*ˆž(ò¯î9”ë=…u='ðÍã®ù›­Zï1¬«—6·AŒÆîÔ)øqÝ: ]š\³·&ã¾ÑC‘z>Ê´°@ ûÒyž/�ï ×u Ƽz©ó[O©0¬¬HìH”ܤ¸×ÿº�â_RÔûGbÀÿÈg‰ý“§à=ezؼp¤Ì�¤¸`¬�<½wâžßñÊùœÀ7Oc‡Fר¥õ!Æ=û@‰Ã/EJø‹ýl¡D>Ïdùµ«N$z2‡÷ì�îÜ^×ñ¾ùžB9„ˆ÷{ÂsŸ,÷ë>‘ãž}$üA‡zŸÇ;gët㼎 \ï!¤óC�zßg°îë|ß<Ï—Î=úù"È?Dé÷—4¹ß‰Ó{C…‚‰^Š”ð îÙ8{oö¡�-%Jø%ä×NÚ@ˆxß&/Íˬ�Õ=á9»‰³kK‘ +j“,ÿl Ã»/¤ˆwâÝ:]¸�¸ægêÆf=µú³ïC�†½hØ7€ÑøÀ€ô X4þ¢È¾/ãæ³\{ýêa„ëVC »†'™u‚‡ý(Ó¯÷Êý!G¾Ÿ Æb=H%íàÆcm¡È%Ý€†¢7‚ì»y¼uÝg°îïtçºÎîE~)ÓBïÀ£—:-ô>ƒugÏÍçôÁý®ÛçéÆi�ën!Æ<Ûg°îû Ö};7Z¦�l–q£yÀr¿ÈÑÏBÄûH“Û¿„ùõ?‰w5�Ú\³gFß¶É<‚w5�ažBÔûA‹y¾g°®!êÕ@†yžç;çyÂuõ�á�7¢ôóR§…> +•°Mvp]�Ãm«{é¼Ð"Þ'rܳ�&ÿ>•‰B;ŽI:ÁŠÅîôùµ…÷<Ïà\Z´óL Þ>…jø«P z'а?ªìJ˜ÜT Ø‘2{U‹ÄÚ È:� Å^¥±W¡@ôP¡à�dè‡÷þÒ¥÷S¡(ôH½÷‘ðQúý#q OºÜþ&ͯ :ø¡Bö’e×-îÙA‰zÞç�ÎïÊyŸÁº_ùçŸ:m(OÁ;¨�ïóÒùC;�Dh3Y~í$Ê@[HQï%ÖÝJ”][jtÐ6 ¶‘$o!Ä<û'Œw òÙE�|¶Qc }$ùwïtç: [÷)¤³‡÷>’eßMä¸gïxçúŽ7Î.ëÙE’}?‰2ÐNºÜþ"ɾ_4¹g+Uní¥Ë®�ô¸½‡÷þ¡]‘¤¢ž­Ã…³÷Þl"ƽ{*ÔÐVº¼mðÒüŒ-ƒvÏ4tcµÎ·íëxß¼­ÆZÓìëУÞ/²ÄûC‘z~ “k?uzÅ BÁ?I3Ðã¶uš;3šHRïWµ üXv'Qïß)Œ£iöÐd<3ùÆ�­!êÕGâ@�tù÷wçh«šçÏ­Æùs«mîÖ:ŽŸ›Çùkë:ܹn“çV[ÙlÙÊvÏ7|lþ¦/Í×ô•õ™;1zFŽlÎÙ¶ù?¶:†Œ¾Æhk¬ ßMÉçmðÒ<� �'j샓eÖlyfŽLÞùÎõ E¼txçqßê?¹ç/Žæ¤óFb¾žA»îSh×…÷¼Pã^ïœó5zjôMŸ[m$Ù÷¡@¿Z¦NÀ{ê”ðv0ñK�~¡„_Ê´Ð#MrÿNw®ïxéj&ίg£±žB%üL›\û ü›0½÷Qäö?�‚ÿ“´’=ÁŠE¿ …â7 ¶�Ä€Ÿ¨±ï%æù"É>¤Ê?»‡p®û„ï|ãÞOºÜþ'NÁ{ +•ðS™ÚHbÀ/”È÷{ÂsöÏá�/šÜ³‰ ù8d¨w7qvíª†vƒŒ E{JÔÐfºìÚL˜Þ[J”ÐvêDªü³•2mªƒ6Ué Vérû‡õ웽¶~ó§Öƒñ>Ó¥×Vºä~%Ë®b̳s¶nž¨±ïS�ÚTÚQž…¶ÐâÞÇl«qþÚüТÞí®û7xl>§ÏÍ"̳•*·6“å×Îù{ûV´Ÿ•�³åº19Ç«f÷Ê}ë›-s Hp;ûÄx÷}é|Í�ãmóB‹y',Wï€éjʾ#(ç‹,÷þP¤ž:¼ó;`¸N³76ËÈݲ̜6«³…û>ˆw5—�®ñC£­guÏ~mÉÀ�egàÐf™6²l ÞY}˜æmöÔúLœmU»³Ö´;KSg6×ì¥Õ2r>˵\]|Ñem«³3rd4¿riñ¯ZÖ´å ×ÕIš�žN­¶ÉSó;\¸{èqïëláþÌÛX-#§�w¼q?QïïpçúNw®ãø¹}Ÿ/Þ7‚ ôF‘ŸIÓû—0¹¶ÎW®ÎÙ¾Õ;`:šgp®Aúy¤Éí‡ +©ÒÂo ”ðO•~©Rð½”Ùõ;\ºŸã÷•2í¨Ò¯v)ð^ÒäÚN ae +þQ¦àÿÄ ¾«L~©ÑA[I̵§PÃ÷éá—*5ôM›_›ˆñï )ê}!Æ<›G�Îïxã>O·îÞÙBŠy7‘ãž 4¸÷�ñþQ$àWÊ ´ Œ +ÚJ$ÚS¦ƒöÑdß Ö»u¶pž†î¬¾és« +ñ:� �Ï |ó;^9ßCHç}íìž/�]c—ækèÔê�îÜ?‚¼©F ï¨ÏA�‰Ã;‚ Ç»ÊáÝt)h'QÚD�}_H‘ï)òÝM—‚7„Œ¶ŒþH ø‹&ùl¢É¼o“×FÛܱÕAˆwö�âÞí3X÷í¾ÎŸœ�³…óK–[-éá?€Ñ9öÀ·^Ów6×è™õ"Æ>-ãÞ�ÃçfëÂÙAŠu7Ñãž­ómû3rb´UÍÆÊÌÙY�pÛ¿ás«eäìlLYö+¦ &#wË8Û¶:‡ëöoÓ¼Ž×í×Ü¥ÑV¶fmóW™º‹óUëB’y'°Í[×:6͇ŨÑa1g2ØÌ�6›v£wáh>3Ùf{VÏÔ•e­l7Öšö¯\+ƒÅ˜ÍØ=´Ç®–‰ÓfcȲa¼ÞòâÞ«ËÚæ¯3wZyæN›µºù¬Ìœ¿ÎäÝXk}å!@„àZBXÑ<˜Ž \Wòì4oiæmŒÞáÂÝB‰|_HQïßèµõŸB¼îÄ齃Â}‡®ÎŒë?á<ÏtÙ½ „þ&Oï:¼ó7�kµu­c­l7vG�ŽZÄûGâm?pB±Î�Šé+ø ìQ¥ƒßé³k!âýð›ÿ!´ûE“|6Õ)¡=5jø›8»vÓæ×þ1´ó9�rµÎÎQêÝ Z0~(o#Ⱦˆ0ÏÜû=a¹›†­ž‰S›ïü�á�¿ñkë5vktNà›bܳ›:¹·e×î!œû8�i¿GðÎ#IÞK•�Z£Ç?{g[WçpÝ>Nœ§‰3£eØÄf™5­L3w&ÓØ‘Ñ3rfrÏ×Î!òÝEŠ÷PâÞ­³WwóÒi‘"ûÄž2¿Z§Ì¯j3¬�ú¼¡8µ>…töMÞ[ÇÙ‹ë5tjGï­#1no«�ß‚±½d¹½qüܾ’˜k'eúÙ5zl3ŽÞ[÷¬»‡ùn¥KÀ»h±ïZÔ»•*·öÓ§÷VÚ ´‡÷¾µM›µªÝÙ�o\/‚üûN™_mäVì‡ïïpán�n›]³—FÏÌÝôLžÖÊæe¸�­ùK›cÆd0þõ�÷]$dî´aÖ·f•«³;a¸Èñ®û0Æù>²:¦ìÚÅ¿Dxà¾KK†Î, ¨Ñ®¶¶ÙXŽõÒ²ºi¯2mÚ,'ûúŠIË~Yßü•f¯,›SÈ&ç ²Ñ7]69¦ ƹ@dñÆe]ëØš>³™Ï,+÷±ÖµƳ#¶¸­ÕÅ?WWŒ™ f}벌Ý��±³gnƒmÅ�]ÁbÒ¾¸æƒVYÕn0kÚ³ŽõYÆMlÞñÆÙ;ݹï3Hg×à�Õ?‰wu’e×An�õ¾P"ŸOsm$Ë@_$öë;ܹ.#Gv•‰»Ãnºjô$_‚Äû@�wu!�”¹íP¥‚Ÿ‰s{!â} Ãº{Ç÷w¾o6àš?šô»§J w¾pŸÏŒŽa³ÃnøÚj'Ðî� ¢mÔhçôÉy›<·º&﬎)óØ˜±,Öš&–í ÏÙ8€n�Æm¦¹K£iêÔæ<¶ùÇÐÎ9Öu™72™wùj=ÓÊ5uhÝ�Í×ЩÕ4tg´ ÚŸq+«­f_ŽÑÎÙ˜³[Ó‡F÷Òy�À9úçðÎ%âÙ>‚uwO8N[c—æoöÖl"Ç=é‘û�$m'Oïm øu¶s§¯íçÂy¿·žç{¾töS§÷~ТÑFzäÚ7|k>çïí#]þyA»?CW6ï|ßì£È?Û |?e~µI”�v¦÷f²üÚ@Švö7Îö´³u¼oÞ&�­¶Ñc«‘(¹½)³«5bôÓ-öÝ;Þ8»†/mŽaëac\\2vwÖP�¶ªõ0~ 1‚“à�ű!Fð;LK,lkfoìjFËÎÜ™a­mÿ‹—}uq±ëMÚWÎ`[6ç 'ËÌ‘…ÑØ¡]­k7ï_$8Xˆ< ¯µBXݲ^V:­WŒÛìÅ{|Dü +ËJgƒÑä�]kÓ²5�iY›mÚ\ÃEÃÆ¬e¯üoiñ+ˆÜo�ˆIûêŠ1 óŠ!“¿¬e¾ËªFƒYÕh°˜3l§»Hð®Yœ&1"‡DÍsE�«§ dwd ž¬ Ãgø §á6ü†÷ðþä4%ãÇ–5T +Ö*B$a¥#ËâU ð'ªüÃ1}¶!áÆ@@¸�à%ù‰åmÑÎK:ûª$9§e̘·ºšI˜„²Rð¨ä¶ÈÉîâ.�Ä\"7(‰�Já!æ¡Q9ø£”2,DT bBöJID3“qIáeŠ+l‹V¶Ø„[T?8Œ¯¨¥ËÄñE¬o€kÜ80cÕLa4±v +Øë 9¶�‡]9àOBRœHæ‰q#ÎÃ}8Ïá8|‡ëpN4sÄ¥ŠžxÔ‘îàIг];P=¹Ôº0–¡50H­ Û PbÄ×8Š– —Èš`)�ŽzS <î©6¹Œ”øíâ…Þ Yl£K™¥aÉËÂ!™bA!(Äß *. ƒtn>*ä\TbˆI˜ji)ˆ¤sr;ÄFä_ªÉ-‘ ’Û ?.ÃANÅC°ŒÍ!ÄŸ�ƒÌxÈ’õã%]Æ8F%9ôÀçÀ&pp«%¼�ЧºYâO3G<ÉúËdx8âH\‰/Qq:ÃA­+ M‚øT’žt“€ñ'.Ä“¸_âE܉ÍàOK +øßúð#æûšp@‰®Ã(è’|VV@òQYÉ'ådß•Þ$¢‘= sC€l£ê6æ KJ¯‘•‚ED1 ƒ€n"¦BjrBÁÃdQeM2‰1D.]<¦©¹KF$…–‹> Åá¡•ˆ=0+HR†y ¨4Ô!:™ÊíZI`1~ÁL³€jfÅleü‹ÂÞ�®ÁêÇ¢�{°@�o¸`ŵÄÜ!60“$Šep1“âEŒ²E4ËðQq7ˆ ñW +ñ7ÈŠÊ/“�Etˆ½@Zp62 k²°ÂÆÅ"¥«…ùQ£°£ö㥇Á|©'O`I›U…›\L% +A!  ½TLbب$?n�,W¬1K%·Ž0!¹,„q±å5âe6vrýSË ˆ€–9qõ¿©ø„SÊ%&æj^æi¸Ë[Ò$‘„#”²Åo±`¡ µ  ¾4­òÆ)ÙÄ šøD´s54Ë[Ì$‘|Ñb %�3 +ßd7ÈŒJð›˜Jü8'—t;G£Ô9Fér•<ùVžt¯(~QÉ–SЦà‘RNC$”Ÿ”L¶œ-¦´!.a2æ·JÄ>+@�YÄøØUJ.{<¥‹›@òà’úà:Þ¸auc„™�ÃtÕã/Z~ìeëÙ²Ç\¸ÄÆYºîƧВV)È$,5‹Û6K—ùIUO¯¡ÎíWPgŸ—S E¯ +˜zm�w|öx"†: å‹é‹Ú“+´Z˜Õ0·±•0ñ /f•*°¨5.Qà néR÷ €‹|hÅoyƒË%Ž.e—,®¨mž®Ê�šªÖQ¦ªš¡ ¿…óYføD*«7£”+÷£.·øÆ3G \´âÓÕ6ËO(u”žPè,)•R†…”R‚�h^*YÄœ\²u¬ñe¬{À²Þx�+\¼„±�¹€}·`¾d±EÍr4’/Š�IÀ&-&½HD'·LPLþ)ÑÉå$_”¥�NÀwG�x?$Qº%Œ,i£)¨Ì•hdÂX‚qÉ#è +)pŒ©X´t ¬f»Æ<¾ºÆAººaÀÌš,°´u&�ììTÅã¤|X"jI(SñHãeŠ*kmc)Wì˜G˜—KšSÉâe˜H'&ä/&$°e"’ÄJÅ"‹•…DÄŒ©X´€L�Ø|QŠ|>'Fl¸*>|á&ZBU+~qŠŸ²„•6Êp*¦˜k„E%—È Ì.‘bÀ#:&·©4&•°� \‚B2p +±×I&¦"”ð&£*X“¶ï,ý±È—ÛÓtÊ'ØÉ%¦×É&g¥ª_%Œ-g�P`Oñ2fÙ£ËÙ¤�.c�)¨¼}zJÁ”¢ê—uˆP³WP8±Z8!»QéáÅB¨��Š™tA“,:ir›% £¾»òÃΓäF«2ÃΪžÚ¼ŒÊÀœfé¤ù“D¬�1 +‰2&bd¬nˆm¦ÅÅ]ö%†ýÁ¸ðXcT,#‘`~œúåèãíå¢ÂJA$ž•ß#¢•_% ”]#0&—Ü€ìsÒ²ïIH¾(@ ù¤ì€ä‹ò#²Ï‰qà’•a&�—“Sú,ShQËd¡eí²€-hˆBºÜ•d9#5]½=„²èéØDPäËØã’/·aUí[_¾rCÆ l`–)µXI—±H#_´‰Vð‚\ÀÐ�H°!�|¹ÃW²tÂ)Y|£j—;Kª§pQÓYPí,|Ö` ú2,3þï"£Æ*°¢Æ&àÅ|×ÀÒà‰,®¡ÅL± ˜ÿ¸ã‹Øeh”9LÄÔFÊojéä65†$ #‘}J�Dú-I}äc¢ä‘¯‰‘È¿$?"ý–qÔ“’ +Éç¤Çd “b/’ÒIB%�“ŽP)™bŠ¥ü‰†ªâq¾¨ÒFYÃÊYæŠ*l‰<¢ŒÕ!âµM^|®+%j0ÕQ›mõ†MgÅH—…H ÈeJ]™cËY%.eyT†ü �‚MÐúbœ£ lä« Jd¶4ÉÞ0ruC€�ÈËí™b®uuΓtµN³Ä*œaô¾ql¢~Øê‰N¯¶èlQnØzL€ÜsQtÜoQnÜd` +¨å¾¡Ã\¸ôI\Ê,/¡ìU&Y¤L,²X™ˆÄ�œds’›…ä¶É©¸»¤ÔR0Фå!ÃOÈ(_ñ�/dwUžÐ†D¦Ü ŒN1¿K<9�„;� rrJÉ›L #FØäÊgLÅ¢Õ,@‹Úæ�+kàT'·Ɔ절øÊˆ£ì€ÈdÖƒL³d\jxn’Ô[05‰¶Pg +C,ç I6»VÏ;û¤TLžbâËTNø{Š‹_–%ÆßÚrZ_¥^Ê„æeÍ×$õ&™b#2ó/uxAË|‘e�Ó8NWÚ&…h!;T¢ªõ2qò“$±Ó¨èè=KŒú +F†Üµ ÙÌ +�ÏF¥†÷$‡-ÅÇO›¢£w-7dí•Ý®2£&[ÂBÌlÉ ­X7ZÚ¹#9´yHtˆ¥E)àu9Qo`b¦à„rv°ã‘~$Rþêr¢ÖnÑA×]IB7ª`_ÊÐ’¹ä€1JhÀš-�$Ù´‘Ç `Kv#Ø¿+TèE)[ìÃU/þPŠ–ÿ¸Ä‹öñˆ-!”*_Í‹�9llãš•€×M0¯S/±´Œ†‰M™Ñé”ü°éŒø¸á’ä¸Û ä°Ë²ð Ó°èèuRzØsN‚ü9':n¼Õ"¿ÐÇ’±í#wŸ!·"n•‡\Mµ‰Lã“RĶµØ¨ç0/h5.;|ÛZî‘csRrÜÀ„Ê0Í¢…ÑÑÓÂð¨ç´©õ°8¡U­ÜŒS¸Øâ"[Æ0_\QsTõb{f®L�eí²ðˆd&#ÂÊÐU9ñ)•6ÇT0vØ ˜ÚÐ zb”<¢–ôÄ+xF#alÆ"Ø]ºÐf +‘Á#{Õ$3ËD +hS@"¦I|íY^Bi[Dg]Hi¯)¥>ÌÊéÿª*î·&8ì?#=ÆÀ@Š”}…¤Šõ ¢m[Ò‚Lžbª%Æó_P€K¡ËeK§XL ¹F@GvK²ˆR`Ìp¬C8ѼõrÐHÓCä¾±Àð`âÊ€I䕨±Ï–"%üV?(ç«!˜³�4e}dùç܈¤IPbFë£öÃ¥ÎC Êœ6nr�¯f£„±%‹|#TÍâñ¦@©ÏT}Ð~Õ…Ïz™!g¯¸˜³�eF]k‰QÛ`pÌuX”ÔwW’ÔrZŠÔvXŽô¹-C:zÐZ/9æYË kÆ,WIÑ» `!kÅLVå†'“ò¦«¬ð]XLÐaZTÐõ”Þíˆ ­‘cjIfh¿¨˜úXJå-*§u�⢓©Æ°m+2:�ÅEÝ«Àè •5Tñ”I„9¥³\XÐdW +¨�(ùˆE¬Ü‡©Uî9-Gj²~Ì>Y1xÌhø1³!¤žëR¤¦Ûb¤Vû„F; GÝVÅG7³R€ï^fÐÚ.7ê6-=<] ^ûÅ�M`†ÌEY¸(9î6'9þ˜* û:e-Ëh­Ee´gY ¥C`!¥µLPˆ¡)QA–eBªS=ý^SL½ÖÒžAŠ&m� æ,wIÑךÄÐb³˜ð¨pf³0•µL\ØtDf�ñõÐQw$ÙX“Ú{ +‹_E‡Ö +»mK_´mŽyPÉ“{3G•5ñ‘,cŽP°h¯lù�OÀØ—(ª´‰]p9s$æþ*à…à¥æK`u–K ‰|˜å‹gtòÅÞ3—¬ÛXõ*��€q(ÀéUƒ¢W _4³X œb?l¥I\ ýZVDÿ"� ÃA£ŸÐC²gH²IS "¦´8ìE•~µ‰]kKi÷ÚbúµVC= õž©œÞßJ +[Û†ÛÒâÏ!)@ËćX¨ÙžÅ9yÏ,L®ìú¡Jg#b¢lƒÍâ h¿·Àð·9æ2=…”µ„‘µUÉÞAt&ÁÅ´îZE­µJXÜqClŒå Ù1¨$ÕÆ£’ê=pÈc’U3 G§¨!{F¶ /°$¼¤È^³´ í)2zÙª [ %†Ÿ«°è~ŠŠ_–%Æ_ã²£ßà˜û–tY—]û…FýeÀŠ¹Ë€3˜r—4 /¨4ˆ0%°¸”Ê®„ÊŽhÊT;ç[Bûˆ-¥tUÐ9D“ÑÚ'A¡�› j½¢”ú/+§žD–ÒZ‹Ê¨‘…”î`t¦P¤’~Àã±ÖÅSî ETÆÒ"*gpò™ aE´S"ãÛÉ1vÖ$Æm¯ºøÊŒŽÕÒ‚¦­°èa`XÌa ¨½ÀBþÓB®³¼¨ë*-úÕʉÞ!ËhÎ9Óy¶bâ§UÉáÕžØøeVnt?FïU`ô°/(ê/+'jSFé E2ç M:ç +@.é 9(k +<$ýT)X,ˆÐn É1ð6°)8f@4üØPü!ªtþù éXØ*©Ý�Jg½ÀÆãìÀF$�!If Bõ³ñ¤óÎZ¢y_›Œj͘´ƒsbCû‡m¹A05è´.8ü¶I[0‰ÖîŠ�ÿæåG§ã’„Þ0¥6¼²Å&@…6#Á– H6M4ـȷ:ælnd³Æ.À <É3cPvB¤™L½ôQ'_M墰žPd’&±%ôƒ= ÕZ0rI3¨�ø+©dCñSÙ™D·¿¦­«F/ qÈáÉfÍAŠ'Ý €Ä_cð?òH‡À(�Æ„…Ø™bV=é H<šç�n~Dœõ0ų¦2-´� ûî J0ëì–ÝSyáA$Ù´-©¬�>¿¶ÒæöS­0¬±¨˜fIp)¥å&%îYŠ +oÆ$ÆíÖäÆÝ³Ä 9 pz­åR~0r†`r®³¼ècR`|™J‹Ú«®øYÕ]¬ ‹ÛGyák*)|MåE­¶zÆÃ¤Û]òŶܨC,p*_ ¢™ÝÐ%+_iQû(/üu5E­Å•öpetî`t¾‚9W(rI_P²)g`Â9_`²)g`Ò9k€â){X¥¹¢”ú/ÕÓ?Bµ”ö :{¸2*S(RI;°IO� ìX~6= R Ú|dÎ^OL?ˆ& ½ÃjgMbË)�e¥tæ *; áØ- Éœ¯R/å ª Ù,,£4‡ÕÐ H6i M:g M:gU<ë,/¡õ—ÓšëŠiÏÊzõc²g`Ò9W ’9KÀáø78Ѭ30áœ!à€¬7·Ú!G¼[ “û9Áü×TP2IhÙGrJ6?AÕ~�òÙÜhôVú+é&Oï7Šü?áº[çN;ÕY(†aÈe±e´¶±Ì¨ã¤ð°éžì¸ÕTmØX-*z˜ÒĦòÖ¨tÿƒ.3 Ç,¦ÀŽÙ«0²Y0�õò‚‚q€ YBªXv*ñ¥�:g H<¹³OB�JŒÅÚ +p:!øà£_£±C• +6˜Áè‹(ù|ã�wåÚNxÿøÍç€×ê G;ÿD*è«PEòùžÁ:”ˆ‡a çÜ¥:ú=T¥£Lö#οzˆr�–$²®j‘XC• +þ©„‚‘ȺÃÐÙC–ÐÙé M\Û2ç1$û“õÕ�KšÂÌY‚ŽJújæ,!Gd�!Ifí …ãwÒìj‹ ùîB¹ 8.é G>g&�>û‡h@N‰.ú©ÓÂ5&i E8g,'Ÿô•�Êz�‹Å_¥ÂО*Ah&-!d—*-´›:½N�‚_ “«àÅ¢‡ +Iöý#I@ +?&;ƒ‰v*â—"=�ÂÐ+ˆqø7»öRæ6Ê,¦t x÷€ëþÑäŸ�$s¾B"¦ØXD² ôJ›[�D !Çc_£±#an{‘# §àÍã�û2pcXkœ��Á“�,ÿê«#–t„�5Ó§·ï|åê¼4¹¦OMÎÙÆÉ5zh³Œœ–f-;#gÖ&–­ÑK«ízŽ—­×ô�̓m]춤‰Ñ60JØ—2½(r/2pê´Ð#PahMúÙAŠw_‰ðW­0ôk^FN;³™Í@ŒwÞBÎ,‰-§´ÕJš©Óû½tvP¢^WÊ ´©Pú2=R&`j¼«‡&õ:ŽÝÂ�̓OYŽJº +Å"�¾Jb CÐÁè-Ù”- ÉœÈXôO¢àîD®¯‚hʦ˜Ê ¦ŒÒxXÒN aEZè¨Pì ^4ö ?*g+’BÇ^¥B±+]v­xˆkJ>eQ<ç ?*ç2 ½Ti¡]4Ég#‰‡ ó~�ž„”Q¯!É&t˜gëlåê Æá;Oä¸g ,Ęg+eî|á<‘ä_=U‚ðG•zªƒ6ŠBŒF;�‰DûHðæI™v*TÃßÄùõP£a¯ã�†AŠx_)3Ð d˜ç‰ûl�¿8™·›GPn)4ÐP‘X[qôO¦ÞoÓ×&[ÕÄ®V¶&7vå)¤ã ßÍ4tlX¿µyÉs{³÷Fëxß¼ŸZMcw6÷ Îu¥M®WðbQ‚ŽH#Ë¿ÚÈò¯.û 5îIÐ1IOð1Ù¯~ˆ­rLö§ÏÁ¡Fd=ujÈ–û<`¹àÅ ïòRêAl�§TÖD‘|ŸéÒ{+Unœ@¼1øÐ¬¼PôBŽy'ð�î9”ëR§…rTÊt\ÎX$ú(}• D(QAÛ2Ð7i~m«“½�Kz +EašÔë<ˆr3e=…â�¾2¢)oX*7hYC‰~A?ƒ�uƒ�µŒþ@‰Ã¿tÙµ�Ä€ Z0ú+©N ÚNÚS¥…LžÜÿ4ö b4ö I4u¾o>ÇÛæwÀq¬Œfg)*¼ÖÑÿ †¤Õ ʺ‚ÊžÀD¢Í$þÞDŒ{wQäÞ­”ˆTùa¤OÀ"Ñ/enÿNw®ÛÜÁÑB�{¾ +¢G�©òïÉlG‰ÚO�ƒÿéÓ{Iê}¿´šF¯lÖñ¾y$1 ‹�ŒÅ/U +¾�óìœ>¸Ï#HWùç�$·~*Å Ÿ0ä’� Ç~4ù‡÷}ílîܺ žZ§™3£gâÈæ8²>#GFß®õð›·ªy9†,f`”ÐoIý¤|ÊDâ=/7–¥ÁCÃQúÑpLÒ f0z"ÉÀyˆ’ofýÖN„ë%N®?`B±‹X­+¹¤ƒõþŽ7î+yú7�Ä€]È1Ï/mnÔ‰Áž c ÁFdŸ2´•6í(=Œ~Á Fß æ 2͇ÕRT|¬'ž5”é`-$ΫŸP¿>R:Lµ„ï`åSŽàC’–2%üNDiJ:%œ6‡"›5U)bç×O�ÞU'ý–4�>Ê �†ãoðâÑ7Y†m¦MpO°b±/hÁè§V ÖK£ÝZ§ðMÎÃÍO¨_e$3[áGæìF£wâ [í¨œ9Hñ¤¯€`ÎU'yÂr5Q$Ÿ7’ôûBŒ{=IÌýQ¦ƒ‹ÃOÁÇdçÅ“Îb"ZgU ¥%訤¸`¬£FÃv’e÷æO­ó¬VMÝ™œ�Ò¢îâc ͈ -USYjÔÐW™8übHvªÒÃ_¤øgûxéÆŒ÷Ä @¿Z$IÀûÇ�ÎVÚÜz,%žõ(›Z +<.éľd˜ç—2½_ŒE$Ê@ûgïIÚT¨†_B�Ç/eBКÄû8Û5�Øæ}ÀtvQãž6*”ð#(‘hO•~$1 Ï �÷!ÄëEb¿N4ÙסF½·‚‰_Á‹E ÄáŽ7nÈ‘Ï/I‚ÿbÝ�Ó×ömðÜêš;¶Ú<§Õ Œ³gäÈ蘲åwö‹s�Ðâ_ZZCŠ}ÿÃPºB�M™‰³ûÃhw@Åa_Êüz°]mÔÉW7ÈáH/¸ÑHcq)q=[2ã–ÛQƒln’âƒpâé©LMÞðµùÚu§Ñ¯‡ð’!ôw@‚ùÀh4 þNœ5xg5§¯^V´ŸÍÊZ[ ±”™F»õÒç¶'¨�X[h’9/ˆ!9S­0¬Ykp²Ic©~f5íXJ<{•5„�^jtÐvòÿ§Ï¯tˆw5öÙR£†¾‚�ʚŒÊ_e‘êÑ;i‚m*SC»ªtQˆ±¯' Qè€X‰÷èG»™IÔ)4\W�p¤/ùÌbU!•ç('ì« }Â�KÙÁ IºÒÏï|çz!�ÏáÂÕ:ݸŽ$<²üóN£_OAˆ˜¡ZZwˆ:WèÑ)k1!õ^MFoF@{TéàçöŒs‰ØB𱻥ÖО-ÁỚ˜¨”hì@„}�h±ïRÔÓ¤óDŽzÚ)QÂ;Á +CIìwû„ï4¹üYK2o¬$™ßJG¤2äó¼û@ˆxN›_;©2È0ÏAâ1yV�0´ÀX¼§B o¨Ë°¶éRÐnÊìj-û쪆6“õ—r“uU +Å®¤Ùõ8mžçK'¡Ç£ÿp„ógH²I+]n?P¢Ýé ¶Á Ä[IóÏÎáºý˜±:ŒÏZ_Ö6õ)´ëT'ýâÚ€ Á/©WÓà¥ÉV6;,f¬›™3Ãò ÖÑ=Šur$m´ÙG+°ÁHûU7“÷V•ÓzDÏ_G¥bìRìrÔó6|k4ÍÞY¶¨Ò�ŽÀC’î d³gH½ìR¥à[HQ�Ç;gÙwS™H¬!à€¬¸P\p£‘¦‚QX?ø9c€²)S¥üK�Ûu +î|HÒ–xʘxÒŽ|ÒR?û‡$œ>ƒ�Mzª´ÐÞéÊýš»4§�íëlá¨H´­nDú(ý“¦à¤àÝ E£=zxÿÚÙK™[;ƒM:ÃO™ +†!�n“køÖ²6Ûµ™çKç¥L {)¤²ˆ/§óÓ¯¥eÔopÒ9GÀ1YK¥(¬ŸJÃu‚‹µ‚Š]‚ŽIA‰D{ˆ‘Ïæ¤ó<‚vô�Š]ëÉiÝ�IèL!Ge‘ÄÓ_‘œÞR3gàÈê¡G½¿¥Jê¿VOÔj\f¸pì: í%̯OsÿMŸ[]ƒ§V?}zµW=$½…–Ÿ +Õð»Õ;ß¹.5¾«L~¨P°Mô¨gϼ•Õü–¯2kZÙfO­-î}§Ìï­tx÷„çþŒœØleûXkZÏÞÙhDú)m&ÍÀûæ®í¶™S³søàlš¹3o-ó²Îî/€ñX‹ÈbJ“°"Ú5,ñ”ŸB»’e׉‡û˜4½ö’µˆÔOC*& ´h×kðÖh!E½?UZhG�ÑhC}‚ï:µšÛ3Öºö³D⽞áçÌ�‰' Eömý‹‡‰ ÁE|Àâ_Y"¬löUçûVÿ<ÞÍA‘z´!œ¼DºíŠLÒH½w®çðÉyC¹ûf/;ùSëK–]ûAŒÈÞÄö:_¹Ú†¯�ÖùÆÑD‘}¿h’Ïþ ×Ý}m§¯ígð®CÐ9SZi3ÐG™ü˜lÊX`@g­-¤„•Ïú€ŠAÛˆÒ¯ +Åá¯:‘(‡d· IJcHŤ¥H ÿУžµíÏ2lct ^š¯ÁS«}ÀsbM˜Þ‰1Pû¾³�ï¦Ê¯V +”ð6ð9hCiÞU¨‡7‚ˆÞA�5Óh·¶Ê}/.æµ5“wc“2¹}ƒÍÚ‚LÚ +eM¡G¦Lu±9òy'S0]A‰¦Ë(ýe…ÔÆF)½G$ ýRŸ†vQ#Ÿ6‡ëö +ñú)Ÿ²”Ò!†dŸ"=üR¢„ö“&øÈ10�Jú� HïäéýC‹�ÊD¡MAGeÁ†d?Šô¬Ë°#’ô‡*¢´U‘IúÉT°¦RaØ5¬~f³À˜Ê¶”Ê"¾¤À~0 –Á ÈY©“Ûuãd™:í•‹a½bÎä°ðÜÜàÆ¢G#±ë|ßj™9m¶æo »ô¹íP€{�ãmó—6ÛªÉ@�yU3uÀr3ΟÜL4Ù‡tÉ­�HÁt�ãMé‡tÙ­…÷j£Ê¿z(òo>ªìR*û¤­„éí²”b³ @ÍZ Ï j<ÖlLÎL—a¯ó7×Âz^Cj¨,Š)Í€…dt¨çëÎf½7ºtðæy+šC֝ܯbcÈè+·Ã¼8—†ܪÁ÷ºÜËxQˆEâäÎS-³VC(g^D³¾€ÊA“z2�¤6BË+—Xdí-4H„I‘MÐR>šìÖQ¦†u‡(§Ø/-'ê5U?NÊ�O{¡1{@Å«Ë)öªº‚Æ69ñ±¤„ÎjDÖU&;ˆ)£´Ö”ÓºC“OzC”PÙÀ)âlt¹�¡FÃÞÁ ICjfm ô«ÅáS»­gýãô©‰Ýð©ÙE’zÚ(»‹~A Å>4É7Óè�YÝ´×™:µkÎßܼó�ë4uhY«™�å3Ö‹_¹Hð@p 2ð­ føÆÂ’B¹ó•“Ï«„TLë™ýųa[4ujY©…µ‡N±À˜f!g=|AÅvPÀ»$*˜ý‘¼Ãun±X(q5³Âƒ&«rÃg!€šE0Ã1«¤é­%¹Ì†qa1¯=ááå’äÐÞQ`ø3%1n±%*´_VN½#žY3 -DÙÌF¥×<ƒuó à›œÓ…›{éfŸÅ:ºIô[s°*ce!ÍRpb)WÁ@¬•<·õÍwmŽA«¿xݵEÓ7†-ºìë:à9Ùúæ¯V8;LÑMžé»Z×è/™:í•QnŽja(H`TÛ‚öâÂBÞ€…+¥q†*AH7°!9?ÀA)#€¡ȤœF#�„¹ ZÔ«wÀrµQ¦m”éW3¸9‡0 „¼ÅŒAµ3ËÄ ¦�Ä[ºéÜ©L mªÑDOUzø«P,v$É.§¿Â¸6{¥¡;«•(»6„Œö†#—?Ä�λʄ¡¤xg[Ï´2Çnuq."¸‡ 8U“~ÁB `ÛµçqNžÙ+‹9ËjYé<Mà˜Ó Bì/ aW]@Á¤2f«t0^E ö¬Š ÖEm5dEª8#A~ýÔ +ÄÙ“Ì9+SYD§r–¦ò—´=†¿ŸÄøY''¼†%žrf·G™"Ö`QPýO’âöR5QK�*Ò:Þ9ùIì±VCgª �K+š,ÇŒ]e«Ù\ÓÈ™uœ½·ß®û:_¹Ú'±®’äËÊÈ™�q0­ü+ÄN{iñ¦Å»´Hp#@p/.< Ü�Ô70Ä€?8 �“y‰ÂÔÃrXàØ…-¡×CÄÙ)ˆ2ÕŠE™̬‡0¤2Óì�Š4N \Ög�o¾°ú‰Å€Ð+ƒ “W aF±Ú/ÐtVŒÔo«Bú¬e†ÜáK‰+“Ù ªŸÙ¯.'jì´—3VÒlÖ–Óùë +Šz Ë ÚÃê'�áH§¬F£¸�.ª”§V ÎJ ^z'\'ãpßä,ë-0¨t–—Pº��FºÉL7™zi I»ÙW‡Å°Ñ_6Ü´ùçñnN œëæ/›l]ë_ü«CN‡au›¿pÀoX¦S°|aõKk¡1£QñQ“aÙAc¿Ä�³T\ÐaTRÔR-e¨QCºjE#�•Å4;¢Ëh�áI§ŒáɦÜÀFälDXû$ÖÑ9á¶YÃRìÌeE'� ¬•’LìR¨—fp£‘Æ�ä3»!È'-AeW’ü?ƒw:\·?㦭¹Ï^gäÆ<‚„b”`v2o§Ï¯í Åã‡òôŠYÉ´Y~5A_úYÀ…x¤øPNg@e…Cøv-Âì£cή]|Ä,þ�BŽÁªÏf}¼ÃFåXK*ý^ƒ(ÿ©¼vHõŠ F£Œ`E#ý@GäüZX#xÁH_5ÓTv'TAZÄRú.ǘ¤‹%l_BÌÒ†Ô¸?<¥�=Ðâ­ c-B )}á‰(6ˆ‘o^Ê$”ò° í(+l°(¨‚�ÈŽ£çgäÌ:NÛ×ù“³ƒ +óì=9_£w6[ÓÈ®:Ý9ºGñNæÙ[ü… ƒ… <ËC‡<-¶fMž¡3 «ñkÃÖø±]yÍ‚/-àN< +¸”Ñ®†!„ñËk<"0Àî.9èmjí7±¶'6nœÎL¦^Z†n ,†¬wé ÂÉpâú¿Ø˜C@Åû%†¬f¥Gë¢c¾µ@kµì�Á¾¼Èvøb"¬“Nl0§X?(³Cb¿¨qozÜ«—:½ô”‰Åy‚ŽËùꩌ%…TÆ�D“¾ ³ñåDvVeÑAXù¬“2¹=érë;¬jÖ dÎK¡^ZFïgùý¥ÅÇ®[5~kX#Ë¿ÚÀ +BÙhÓoîQÄ“©b ÎE›~3 –°-nkmYÛf0B¹,‘& – Eøj@…"vC%Â@,Ð", ,²ã+ì‹ ùƒ¨X©†µÐcßÌ*H;è!)ce1Íf@:ßSXØeUb|DIJõͳ¾ý0*ç_L¯‚ˆå¢Î¿l‘²vÊ +Û,IŒ›‹‰ioÒüÚ;�t¾ç[wû„ëîÂ8í ›�ë3ÍÚÿ1¤³u¾rµŒÛ-ÇŒe¿¬jÿÊ¿¶ºx† 8 xS�þɸ��îÅ«ÒîT™—�9ã²æÍ_2öVLœº§%!TÊ÷u`Hä—Ó +D0¢ÎÀëfW‚ œ°`œ»¶°�E €"›� +(Öc¡Ac�!•à8¤°X{y)ý‚=ˆ”u–J¡ÓD�ˆˆ™”Ò5zØ�>ÁýÀ +EÚI4\Yré§ÓÂùÖØ{‰ +íå„=bÊh½aIh¶C*è<"I¨—p£ÑZÌÓÎÌ•ÕÖ²3V‡Åˆu/¤J/�•Å4û“˜7s 8p ,p¬ XÖ¶lfo,KógvµÒÙ^T6±"Æ0Â~:÷¼¬t´góò`ƒÛµÀK¥WÖø†××8Ø÷I–¾)# [á’Õ[Ë}…óº4ê•{ñèžÄ»¬‡L`ߨôðtT„tš‹ ÚC¨X˜b?(`âzE@†,×ÈÃâCþðÅÄ5ª„¢ì$j(#iné#M@ZésÛ‰$ûº’¦—f CR&á唯JIáéUÝ;yោ–³’r• +D/„¨w÷„çl£IÀ~Ãe›ù�åâhàUSWÜ…1h R/K´é—º“@3lþc¶ À(˜Pe÷ ‰tð ^pÕv IØ×XˆW‡1 / @õ¢fôëAjf˜ûP`VO>Á¢X f—>¿sÒ¦ làÄ¡Ì$ú­�6ýè.,ê»Õw�fwå°B–z2"âlD³M¤á¹Èr›ý€*Ö¾Òâ¯)aAÆUµ3U‚í¥HÁ{ª³P j˜L +Tp¬ç¯ŽLÆLûùü…å^Y$¸‡ XÜKD«œç 4ˆàV :pDð{Äÿp¥�ÿ­¬ʼ«ÌÞíÅo0ü‹A3.)(Ž]Æ*Ht�¡h‘�±x•�­xÉc%]²~„q)«`†d6A�GùB•Ðkˆ¨¸Þ9òƒXË +Zg°Ò™ÕPE“ÖúRÚËžÀ¸o)1¾Ú‘÷—”}C“ÎÙ+ +èíáʨœF#äØ'÷ÞÍS&e+›Xª£Ù0ë'Pðß åS¶‰ #ñ„´n£ñóÊi¹Ÿ¯œ¾zq-,§K—í *:ƒE)Ὢ˜¨qþ沃œƒ…<ëÊïê�ÅŶF™z½ÌÆJ®‹Tø{c®Æ/HìœÞ…ð�¡'"çt. ¨F`ˆ c$ &»M Œ|ÿ«*`1$‰G8AH[XýÄb¿¬ ÿ† Ñ*þpB&ˆDÉ}6¥†-õbpzüÃXQ˜µ³à˜ q){C’ãž`3‹T饑2»ô)!­aЧŒÁÉg–È’¯Þ”«�·7U(¢} Äá‡Eœ•>e%ÐÀÚGñnž¹+»ÊÈÙÙ91ÚÆ�mÞéÖÕ5zg3÷úÒâWW¸S \ -¹²0™93¯š®\ד G®­qޝ>&¶5Öáµ5ÂÅ5†¢Õ5#Â@ËküEÄ…±�Vc\p‚ËBJx�· 1ŽFaKè‘× I:¯!À”�¹Àœ 1DÅMþÑ8€q²Q§ß !‰å• 'æ+2³ª\^C(p"KWYá¿°¤ £J(ÊL¡…Ù ZN¯"(!_­Ä�Ç Ððcª0l @:ç£Æ?íO—®Lˆ{—ÄÍ;“1³Ç8–/.!Á®`ª¿±ZU5媈œì +ÆIpdy—í +Ô õ°ñ~[¤Ô ¢hvŬÙ"xÙW‡Ÿ{$*Œa��ÇXHÄ�Ë àÚ7.61Ê]Ì"U±:Œ YBª&VéS0‹…aŠGTµ‹L¸Ø´•÷J³ l@ΚhÒ°„ÎR,ÒD‘|žÈ°_Hµì÷S3­UÑ …a}€E"]£Qž�Š)/i‚iž ãWFpì—?’ä‹:t«=Ïâàů&„àYºdànìÎ`=S'†eú Ï lDfyó²4ykaC™\0¬ñ‰°zÌãÚÀ—YЀæzÌE ÄX5ëçG‹=¨âP:E¼éFˆ?ñqÃB³ H9C™‚µt ó¾†&å)‡tVX<&=î.#b‹<�˜}“¤Šµ1±q‹ƒ:3y~k(ÍCo¥€¹WyQèR*Oà±™•0ãr¶ò±‰�à áÇ%V)S�¶’i³V²Ûæ`ךwi]ù† YœÄ‡<ŠqôÒ‰?šÉÄ–>êìëB—yó蟌 G#öÔ/+'¡_R*&Á˜@àPÊH¿^9”w"„R€CPqÍ’0~!bø‡ˆ c"ŒY„0jâˆYfÅɰ p +’ølEŵX»¶‡¢^­^8½(„ ýš¢±x}P¤òZƒÁ!ë=é!F‡¤ÆØ—–Sš:¶,Mž¶fk–=õ›©t(b³ °"ÌŒ ޾Ö$†öKÊŠ™ªRR ÌR½XÌf V+-°×ÌÀ&Ib³U Bg%Ð"Ìêe�TxF´õKË¡‹+ã"Fd¹’Ržôä€wÈ�(S/lCPòÆ/¾*$ +ö@Ë%‡ñ‹.@t‘7#™t9›€Á„¡àt„P�o–�Œ½6%ܰhN×2Ìœ®U�U³dä�Ö8Iˆ³q–¯¼.Ph´0?f-´À2Ø)ƒ@©\§*äãiYRwUö- t„‘u-Ÿ> ‹§¯baèkøÌhœ­——>Šw3–ØŽÅÝödÇ·ê�)ó+!x‚ \I*W’ºÀ“ ®ø•Ë‹�ŒÅ®Áˆ¥§úüšíü¹•YÇäÎÇ®aûaã"A³Eãºp]cã&`~¬…«�Áh¥�èZàE�¹�P0vÂ&Å\>·µ€HÐÄüqŽÃ33Ä5X=Ø¡¼Û2€B;Á¦ "‹©|¥RÂÛV`ø8'8´ƒBRÇúŒÑÒOLÅ2¤bvªÐD»És°er³˜\f±^ZÐ!¾œÊz\fŸB{OâÞÕÁM0 ¨f•ªy-‚ä»y�­¹… $øÖVQ§Ÿlƒ‰Ñ»«©7XŠ;.Œ�ÚÀM� ³ž'txÊ—~cKÚd0d‰PÐÁU 0^)@V;€Ö†1” Æ8°’¶TvˆYÔVàÙ«\ÂÑW’ùá43\Æ + Š×ueØ¡¬ã +Êä Ó1”Ëbó‰ÕkòC«'…‡ÖK5†(š4Ó§·ÿ„ódÞ´2φmÉÔ‰a….õdX‘ óâB®­Ü˜G`• I°X@½¼Æ;¾:Œgt1ñ@Üj`@©–'ùú‚1`®ŒÑåÌS•Ôt +]wˆ”?¢Ë‹0š»9/Á.¢C T?}<… F�YP�f·q0{¬Ê%6þÂU7F L;6`«ÃX Ÿ[¬˜Än�g,àG3A|Ã(�S;Æ8¶Êè1—]©á³ �?`1•³0‘Ýæ4L/µˆý¶*¤›UÉÑáTwØ?K _á‰dMóW–íiœË’(€ yÃL€Íð•yñ%ÑÄ¢,pŠ”>(E‹× ኊQÉÕ3ØýªÁ³ûšY“û2¢Lüúÿðòëâ% –¦vôF«yâŠÛÕ:Äx—Ñ /W(Ü#ƒ#4ŒuŒø?ÑÂ0Vò¡ )2ì 1¼šIñvÅûºð­P|BQ‡ê âL7?|IƇñœ„zœúÅxd˽¨C•¬V�Q;ÂX_%‘¬A„éìdYjøEPÈ…¹í!1~X“4…œò�2#—Øf=Ôy‚“Íë D0 +(Áå6®rå€õrñB£ Öj è‹0ƒ*b°c^}|äëmœ%ìo &k6†Ö5.Â%��xÉc'\Ùq”-´ñ^sã€Á�+CDÕø¡*�¬�bVÜO�âU>C܇ïpN#Å£ÄÁ",°aLÛ22ýÃB¥Ó"0�ÅÀÄð[8¥£bt=Pdœ½4ú÷!ì�¤;H •+¹¤möÔ:Ï +¡Å+HÀ²º‰yuËè2¼’@od¢€ h0Yº±–+ÙXõ+ÄøÈ«± Wc!üx‹Ö6 ² Ž0²‘‰W,˜s²{¤„xÒIßÚ˜]Yã-YÆG°.ŒU=Øã´ÐÆà: €k#0¢ÂX‰£ñç…+‰hvÀ]qëŽe@ůŒšøï¡á6)N¼³»Ø¨g(%l8"k·³šw�yq®°âæ*åsµÌUܼ_°ÐacØ#]° +(áu6>‚ÕÿàªiL�3 ·µäƒqÄØXKWXËõÙ\÷x�­ë_ãB´� àŧj€kdí�€°¿$�½6øvaÆ`×qB$á4DÒ ç8[àaùŠÃ‰ˆÀÃ$Lq +JJ\JGˆCÝ ñ$!Äu¸•“¨0�,“„ýR“p>.‡©démP’½ÇE®|`œÐ‹V ø•:´˜YÞèröer¥:‘ÒåÀDÉ’�“\鱑/°qj—ÕøÄÖ-žZ `:¢�§lý�,Ÿ +` ‡ ð#QXIã.�€L6îÒ6~¢õa¼$„„zÈâgÁ†Æ]…˜E™÷ðUU[)àlp¼ øà•ï AÅß®¢ø˜(~}j₊8ÔMÐ O’’Y´;„�…xm˜R×Mña³1¡¡½»ÀðdØèd Èøa (ѱ šõ2€ŠyM�þÝ#Ì-æ*!�`Uª”\LFGøñ–-°�z^˜\ÇÆØ1¾’aLÄ.±„¦ nèIõj 1g!†RkÅ¿S6 +@O1«Ø`a¬Äı¬ªqLcÔ =M¯dL¸Ià�ß/”pB¹§óp€ƒUñ,§"ÞáȈW0"âtŒ¬œFÃ�¨=Ï!IEø+›ŒÙ«ž¸�&¹�{è"ÁÁè¯ËH%]Jp�Sq7�J š“—LôöÆa°~¬ÊõÇU¸²c(["ÆND—øP´ÚØ!jŒ‚C†± Æ4<ð$ _°éÁGáfÈvÂÄ ;«ŠrŽN É±u0ScÏÁM�°œ"‰j‚ 2˜‰¸€G¦à}s§Á€o)0À-q%#Ä…fpŽf„„Št?ª& gr‹4éì&CXDUÐHêävH•Þ4ÄÝ 4&¹@lLr�̰DLukJ©sœá…ŒPH^n|XÝø€0¬±Æ1°€�$j¸¡±gaçˆ×�–Ç +B]Ä�à™áÀ˜cXUãœÆ&4y�Ô0ƒƒN×ãÅ· �8ƒž!þDÄ�h‚¸ÑŒ_š!âL4CA¼GGLjÿðÔÐqA˜Ç^FP†‰hBö%ñòèwäè¤/%.d›¨ªq˜‡<:1 w€bžJ�›|‘åÌÒô \dŠUµA/\ºÛ¸ËWÚ8Œ—‡ñ•Æ_°’Æ3*ì`�Œ scÍŠcÒ 8£W^‡EÅÿ(N/€â_ x…¤%.AH‰7Râ ‚”xƒ &®ÁÉ¿"˜âfWSœ/DžaŠƒXRâ |Šx“ OÒ©©‘á4xŒ„„vØ06nŒ%L�Š•^¶À�yÄøE˜Ö¸€¯²R±•‚»J¶t>AÀ3ðI—ÎÀ&Y6·KV-»BŠX~}ì¸ôþ QYèCSRQâ!ª”´7ìjœ+Â8 ¤1êž(�dX¸EàŒa8Ü'>öÀˆ‡€2âXIF\KJ‰{%Pâõƒ&~Ghâ^RI|K +‰`2ràcÄ›d†�¬B|ˆqÞÃq¸�•ï3ÈÛR€°I°‘NÎ@&G<ÿV¿hþ99²oÉ•É@#6$»=ŒTz{@©üîH"ãPAɸÒ1”‰Ü +7ÜØ X‡q”Æ*6è8!¼²ªHcÀµ ø¤—ïj�âo xZƒ'NÆà‰kE%q CE<ÁχÚâP:BT-XGÅðÉkÿËjâ~YL¼�ƒ÷~˜âßRœow“ÅeN|C‘·BâtŒx‚�"^…cÄ—¬CÜg‡óð>Ä{x’UˆCÕq#™¡šnMòAÂ0’5©Éäβ ¤f`’*˜[$Z.·©U*·DR+»AhTr‹È ìYBÙRd’H¢¤$�ð£ç½="]hãTDxÌ`µa�wθfGÀ½¦øÖŸ£ÀÄÇ"$q/)$޵„Ä) ñDF|Á�²âGV~díáNÖ î$3Ä™d„¸’Uˆ'Éq$kÐŒ•„‚) c'"•brSƒxB©òÉ]R%tëÄÊ'·IMnjÊÀT&”] P.¿>˜`†s@©X&"ê5µ±^ÆD°hx¬]à^ øˆ¢#¾ˆGÉôp©>E#Ä�¬?\‰q!ÎËb|8“ ?Šâ:\‡×ð.Ãgd"P°5NÕzT¡åL Vb),"JÙ˜D�rÉ–rÉV<øã“Òû#æ÷G�]"©Ÿ‚>Žl~}Áä‘Ãr0‡ŠJD�†)¢ ¥�èb—jˆá´%p½xö· +:âI܇ûÜð^ƒÃe8�¿9@¸Ça¶* …lV~Ô¨ì±aé ²Ãr{¤Éf·ˆª§ ÕM¯(™]!O,½?Šl +öhB Eî0ˆØRTI¤‹)h|ã-Yc!hT´]ànð?E82œf‡ëp¾Ã¸Ïá9|‡çÔX¹L ·ZeŽ�˜l ÷°ìñ—-1 /f˜_'œ�|‘|M Dú-A2é-R X\¢ +›%e?KE#ˆ”„B0-�$bB"W‚F™«<¥âFY:å­2‘HãäWH Æ[ȶY�CŒ¡„°†’Qànª8#%.EÄ�8â:ü†ßÌð.cÃmx�•ËÐp.Ãi8ç8‡ïpë§ÐÀ�Õ/ kHa…ñŽxc\+é�“ÝTâ-’”Þ$¥•†H-S/!“|ňC)¿=vX +êbùõ‘ò;äEfWH�KÁ5*½>lX†y¬¨LÔ±éØiïïÈ󈱖,Ÿ%ß |/DîeÀŸ²)¢áœÇX¹•Ëìp žæu!ĸ‰¾ñØ·±°>ÆÂ•6ã¥HÄ{2ðK.‘(”^4ÄÝ 6(»B^ˆ»?tT†wÀ  û ­4‚I9øC´rðG©¥àŽ—•Nø01›ìU–˜Â6YÀ³F%V´„\°ôµ1¯cYDT;ð· +8Õ ¿á9|†Óðcå211<<ˆûð!ŽÃydx ç<†ßpN´óĹ4HXâQ n<€¯É#Þ•%™ŽR¸‰RЧÀSšVq£$‚)îa1É2Cü ¢"ÓñÈ d .d˜�I‘[!9$‹è rJ R*Ùõñƒ2ÜãÓÄ·ÅU/6ÚØÉ•;#ë�Á\‹–‚àgð@Gœˆóðk ïá6(Ɔóp~Ãgx�•sÎ9·rÎ9çœsÎy Ÿá5‡ëp¾Ãi¸ ·rnÃuøÿáA<‰W!Uñ61 ^c-#,»ª–�zTz@ò­î¸´œ¢÷= 2"<"tÍE%WOa2®ª§ O HÈ\]µ´723ìÓÙÓ]Ö“U„¼xêÐîìÈ®#j²;2gqYSSc1¼�£ŠBÊz +Ea�ÞÙ\£§F@Ñ8oM1í]WP»×Ù VP¯C‹�fNíH øAI(½”éšÜ«ÈXœÔpœ£J xö£Èí�"-ô X,6qvï"G?[)“Û”)ø‡ù~Ï¡\ßÇÕ@�y³Ô‰D/i%Ë.ŠC)í¡Ä¿¤ˆ‡ D¢MUâðK™~Æ=ƒ“ÏlדÑ/G%ÍÔÙ*Ükº´‡ùþ�#Ÿ�~A‹FO¡€LºÌú�IB“´“ô‹ÅnU€äOXïïlç€ñ$Ø ¬!Ì€”ñاP ÿQäö+Yv!ÌBôó?„v<Þ9¥Ì-ªá7¢ôóE“}ýÉüÀHôS¤‡ÈqÏÖä%âý›¼·¾ã•óO à"˽?ÄÈWôè•2¹=JtОE<šü³ hôhL~*íÀ8Ï38×›:¿”ia ÞY�Ó×y¾vC‹{¿ +¢ê Û<`¹?ÃVFsƯ[_4si´Rf ­ Å"Ò%à×ù¾ù%ήw +ôB‹z¿§Ð®Öé›ûM�Þÿäé½Âx÷Nî )ê})?Õèbwò/mn¿Q%Ÿ­ó…ó9Û6oàôk•c²O�~>8À·ºÉs°[õ°”)üÀ”/ 锫P<ÎO¢ƒ]ÉÒû•8»ý¨2ð�Iú*H¦ü4ö5{krL[ÿª 4£} +íúÕqß@¤³†C¬$þÚS§ˆýésÐAn½‘ØÏ=þÕP¢‚¶“gØ-Úù¾4YÆîÎÒð™ÉR¦…þ�IºBNùŒÉº)ŒD È�SÊ ´”8üLš^¡Ç½:Äs…"œY K@�Ä€*üäIèô)„˜g!fíõÄôc—œÚXR@g,?T(X ÅŸáçŒaÉæ¼AµsžÐC²+XáØ›2Ð"Þ‚ŒËÉ'}�ç‘$à‡ü8(ë¨ÑA{¨q¯‰s{3Xé¼`TºÜ5æõ#Ë?�$4šì ^0ú Hz©Rð­(ç}ÂvIš�þÇðΑïçláj›¾´šH’¯#hQh#X�è§P ŠÃOÇä?€J¸³�û=„t=‚uß&�­ö!¤óJ›Û�à„âo0CÈq�V²ìzž¯�ç¾rZ]3mi´UŽÉ�†ã¿ú1Ù�*ÿÄ®X»±zç+Wó„çºd^M#‡FAþy!E½OcgÖeÐÈ.ü¨ôXB8?‚ˆ^Çûöèƒól%Ê®=…j(Á†¸~´âYÓPPØY''¼‡(žu”hb úõvˆë H6i :,ç +=6å«¥µ+¦4‹ÝP�Ž9›Ã¬hýê¾Ó@D³®BÁX”*-„ÑøœPìOœ„U'kúÐh+ÜÇÖð¡ÑLœ_¿ …cw@ƒRž s¦B5|âßI”�@a¾�h3XÑè PÔ ¿UþÕ‚`ÊTIg 6(k¢a[ÉÒû‹ ý|ŠÅŽ@ÅbÔhØ.rô;pjho(¢Ù5á¬ÀÐñ¶aH‚YA!ýŠŠÛ«Ê ¢H½OSWVãü±yo]oð‚Ñk5 õŽxÒM™‚©òï>Šü»0IJfàBñ¦2Õ!îNÚOžÞ›IÓû§L„í2 +ÒåÖÿÚ%Yn¿UHŸ!³/pÁè“,»ÞI3l/há8…jø¨ üG˜}Ÿ†�Œž¡»eœ-[}Dùç“*»^� ÆÚÁŒÆE +¦yS«iîÒh?¸zGP®æÛÕO¦Þ?¡HåܠƢ�ÒÁ%JhG…~1cnksÇV;} +þ @,é 9&û(øÛô¥Ñ\{År¬–Ëf­luVÇûæïê E½ÿ$úµƒñþÌÛmmÓb‹$ûþ˜ÞAŒÅO•‚ðßpÕ:„ŒöT¨¡í †ãoà±3qnm¦Lî �†ãÇ‚âY{@"J‡%)Õ¦Qa6W#FZ›dX&)¼O—¯� E»‘Ì™KúBj§¼u%Å…tT'í ?"} Co`TÐ^²$jôó>(;‘µ”(¢Wªüz¡ƒŸ� Ç~aˆe}€ázÄû7€h¾fï¬æùÆ%ü +T,~*ÓBž/�ßéÒùœ­\ý“x…*h[‰¬#䈬 ”z$Ìmgêìú©ÒB›ÊDá_ÂÜÞ9€o]§w4É%ïu¸±,¿é0^gÃx ³¢ýy¦î–‹ ýÀ�€Š}0âÙ‡õî>7û§ÐÎîùÒÙF�vÍYŸÉÓΜìú…µ„³_øQé7 Éô ^$êxß(øÐ”;¨ŠÊ]MHÿ—”}frÂCRz+@ÑèiàÒh¤Ë¾»�ŒD +ô«…" û'»�t›³†ÔÏ9KɧÇJíìU¨ˆM™\-á�VQÏž"=üvXö 5&í)ÓAÛéÓC}ú0 ½• KÚLº‚�ÊA +D&LîÝ#8w Äá¯À³ž0ƒÒW™@¤:EôMšà/ZhO}**î!ÀA÷,ÎÉK Ünàt°?…v(ë F>³¨Œfé(%î« �‘ËB’´QäßÏ ”£qþÜj�ï[íShç�<ÃÖ´šæ_]_9|p¶‡ÔÏ’m5 µIDí#´„v E8çG4=àoòô:�Y3Pñè�"·ÿœç‘"»žÁŠFoá¥C\ÿ âý:µºæ.�ÞñÊù"H¾ûÉü¥J =•i¡­€£¯:‘è‹ý6Áw‘ÞªF¤7ˆ±é“k/xÁè˜(ü^,Ú™„bM—‚7S&÷RÔûG”}wU +Cû�Iº‚Mù*ˆ%Ý€Æ#eZ؇&õ:cmdùW@ ÷§ÎÁžð\çöÞùÎõ›>¶zOL¶®u°˜2,˜ŒÜ-óÒùžÂºnÓgæo×ê£JÀ.'¡¶‚ŽIð¦B5ôG�7 ÝYÝSw×ø™Õ1e×0^‡Øbìûž`ë™XÖŠVcqþÜê'PÂz“OY†¢Âƒ JC¨Ù£@ýçWû}‚¼³�Ä}7¤×S�"VøqIwpÒY_ÙÀìK–Û»æ.�ža+£oðØþÎV.ÁŠÃû F{ +ôð3UvµG�Ûÿ¤IhU@”õS§÷Æùkë3qfô ›ßÙÎý#Èí_Àñ¶*@ò+8ñØŸ2¿Ú#È@[éñk3HÁhO}fÚ¢hÎ7‚l2ÍžÚÕf›6ïxçºÔi¡ï…Tu[Eý[XFý†&ô!’ÝÃ*(��Èe_²ÜjsþÚ´3ofµÐ žØ„bïÕ„Ô–0c²/x�ho�ò)c(Â9G¸!Ù…ùü“'‚�È L>e +;0ç C8é¬$ž>Ã�ÌÚ +ä—*þD�~þhÒ�©lC…†ÅÜ B2�(ÿïĤ>Ãb L4Úä±Õ3pbsŒÚ|�!Ëb +ïl+�¿ +-Þ‡ï­'hÁȵJê±TGå+i|þÈÒÏW©0ü \,ú¦L0RäÖ®:M´‘"m=¶:†lãY[<bKæíËK™ÛŸÀcG² ô3ug²µÍÆÖð¥ÉH—€ÿg�¦©;›yÂuõ£Ÿ·–i±\ó’yÓÊ6�htŒÚ|埗ŒÙ|ƒw¸æeÞ´³Ì›V–Q“}é<)žÙ--%j >(g¤KÀÃ×VÛà±étá¤J %ÀXô +`,Òä•ÕÖµŸ�9›e™¶1Ú&OÍ×à¥-]vm¥É®6¨Pï>r ´£FÃvÕ‰D�`E¢gÚìÖì¡u8[¶®Õ² ›m“'w-ÞM•‚Z)PÂ;êsð†Ò¼«NY3‘v»UÎöâZ¯-Ìí#˜µ�L™Ê„“)˜¾R’™ÍÂ2:ƒM)ý¿‰ m‰é]ešh+EŠ!Mò�étÝî%ñVë!õ³ÎBÚ©JPâ^_Úôv*…A +Ä?aG%�•ä“6Àyx売ž`£²‹8"ÚÁ–˜ÚYG>=Žÿ ´«-šÜûOž‚·"š}CÍ~ Ä¡� �MÔØ÷¥D}U E¿”Ùý:Û¸O#wV×Ì�y>·ÏfÓá{û̳…ñn¾4Ïi\œ&Q‚ð3]vï™83šw:#VÓV±^ó.-/þ!Ãëš-KÐ!ÙIp9!c©ŠÊlDv/ûŠBM®¯JQø£J…ý<e‘âß-£¦¥ùÕ‹‹Ó\]œ ,Œ§PÎ[‰¬!ô€¬�,;Í™Ìm¬—ÞÙ5&­ƒñ�í²¢ù/î实ýš‰#›ußê™:*ÒÃou“Ž`C\O¡ò€ç¾Î7®WÁü_\L}/¤4• +C¥Ê­�$ x1þ& +Å>ðè¼-ð-âð´Ÿ@Á_æí¦kòÎ|‘£žXOXΦÏÌ!òÝG�}7Òc Iì§E¢ ´›.ï&ͯ]¹w1ÚÙ6xj}N[×ä�y¢Æ9:¨1¯S±(쪈Ê_RJôKHi§ÌÁ)?Žÿª†ØNP‚ÑN‚ìÚL•]mP"ž�ÏAûJ‡e�`CR‡;×qúäêB½Ît鵓&¹6{k�ÆÎ¬ßô¥y<´�ÃÇvãü©Ý7}h7ŽŸ›·¦y™_³¼¸Öëk©së¡èlŒÝ±#²¸xvë è0Ïg8Â9kp"н‚9˜2*ƒ�J'hqø‹ý õn¦ËîÏ Þ÷ ÛÍÀ™uþ¯×˜1[ãw6ï|áþ�¡�OúüóU*{…#‘>ÊøÓÐ�ecÆæ¯˜2ÙËúf_uÀrsP#^·ñkËZÛÆÂjöÔ²N¢Þ¿´hÛìµ}*-þšZP®ª&æ%¡^K6É´R;ŨDo Â=¯ã•«“Ä\;‚ ÉÎèõIœûT%µr@ÚNŸÜ[ÌöyãúR&àÝ´Ùµ™ÄÞ;(1Ï%þ%eþÙ7~l=çÛö“8gS•ÔZá€üMž�Ú@´£œ=8;ç/®sã¾�žZÍí/0®Õë#¸œÒ[ONk&I@9›n­ïtÝ´}8~p5’ð3�±Ø-Á¤¬hôH—Eb¿ I2Ù¼N8Ž>²¤5L!Åú(/ü“R;B q}5±Wè1é«Pí$H@1 0ÞýS('6ô¨g=êÙDŽ{rT*Yv=Žß[Ÿic“}ï’&ƒëno›=§­gàÈj8³.S&Fˬݵõ¬Ï< ¢+¦,{Õ|«½’æß-5jè—,¿6�wî5*6iö +@<³]«¥¿Â�Ìy¨qïïxá~ÍÝ™ÏéƒãùÊÙ5tkg6ÖšfÓ2nbôMÛØ5LÊô°= ½;Hñ¤‡$ó¼UMì:ƒ‡v5Êô«'쨤À` ŠÌ›�JõTŠB)ë¯'¥vWÇ3³Uwˆ,¢J‘òcÍA +'ía”A%´“�2ê³–t~+“]A Æn]§:Eô d,ÆœÕXë[}µùS›§J íT>=ˆ&™·RïmˆÖq¸j= ð;} +~'Oð'jôóIb®U:øw引Íg­põÕæ/m±êéA@Ñ´iðÈh"I½ß$ÚmÒüÚ9|rž‡{÷‹ýìA™ËËÚ6ƒ5‘zûJdí³;›„ øiôÎj0ÖYW@; +©˜”^Äêhÿ@ô@ŠÄ:j4l/aví£KBKFn,KSyÑ·;ˆŒ© ¡¡AU¦*EüR¡‡‘ïîáÆ�-æ‰I•‚ï£ÈK>ë.'žÚ G:ç&ήýC¨×ïþ‘d —2=ÜñÆÙ3pd½F¯ è°îÖñ²Ý=a¹ÛêVgeÞz–æ-;“g†Í¯aHýÌj]1­g)*üµ ê±ôO¡J•ú,½�÷Φ±3ë8€lJ’‚ÿi3l-ÒÉ¥Ñ=‚sc?Œ÷«'‘%‚ke‘BÜó ¨„Ö„hÊJ›Û¯ó�£}ëh¦Îo]¥Ñ/ez;§_íà‡#Ýäü!̉ëZª�;³´Ôw‡(¤2‚‰u‹Äz�ŽFšê! !Hä¬� +ç<•bÐ+uZ¡œ;H•=,ù¬³˜ˆÖZKD¿W’O1M:ï 6*}Àw6 Ymeó2ŸÃº¬e5–çKçÔ@´·¤lŠ…hÂi?‘ +þš¿´yOLr¼ó0"i.ûQ$ C6‡Å õ,� �_Ñ&!…S›ÅdÓn +ýú¼²¬5®csÛê£InOÐÂQŠ”ð+Yvÿ…P&¼ª„àç Ã}¹±,�žv'n¦jqHcXÂ9W’9K…†ï<µšfîl† +×ì¡Ñ2mer�×Înâüz8ëBÄ3ÕO™DS B@å®&¤? Ié"5tæòâZX$äˆäв%ÙñÁœ¤èPk¾;(QgÎ1Þ‰Q�Ú `$ÞI”ÚB:¿„ɵ;¤b~­%šb!šlÚWA$=aŸw®ïpå€ñ~QäÞÍd¹Õ*Uno£È½»©s{Yúy£L>Ïžë2pÚìŒ�=Ûü¥ÍLžß³'2l°**h :,å¾Ã­ë=_»·2£Ç?Û*Hd=bɧ͡ g�„ øç>�"^§"Ô>€±x+]Þ7~m;‚˪V_{ éjª‡Q'Â^HŒ×kðÒ䘵þÅíì— \Y˜XnÞ1œ“ ëè#M@ºA�ÇYLŠ  í•ýP: +µ°NÊäö©„}CP9ë€Qì +ªßÊ"ú¸(üG–~>êD¸7�áØ´ˆì„HÖŠdÎYJ:?ÍÄT l‰©�•”†ãÑÆÙ{û2kZ™ÆÎ¬1úi7$ÙìVAkW@iP<ï «œt”‰Âúg¯¨Ò¯fã±ÖàD4Ëáˆ'½téý7�kõ àšOš¼7 Ѭ=¬‚Öª€Ò!œpÞ�dÎP¦ƒµObmÃM›} ñè(SÃú‚Ïì‡'¢J�žh²¯?�‚»…&•5й��EÿƒxW[×îk�_Z6)Ô¯†*¦!à€¬¸`ôJš]ÿ4 +î `4ÒM¡_¯Ã•«{ÀwõOïîßÕ>ƒz58oVñsr¢ƒx"J_‚Yk)ýµÔ±©œhqÏ–qÓÖC�z¬‰©-KYÑ'ìàÄ6ˆYG}"v&MA›¼6šfÎŒvòôÞˆ`v <,ÿª¡çË}:´9‡ WK�†ï)m¨Ñ°RÄû1c=Ë»¸¶bÄf,Í]Ùt¨ç™*»Ú§Î®v‰r+F—Ö­k‹Ãm«ïú†µƒëŸE<š¾ZÛþ—LÜØuèñ�þ€€‰ëÜåýåeEÓ&¸çôÉý/^gÚ÷&ͯݾë8m=©²ë+©¬¹’ˆÚ�xÒvhsÐ"ÞUŽHû©3lÓÀ¡Í¼¾‚Å Õ¢€Î¤xÒO¤‚>'ìFǨÉ^œœ† „¬²ú«¦oMæYœ“ƒ$ófølS .�áɦšg°Ž–�“eâ´2aœw +ýú +C2çú‰Šß¶¤†öGYñ58Ù¤•,»§Îû Þ}3$éJBë D8g(ÐÁ_´ØwÿÖÙ;Ü$1÷+@Áè-Á¬�<Á¿FŽ­¾ñs›‹&ùl*m0ýNw®ÓÌ•ÕAŠw?*”ð3`áøÀ˜¬Ôx¤•2¹Ýǰ®þ çùn�Ÿq+£¹Ç~YÛIJB‹{:ݸ>ó†6Ûà¥ù®ÜL X+az;O¸®¶¦}0~%!¢æOíŠauS±EtöA¼«ùŒ‹ænM>Âü«‘2§^Î!Æ+ÂzlÌbThø-,&f'Ͱý3ˆ÷¸`ôôª‰Û;AqO B9/iv}U +Å~ …"=ÔÈçiìÊf«ÚŒ¥¹3›‡÷ì'MA­¢ž-§�cȲ`l?Œkô8؈üþ"!3wciöή¼‹C¯pÀçÊÐ!Æ,Ëuãe“‹.ÿæžÃ»Ù‡±Ž^òìÖB’z´L›XV†lŒbìë +X¬—µÜ!¾lµÿ¥åDmÅ3»¤éívLÒ"TKkmb|7p áñ( ,ÛÄ„-!‡¸v0ñ3páA†dï€ä“ád´ÎRíRAg)RÄn$¹í;^¹>#g–µºu¬ÍŸ ĈG×ü�ecÎì/«›³ñc›qáfkÚ¿ZÝ´Wï�ÙõE��¾Ç�®¦¡+›¹óâu×–Í_ÚÌ*hm ‹Y«Áàΰpb`>ˆy3æ¶>ªì6}iu }µ®ÕaV¸ÿ5d©WWH²™… {œ>8ÿ6&˰�ew¾uó”ý‹K +Z‚ŽIšgp®óÒù +?2é PB³'é!Æ< Â<ûH2Ð6@ŠÕ䓯RâY8�è·oµ�¡]ZÜûBˆ~ÞGP¯Eþý¥Lo•‰ÅnaȦü¡K©lÁ‰¦Luâ°f�æ=ûų]]4zjÙ#̾¿Ô¹õA‰z]G®Ö”›q¸qØŸFà�„ꨳTaX£ +ee8€Ã@ÒQo�c @0 ÄÓéPÒ®<€A¤&¡œBÌÄ Ð­˜UÓ–C ûWMëØÜœ|±àR¨¦§5XM¡øfY²å™„Öec„'ê(ôL™Ì³Î1}ä­:BÕ¬“ðù6˜¨MKÞjØe´i6í¼Çªß¿î*›vÂólÎkM6aõ¶£›& «�IÞ€j›ÐŽÿ$´¬¨MRïÖ¦ÐÝÕ¦…þUxù»Sxáo15oÃÛÿNî§Žóä’yøÅt,ÌŠ@n7.Kx=Ùº}DØÙªÚœ$Û/« ÞÔñ˜˜`.üW.ø Ð1ËJæ›"ùÒÿ¯\�)VØé<ß§ä›þÁ×UØt’Õz�vóšºuòºà"f‘ò«”îH„KŠÁ£‚ÑRÌŸé#ý!„yçHöúwüHXúû†äÏæÍáe9‹ÐÇ;¶Àd~q�õ½Ü1ŽOêU2èOä|_YÞJSæ‹ûñ”=:íêë!{yÉí¾­³[› cÃ\¬#~šwÓ±œO_á÷Ù^O.sØkç{X*. àbÄ5¼çóU þB?Y…àÒÚ;vÕœ+¼•×<«=Ýz{šðÃ…›•vg©ÀüRêïaì;Ò�ï4Ê%z|yžíúNÂq–ÄÜ +¶5ˆE}·`ƒî¬×ªŸ•œû8ãa·Èþ¬Ë)¨¶æ|…çÔTØë^<|Âes<Ì1ü¨�Ð�*À[_çý~`Æp³ Ñaöù/GäžÌµ7âËxzƒc�œdpaæMHæJøD%ü¸Ïü¬OâÎܲ Ä£e–Oÿð,Ý’‚kU[TÎ~ÖG×n+~r2¹ù=œêÞ}ßq^绯ñ÷ éŽ)ñŽŽ‘ÆÒv¾æÏÏ�Éü¨gsx†OöÂ= 2“3yîãú¯> bÐJštæÏªgaë–ÆÜ +©1ùÍÁŠý}v5ÚuÈý³rz~ügü¬2üÔ³f øYóLÃ=÷GatÿsèÏz“›‚oºã/Ãxa§ü{å~¯ÂÃî»é (š„9êáo¥”Ä?ëÅïÀ5ÍÀà\÷³¶éÉúÿÛç>VÀoÛ€Q¯æ¼XŽ£zìš#íùgm?³-äÛÅq-Ö#~àóóõjBçž×à�‚8 m`<ŠÝ/~°�Yª Ãàèt èë�ìŸ|>®ù †¯kÝšåQÑÇóƒáq3ìk�‹`Ë‘·ÆƒùûÏzœ5ÓçÖÓ{ì,þ¬ïp‹¶ß!MŽöàОYˆ~™áÒB+🵮{?ã¿DuÇßäü¬¸Nì[e5Ïãæ×±¿qÕ«Lƒ+K&É>÷³ +ÜÜÊÇ`ÛYƒýpðK²ê¼�}øY²7Âc:œ’Ý'ÊÜ«OKðιŒû_lH hžâÖãã ûåᕇ»QÂÏ*èU#bVå¼æÌ‡LˆyL9[’´ŸÁ87¿Îþ¯ÀÖ:œÄÙ§!ž{ОNo?3õà˜Ï9Ípy?žë-°åÅãpùŸYHrÚÚ™Z`;޵²hsbÈk§¾­ì t]ûy¿ØvŠ—@�ÔOi|0AÊg<–™àa‰û™ð¹ÃÔ"l”´ëÒUž{•-{C="Ö–€�P.Iõ[„†¿Šàд„˜[XLŽA(¨_HóÛ§:[Ë^û×¶>�C°¢;�· +÷ ÉïÑ ¾Ó–Wcw¤ß>,ô/§é�Kþ/†‡ùþ$-3ÿ& /<.Ÿw ü»?Føº@ÎZ"–Iwä�àæªýr€zvÞB’u 3’íë§ÕÃ�‰¥Äq˜�èæJÒÕ„v¼ÅE³U�K ™Ú�õÓkÑÐ>û:G4¡t§"Þ¯ ¼ Áò›†…îpíûð~w?zSHšRî-¦°®'ßµøú¾u È’X#&ýÄ�&ÿߎ>a)î¦$c ´’Ó“œWL…sëIóÞ }Ùh±›“^DZ9OÊ r­ÞŒÚtOåÒë\åÎ9ÑœmݪÛZÿ%[51ó›£þ<ïÍTªg_³ÿɹV·Ì»O»šw(®{Œ½ |” †In ¢ìr¼U,O€|ÿé6]­£õ� ZíÌj +¥k¯ž‰³ÈÃäžÿÿÿç·#Íd7]Óaå—ÄT®ÁÓº˜–MëókîùM¬)ŸmÁÔ‘xsŠKힿÆt¬ßÔ³n.%ŽÇŸ*f™ r&h,fÆä³Úkî´âßÊmûïßUùõ½òȵva›ÉæësŒ;A¢ûj¨Wels¿›‹쑸§ •oö´!>BàÝj[ŽÌ#¤Ý*ï«�œÑþjdöÉ3kê&Ï–0¬éµéX�ØÛlh¨=lÔÚtëLî°Yz,«Ž~”Y·ÖÇL}Á3žÆåP,ñBÝ5˜·œf”¢¯pÇRÛ~£ˆúLb=sy:üóä9�³ûúw}“kHú¶ã½µ: <¯Óxïž×62~~<Na9¥daåã[ä· 0MºX•ø;ïs¢é¶Ëü™>ü½;ïÜõ ¿šp¬\ÿí‘¿“æFè/èöu¹Ûô�Öãaç‹ýÑ"·=1û?�–™,üsh²'àåÆu,Þ%9Çtþ3úÙ¤žÚ¨ÈtŽ$iäAƒPø).þ C‡‹ŽóŠêC¾»Û¸“ ?ñUf†Ý¾eýŸúô3~ÏOÝ(…£?:9UT_UÓƒ(°bŒ!¿iµïÓ'ü™w8lÔø¿¦Ã9h‰w¥ÃŒÏÚCŽW0Ì×ú=ê7ÅЮ°ÍìÑÎ)±¹&Ä樎­ 6û¼?šü´ÇãjÊjûúœ8Í}£f5{|É ¸�œ½a [ÜBÀ­Œó8®(4T=ïøè¨µ‡Ôö9px}Í·õÉÍÉáu8Û×^.Ÿ�,÷¦a[úLp‚ ?³Góýn»:î+â”nýc“=ž­Ò�’ŽCßbÁÂ+�ó÷*ý?êÄ×UŸY—…{ÒEŸß! \{ÎaïÅR|Êg*é¢ã¿ 9dkÚ/'�É.KzÉ™7,žÍü@æž³aAYÛ;Ž;¦z‡Ñâz¸8Aâz(£8꺽]ÄD¾ÿ˜¢ûe^º¸Ý¸d$�n¶GDMçm˜txO¢Sbäœ:ƒ·ùugÌ)ãí0:ž¨­ÌEí‚£F=Æ4JÑj$Sç6°Ò éZb`üWÿß›æÿ~Ú è¨»–˜È(šÁ=•Ž>Ðð¶1à¸;“´"ai¦VþSbw¢Ì¾…(N¸ ˜öá +ö¹ç÷ϰç‰%7�Èû)&6.©Û$é%t;r‰¹*8ì°ÑæÀi£"›ˆ|8]»ÇÃÞ‹�ÍXe‹¢Iòn5#´¬,�*±O¬›*ÓâˆtJˆ¡((„þó¾mAm[ð¨ÃüÔ|ä�jmÇ.�=ˆœ#PѬ}Ñ@ò€BôxŠ‘þ‰aôÀ¹¸h¾‡ÛÑ‘)Ý‚Ið¼™S·•\ÏËî~ntÝÈsE°h¹ø¦+¯º"Îj…4À�ƒFÕ”¢‰pP/îý¸¨Þa¡×f¿Y§ø­øôÁܹÀéˆAV™W¥¢ —¿üß”±¨wºáo¿54ã@=’yž?VFqœ!+ÍÖÅïWö×JôÝDã̳CsØ‹Ïl*±še®ZŽjÆ.6(ÈK:²`1Û µí–û”Ä`qÉÙm�ŠÔ|úâ¬ï�/?úè{&�F—7-þ¤áI2]vX™Ê£ÈlÏ…;Üç’ È3©¿•�q¦�ËN&6ƒ…ð¬9&<£ÜçÚ¦e"Èñ/ Yn‚›Ž +ĉK@ïÅnêX]Å·Ù`tvP›ª‡qÕ ¨Jžl‘=•ÒÕ- O­ëf™í2ö§€.Þ*ä"¸;!¨è +Ú Kà?©ý†T•ªqŒpoÄ8þpœ‘fû�t–F¦êE !3ƒ 8'ŽXÈgáú`g_QMë;2ã¼}¢à˜šè²À5_=¢³'AfÔ‚ˆ�¸‰À4¼T¥©:é­¦èÿlãz¯¼Bš[-ú˜—A�.g+H)ÐO´^šðq|Óô‰[µÅ`óQ½Ÿ}àÏáæ]ĉ1}3¢fUlX—V?þâÚÝ }gT¼™>#HœÈŠ�ÝÁ Bí$!€&‚%/Á H¨Qe{3ò—LI†Ì�×ó$¯”A¸¥íFî0¶‚ó/§´¶-csíO€®�œäýâÊ·anw—/é»ò?ø›4è dÂ@R,2§_÷Ø$Å+(’(k&ÓRø�`…ІÄߟçñçyžú<Ïó<Õ=nÀ½WdaFYWôUUqîXU‹¿8°êR©ƒ‡ä¹ÓݘnU9“ V¾+îáQ±X“Õq¹¬™9×s r2ŸàƒTø hHœ$ÂTÎE«nåžÑLß�xªÌü»$×�8.”5ùç$®l¸PÖ$äœÿoŒ‡seHæPý±Ëä„9‹·¬ÉÃk˜ô8‹#%gªp€,kЬLF®ÀŠt­¾tçÝ+�@0ØÀÿÛ½%K„g2ÒÞ¹~î3y`˜Ž,>v1Qde2äñ3™è&¼ … B*„ƒ >Þy»KŽLšJtoL+ë’À/gÊž{ÁÜË-ÞRI�ÔÁMÜéÎŒ’ ²¾f;¿U0ÄÁÎM¹“¶�]ŠÕ�P¨ËŠHhH&ºÐe=wf�¸` îySŒƒÂöù¨œÕ:|×(:)ÛØ“:†ïÔv¿k vžM'œŽÝ:noŒÖ4Uvkªãù¨uÓÊöW]­Ó�Ť5mb›6iwM?êÚdZÇ“J9_åH§ôWã›®¡ØÆnù&»´Ö>Û,Ü‚»½e0H ¬`Àƒ\aÇ)P€`Ѐ + ø€@(ZäÂ0’厀¤+ÃRIÒ´a)Z•–Úç‘¡µ$°Ëºd( )˸SÅ©€Þ&îø=>}–­tì­©ÏB #�Ž%mÇݾ)iœðÍúl‹Z3©ó=[jß;®Õé|?'íÝk¶Ž#ŒÖãkž2Œ®=þ ¿·õÅøÜñ£\%�› «WøšÎg9J¯ýà¼ÓsIOÔžÎé“íqØ_ŸuÓã|յǣcû,ÏHã|•ò¯B×(¯S> å‹Ð­÷;îînZÛt�Ó—ïø)þ¦”å«qRÇRVÇÿŽN˜r»k×ÎpJQ›°ü)ŸeÙnnwü–å³Ð5 ·cwך6aZ›¾:aw–o6LÛ„é?�ÓW¡s¿5»;–Ý#Tâ@,0 bT¸3„\#ϦMxºÖ8ëα¥ì;×v~Jñt¶ß:pOøßµ†g›´¦MøŸç;þ~~_kÜµŽºj­ãù($P‘‚(bx’Ú('|ÔzkØcÿ‹V÷íhó-jMW[ŸÇÿ~îÞµ¦>gÙ®5lß?Jè˜RÛݳëó‡Þ®°‰îBèï¿QðIÇé‹Ð[@ +(BZˆ%°çF×1ÝŒ$B6än l”G×FpئPþœ ¥œ³ÝòW·ÝÒ-}Ý0†OkO÷®_ý«}ÔRk3­ll3©‡N¨ÃZ®c ÉC@pH%CI;ÞÀ@†Q@ †A0 €€H @Q€1 G”ºGô>(îó:�ƒôtm('ÀÄ¢W6s<1Têz;îœrÒeù¾Ê-ÞrÈôl ©vE»lAÎ��\àîÊ‹Ç_±°|›@?Dó²ÍŠÊNL⢠#ÍL)�øÜtM¦•àV4ù.…ƒ¿8wdhÙ�m�`h€‘Zå’_kÀ›Í¤ +jÂL3S„E®�Ü?T«d¸lî/–&bÀ¦UTHx¦ßbö‰¡+£ºèëyT>ôýˆ¾1c$ü! 4…,WºÌ 5ÏDæd¯�aÈi¼¦Þ2FALº„€|–ß�Pí Ý»´2æ.lŸ8á`NœËªÑš#~ö^„ù¡ºöö¬òa [/ˆ{Œ3eýmt�%,gDn+ŸÿF�\mà´*Q?}Š™‹ÔOa’{©JJ ¨£›ÔA˘-Á�…|¾Žåbˆu?7Œ¼oNoò›190<ÉéïÛoSVH“\V(ØzyWHÇ•nbòM|N�ºà"_ã«M:bº‹¢s "B}¿½sùý‹®²œD#�åÞ"ÄLÏMõº tQèyQE;jnà]‘ào^IKo¦ÌÈŽ­®#Ô#³HiŒ¦A¬ÛΉé þGϤ†ï893Ê¡09‘k>üß ®Ñ7ÖòMŠ´Ù¢Ãs:ç߸ëˆÞ²;Yù~‡•)1Î †˜Û¿õüC®±|(ˆ�%æ×™ˆ?Øÿ•ÂÇ ,�  +´Aãd³"y<¯I6q¥ñ»ÓcúREÒ»ÐÊFx§û¬&”J°Þò“,µ�åQD;òÖØ Љ!0 ü•Ìüî$îˆÁ÷¡dJžfs~’²hú4cóáì–§e„bíú�T½¹+ìÓ6Àâ‘:•Û$!#î1ˆõ«Í³,/뇬Ìü2…­§¶yàW6n“rtG¤;=€�úM;[ª°Š˜žßb¿L]\’šŸ¿ƒæÙ%›(¯œ’Ñf‡aMá +£½ù´ámK5ÙìAF—÷é¨kf<äñ-7�0&òÕyùGìêpqéC?Àá04å"†`Ñ€×'·~»ü,ýªŽ T_åÁ[Höâ/ÛÜé·k² ާûRú(†yTá-ÝÛ dåDn�2²¡P|yBJÀ€n•ŠÀa¥”ÓÖéq¾7w5p7±ÃÑ„DYôTË÷´-U½3�¾]J‘@-Þ›kƱGšÌ?gà¾UWzw„Çå:óò2Ëœ¢©¡ò®Õ”/Â>,ÿpŠD�d¼v  Z³òÒÖùÉh~GôÄÐL²¥=¼» ïzÑ6gèÌû"K¦àOÉLšÎ„F» z…þëx¡ážØ�wÑIÄçìHÚ¯…ã\ÎÈ'Ÿ@ui�ž¨6¯EG‡HE´©eG‡÷îPLl÷2´Üol¾¦®3”}�vÈ$J-Š6Þ©ÈÏ}¦¦M!%±èÆdQ@øŠÏEH ÖÁÀ¿Á<³üº»ÎI@õ\˜fm­ÃÝ�µ ¿0ɧ4†ÞC1õ1¬ÜÏ$€Úð6 qûßÕŸÆYJ–ÏB4¸Æ·¹áò{ xÌLKôßgñ˜ö;ÜÇùÖêQUá¾={Žì)‘ ëHGOoÇÇs¬‘ôP¨¬Ð¬¢^!ѱ{T "7–Ñõ¡&Q\±Þ»& äÉÏ?ì>U”#\ªEJ²$þ]¡–çëyhd+8å©n°ul¥x~pHZ" KØ TWôU¨Lt½èå6¥0xÑ¡Ìo#_v$t­ïÑÖw:`´dÿN�¥·rG ’„PZX†Å9EH(ŸŽçoCÊšt;ۆýiŸÅø)Ñ_�ô%�oX‰ã̹ý` ÀŒn\Å•®Ày11�N¤ù =%ëN fk WœŒ²OÙ)ŒZs»Ì)�Û´-lOïåjòÙÅ=S´I/m6Œœš´6Øì]Ä3‘]÷|ø”¯óRm’—¸C} °O!£WKah³õøÊG)„z�(_Þ‰Z'aÛ7{<ÓÁ’ï_ÙdмL–g�¤ŒìÒæß#|þ®…× O�`:SHb +éuÄsGÇéîNÚà9 °+Žëaœ¹MaºSÐâ$Ë…·ÁH2ôP”P (<= §¸Ö^‡2}Rm/¡;­7~†.(Ôéª}ÖÅÙɶvÒé”Ï'—ø/ÊØ�™UÜÆ€×x}-æ¸u.~Û¬Ñês£Q€h—+"fŽÅ¸¶gÈi¶Ø+¤‹¥*W¥�ŽY`ŠŠDû`-|üÓ*2°Óx‚0¨¤Ð,VÙ)#kÉ;í}â¿ìÓ�ƒÆþž²�´Lô4˜›N‰¿Ì涉‹BöŒŸO,� K%þ�ØÆ“®Ò²¬p<€†ë%6ƒü§Ñªº¿ÝÔ²¬;‘ñ0¿ã3�öSJ!,™8)†oë.äJåÅC$w-$Â$,žx…LÔ3àg£)�¶?;1Lv¥Šx¶3›n‰KÆÁ–†þëÎ¥¨ wÀ˚Á»0qZZ*XPZ––b«†aP‡.8_,jÍE-ljnáØSs^„H.ýècÞ€XøTO'Ÿ Õvsù×A¿>Ð/}5Ø ñ+L„eu“g·àãig1Ħ誌wu–ñAÍR¹ŒcÇôùkúXöF\s_bþÕõº}aº3–`;¢±öÿ2”Øy›VòpË4]¯)Ã*_¯hhÒñÁ’å…0Å´«¦JÞˆpl%7›!_¢ˆÊíjïÞYG!ƒm¹<1©ï%}ȤRQùZŠÒ¢Î0?9â˰àÊ¢%c¼»¸Hî'U9ÛêË@?Yúħç\½ «Q“XzÕE‡ÎhSÆ2‘¤Ã„D¶Wµ™…V‚—Ó*XÃ5QeŽgX=ä¡KüÃÂyU—1fB ÒÕô3¤r¨N§ÃR (fùF÷£@g£I†ó¸Z¹2vàpàÁŸP�G:b¡¹xeäËà\#°BËÁ²»¡}Ïñ1ðw.ðÂy8ŒÞÉÅ 5¶67ÒF/àûWÜÀú§b¡ æB÷IC ´Ç1“Àƒ¾äÀ“LáG ø=ê�ÿíZ šÌT{‘,žØ¨~6Zô!s–I”L ªúª®+Úï|4Þ\õS¨Tßô�#=à £xvsI9z%¡�‰+À„6:·õŽ0V?ëp6Ý{§­ä™Á‡£û6 ¹ª7‘ØuUþ ×ò FÀoÅMàè®LûvvŠi/,ðº^,òª”¹wÞ Äµà¬-C™? pgŠ…jçí–_‚Q¤+°âÄКÚý¤F\œ¹VaQÇ0&L¾ƒ œÆÉ@ 6Sð.€¸3èL�Ü  ãÂàY%£ƒ4&ô‹Q³D�3 +kò–ZˆŒ;J¯SEÊGúã¼­O:-ïÃ×¼½IÃÕ›ª1³]ù\o"/S0(Sœ¾R&ú8ïÌb^&Èv;�ÛÃíLÊø¦ÙÌèŦy'·jèO\Ýüédl.Óå‹8ìŽõ—@ª*µ6{žÖUÑ +ÿ��‹Ê±yD^vÞåB±Cq¶Žàš¬ÞŠª/±º\Ë'ÇZ:„±álPýHÀÒõ·%—ø7ê¢_*2Šèô‹!ypã 4Ù ìRÌþ%;s m£[Î'—,èÕ=Õ­y«˜×aQ’ÕȦ£7]Öõ-ü¥TªG‡Q¯šZçÿiNpZ™v÷K5C…êœy0Fò«Û' �W4Œ©‰ =£Y¾ž%æ(–¾¿8‡-™ê<¾œb&£€™²â0°t\Ï�ð ¶•Fÿ^*€ÿVXYvšiaçAj˜Û2ƒàËõ‰Ñ•GR¦ß ‹F1˜õ(v°‰¬—a@¿C3Û‰“_¨äÃôˆïxÁ3UQq»¸áw¶�&“¸í4y±ù.7BÀ%°ê!ñ%rk¨;¨“Y_?Ö+æ¸0çEðôõ¤Hâñ†P!sŸôÃÇ[棷BúÉ LQ…¦hƒú±uŽ ÛÑÜJD çâv!úÅá=IºW"Mð0/–ϘídÏé=>1¬¥ñ_µìCÊ”\Œ-ÊÓ +£ù‚W¥9¥ñeBùIo 3ÝPwp(Ó�˜±Ñˆ2;i²mQc 5� EüŠ£�ÒÏÇ­ØføQ‹�¢(÷¯„zKž9o\º¸ûMèÖƒËN¦X^EíV¸yQZ�L3_lŠ·WÁÍS¨•¬�ôè”Ó?4_ CÁ“èˆ^GHº£UªoÈÆ… C=KSIYì»pÎëPäpÞ•Ä}é"Sžã¢»%¢8t�|•«Åtð‹P —Ù>nNŽÃ †XÉî Óî ˆØºzp/{®ãZkm4 }e–¹!Æx–…�3ˆªk�LÇba_Ÿ(öXág$;WzÁ—wŽ´®º–ý) +éJæ\ð:�á¸b°yÈTÒ99”ñHL'Ù·­¹n–´z¯N×ìÊ-.ÆÄAŽ^5ãšPq‘Ib 'N­ ÝL‰š‹»‹°E¡d\fy; ›’A!vP1‘&gZ.Óÿ°gÉëu;V`0ör—î8ÝŠ„á#r­öv¯$`$à]É2,rtÜF�ñÊ™ð…¨ÁÞÒ<EФ:;` -µõ\}KcçñK%‹ì’O8¹YÜ�ÅéÞÜù t ý›Q‘ehÂbÚ9°��EÀÌ€-ÖòúF6¡°hÏà__:Z³|<ÀRÈ�üþÅ"׌Z‰Ï°À¨©µ9ðƒï ¨¤°‰s ÇpÏ å,åA™Ò­Nã¶wTC� hdô5— ‚ aÞ/÷9I|Þ«U/É2Ožôì.áV,º0MEâß}ÒáŒ4cƒß€Ïi ó3¥øBR’H³–Û!²SÀše:rpYÜ ×J8礘ÆVN%Ø‘,t{rÉóÿ³ƒ©/qF*Ù\d*)›'‘YÔ7ˆµ Ð jvj2ìxS¶N…®µ€;Ù(#C“ÑÛ“Œ¡é†8�î·ÕÔÙ*jézH©aš£¿.à.‚7†V­^¢•ëï'?:sߘZ@$Ò­öÕ|¶Ll`èˆÊÆîdp°«Ú ˜µí×Ü© ¢›&³¶Ò.º-¾S/EÖ«(ù1“¦ ¯�‡È‘€ßAM�™%¬´€esÛCóâ«.rÝ0cL+¸Д镄ÊΊZ2±Íqwríj–(ôwT“™9`Ò* º¼Ý]ÒEÞìÂhÄ£ÛŒ +¯§ÃèÆy ±‚Õ$¤ªÕ`« ½Ö'2wd‚Q ÉcÈ¿n}Ï +Ûµ¾·@¡Þ¬œkŸY8°0.ä:ï­µÚi1]F<°€€XÀ0Ìj1]†Fg¦Su‚3g¬…1¶¿ªWl_ùþ +oü+mmþU¾¿²ù®tþÕß_uðÿ +ç_mŒ?ÿ¯®Æø®êþ•ý_i|Wÿjã�‘¹˜+wÛ¸úWÖÖ_Yû+�«ÅtàÌtÿ*o]…ï¯2¾+»õõWßÕ¼³C»Ý6ff_ãÅŒЦfŒÌ�¡Ù]Ød%9šŠg‹g¦×ÎŒ»­�¼ßÌ7×�ŸbÌé_®¹Ý{ZÞ¹þ…êDàÔϵýZ÷¶ýŠ8ÈAÁ©§ž|vŒ«¥ÛÒ=9ïF³v¨N4õì³÷Ú§îD³ÏvpöÝÁy78uW›]³'Èà|à� "ðÀ _ó¦ÙÞ™ñkº]ûòß¶~Ws}ÕZSû¼sa½A¢žož©¥Õ‚ÏmW»ô‚“FQl»Õìjub®3µ�ëêï½wªÿAl±Å¿®T÷§ù‚ú‚VƒÝ‚û­Feúéþùý¿YÊ+x©�¸ckëå™Ú‰­­¶Zð C +LÀÀ‚dà(‘¨†¡P¢Šb}(Ö‡B±>ÖסXŠõ±>Ö'â¼²>ÍÜ͵®wöŸ"SkT'Bu¢NÔ‰P"”…²Ûn�Ú{ÓÞißÕ‰î}Zm‡ªÖZkuïP�(c�9Õû+Ýjw"mÿ)ç¼Cu"‹Q•âN”³î—f~cZëÛ¯=Jƒù-~écÌû¥–[jyÞ¶R‹wíP�ç�sÕߌû¥˜bÐÒb®Qœƒ�hkºk¼Ñ·ï}µ¶ôQUÔ+ï¶þoöŽu~ËûÍ·rÜ9®´“¬Ä +ÖKaÉÖ…lÞÊhž@ÖWb}¬�õ±>Ö× +±¾’™ÓØù¬ŽÃÉh"Z+$e£:³ÉP/±El§ºù`}32LÍ*TÈ•fg}¬�õ A !|µ,^g½óïCX_ áhb}¬¯ó|°¾ÎaÎÈ0Õ)u®�î¹~¯¶ÿÝŸmÖšÖ­ñÞÿØÖ½çãþ�í~LùÆ{W¾óóz­Íš¿µzÓ]·®»^»÷Üs�÷ãª1ÕÛöþ¶ë|)Çckõo»ñ÷\ÿ¥Ù^m{Í»Þz÷æÚ~Çõv{7ÏYó͵Î_1ÏÏ-Þú³¥Xïý¯oÅßרâÞ)¯qÔµàÓkmW2«¦MÀPƒdxü4sÝŸõ±>žóß«·½µCÙ]9µ¶CuŠÁš¥zæïÂ�É{å×ͳ͹æª;­SÛXëc}¬�õ±>ÖÇúXëc}¬�õ±>ÖÇúXëc}¬�õ±>Ö'êóvû=×ü3gl{ÅSÿι>ýN„íįé_¼à¶ ßàÄ`'zœ/ÝŸé�X×JwçÊvf}±¶�«^�nÛÓû÷ëíP¶s­;îûR›7Åà¼WãÞiƒ™b};Î9Sü÷ùÛçö;Wµ8_Ê;TþM­¹9ÍÿàìÝ•ñßÏ®û ½vüÜjÚi~ðñíÛliµÚ‚]í; ,@jP� X€Øàç¶zsÎóƒµ^iÅ÷V|ëÅ÷V|EµÚµfšÿÕ·ƒóƒOq‡¢Õ~sÅúñóœ5½ö;ÑÌÞgþ· ¸Ð`H.×!l7çv[p^›wµ×ÚZø1«¥÷ßv{õw|5î{o~¿sZñÎ÷æŒsÆ£æÍ]­âZÿëæõ¿bŽÁ¹yýïjwßz¯µ·ÖŠk­÷Z{kÝà¬÷Z{uµúæ{s¾ûfjÁ×OüSƒšëPÝXƒ“ƒwm/8-8qÖUߌqƺë�­œ»ë=1·œœYç5385ÎÜ¥&ÖzÏ=õÞjjpj n®k¦Wó¾7ÞçÌk‹ñ¿½kÌwÆ;c´àX`á¸P Ø>¹µšk®¹æ”ïn„‰µ¦•D�(šñÌtïÊóµ½Ó +úÐTó~¿Þׯ+­sWœóÕÕ¾í<×Þ¿×.cVƒ €a‚ó°�ƒXXA.$à¹ã~éÆóyß4§&îoAL'²Ú>Ý—^ûÜRÞ‰æÎõZ{¹¥üíÍœVû¶½/8ííD¼vÚLßÞÎ+í�è¾¥xÞ[ñ¥ØRüó+ˆ+ïÔ‚�ëŽïƒ3cpò>ÅZkL/8s¯ÝÒmW»¥NduþM9h¿>}pâ‰óÞ˜f°ÍmÿŸîI­¦Ó×ú9½�ÈÒ<ûžVS<ëϺÒmk½—v;qÎ�;ÑÔ|Ú©¶·Óý5gª»ZßÜÒ¼»œfŸ¶› „™¶ã`YÖ×ÁPØÒ‹âtOü ýi7ÈY�õ±>ÖÇúXÈúj¬�õu¹ÜëcÖY;ŽÖÑnÖZÓ}-8÷Ók-º-} ^�i·ô‚sãz÷cº§½¹kú`G›¼OýØZzA»­Å™êïTƒß»5ý‰µåšî‰+Æ6[Ë©µVßÜéÛlç­9ÌC"p'·³ê‰CøïƒÓòìšÕ‚ Þš&B�ØîÌ58ÿЬµìD�?Ïöv£YE|Ú÷ÌÓÖ‹âÎÞ;­vþÿ‚v(þüëå6w´i1þLíÜ;ß‚(¼ïÛ�©µ(°«¡¶ÕµÉwF=ªg\¯¾\w£YE@‰~r ’9}‚ª�|ëNí±ŸjpîKñ[þNt78¹­·V©ýšõ°@.p;_ßk¿«ÁÞVX‡¢Í··CÍ®6ÿ¯ý›©½Vk¬58«õ4ñu«½p¸Q´‰²ñF_£ë7­$Gœq8œŒ£¯Ñ…Ë‘GÂÛ\.’¾F—ÍR .[Ž´‰¥¬lÝL0ãΕ»F1VÞn0›L, q”_# v{‰"­Egµܬ„šËÒš G‘N3dÊ 1bi“Ñš«F{ÑÒkåHZÚ`¸[IG®g‘F»¹H`¡-È€‚XÐÁ 0t$ ‚p‚ �–è1_8Ü‹ÒJÒº( iT, k2:âp�Ž¾Ø¨¡ŽŽ†Š4í´ÉæÍ6û ”!š8MtÍ·'�H-'{‰© Êh )+m yuæƒXb c¢GÕråhk9‹’ÑB¤µBÌ|°ö�,ÇÝmÎÿ·n~-Ö¶ZtJ—lTŠ$ šÈÁÐ æ&+à ²´ÝJ¥H¶r–¶¦à¸o²qœ$ˆ4\íæš8ÍUÚd#nE¸áFYéæ5H_¶Sî{Ú­q·�¾i4Q+‘i{y Q¥†h iª›,FÊlaG Å\²˜)ޏÚM$Òd°Û¨z‰­IV†¸ÚMT©È1T�@3²–$ˆåm2Z™»Uš(‚ˆš%”2Z˜Ã˜%6Y?�J -W:�f´Rl1¶[Ùx›xMÝÜMD£‰¢&¢,ï<—0e_ÿx»ÑJ›g QbI)‡%Æ|Ún¹«Ä&A”up˜ˆ]a _„L[ï¿¥K6i­pC¬…´ÐuC‘£Ó(–¶“ë6*à š¨�™%LD•2ë%M– e´pÊJ[›Úž@©ñF»‰ÊNƒõöùkpò׬Ïv“•2ò¦Ê¸mÐÄŒ}3@ªé\ÐA0`X_ji¹Œyuå%-ÂĈ<…âÂÝFLDÑr81ƒ¶n4SÆÝ6:ÊX‰,ã%Š´bI˜+±…¨Zæpeîbö¨Þ#ÅÔì'Ð,Gqâr¥¨1Eð (èA(À n„ ëë8•–á+,ÅÚ0òeËÝB¶„‰¨Ú­,o­º¹ÅTkü$�ð%¸d4ÒŽ#m)j”¢FX†#‘†«…¤ƒ¶v+ÉœK‡V(³¥¨1†"XYËu£aXÉúÈ1�EÚ,˜Rãò«KMÖ +KÙ‹4KÄ�)Žªií âôe}7W�Êp¯Íún8’”Ñ‘œ/ÙX2KpÙÄ1Ä‘¶p,g?�^6‘ÖšbÒn¤LFÎQÄ…¸Ù[ÓÞœõü9_cxk•c8û‰?þ\2ËÜmæºÙf¯i;¬¯ã?�>Ìg4L¬’”¡Bª·�Û|wÇÏqƶnÝ{Î;g�íî¶ëúûíœ×šýwµToŽmÝÙr[íÍy×·ö7þïU÷ü9뻹½™ëǯŸß|ëû÷f½µt€!&dè€N˜-Ñ3-~Oh>Æï ÍZï÷Në|k¿v^ã«׫­ ™&®Ïiæ·ö¬ïoËÿVË·µwg›-¶ù~ß}Û½-{ßžŸÿ~«ïÍý­~‹ïÍún}óýû›ï}i­öÚçÝjüö­'ó_íÆÕj®y®ù{}Ì¿Z�mݽv®÷Þ½â­÷ö„æÞÓÜ[߯œß›-¾6ç~µÍÕÚ}oçuk¼÷_}õ¾;{BÓfOÆšúßVýUg}÷ÛÚ7í¼f\sϵòmkåÕ~ÕsOhÖÕ´32¼`0Óv.U‡›".U—€\˜€.H 'Ìïb¡àên+­ó¬·æ˜oüo7®úù[í1MÝÿZŽ¿c~iÿ�wÝ:óÏ¿5ÏÖ>­ûöº¿òšíÆüþ?Ö¶öÌ-§UóËsö˜f½:k½yµµîÎ{Ý;ß�sýÞxc¬/ï¼ßjŸ[�ijm±î{[~ù×Ê5þM+×¼n~{Õ™ïŒí¿Õ¼ë¯|÷¬/¯šó¾/Öü9ÎõòמдÛcš—Çúï_l¿îloו_\»¾�{B³w�if{û߬_cÛyÕ8cžñµúµ¶ÖšøZ�iÞl9Î{sü¹o«1Çzÿæš_Þû~M묷ﮱ¾x÷½ëÆûyÏ6ÛKëÌÖZÍí}½mÝoÿÖÉùç½õÝ÷³¾ü;­öï®ý-Ïœb�7·xïÛ7®\÷ËëÍ»Úýö»'4«Ç4yίi�›c\oµ¸ÖŒëí{g�3Þ½r[ñÆöêÎ1¶Ûölo®”÷ü�_Û{õÅÙzB3c�i~µ”㛯ÝÛ>߸ö­mżîÍ¿ïçÿ®ÕzB³~¯YWŒ­Õ•¿¶X÷ý¯sÎüyÞõÚÚyïc�ijœñ¶ôùåyó­±åºê~»æßúý÷ï~y½˜së Íkõ¿'4í¶ž ÌÎÖš]ëÝóçÞ5Öx_Ë-¿öj»¯¶ßmsþç»jloµ6ãl{¾;gOhâŠ+æu¿þz«æoq­µïº9ïúõîûfÛ5Ç7klkç]cûß®ŸÛ¿ç\oÍ·^«q½Ù>ÇÛš–[�iî|?æ¹îÿ×ûwç9÷Ý/çÛfm·îÖcš[¼éÅÙV‹oþýÿ~ï½?ßü滳þŒ[þŸuµü¹½™gËqÝ{ãÇœ÷ŽñßûÓü·´Î‹;­³VÛmݸ[œëÖ|?®ýö¬yÏžÐÔÛf^uíôZl/¯õùí[g®+®ü~Å´jží­×š[Üÿßâ}qÖ�oþÿ—ï›éÝs›ûß[»Ç4-æ<×ßv[kuÖ�÷ýúx?ß´Î×X_Ohò÷˜æ¾ïé¿;¯ørËëßÿºuý×øj]3ö„f¿Ó¼ÝzpÿªqÅü­½µZ›¹½Úrþ�ó^µ~½¿âºëeÀ .ì­/Í7ó¬õg»¯(�!W[5î־Ɵï¶wg|ñ÷ x ökñk¼·¥<×ßßÊ-ÇÙ:xœñÛËmÎüZmœ�¯}ZñkÝ»þûÕŠ fÈ”�ÁtáGèRu]…�–jÖY;±,EZ¨zÙH¬~}ØfKÿwžzS2ë�Á„dðANÈÿ¯ùëK+WÚLj2Z­$a·Ï—VPë_éý«]u¬ U³âç{ïlkå÷¹­¶Þÿl÷ÖÙæLkíwßÌñÊF$)Ú.È—Mĉ£¾!Š +Cš˜!ÒJl Ì|˜ÓaÂÄ “Ö…©Ó-þJ« °�œ2@'¬€'üεÚϼ׌¹Îúî½ïþ¯uWýVW›_뺿îœw®yïªëî¸ë[ëý]/­¼Þ�sî½îÇüuæÿo/Ï•w˜kºó~üyg^yÖù/ßvß¿6ã»÷­ÏwÇ™?½ßï¦uê�i�–Þ‹ÿÞ�5ïóß½Óï8ïœïÞxs}+ïUãŽ;·œ_Z7¾¶k[ó®<㊳æ}Ú·ÖnËs¦µË0™vRL‹ÿóÆ5篸sýù«Î7w‹«}]µÍ¹v.@ Ô×…žåA�Œƒ*e ±ŠŠ#<dȦâ#vÄ(Db›Šø¡°ø\†b4u““ÈÂq2Éd@§OÅçTBÓÔ&>¤ØÇÄlT�&D½NŒ™Í‰)åúDL_·±ÈœL#Ì&OÆÁ3‘åE«Ä1-J=Öy(‘&—HM##�¼)£á!’ìcé�4R$¸(æ!2¢ô‚bDZ\ìÑ+÷Ñ0ê0µ`"вÔ}P]�}ÝÆ5âBSh$4V\E¨–Ãè!“Ä% /& ˜G‰¨äEÉB š‰Œx ܬö’ð>Œ†Äån�DÇtÑ‘è!nŒqJa„ +…»ˆ->3FXp2ÜçëvŠësÒTX|DZ#Ïg¬0q>7Sà< ,�+Zx2/ÍÇ#‰=$`'™!N®˜ +Bf"’€ÈL…ð¨aùp¡ðx= DFäe4z(™DøáF«hxøº‘gC怋‰ ¦ +…ƒG)98`€È4Žë" "I‡ˆ‘0:®Ãêª +ñûK\Tº‰D…Œ[<|(pUpÜÄ÷¸H�Ôz‘[s_{öŸŽýQ1bÚ_WzeúÖx¥Ù�~¢iZK³426´F'TÒ$–ëˆ�F£·†s’k èÊ™‹GþD"S¦l�žKÍl“ɆKb²DÄeƒæS6 ز!Mæ´“Ì×El|d™ +ïF’ñQ|d^3ªÊ�£S†’#™ +µ3ª�¨“q‰¡K†R*1º†hŽâ…q…«Æ¢aÛÀ|F›æã¡D”ÜăqÐ]c>:Œb<Ïç‚ùº‰Yya�PµËDÃ.©Xnä‚ÃÈ<.Ÿ•êòÒ¸8]ð­"tY=`<\¨H¾|tDðz�–‹GÌY¨ÇÈf Éb‰Ëjhi™0(X<ˆÅ6ûHY*:´Éòu�LÂbU–ÊÆ\ÍB-¯Ùł×g©¹`¸OEÂ2ù´Ìäõ�Ìh#ßGIù$èÖø2žŠÔÈ™> X }‹Ä·•ú³…ö[]r›eñaù¾Î“{ð>j*q 9’dÖ«HÁB*aƒª£³j09P�ÉÉhÈ›Tº ;5x¸ŠOà kÕj^n0©x¨ 1ÓȦ¡DĹ4À".( % Y‰‰2l•¯3©x“JM£D¬Œ¡*¬Pp]¤rÉT@* Çd£‚1VªŠgæ�ª”ªÆDå‚ÂbT¡`’!Q�=�@TȰÆ+›kÉrmeBk€�–,S‰E`K¥aÔ T4Ç¥¤|݉«p\¥F9á�Qþô£L|­Eay, +…ZQ +BÙè(9(6 Z§^”‹‰ŠB;‰œ(¸b¤DÑȈ‰(' £"¼|(+›Ãs±à +“LùºZÊ^ÊC˜ª’�)5B´¡p|Ä0K%d¡¤B¸ +%ž<ÊÄsJïÄÃuâ™èÅÍ;q¨yÕ2óÊðóPŸÌkÀ}¼<�ŒŽ–ÇÉ$ÞŠ†1âmŽ�•÷u1‹‚GâÜ><ŠI®Ããø¨N�4<¨…á�´Eåe6ë Q(VxÜÅÉ[á¾ OâDUòF ÉcUBDÞHãò6Ö&á•x:"¼ ŠÏÇÛ�÷u¥Ëõà�xÞ.uï¦$¸7²ìöF*íQ@X²§9r×k(+×7i= U­zYIÇË€-ê™�V ×1ÒÁñBŒ�G*4ÞMƒ”ñBhÏT.Þ×å �ÅóbûóZ›oð,FœŠç b¡xÏó¼ô<¯/É�è¡+T~Ô*žçy"¯Åó4ÝAã�|Xd<Šˆ&Cñ<Ï+)"Th¼¯c�ôP³ÏÆÓ0‰q¼Ý��ñ>´![*‚U²ò•¡Ïh8�|l8)LˆC¶F–id™>'+Ó)¤X�bAŠ)ödåáñáñaêŸ JhïÉÊ´2}6¢“ ¬þÉv�Õ¯Ã�àD+’Õ|V$ËñaÑi쌫�‘bçÖaú@ +/¬P¬#+׫ÀY*2›#âb8TÜÎ×QÜ“~s6‚¢†Á ÷RÔ2‡†KQÓä(lDéùºrt:™�0'“S†„‰(KH‘G˜ˆ ë#ô%vb¢äÉODùE”%–sê³N3ñ%œÔ¦SïÛJ;<—ÿºé…ɸ0QDÏåU¦ÜˆçÂgçó¦É3}6&ŽÊsùOQB[¢°(á·nßú�FH#¤ÑÅq@H#«É’FH_w±"YÒÈèo•…h.ª…|Ýè–âü(äG!š\)äëHªÏFiD¢¡—H¹WeÄ +;h¦ØÅ‡Uú:��…z*‚U*‚U*‚Å>B‚|„Þvx.ÿÑá¹¼H�,Ógda`µ ñ\ã endstream endobj 26 0 obj <>stream +ÀÁ8€›¢„VTS”°“¢„4E S”�bOV¦“ÕêE 7&Jh/œ“ ìëÒñ‘éøèø8­Hö䫚“ ¬nf¤X;ÏæëPð¤±wFŠõÖaºuÜ:8>n �Íd�Æ^:oym|l|l|d2$ädH&ªHƒÉXV(ÖT¦šÊôÑø¨aH&ªˆ¼….’‰z¹†CÄ×Uà*ð×y_W�Ëx8"*p–#âVà4_ç}·T9Dl(òIo¸7Ga#º[ÅIo¸é“Ú舸_w± ×RÔ,‡†{)j´a"¾“•éó1²dÒØr„‰Ø`ˆ2_wÙ”_w29UJRäO&ÏFŠ�ÂË«žLh¦üù:òSž8X^UÔ}W) UñÐp'J2$LÄf‚2ûýÄm}6?QbêŸÙHS!G>Bò)?00Š…}œ\`Õô2½:D±r°B±VfŠN!ô_gza0-�¬ãõÙÄ¥!´¦WÉ„éâÃsùPÆÈg'˜‡qBE°ºá¤<˜çä[¤Ï­åD¾b8)Ïå/9Ö©Ï>›y �| gÉV×á(Å*1²¾®3Z‘¬¥Âtq�)VÒˆ¥¦2},T%Ö +‚ª°RÇŠTr@,òÙ�:äF¿®>€4„baUƒ1}ÝÃæá†Ûš+…á‘á©rçžjÄå¿®2c�o$Æ:!Hco)ÎGú|6—!ÙQˆ©>ÂÏB2QU¦þ)]TŸÍˆ¼…nóhyd�ïË5”JhX^U¤E››EõÙ`dhàNµmHp‘—C8–ƒc%D!—W¥åJõª GÔ +?8x  Ìz¸¶† n +± }`XpÝëk% +‚W9}R©hjIrã×5h` ‚fz@Å>ð¯X,¯šH”ìâ«hŒÐFçU¢|€à›ñib™ìPPå¹’ME'$¹hHTªeG‚B�(Œ6@JF ;b£$º”‰ÏFŒ£IÃDÿ„ÄɆ2"/gp°y m6^˜ (2¡À­ü�Äõ@äO`>—· È\<ùJˆ‡ ÛH¶T˜€ØÌKö3 {sQP0*¬F«4b ÙJ+õ$ÝbÂ\¡¡vdu–fU‰Õ}à©�3©{…ÇQCáC¤šˆì +…ª +‘´Gµõ�Š +Á`IG¥Áx<ŽŽ:B·—«AO<ŽšC0)‰�JC¼J‡Få„àq,g"�ÊÊ8¹.*‰ǯã^ªŠtÅ UA0Gd j…¿¨ê¡€I¤ª‘¥ZŸÍ˜Ábþ|6¸ ÆÕ^׳äZ6‡÷jå‹T$õ3ÇσǯC¡8"îŒä¹™P ‡ˆØEè=�Âòª ʧ-¾®bÁ=1S!rh¸›×%–9$L5!‘J‘åUU×ÖZXpòë@Sˆ©f€œ,Hèa”Ø7Á5}ƹ9l¾nôX^Uâƒ4r‚ñ\pªèæFÂTRÂKÒôÍÏÜV´ǯ;a”ËQ¡H$5µ*ÁDˆ·¯c}ŒŒp,>‡™ŒÃbCŠT½ +¡6(&_7²Á¡ ¹„džŠ=j*Ž%b:HÏHǃ†õÚ¨€¨>R;ê×�H6tJã$;mx¥jäVP¾.w‚Ò¨›î„VLˆ´ îAñ¯ËQN£†ª! +˜¥yW4±8¡*n[^µâ4'¹ÆšÐ�‰ ™¡@”f+RŒz e‚]Ç0qÄD”ÅÓ`ÚØ¸•* $"óüU4Ù¡³Q£¢TS9}RÉa‰’Q{€Øˆdð×™ÄÐ%Ëäs!G¶ŠÜ8�cƃøº‹¬³-6À‘NÉ‹#³¾.£Õ!2Š�Z �¥™ö¾\TŸ‡¶¼ê¥Ìßõ |]†â]«‘ƒ|HaÅ¥¢Q·Š©f¤dŸ®D!1B¥ºé°ÞPѵT¿ŽÄƒø4’ܘºp=l>2¸Ã¶1>_Ò#EÃqT2! �Mgdly“×&>*á bxeM Ü©ª‘‘ù0Ž$jbyÕ¯£™òg«‰-ž‰—�«ñ"#ò²ÍZ^µ¤c~ÃVâ¤Hdá4ògs’ÉýºÙ(6$¡Œp¢~H."p�"68ˆ îsËØ‡ó|6^G] +òbq<9�ñ4JÏñB^,Ž÷}]Ã%1FFäeœ¤±6Èg“ã¨K5$2ÏTzŽG²âç]\èY‡ÜÆÛ0‰q¼um¼¯›0‰q¼“ ÜÅ‹é Ïæ? 6 +=•‰™©ÃF¡r#â>`\ ü©M§Þ2±S›Ní´:"6›$6]I‚¸aÃ2=d`beˆk%"9Þb.[È*±ÉD¸¥FÇ‘H“Åè°/Cš¶†5vˆP‰Q¤×)áÉã$HøÉ×Qx›Ž„¥¡ÂU.êã >­ØÍãNPÜJº97Oí¸l+{!Ù.s±Q”~¤VÓ€�¨}j”šhs 4ŽA‚ …8r¶A£ŽÙ‹fé3Ï©gRÛ¬4LN.•6ùº ÊÂ& qàÀ�=dVX6Ž3Z¬£ÖQÅl7æÝÈY Ë¢qQrºK®lË}x\À\'–ñ ¢‰! +•˜+3!µ×X]/“Ž~…§Š›+Ä’®¯ë(-¸+£¥[ªŽWGK¥Êxha¨N¹PC%A!ôè�ˆW­SCè*CFHfÖ&"É�p†dûp±¶ÅœX$ÊâÐlÙŒ‚w1RÑ!Ù´�›JƒC¤å`ñ òu*â‰|p­ÕŠ‚ì­<—'³ú´F^!#%·"DCÂl*CªQ¸9p6(D b`²Ð£À?D¤Å!2Ù $WjVòñ€Ù\|˜ˆLèÒ6zP>NÁƒVòø:¬Êx˜�,°ŽÔ'–ê°ž‘Ü1{Ø90$4(p8T„„8*. G V–l\`T|c³¢x¸ÌňF+£cÒà°ˆ +‹ZFD‡wdl“‘QULFFo`_÷aúåVâ9˜ˆ‡QŽ”,UnqRUÂØ«.3ÒÉ'G ¹ø:2ºx(¥èÅhQ³°¨d‹’YÄJöÆÂ{™¤^Ʊ‘…dŸT‡g3)“�ŽJ +š”¨ds(t‘ž´" ó0h$‘‰„¢©(uhÔcT0rRD,ƒ¡PRHqQd�a£²¡ +apæìزðQÆÔ3OÔY¬³’}œÝ’µþN°Z¥ÇiŠã[ôuv:ÿ[3Ú«—,Ðt�n¾QörÈÆ+QÕ<ò{kî/ ³ì…Ô™ýžåÀõ¦û¿Aå.šÖa{–ÍOþ¥ßý[ZéJ‹ÀäâÅQ+ûÙ+”·;ÙÝž¡'¼µÏlÀ‚ÆÕ}XÁs j5°/àJ‰õÜ0ë:5¤}i·ïB¼ñÓlˆÖ!lNhuU¸O¸ŠË  "Eä6enÒµ×kGgg\Œóhcž5�­Ü†­y9аõ|Œ¥wñЦƒÁ¾¢yÂ3¬1Ò‰ĭǯÆ­Qs1ê+à}Z¥¡Âl0ú[§Ñdæ5‡ò{Y¨.—£u ŒXè2xËd´à7çoéý•XR´=.˹ùŽŽJF™ +†AÜG¯™ä>hÇT. +ÀgšìpïKõÀ¤\û‡¸Ÿ‡b™‹xàg1o”½h M#µ²#µU’’€Ðs/dïYÒÇ€ –,–3å~ä»RK€«ã.kÀ·ú6e +›Þ£ +þ§íõH=à T—ÀÉ~1øðƒÖZ›6ÿãr~Õ1C™A­ÍQÔ²¦�1銣ÉÒ&hKN ìÞ‘ZU©EÛæjÉ sÁ¦êhy(Þà¬}­ý‰qB�M j.cÇeLöd ¸á~Oq2a9…6ˆ-ù†£V²BŒ¨¬Ñ°ŒÑõlÚgÏ)s2/j?À轟ÎÛeJÏ@̽‰6¾ƒJx‹Bé r'·D›Ò¸`û) €Ø5iŸC +¡”Ö¢€b¶&aÉ ¡/ñ?¨x¨S‰=h˜ÿº_25´îMлóš±ßç^Î’Ý!/“Ç6ÊW)$컩‚²>ó6E:qK4@�Ä!=HlW]‹ûëÄ­,fz.ëw'QÆöÄí÷tv¹Ï9õ½x]@–›Š«\ÙöÄÍF¥Ê«oð£IÞ(õ»ÄHDë3jâr:žŒþÔøI1í5«‰ûç÷@שâöz¬c=qÚÝçîò°B�c©× 5À.ÀÍP3ª¢v5’D09NŸÂe£8j,˜Ì ÚßhQ7*h±�šÅó>õ´Y¼Â÷žÆð£ÒäÔ�Ô†x½"·gmVîhì¨pØQ®T%vÅÄ!rÏ6Tz­xѧç¿—ÌÒ�‰î¿Þ‘nÞuVÛŽ˜½©¼þlÚM!DœlžÝ½?S)Z@zzŸ²ÞZue( û1ûc’Vjv£ÊÈ x§NXñ V¤%Ê}9!ýÌ9ªðóÄP®?W:Ä ¯>EçÓJ©2mdó„„Љƒ(#Ǫ=-@B]ÑÓååÌSc ›mHæë*R;ÀïPIi–‡…q…o]&ª2ÎÊè&Ya¢$A¦X·ÊHÙèù!rÚÂ+Á[;®|¬mš3H¯Èu�É™ ,’DidëØ“)¬íÜ{ÜÅÜ™Œ(˜!0ÇHðõ˜äÊü\%8þ¸JëB¶ß÷­¡ZvÑ�5‰¶,© ï—ÇÙ�¬nqäð®Ó^ÿËeÂúÞx‡R5×"‚�âLíK!÷Лþ73|ñ¯x5¯ Ýþ;R·X®/Õ<Ýñmh_H=¢vè°Ÿ’ÀNÃKõŒ_U ¢†²_„C£bgtDûúóÜ_ ¶?ÙÏ�!ñgn‚XA¬Ò7ù�ò”#ì9ÌñhÀë'ÓøuŠpì$u%×é¬ìa9?5µò†,é õ!©Í±�èMÊìÁœì*&·áoYœ‰ô“ÉÅØFYJ÷àõ§qÖÙ4?Å8xï…¨Ì7§nóo. bŒ{LO¢/×^;ÔÈ�ƒg ÏêÄñ'&:À™e$Ì¡hûÃ#j“? 1EAÝ#+øìEÚß»æw[%œ¹94ïà™çæ*ïs´¯{™ôf· §Mxw5³mn4ý£%“xWòBòw¾Ì/„‚Ÿ›\,¡|nXPMä?<_EGC9SäÈB '&jµƒØºf„áöl-.þ!ö8`še}è â°ëPå�¯„.ŸÎ”š6~:×þ¤.èßp‘,ßcÂrâØÉ”Sßt�ÂDPé +Jk;¹Éš·Úopÿ98WrbLúΆd‘¨lsÒÝ�i¥: ·´ †-™3ÔD牠GÈ�óä¨Ñ‘<=žA¨þ ¹CÖ¬28‚òn`ô„4b?¬]‚«O„+]fÌÇó9ÕöÉ-½½®T³ž€w!¶û£ 3TWY� ˆÚ«žû¿µ—.K˜·¨*ç�‰ ëïvŽëÛ Ò•m:ðAJ†Á£«bMZÑå�Ô»…µ£k§\\”7z›á)~mÝG÷�^xaEDÑ�÷£Û]÷# §StK’xÆptµ�ÉÇ¢[!]À¿ùÊà+ºR¡m‰0ûèJ‰ÁmVto#Gïí;fGÑåÚþt G׃AXtݺèè‚Ç{¥ÌÉ]q[¶3çut‡ÇK uòC:GmÁ¢+sYmwÀí¸YQÁ#1,§IÞò?Ì¡,¿/”¤Îõ1ñ¥µ"�†mJø‚K®ÙÊâ6?ƒ.´’D%bNI"›SpŽèsòæ�,H½\£4EþÑåÅ@W‚k2š€Ÿ·^ãvl@O"䓉u¤M{’‘4E-œÎ^P¥Ù¯Ö µ˜Å°ªPÝùš¹¾zÔ^ý4)Ò*?³”п˜Qe(k¤_‹e�›ï�oœ:|p«GLùý�å»ÃnC$ý* ­‹E¤£,µ}§!#z¨^•K3ú}j¯[8-Žø–ÈhÂül.;1â#–!ÁÂG´1½xÔÂq�¤îôúayo4Æ\ýBG„ëÉò¦.FÉæžxÄ#c/C7|!É84¾›ø�æ‚|ÄCKDðá0iibÄpÚëüa*2(¸JÂ×¹â ÛÓ“¢sÙžõ}¾�=Hêm +“—nõÄíå¹WïI¬åS €%­É­ÓÄ¢Þ߯UÐc� ²ûÀ`°•3Ñ*Ðô‘GR¸½2ÁЊ؛ï[†ÏçÏ�;f•—ñßIhÒötëéãt"¹UÝ " �OÑ_íÃ$EŸÖèÙ;†x¬ ¶L›Q{ÞÛ•“02Œž ‚©EܘÄÇ™–R�ÚüßN�ݱ�¤n«-¡ç\�ÈÑ8µ +nhË|L°®†ŠŠWƃÝD5n݄ϕ“½.TSðúŽ^ö¥Š`j¶v8™è{<ìo£à'kSÎQ¤“XFms%wÆ�¬m/Aû\ùB±Ld£yçÌû,£²â½¼~„ù-ýnÓ$Ø«?fayù¶ Bq3¸›§†AWP{Vw³Iõ kHvë¶šõC �÷Ö×ñ—•Å:]^5‹Ý³ƒ<é¾B´N%Yc£À¤£\G�™lçÍBHÓ4Œ“ Ñÿ:[…}®òæÎ GO¿€çG”GšÓbã,\’�˜’å:ëó¸Nã9áþ˜Ÿ вÎ^}…ƒp�'ÿuÌî÷cüÓÅ�Âê Ô„Ô¹`ñ]«[›ê@*ûÆì1ëÉaªO<úx٘Ϸ4!æV~lI#ª3ºP.ï,16A~´[ òVßÿ[o(™!§ÕX;•ÔÒ9ÎG>˜ÌÃY²ðä«Áó¨À<Þ)½«OOȈy\Jêšj„÷8lŸs¤¥¬È#­z)ÿùšþrV!éf¹úá RÞ–./~.·+ï#e@–clE·Ö|¶%Jô2m±û¼h1Lt=è +„üB€§^Ì1, zÔÉ´&¨ÍW²‹ƒc&ïà*\»ñˆgDeŽ�5ú{#µÏªÅùR­b8E}âÀBicŠŒlGb—�a0¹OçÍ \!Ζ¤}ØÝ £A–üÔøöBêú,Ï#tÝ3§IíÎÖI²…ˆÛs'›.ÓÎ/s~‰ãâ^<®Ÿ‘5û¸óPÔˆ™Ô=Ó¦xM€�D·È<¯«a"ÌÜeû¨†¬�ch¾,zׂÊÞîá°‰ýœN  Á1ÌMg û”á°Z§Žb0÷S¶�u¢À›áÇüç Éa¹e'„cf!x#±rsxO'o§3þM­‚ ÊXÔ‘º0d\ãVw,Øf›UAAp +Ðó�zÙCJä6óYü@�£ÄÜC¯ ^{)Ž CÂtpÑ ceÅÄc,x wæÚàÍØ�pi›f¿Z΄Žei˜ëì.©çó1/jî÷n^@H’)ÈÓþÏ%ã8C�žUMN-�&³†JÊ®Ð`­ßcüààu–rY@'¾Yòb¯ÌABÞÂ;˜¤�=uf˜l½³Ña„»(d²ÝÖ[>ž?Šr‚ËØã¥ÞÄù)[yâ2ÚµŽÌ¦ç�ÿ9t²ìÓyᘹ@>�qkO‰ìpkDÏ$~L>�PÜCõImFý½™>³?=¿•‡ú™càP½uÑÕ!/+¼@1 ÂD0ùW)RL/Ø)0@Ç[);eýJ <68<\‹êzË5#ó’ñš—0ôú’YžI€$hd]XŸ2k@'[n1 À܇H)C{[䔨R²5�ePHÅd4Qɪ¾æ¤› +c ßŒ„ªYoBï@�Ú‘5ø%^Ø›,9tÀIæÿ0ÁßÄ¿%5xˆHdШŸõ4ÅqìGÒPí›ýNÊΦE ÛaZ¾E¡âÈ3÷g°%=¨¬¯u†Aæd¹a2̓ +�q\AÜè¤;‡k®VÐ ´îqYž™üj’µG‡¨æ¿d,,5œ çêÈÚ½&s�»ÿÞÆµ@/å”Ê[$B�à‡:ÅÉ­³g²,˜Èý‡ÚOÛ›Òz,U“ši>ÜPB�Éœ¿ñ­vh�/)leЭÁ?ze¿¶ð1NŒÝ[q(c‚’`g!s¯³¼|M@i„¦Œ#Á;Ej²ÁF´N‘·-£jbU+`Pêµ¾35 +ùþ%2Ð"™J¡B3�}Œ5ÒTI8*IÜšñ» àpí½ÌàÿtÂMi³bhû¦Ðv˜¸rÍÔ,\}¤»uU� „”x4EÓ«+¸ó[d«CE¥\˜>Mš«ÙˆFCaGÓÕ±*DL€¯€ЩÈåO“yp �Q!ÜX…”‚¾µ†ÉöôÛ°ƒ T㽓Á²ô¬t En` :ç’©¥›–+ŽÉvåÁÍ,Jrêï§x‰@W•ûapÕùÓa¥†åþ©”$~㥀 ê +oa |,½3"'ÝZ7Ð'Œ«_ÜŒI!mqOpô®®ë­»ù·¶ý ZóòwQ›€(Ÿ'�R³çÌ%Ÿ¤gv“í�HšqïÙØè•³{�)cSÑÓ†’�êÖÇ7“ûµRÖJ¦Îè±9-ÝHÀKíõcx_!ôå?eÜDwúl£¬çž?MmÎá0ÆØ¿íðlÐãïUäõç7ƒíõeÛÏjîÿ?Ê¡½0ñ G ÷a ·,™‰f“¯Ec°±�šÈ�› ŠxiE÷÷„ؤß )«rÃÚ†8ðoÐh­´ºL ƒ"Äè;–e”Üjšo1¬,HZÙˆŽ��Ó‡‘µi±ŒSá�žBomÑøÀº¨%Ôƒ_òóˆsC¶†Smöà�µf—W %� ¢ Ø�¶åKd‡`˜{=ufe`·é½d¤ìJì“8n]¼72óûB7ØòËä{ÿçh"mZ!÷°Ì—xCžÓÛo±‚É#þH»gb‡gÀì” EYƒ`"<@ ?8ig¯�Õé•ÕW)3µ«|�00Qçy:I`¢~£�°X”8²4Ä_Ø[pKM’²vñd{Eç´I 5u!¥ò„0Ь.¶ ®#w2ãݤϯžå¼Ö|ó9DJÁ¶ =ê>NCPú\Kå5•ºâ雉‰6qåCâ¢ô(ÖV츲Õ9Ù¼ + ¸í‰ÁËIÅ]Ìó[i‘È®ö(½ +Í*·ô8�Zcáì±�YH¾§^ü`¼�ÌÖï÷Ö ì�¦ø^Ü#LJ; +ŽZa¯ØÆ…gò»Åsmþ1¹{'ÜÄJEv\‰b>i™ ÒÜk®O’ËÕ&ãÕ\�>ñ�»ˆåã,]F›ÃTÉZç�\C°ë*ÃÉê#¡_Ø÷j0ùL?~H'qΞõ4ñ5ùž˜,ˆðrÄ⻺„®m×¥Âë„3^íCY5*(Õý!mì= ’F2ÀëYȱ¯ä•eÔï¹Ðv‚ä„o.0»Zv ›#JÅrüc•�êdL|alVöD~zàu³©…U=Wü¦ãS¹¬€o0j‚í‚ߦD/ÎÌäQÈD�b¬›9«Ð�Ž@åCïeµO±•˜Q¿—íœC[³™5�Bü&óÁ'öx§§êU+{¨"ÙÆ8šKMVè ¦ãë×·[1Ó«à>(— N"ðMÃäâ#îxáJ«ÎožÀZ;‡ òNÕ²_¡[™—튯àUM:¿.®ïø¶/ +5g«4°¼ŸÅ9à ì ežé4óE«ë'-Ç9r 0Bïža: …çªÌ°öB=™‹¡æ:å +›“ÔO �s¬nòˆ¯<'öˆëˆMNÆ�8ãs÷N€_µîÖûöôÇ“™ï8Hö�ÉŸjlEy\BMçQ‡ÒµÅ¥4æ8`DSC0à oMË3jólÅx�™†[Îe÷ƒ•±ÐýOzªû +Jz5=Jž®º.9Yá̊7a/õ.ËSa’ч‰uKzÉQ‡äÛ£Ÿ™1†#xb­ÄÀ¡-©òd÷ÄÈ8¯¹Ò¨¡�¸©Ù7|— sEºîÊÄCë´jZéA×.ÓŒyWëóL�ªÒµ0óðïÀfãiJ€“}Á?=ÛþEd�—sruë?@Ј#†õŠÄ´¿}",è«Ë.·jõ§òÄ“Ç L¹y–ødÎfu8‘ô;|üŒlƒÁ" Ç`CõdCPbC'¤Ãg;•¨ˆ¤âX+Û kŒ‘ó•.d`ñŠCD(Csi=<´aºK¸õ©yWÊ/­$ž¡ ‹É9hl@ŽU£(ä>óƒéóÝ¥àëÅ¥bØl…¡¯—¬Npi-¹º±<T2¦þ—â8 ¡°%�Ë“–#÷YRÅŸ¸:ÐÑžÿ|ò¬¬œÒé»-Š“^*¹nó¸¦(þUåäµH^-à}%Ä;öJ±sy|ð&pzÃIPuIJßüÚl„¢XòŽ.S³ÏÊúO19šFä(Ÿ¸ã.ÅzóÛ…˜ ŠJ.U±Ô'YØ­$4o¾š/ÀÍ3š½Ê¥¾³6¯ôdéǨxè:û¢‘7 o¹o7ò" ‡òu7[{üœp�U™S ;´’ïà€x€ÑàŒÔ—žÃ£a”íX�š.6õ41©ú£YÕªiÔ·í\4Ã3ª‚&[pqdÙFšv74"e¨®bQ¹õ€xþbÛ6µõ&×ÛUaiñ=-áÊôcVD¨ÏªÖ]Å<ŒÅy#Ú H8Åõäásì�B W(Ô£ûWlŸ$¸‘åóDùZz„)º<�…[3xÑË”‰ÿµÒj„ëÃÏï¦<Íøs:ߟÜSª¸dV�7 &¯�mõ?æ¦PÍ\IÉXš�‡[qBkµ.#µ3»Õ�rW fA. Á|`�z1��Û8ºX¨5À9=›5(xÞ�›OÊÄ!æè–²1b`ÌžïBû\Ûûd.@¥íq m׆[æZýi +äfÜÑi¶XªúT ÐÃÕPîÏ’`³õf? —Ë%]ë‰üD³åN‡’‹3«XàyÉÛ¬ææ/$··¤œý¬oL˜Éù÷Sú|üj…èÒÝÇ�”¥@õ81OE4Â6MÈuØÉÇ;ǵºVðn,p7±]%yWù£j¶›94—oÒ�X+U‹? +gñȩ˙!|WöP‡�åJöÏáñ:ZûˆûÀ�ÕšêÑà«‹ÎDÒ¸‚¶±Sÿ¤3 w.Ï*Å—¶¸ ú¶æû¡œÐÛ2ã~ž(´žî˜(¸ƒ‘‰ '†«Š‡ˆ[²%µÖL�áKx�•�å0ÿ“ÜnH"‚¹Öªì:‹ËÉé´Ç‹{™®ƒQ¬´¥a©\Â=褢ʡž¥š½Ò LÖËK+™'J:�üßP5Ö91åñ³¦ÔuÆeKŒ>*I5Ž:¢{ ÏË@O‹Y5÷#ŽMì'u±˜ +ÆAØ/½²H,É!��rV-ë”±³‰Â¨òšª33á*çf½[Ypº2aüKù@HØÅ\Ax&V~+\r8PÀY�l9H…æèUe9½{.qÍÈ,a²…2]c8W~O5-$ ïœk³÷F.t�9]i©JdBž!:}Ùpßj�1�ôT›×(rhý‡ØBgg¢H쨩 ‰/4Úë¼— ½UiG5îEk;"�'WÉ£1ÊV›g\UÇŠ‰/žˆœ�ƒf( HOl尿zc;km6ª¸^ŒñýMV¨Ôþ­=î¿ÍQæÃ Þ„Rò_J:UÆOÕÈTôª«Š$_†QâÇZƒa+¾u--]�¾eÀlb‘Poôçu¦:Y)˜@²£Aq%FÖR€}ÃT4 Ý}:ˆMÛ¢Sƒã Ã×�š.¹Lû*¾›0ª—_é´HË‹³Í—«!=Ê*ý¿[>µÙÕ«�™ˆ êçúÑÞÕþª<âì²ñ¬t૞ÚÛ3œ(héþkžÒ[-Ë$ì·bcÊß nT™Ûì<Ú´¬·{;?bæ¾9A$æ£Hu(�1Ì.Ð9jü�£H;á€8%µ«éfµhç— Ö\Dóä;(¸�üøG~ÁÏU©ÿîã÷lŠie!T,$†P‡ýLàÓBWÊê*®œÏ’Æ7ê#ýÎ^ G +ëÐÉ�ṗ‰s­*w \áÖ•ƒõy58†nB2LˆõEKˆ7cÃ!�¿·†Uú~¨ÄŽP[È!!âEà Ña8”6Sœq5š?¥iA“à¼!bSéã¬Ê�â4Žl{uÌœ©4Ã[·ŒF]nèšÎ¨X‹7¥©Úîn ®²pžV{ûÃ[$ø(ãPì=i)–ˆä4qB>5q£N(6+¯Ù\nÔD„K«;¤Ð™vý +& X!ø#¾–šs¡ÑØG%Y郠a¸ p9\ÐÃMn0¡ Ò+r¸R²)à¼}Cx«•–S+ûRµŸL,ü;Õë“™£íD²XúÀÇÿ¾c;1Å”‹?o›8px—�·“r[�–Ùw@…É_·žz56«·�PwëÑÔ—s? >o»Wªa˜¡J˜YèÑbŇvã(pÃ+ãÞ&žozÛ$èR⾓fÁÙÏ»z�‹œlÞæ¡~�•"rÖizx£ÅG“qè™ÛVTÒÒ•$DbðÞÐFç¦Mô(ÄODÅFS*Ð3òÕZ Ç„bßïÓ}�Ç 1R)f>p¡…~OHøãf#’ĈþˆÖ~¤ÒL" |±D …Î盛ËP�#*ÔR~ZÀÍÃ;g~ì”Ôòk æÃIÖ#Zé3ªÎ™›Ï¤ýb2ãLr+oÜ"ÚâŸH^$Íà&ŒQsÔ}’¸ñÖ^AÚ~Ì\\³©˜3IÑé&7qµE»XîzYcð}®äß2äoÅþÙz)¿�9�Þ�kÛxŸæ’JëFö1Úã÷µ.¤¡Dˬ¶Û¸ÐÎ>³í-³cfü~Uj”?–çñápþ®ëHZ%KÔa×#§'ý2!õ:ñ�ó¯$éEù>²ux££híûÅ™á(ÅØioKEÜ^i. oȈî›ÖÜôbw�Ö²ûáÛÝð&÷«“b±Ê9^ü~5Vì¾û—“Á1¶ÄÞçÐ092LžM=Ü­ø‡û”ÀRßüäÒ+ñ'•W9ÚDm,�ØHIp„ÐBßýñ®p'DÝ(Ft¤Ù|Ž9îÔ%g*¾uœ–Ì$¥ +á­ šòÌ<‡�”Ñm5=‚³� ÷_ÿÉà€¿�+Y€•“¥‘ë�Vo~vý’Ô6˜xŸªƒûàâË»dÿøCÕ�7éÒ25ÖÅ1sªN#'Ç +a5¯÷Ø`y)].¥X^p½zHéîºÃnÔƒ‰ü Êú)Rp€Y«ÛÏëîè´9žh’l~o3�¦eÛQ—jȭĸ+‡yv£Ž2¶ürí¬¤àª€L®.úÀãŠô`&uÀÇ„Ë5!ÛDÓH3ïÈ�‚L7qðC=^+RÙxBdR­wtkƒPÔˆ…H;‘î‚’dN½T +£3±„*ª»àU—ÎÙÐÈJÝfŽ¡v P6Ý‚ã¶$¾g‰“í3aìôû,¿ŒŸ¦è»�m+7…;¯Á?¤Ù#º2#<ÍËÖê›æ)¬eUJ­WD�·×h}± +è°q:.lÑ Ä:T�J{p@†K¼·‚|ýUª^äÕ .Ò€ÓdP|]¸t—özK±ˆpÞ?CBè¾W��^Õ;É�4 i_%û‘O� €å.ØÑB·©¶™r²%…Ê*ñ(×_ÿ¯WIb£á¤Ý#1ª@%�R)0<æn°ÌÜc7¬ê_ó\ۮƛôekMxP• 1­ÅRì}‚NFñ•ûènî–øÏ5QËeà¡Ë�×|鞢Þ +”P +%ö Xb�o_={§Å»÷xw|—�^¤êÄv�Öó¹jà@iÅ*ˆ3VО½jœ©ýê…]y,âÏ–‡å÷Wy3  H´ê/ñö†çàT“_]¼·¬îϵʟFl1Ç»ìA˜®÷»tÍäŸz¾Âlªþ{é0ÎGÿ±º8ÎwEaðîú*â4JûÔÌ(Úœ±EhƒKÀBš +¬‹Óˆ5y$Ø=IŠdí{DM""$öB(â?D„ ÀÞðlb|ä[ Н$šØ%€û u5“O@­W?1"Phÿ;ÉßG·ö ^=íùä0Ï[˜<3“ç ä“\ÕG§ýÆýöÈ\Èc•;;Ÿ&tú$¹@Wî_‡[^Gœ¶|”o^4¡HòæÆ3p!!îD²F�9GxU×iæ0*ŠæÂÖÃñE…¬ùrÿ�±A'%Ö`æ<e™ÛV +¤LGÖ¸š9ÝËQÈø€ßé‘Oo—[ô´qÌ/Öh”+ŽAôÞ‹dãy´:ï¢:à ¿TÇëûA®dä0Ûцà)29¹ä$ïŠ&Ešoõ¸å§2P¶ɉµÌ–›£%°h“Vfyc¾Õú $8yíf2…ì ßÑCQ²»6é6llÿ p�LÂàÈ:ÒÑbµÃé­^”Eá¾ +“�ncó7Á2©—ͪÍ!+ªp5¾<±Í;æÆf´òl‰g=2°,Ço¢`ƒšžmd˜Æ~n™\Ÿ•öЦ–_)àr“q!~e¶.œŽõÏ‘¾w˜‰˜ÐÊaû�{FÝ�~•Án ö™¨E +V±×Ù®CÕc¥ªªÎtÖÍ-Äúeš‚ˆ%r!¨·“ˆ>‚ÿÌš%=¼,ß<Î5qŠõ5œ±•»À?®¦KX÷‹•EîcR/|_>ûÚà©•mb «",ü…<»brcb N˜rsÛ�%OcL6ÑMx#Ÿ†š%‡eñ6HU…*m,ýc `@î"P" +D·hîZ+î3Цî#¹Z­´!‚*¿’‰ëÖ�NvTS[.(â!ˆ…SOy�^SÅ7E«”ˆ£Þu+;<ÖT¢$Ì¥\iðÏbmmã��ÌuR‡„N|ŽÈN’Û•,£:*üV>ÀœyþÃÆÂÌ™¤ÙžMÓ;xlgžéÏ(·Ax¸Œ1*£¨wéP–Å0:û;¥Æuƒòƒ-ñÉ“6�cc˜4m;Tk5¤“æ,Â¥ùTú3ÝHè‘=’*Ïòn8w°7¤%üÕx‘Чí‰Â¡Q±¥©­ÎæîûÏ(Çþø‡<›Q‡^4ߣ›Š fNOvQâ)1¤z.\¡ I˜TÉdËî[]ŠòKýÐF•Å:%1ãcÒKXˆ>Ú´€G'< 5? +·Çu“ñE•a`&ÔÞlY—ÝŒ’%ç¤W¡á.ÆgQüÁq$à‚T?YÍJV@„d }MJŠàÙV%1 ¶%pß;t$m“NzIH�§¹�:Ì ù/À¢uto$l|)½JDßtb¦îÞÛÎö±wþMpÕoµ—ª±Ò ¨Êi*º='ºª5pBÀu{8ˆ3媸¾û}}nÏ©SN×¾©eï†Þ²%EüÃ*Óõ¿ù öÍpdädËÚ/ЕÿÒ ‘Å0ô.`Áuó ”ÎbcÍ[jÊ §süc€Ù𨩉~y“�7�|ÛLËoÞ÷ùKçÞS÷¶ÈÓ‰ø¤R­ãjwÊ×v¢:šø­´tq>ó}ÙL:ê p©a [mJ® =ÆU§!Š!j;à;ŽÂûPCÞ²ƒƒWŒÒˆ4ûòÁØ‹%ˆçFyKÿ—¹u`Å®L âÓ­”&+¦"S×Çñ–å¦jŒÑÓÖ“�ýü{æÅQWC…θè ën†RbÝRˆ¹5dR€ÙÄÑr á΋¹�Ì®¤¬N· +uÅåCÇ<à æ0£]WHn÷Š«¡Í'oä,žaÔåþ&¾�cžW¸LëTX[É“¸j‰¨“çª�²ÕâôÔ$ó¯õø|éÊ�—.}¢‹„Á¥«¥`]™ÕzRÇc ÿÙN8Æ++=?ÿ¸õ� uyb£R?áqúééH5ÏBg0Ã#¨d–³Î”©(7‡GkÜðxN÷[9½àð «ÍðXD§ò{“× ¶«ÅÉx¹‚(«i3£%aûõ¸³eúOÉ,ÓDÅŸ/ÓÂi®B·eS^àD¢3Øqñk ‡{}HêÆåÇ—R{Zºýo·[\-ÊfM®ó>GÑ-&•ÊÐâ,L…4S»oWÀŸ"…¸+m•�.Š8¥/@OÉmijäRo.ƒšÝgX¨–N­Ÿ*癹k…-³ÀÕïЃIÓŸ’Ç8�KFM|e_Kk“6j9¥c>Ë•"=N±ÝñEqô˜503Í»ðz¢ñŠœ#Üí—5ÜÅMj³ôcƒpÃÔ¦b¬7$X­×RªQ¹Ð FHd·šH˜ó/oÂY´÷opéCmà�ß°d,ÙTö¹i˜žÛDšßX…Èdd«€æèʼxuáÂÍéhŠq™döWM‡Ñ˜‡Êæö [”#o-÷&äh(äèË=V•™Š_…$I>rÕ½G¢kTv¸ýåý ä‚f"ª/–îwœvp>ê lD‡²‘9å9¯�8ªÆÀ¡‹S¦æMùEn~ÏÒFÆfó,/­á52„3ñ»NÀå ìåF£9m'Ü2BV} †ÇCÁ�_’øIdI¸p÷€Ïœ–¦sºõ,�rSCgÆMÀÍïw+N¡%Q°EŠWPÒ,î$|¢Êr2&� I­Üƒ�¤<$£oS“°—Tµ¯? £ø§Œ²›ëYH]ÑÑäøl¡³ÉkonÙÅŠÿ3dʨ‡p Ì"šy¤ÝÏ#–-9«–N”Eø(§r—7†4‘€Í‚Wá,f( °áZâ,÷Ø”Ô71·Rኑd÷þI«GP�(Aíþ%ƒÈ£Â²7µ-ò³MèØ+˜Ã7@ù]‘”*‰QzR§8ÄTzž$´’L°©æÐÀH=ãà>õäøñ”¾�ÓÖ¤�Àíú¡òðtýOÉ0Ï^8n&¸C6ô\ìƒÞï^‘f€ÐvfÈqËð� +˜ ‚ĺa[óí™êTˆZ¡ù �íz(ÿÁw.U1Z{Jy·¤" ø2� ¼Zσ¿?’½Xtm]ÚD‡?À¤ +µK¬¾?Çï«ï(ÛVV‡XVޱւȹå É“Ø"@ ?(ó¤êátÞ0ú«#DäþåU.Á†ú@°VOÖRkyzähÁŠŒŽô—™¥ñ×ÇPU8¡~ElÊœð}Év‘’R7ôÔ43yWϸmÕ‹ëLÂðç3jâRG)?�¦?� à;îXwDðÓÉ(žÿ̅΋êñ…|L6P4�>¹&O#5tµÐ +s‚ãî…}z…-Ïl•#èžàÞ…_|~#ʹaÁ°f´%»ÝØ8‡ÇÍß«±$²ÜAJ¬qû²MäðæÒ8+O\ S�à‚3]ÐäŒ4t"ˆKX‰¼?Œ'a{:öÁËn÷Dsõ{¹@¡ ,þá'ºöõu� &k@g)[•ÿZÓÛ?#CIyµ©.÷¸ñ‚cèT•öÔOÔZ~ëg“A!Ù�S�!Ä8¦XM™´q�d®Åù!ébË\åé<Ä´c”iç6+"¿”È\\ø@AîE Mñ$³¹ó´§‘èª-éãEÃ;Ĭ�œô!åv¨-¥å˜0+¨õè. ÉÑ$äŸ lòˆ÷ÿ^ÅZ“ç*‡¿îµ(ÿYLšOÏ©ÍÒÁd¡ùÕ§ŒPà-7ôwš”>SâÚùÁ“Âí:xK'K‡®z.zYêøèôš´ë5hoÛ¤©µ&¬¦´£UeluÑVðÚRßµ|§¼”·³<øü^¹ì³ºxUËÕ²#9>ÓbéÒæ‘$`™žm9»l µàü�¡2ã²å8p÷�³Þ§ªQ—~ÝÆ8âÉÛ‡Ý&S jmª–OœusŽî�E#7§@¾�P¢±Ìˆ¼'õÖ’zÒ,T|Ìõ˜Où…|0áѪÆ"Ó‰¦êÎHÞöÛ‘ÊA–¦4—Öë™õzýÜ3฀PˬûÀ߯ºn�\îÙì]î]W6¥À*&®æÉ�q‡6œrR@ÏxŽÒª¾%ê¶5ô$k¥ý ’Æ« LAJõb2<MÊßSÃæÅ›×YÁš “¢«,Dìpl1ô�Õ/¿ ؉^×z�ÔÃÍЃ`ªiw…8ˆ>…2½ž8«Jç+ax®(~3JÔ&Ð’g~“Ð9›ˆøp×‡Êø#¯jî/ÎóÛ!á{�Ú#•ZòîÞ…`5 ;üÔE¤Ñª±?¸6��þÿ+!‚jàÃ7{à2f¥gu¢ mCt‚I1@±Ê¢%ïf¾5”ª¯†Ê{<™6"±`K8¨eKVÿOþ³"kòÙš–ŽY:T‰–—^Îp£'ór½TgÒåhö•2„y‚¬­â_I 5ø€L^¶ÇþµoûÏŸ©W%]À~°^N$?ˆPa¾ì��\èsœÎîõPÒ”Ÿ³.ÕUÆ\”û×T+*FÐ&âÞrá!Í 9‹VÚ(PŸfd”¥ü :ìî«^“£eËê…}t}[ŠÉÞØK/À§H™Y÷q¢Í®„ŒŽà��rÜ¿D&­¬é�¡yµ¥á˜lAfXý.¸û`療Ü·â=Æ”6¶oV)5ŵ~ÏòˆÄÒ‘ËMîï“Ç‹€W!>'\Zªá¡#!Â}Ú¥¬„ð󉬸‰3n“ ³'h­ŸX¼â�ø3‚ì�Hã|‘ÊÞ1\P"H5 @ÿ˜]ÿZ+rîûžÐùßêóBþ‚¤8ñgbíµñn=ÇËóÁέÇ@2òEÅþAx%ø‹Ó‡XÒ¤æ–Ю|HÔà (èƒpýW¬fÄÊÍ6 +%Îãÿ…æ\Ÿ¯L°äÈïïŽ 3®CÛ ØÕ5[¹ZÁ‚‰àáUèÛøoq2UÆ”À\”3†ç³qyƒ—©ƒîÝ&2H¸AˆìUj²ÐmDn:7¢�Ù˜6¢%ò„§qÉY艈Þ?Õ¦ƒšøRŽà¥ñ½&„‚‚n£ÑŸMS` zä„!6Á$ÔøòÇË¡½jŠØ lÿ ž�Ð v•ï)况²rT~£å›ÙkS^žv½£\W®PD¨Žîq$wó…“)¨�¡ß¤ÆT2§ÜŸç>Tý&-ØG.þ¶vŠ­up H÷9ÍæHýÞ8no÷£»·Ês÷ÝEx+M”èO\{Jƒc§z©NA®•BW)¨1¬uänŽiî+�3«íßûn[àf÷žGX6{¹åX¥GµÚÇ3/`õÑùf{ªoDoâÊ ¬„dLDX. >(*ڀˤ$l@"�’`æÄ>\Np5XÏfa48YëÙlÛ±*ı؎mTØa86ZφÓ#2Jzyñl"BëÙ¬“‘¥p0¸HÛŒ‰«$Ä<À HÇ` lDJÈ:,ÝBR�€ÈÇ…By &<$�g³Ö³i0’JrQ�± †‚°áziÀ› Ì2pÜj˜ÈȘ¸Õ€²Ž™Š|@@zå¨è�€‰@ñP‘ÒÙ¨°gã(¡‘‹TÙáB¤�ª`J‹ÅÇ!%"ƒŽ«$LD<]#Hɦ%d�’ŽI, „Yp‘$!‹a‰y€§ó)É„tl}„tl¦òéLgLg3&nó y™”�M4 „ E@�ÍfLœ�€:ÖùuŒ’ªŽ + +Ž3Ù`PDD°˜DJÈ$(@F.òÃÓ1‹œ£¤År"êÃ9�PDÇ ÅÙØL@�1Îp qÒA⡲ Ȉù¨ð’aÛ1Jú -+2"ô£ª"(6¡õl6,<‰Ðz6" +')"ë¬3™—wÈÅÐÒ1Jº0²ž�'Bιäèi>`œœttì:)>î3q" � ú…”Ž�.Lh:Q@cf]\xÎ�ÉÇçAG#Jz‰@IÀÑEÀ�tì¤G8Œ’”èh¤9éà¸àQa“†˜— #°¡Ã4 +htDVlH$tS�$L*PìÂŒR ™…‹–hTš +8š˜ +!ëÙ°ˆŒ¢¤ :24(ÔgF3Q±ABAäáÕƒZqiˆ8ÐyªEÂ�‚JÃe€;„�Pš Œ`@ø!�Ë„ +BG#™Ú±ÑCCÄPR„ŸÌŒF`nf4 1£Çh4BaÅ HÅd2Á‰A 111•&!Âi`#›±011bDld‚Çû4 À$ˆ\@,„—;9àñaÕ#%}L¨Ða +tEE�Ã:88¬BË„›’*6Á3`“AÁbi$„L 4¡yØ@iF)! 6¡Ý`ð@ ~*d:xF£�è$ô�aHÙ @ �` +FI"�ç‰~.)itr�‚’>”t�‘( ‰Š³‰(ð !e /2"�gãáAI2" ›ã„pä-è-8r!÷|f¼Åƒ  ÞrUR¥#…ƒƒâH“6LL  $Üp°Ñ‘1q£™ÏKJ„„ÃäBü̘¼°4øHˆÜ@gAõCCI�HÌâci*(IBGˆ x…2�é`x`‚/ ò€=Z”H8xq¸à y‘„†XVN܃‚ŽBCƒE+6 efôÀP1=2舺˜ÑƒÃäú$Øàh¬•�ƒ‚Nɨ4”ä ƒ£QÌŒ�õœ�h”�¬OŒ +ȣ ŒQ‰QYltB5H°€@�Š’ŒN@"ø��>`ÀÀAÄ�r†ƒ`Épì./BŽ…P ¨Âæ¦%¦Âˆ‰„Èg4òl`Lˆ(ˆDD&¢ ²‘½„€^B@$2(!£>%d›“‘) ‰�hJä" X D.’’R#Q((²€…(.–€%%xy Ðiy‡d 1$$xP��óe6".b(0ñ*Jâ0‘(‰ý€`¶á`ÃDÈCÈŒŒ‰óK7+K´_^¾}iëØ¾7MÑ×W×™¡_§ÿj÷çW^3#Ÿî¯wS^Ó>Éeº}3ÿýíô¥_M�Žæè÷¾º×ÔkVÓ3»vï*’•É×6í·/§ÆÎ7O4Û6äÍÎ˼Uij=Çi™µsOÍüMùMsµj5ywº>þM«Û®yÎZMy¿hj¨¸t[‘ñÌò/- ›³WõòØôsí÷µ¹µ'gf´>oã~ýDw[¾ÞÝL~žÔúU×xí8ÙúºO÷ný¦ÃÛdôó}wí>[ɆϨk¹Ý¹˜¸ì츹ɞ¾Žu[wS›µÚö›° YßÛõ_Û½ýÖ³:ú:÷K^xÏ̆íÎÕ¯×CGÝ[眬‹ípÿÙõOú|æê¦äí}ïµm·» ÛÓÎñî­c¯¦gÝÓÅ«êù\M¹Ç¾þ5WW“ï±k¢·/Ýåå=ésÏn[Myé»ù&Ëmçu·¯ë‘ïv÷¶šra²m¥ÉÛ¸6ùº×xq«ë-/u>dÏtï^æ…Ú'½LˆÚ›§«¹¬ŸNÖ›rÛûy97ã÷IŸ­¦Eæ^í¼¸¿î·B^÷­¦Ã}6T䥿hŠÇ‡k\¹›÷bä…jÜÈ7]«¯ùræjzÄã{ç…ÏÕôi¼~ÝÕ”lˆ½ô÷&]ÚÝ�¿è¸Ì÷Ú¦WSærß“>Óåþ-¯s¯úø,q]~5õb[oÕ•Ï'}üí&6]ÿŠÛÒôsŸÛŸáZï“>æ¦~g¿Ú¿²fô›œqõl©ÅlŸÛûíÛÏù«Iß<{iïWz¾º+õýWæß´¸îßäÞx_MÈÒÓqïyá{5ý:ÿkl¯¦ó¶ÕµŸÞ˜ËÖ«É37u­JÛ]Œ~gØw«]MlŒ†ˆÝÕ.JÃÆ\É\=ú_k¯<ÞÃC^¿þÈû®ËÛìÞŸôyµ¥¼Öë7)?.®ßtßîŽ+y«IÓÏÝ—®Ÿôáí·í…ë7-ú¯??éå1‘ÛVïV/cëïïù„‡Ì(åN82&“`>&‰›P€€ˆ�Tv²é$Š’X`8Nî(‰’(‰’(‰’ B�& Bó!0‚b;VƒŽƒÐíDƒÈEJˆyP]Hé‡%t‚U Œ$‚ÈCÆv¬á <ø|€Rf6HN6º¢©<´ÕmÇ2D`€„.<.V>¼R@Bc´,ª•vX9ÎYðŒ6›*”‰ÊUˆ, ]Ç3”t:ë¨B! +  £$�:MÅY& еl0|€®c�s«M6œ“€± †aÁŽbéÊÆ¥ø‚èBŽLPRk„@ 1çáPù¹8È)Ô‡s2ÏA‹j}6 Á6`(‰–‘ÉXTÔL ÅEG£� %QÒ:ÍË$ØÀ€LèŒJ.Œx(8:"ž �ˆ �‘tœ�ŽÆhô°²á AiØ@UQá%#¤ 4¡!12M_©¬ ‡ƒ�€:¡;°žÍÒ ;°ž Hê" óþÀ5€r¡,V M8×Ãà¢Ñ¢ó#ª�G„%¢À�‘‹$À1À!7œ �’ Î BT(‘QA„"´º�bAÁN”XüZÙh@Si<.$4 .$8šÅLDƒ8“ކ„‚ކ’f:LP|`ƒ‘ðƒõlv"`sò�”ô”” ÕHðHLtŒÈ¬R�Ya¹™ŽE`èÌDfe‘È<¬¿´P  PÒIjLP%Y<¨bÒbZHZ:v�â�Ū-ÊBI™¢Ò± +èñ4Q9*“ðøP9#Q鈨T *)šÒpâ"�”„ ޤ¤c, D N@â)©j€òQÒPÑàÙlÒAÈ„HHÇ.�ht@B:6aH9ÿp‹ �‰Ž�ŽUÐ;™¡ó% ÑÀ ΋-� çš°u̅ʈ0À!)‰’B$8–̨4¾á 9°V6š 6�Jƒ�!ÁÑt|ÃACI-ž�f¢Siô@…„¦‚Ét0Œt4Ø(Ð #Ô©ˆÓÁH Ò`ÀAI (ÏàÃÀ†cFc$Úh˜‰FX`DHœ©#yCI{ÒÁd*0*$ÈXapÙè ¤ > /#ù`BÊÆC®ƒ©"ø��"�*5¨OmG2ÉÀˆ>ú2£¢ ! +hY©àò¡ ÅX˜àâÂAA1!夕³ëÉÿ4›Õ=Ïo1Û—×Îue÷I+¨Ï–ßkù¦�¾ ½šrÝúø}a:f/~îjêõsüÉ õpB}ô´s´Ÿ²‘í�QUït¯›¿Ý²³‘•ÛÏí7í¦?ÃÅj:­â-ûúz³{KVÓeü™¦hèö‡æŠ¹‰úÌÜߘÌìùk¨ÆßŒÚ¦¦ÿ—ž¹þ™îÌˉv¶§Èœ¨Ìÿ;ýª/fÞ^ã{r.Û¿&'£#ºn²ß¹+gcg²ÙïgšßúKÍ^D3<¼©[óòZ�ÑNíqµ­*¯w[ã®ËSÄïµ=uo³ËSésx¾ÔìÔíuž*s®ãŸÒRñtsÙ7›ªô™”yJäÔ^·h‹15Ù—ÿ©ôÉö·åš¦ëwŸi«ÍTúàòÏIÕ�—שz:óÂãI¯SúLîçܧÈë–në·Ü‡·þ~!zææ2þÉW{¡{N¨x¹�ýðx­+ÿô¶Ë=·m1êºÇÿé|™~NûÜyÌ—èËÿSz™|õì&Ï�ÅÏêò3E˵ö›ö§ôq_©Ÿê>¹¯~vZÞa/sŸØôì×goçúöüü_Ê>ùçÝ®VO÷œÔ'Ål^Î6å½ñ®^Oéóz޽ºîËÖsúÝk¾U]œžÊ“«j.ü�¶Óze{N‹ÝϽPwÒÏ_=Û]¯ŸÒçuÚN\k¦É­ªË]Ñn%æ%çúàžîúµßs[ì¦ÉÙºþë�í´_Y¿—ãõêäÞ¹½©»®oU÷^�Ù×ñNŠî¹Îw·å‰:=Ÿ:ÞŸ®3v=^·¨wؼ™Q¿uñVZO—»† õ¸ß–6ªôÁµol^v®ïŠ‹ËØ§óô^{¨ÓßíÚ÷iÑÖsŸWÞiæ²{~_w§Ó³Þ½ÞkÛº{]z?åÂ7MVïÄ_|?!Þú5• +°™ ós\k�þ´KÛ /5õ–ÿ4$—‰u«)õOz™žO"0NL"¤—ÉÔ'|v$N¨éåÓ�O¨Êr=»ßïÕWÚúå®7^·úúñúUéswf™w—·Ây§ÉÝÛÓ¦½µsf'«5ö‹$Z·ê„:¡N¨ê„ú8¡N¨ê儊8Mík×õ[È;íºu®¾r.Åm<î»ÿůìêl¯î½®ujFTÿkÕ^ª;eš�ùæê„ÍÏËn¥Ï”«Sfzêä›~hÛkÇŽêˆlæ¹ÞÖÖ¬Y¥Ï»³#/G]vLß3[s4×Õ¹šš«ª!ã½ÚJ+“b¿©ón#oó¶òýBVíOÆ<[Ý�üc}Æß\DO[Ç:᯳vZ2¶½�NmßÉû9›ÝõZêó»Ý²lMV~[†=ñ¿ÛãvÚ1žJ+S^¿½ïío³òâÕiÙVýÖî­ªcûõö©ôÁl½mÙ Y¥Og³zÿÂ>V5ÿGÏþ¶æTécïꋘ›®Žø:Ý­mNn®é»ÔvúóõsTÃI›ù:u¥•©Mû˜û¯ßS=ÚÙ†­»Òy:FÇÕ¾Ã\ýۻ̌½V_&åNcn´d)&cÚ“f¸úDì3^œYMÊìlz~8íjVÓù2çB?ìÌ^CÅ|;Æ©_÷v•õyý_5Ý?þ.t¿µ5\ì…ö'­L|ç§íëÞÎûÙ8q}2_~c/}o}CÉeò]5=_‰Ž ÏpoO×µ×cV¦¨Ê·Ô9Fˆ|N2BB2 +œ(ð8 -Èñ=ÕeÞæ¶n³šòú,?Ñд»›q!ço.dCieBk|ôÍ»…œÕÔæ|Æk8�¦1·REQ”$Š�b¡€ +b&Ѩ‚LŒhaEH± ’I™DH‰‰ �D.RF ªj$ +@ÊÇ&Ûš©íI+Øùÿ‰ÈÜ+õnÑn¹f&ï}¾«´òy+YÿV—Ùv¶±fçõ~§�øéŸköñ™êRÄV¾Ûÿ[Ãeií¯Œ~Øï»²ê{š¿Þ6Ï»[ÕÞÑzý­ÍÓ–¿vîòDõlsÃô[Îo[ÜÕC­²}ujúï½VÓ{öó/L½Ó³<[Џ©ß¿uÓïW.²ï!êd·µšXýqsuz{ßu×óµ©ÕÔÙ~ºwž†¹ëSϳ—õlûÿ�W“wîdÊ|Ç+­­ó?qYón·Õ5Ïúì]&Tþf\i[�Ú±ÿˤ®†~š¯¼,¿÷ð.Õl¡ïtìvÝ|íœÈv}¶¶úÏwÒ3ãÌ¥¨Õäˆgö¹y˜ÞÞ÷‹=«é/5WZѼµ{[ìÙ™Æmí缯ow×—¾>wñ•3w)wªõ:Ýk}[ÎÌÅØ�fÉûÒiKW+ÄÜscõ5­&6ËÍ³Ý ÿ-oýõ”ÙWZÙÖûïÜ�üp{ñ67Õu:u^kK­NÕÿ3‘ÝMýîß?óý'›ygëÔ|̸þ›Ñ~™«ä2yŸ§â¿i•}�ŠöjˆªîüŽ ‚9qËŠêxk‰êŠûilK]«Él±-²±åûîíºÛI5•UOUoyͲÍXu»—÷uá-Û»ïeöZ×jºÌßËÝõ_x;�¯2殯}ÅÛéø’±Ñl«Iy·WúLh¬y¶ëƧ~ÝÓî:êa·õR{Ô]ôÛ^»Ûk÷§Ê¸Œú‰ˆxv–†¼¸¹šïTúL½¹jkì3<|Wång7?L�RÓVѲ·rõVt][|³»Å¨“²&÷êzêú[ÇïÛ×ìûFD¶SåãcNuÎ_ºöŸk_«Éµs}ïÓ½[÷»×üº¯ïc³jê.}­&ecÆ6{{eKSƼ劋èjÿnªÒK7®ôSÝéãÃü1?Ýz¼zf‰ývǬ¼ü˹üÝ,�¹±½Ñ—s×þY�[]m7×”Uû>—³NëxɎݬÒGuOÕM]W'·®æ®æît¶[î]‡[]?ëë.ßfÝÆÅ¸•›ùöêºÜÕÉ}�yçVÓ²™±²§ázÝlãnÝjÂûV�^³õÛ;ÝÖîVSÞâ™.U�üxÛͱUqñ›ýZ²n5q¿³Þ-kjâ¡·?ºíúÕIñÏ/M}ù¯¶JûkV<4õFSîɷÖ}—†z{»¾ØÕõ^¥Oë­}í®úÓLO½×�«i3—ÿ¥�™æ¥Û:ãjêÎÍ�,×ÿVSÿr[ãßNýÞÿýÖÎÇj—‰ÈØšè¬Žê¹ 7QÕín=î¤êÝ­‹}«É}ß}«Ã=Ë�ò¯Ñ¼ý\W?#3»:úòÜi™ÍýŒ u¡%ãÚ¿­c÷ö»sw>[äS÷Æ}Í¥‰ÆÞku'tït´Ó­&ÇEý•ºšxÿìîëq«‰Û.õÝtßðxyöíù{îÞYþ.»eûƵe¸ÒÇ-¿UÙt¯ •mÍ}›ý±7¯ÙuOm¯=½ûwÍo•yõ9ýÛVÓwÞ£{âÞkû1ö+kþÛÚVÓ¾²³¶ž*º5;ûÚ$9gäeÃìܵ9é¡´âlÈ·ºío­«´âišëÉù¶öuú~ÔîÆÿ\×[M‰‹¹ªÌÿ·–÷è¬+w«ÉúXÑVÞJ)$fØ–¿÷§kÜŸÛiöyk$ra�I QPˆ,¤HP À„�)#–”.)d&Àx°Àp”„~ïØÏµyŽÖ¨TQ&Å©X�²$TJª™& ã0@$ eÂñHÈYà€�\,HD4"†ƒ¡` ¤(!ˆ¢0È Ä2 +!« È¡×öª)í§vN}Óa©/&+ @×(�Э“Ô;ˆD‡XœnoIjßp“T�¦|aLÒúßÌ?boŒ]€2öd3ì&:v8#<%í•þ‚¬êi–MºDÆZˆ­b}ƒ&ë'ßÌÑCU£qê&úrùÐc<+Ù¸;N0åH(¯C#‘ZîH” cƒ+–ò‡¯ÑClÈZ±ñyÝ?9æJ+¿Þþ�Å­A +Ï^ní|;Ñ8|ÉÃT!ž+-2t6-Zt¾öÆÿD/)� ¼IN‰- ŽÀKEôUMå¡Ì•Á­ê©?!mf ?ù�¸º•¢ˆ!ûЀ 2…{}¦ËöèÆØ_³–„2X}…#ÃKÚú2g�PÀцǞ½€àj¤„éQñ–«Öm±R£?%«˜§v^ƒ�— ug�êØ¤'É{Æòò�P,Àl+"¹†»W» îWœˆ˜½:’7ë£dèqñ4^ýü^ƒ­?@3ŽÈ=¨ ¦èûæ}Dá±µ̱bÇ;xs2r2q�ÿb Ù�Y¹6‹—”{•ch&C¶òŽ£®_>Ãê'0RÆLÏåáèp¬æU|T¥„£(¾ÍÛ¢‘ Z2`mæð?áÌ®…D=ª©3’óSN•Ø`-³óŒÄÇ­ïB˜M¶N B.øÐ0þ$‘h ûˆ•ý$Íïfjm6œ/ÜËË¢Œ‚¬p…åÝ™ÖÌÎE}wÛ%ÎæZ}Æûr¤["ÝJ‰‚~"©F…t�b˜k°©L$`2meÆk„$§ÜGÐú´uK¢ã¿ÎC䱂’œf­4T?'WÔ ½�ÝSD +‰î:ã<û8Qœ¸BßÔJ�˜>P“uCô$ÃÕ5ÈÊÌTÔÓK³È3Èñ¬ÖT–„á-ÐÞìƒó$FÙ%Õ/™$Ëý*X—z¤^Zðõœy!‚¼@7�y9ÀcÌŽŸA6QwAˆ‚ª¶s�óMôÍpÞ×ðùÍ|D­¿p4÷­‘aF¼E~˜À¶±ú«†KÉSbx¯ñG«q¯2¤—æÍmžf a÷³^ïw©Ow”g¸W¡Ê˜T£¹ü¬F +Ý;�suá¼,BpÏn%‹=+2B#K†íE1§|¢a©¡¨Ïwí5iâ‹¥ÃQåJÇ¥pí…mFd:Õ©!ôŽYב(©!tjVøuŨ„+£Ô\Q¼L¨Qv�Ž\&f}¬D&¡ÇùçÎq ‰íºä0’½r=h’Ûl“¹xH“kÓk×鑯û¥¹\–dbÄë�©©\fÙyÛ€fËO³¸m®û—ìâ˜ÂÂ\%K«·#(´Ý©#•-Å5Ì(UÄ|³˜Ï2׆=Þ"–·GN¦__i,Îù_µ‰dè³RѦÍyµÚ·$œþQäé¨a`sí®�¾‹µ3bí +ÌÝ2Í¢i¶#b€ÕØBÈ/2Œ %²kÍæ ÞÆè|X°·Û-ËAÒþ’S +…žUáË ³U;™V×5¢\ÈÄšW¡‚—b™pE³4 ŽAÍ,í~`È9Qªt°\±\˪‘7ZÉN»®Y£å¼~èâÇ´7;$‹ýD¶´lZ„"}ÞKoÓã”ÒÎÖ+\ÔæÅœrºª� dÖþ +;x$.­P�¤ EmQŸ'ÁƳDô0bbSuYÚÄ‚ˆ¤ÑO¨ hÜê2 £Ar¢bù÷Ö`Û�å©TŽ<"[*Û®-3dE‡šÌ7b)s-)Jjòaë•Ì#bâ4¤’rb+EO{·ns\cø5b?²‚¤!bñ€?v.éLGrý¹§–D¿lB$æOÍÊÇ©âþI¢Eë¨Ç÷4Heb[vጥU?Þ=n/I$:©€ü»Zª2vˆ…˜uexb¾©™’=Ö‚ýt@ÆlûÚœdUÌ<{D¬¨ž ˆ/¾èe5©œB?ѾÌfP<1c ¬¿ÓøX4-Þ²Áz¬ß‘§�*ý|çÃÙJ1{“]¯–;©ï¤q(MˆåÇ/�â#ê„$šÎä’|„Öez§æ=qPMô–dÇÖ‡PÕÇžA¯b²Q}¡Î)HÃÑ¿î�¬¿^b”O3²§©ìW“øÏ +ÎmBÚó‹‹»órcý˜Õ9· ¬$9é4º£Ðð1xÇ@`gTÚèvI[Ÿ\þ¹eÛ• p$;J)PXƒ‘_oË! HUV5î\þÒ‡oÅHê#¹›;Û‚:>¢æ'ßÿÊO¤7òC®öÿ"zpÏœÙiC‹9�Ià¡êÔ´8ÝÂâ£_b'PÕ!‡oE®²ÿ!°F»ëÿ2‰£dæ9O9†HáƒþBp"ŽL Þ�fí^wó¼‹Ýgm™¶âe¶=¸`ú<¬“û�„I;Qÿ¶|4µ[‰ˆk…,ñõi™0Ó øo¨$J†¨³LdÕ�8C}/J O\ÌÉâò¨D#£ßÞYÅáá*f×�¾§èý_P$ÊùO6.ÝÃÀ{Í·Ó‘…,öcž¤+×õéé«…¤„ ÅÄnGÀÈç×Ã9ØC\u€�næ×Vˆš»ßœ´„ 3õ4Á­ª�÷P€�R·ÀÛpùEÊ;ÝY¨/ÙQœâ +-¢‰R2° ÅYváQsBµª_(ú¤Âû�5H\É ì©›�Æ É&þÕšgÆ…¢N(}f I+èRO…hðfìÓÆuéôF'5sU1ê%ÚQ”êÂüª0$ÝÃñó›Ã¨Lÿ4i7Tþ0.ÕÞ�ažäú²¾3’h»í\¨™Â"—Ö‘ l×Àß»1”xFÉæüËäŸÃŠpˆág­GNÕ3LIÚJB sר3™q…N”ͪ4æ (^ˆuç¨] êU£ÉÓé¥äœÁZMN˜=û‘{`Kg@¯tºó=èÓ®vF̆­E:[°·z'g£®-#܉ÜÏO�™¿Kðš°.óö¯iÑa™C-ëa<ž KÇÍ&À�ÁnÁÞXÔÛ)S9 +O½ƒu\ vµªº°wÃ{’‹çÚêàÔʪG«’BMZ‹ƒ7……i*UB-f¯’<h;}+n½”9€_Ú9°ïNMÉ xƲ3ü¦¡ÐòN6ÒF¶CÙ¹—ßÈ‚D“KPH&K]aù@‡Í Œý!ȟʾº]¢Òו£æ’ìåÙ!?ñ¶ i S×£6ßDX«${v´²7(©hìÔi<µ%B¸ ä—*uÇj�Tr´Õš¬t ù`ðú!åIìÏÚ3NéSܺ;˜ó­.•#ý4•t¼BäùaHëK 8Q­Ò¡µü¬{|Á¯–‚ã𥤲&K†o°t7Þ-ˆ ”·s’ž¦ƒh¡Ôa'µã‰v�ÍANÑöµd†KÔ.a3ªôè .ÄÈÎ Sðb]_¬€‡ø/ÜþÓ8î(Féq{F]¬TÊ!¥ÍK–ï‡ÔÇ 1éÄÉ€±=áˆ�©u( +f ø“˜‰ììñcb�ø.¥Äì;^:^VP¿c×Í.µË¦OÂò®Œ>‹>uõ÷�à­½âe‹*k•>bf‡#ðê�·¼"X”·¨÷�´�ÚP>ƒbî@›0<½ Õ'Ë<¬OÞ E1&³}¬> (‚æLQ¤ákªÈ2u'hIdŸX(ÍÖFl=y,¿õ ºò5õZûÂþT½™*EU8rž–þ…M,ý¼ò×@üüÒDVȆ€VŒæ>vV%²‰§‹€¥ÂVy'„S»D 5óÜ[™lg*&>'Ú0Šà�z{Z×¶‘S¸Å$æ1ƒ+e loKؤ‹ÿG‘‘Fñ ÙpFºcw·Ó(P;_÷ ç;´ú–Ã%¬÷‚øJ{®«¾‚,·UÁ Û)Y…ÞpÕÒ‰ýVEûªg*Å,ª“Ï>7¢)¡£ýÜâ»Çã"©×SrTôHQÝiôƒ­ ê—UàÄ!롚e4“ñ5�úÅy$åÉÏN×Ðí;ò8Xñ…oŠ-69*ÖåmI7()IL#^â¥,6jÞC %Þ†ZÛjB“CÿÑ·BepËÛ7“:Q¼5W9ÄÁ�@ÕÉ ˆ?Æ;!iZ».' …ë +ÓS^¸™÷Â^Dñ3ÕÇ.ÆR•áÝeFJî²êH9„ôÄPÈŸ¡›¨ñôŸ˜¾lpÃCFë0ý-y >ZVPp~ ‡œJœX˜þ„ÕÞ•�”ðé7YÂÁ¨M‡“© ”šžØL _I«Ùí¹Q=sbP!.V½ã¢øý‘Ìr£UîbŠÊ~ß�5îœ1G~à +Ô(Q4q øúb±&¨!“ß‘®WdwÔþº¾‘Uß’j£,u[Á°I�ÍUÖü¤TÂVk˽™œÐNš§òol‘2<¿–­‰¿—ÊEH6#‚LÊ™¿¡± ÎÁ7«º2Qƽ9¾3Àñ÷1AJ'*Å`ƒÆ¸×²eþ#½¡Ã :ÉåQ·Ð·mÇW¿üÏò,šÚ ¸~œ‰¶v¦xðJû û#+›0œü¿uqdú×âUY�ÏÇ”ð¢\¾0 #·@ÿM,¨oï9'¡½ÜصZ¹¥;øYF‹Hì8I�­#*l¶xþ0t�¼Ø?Ìêq@áøÛœ™|!fm�é7+°çÛ“5 �ð*Øœ3†‡¨B\—aN¸êe£ãÒË“µ�Ù mqƇ@Ãtšù˜çë fÇ_tË”‹fú�Éu㪚(ñVŸŠÉJ.œ¾9á©Ul�[™ª6¤oZѹJ‹ ¸•ª�›‘®Ü +�ÙÐO©.m>×RI . ÓKØ|v<*NwˆËÔ.àøh >^xß/€^ È8/~èà ˜sSXK·©ƒé°9Aµ¦¼ +�ðBræ.]•‚: +' ż˜iå?×4¢º?Rœ<§ Ϧ~®ñD…�ö Sò[gV!cc?¬ËH›ä¯p/|y‹ e§ç*äÂÖ(\ +ÙîÕ«dÔ¼2★ùŒ�¤ÞuÙnDM«tã½Ì ϧ¬3Rf9:'¸ôhC!x†•ðÄ}TŽGxy¤‚5s†²È¸ÓR­#E£9æ •®è—åÈüf�1ÁYÒ€M\Àúèç±ÅjWØÔ®'3*¼:ØÔÌÃ-… ÛsF¢éñË…íúhÐó +ó[·0}£6C÷7݉÷æøÒã¨A¤»ÎËj~`JËL`*Õ0ôHE甘byw{ó³R™ÚGÑé4̉W¹îñ'2A¿¿©Ñ2ìé ~~C@QºáFùk¸ù£Cš+NpAçˆn L§3ÂÓx)KöC§™S²Ò€ñÿTU@y [ŸÁ¤€[ãøyEéÊ™‰WHªÀŸ³c„óÔcX*3=2Gžx´ÿ\"SÎ/Š¥âè¡Å£4¯o6J¤&W4Y”RQ[䀥~Ží +ôè�­&¡¼~Ó�¥Ãâ™Ì±š‚áè™�>§™60fhNþ1˜¹=Ä)QCͨ:û%øÈT6E ÌÚÞÌ�¿cú\:!@·5 *£xÓ…Žåf[„Ìd¤N–T¡.åˆü)@s}ò‘!öÎN Z'¡ °+>‘Hf¡o8ÈkªŽ†§šcˆòGN´8ÈNÑýïEÔ‰xP…O�p>$øˆ ¢R §Ñ¢¢â=%÷#c!†P% Nwü›£ZéaYúÒb.†Vp\·�¦“kKÑ5ªiŠñ-æŽpšÕ ª,ùe%‡ÿL p‘å45IiF+dе”RÍCüÎg¥g°*¢-Õ`HIÊni Ö·=๻v#0�u!úÆ€ÊK2ybú÷ßÖ�³ã¼OYÌ‘øz®Ÿhû^u9‘(�8žÀ#¹5;¤‚n(SÐV®SCIz‹Œ½¶“öãä·bÁÌä—Kà7²@=:¹å]‘LÛÉNÞɃ§ Æ¡é˜/%Ór‚bC5£ÜË“ˆÔ¤Ê¬¿nHf`aµQv†2ÎŽ§ˆ ?†{A‹*?ëSFÌHtÄ¿ïǯÞDŒáLÄ0brC×Dyh£Ï@–ÂíþËä"ï”ðÀ:—–&_?ûT_^)�³Ä¢$êè,’y�4%)ÈÄ8&„ê6Tf)ëWï”›·&cÉ.eE/Áô�"Þò¸<(ftm¸+jvÓ5×14ç!ÎÚì (pªÑ“´uÈ|ÁŒ—d©Ù~w*4ÇÐ+Ý3œÅ{îSÊëpƒéÇjÉ«ËoãëxFVøë§+—|?²|�˜!þ: É¿c`XŸ¢fƒ'‡væ—¯º` ŽÓû©ø0ê„Úó㦚iÚ…³t€Ø¦<ó»�Õ)˜¨\ºvövBuûoø +³T¤Ond{¶U­€É9]¿*Œ<=3(+§˜Ofà¸cù!¢m®ý´úÇaF ZɾÅ1ÁBÁP]3ÕºÞÃN齿X©Ýzb1ÒpkXáŽ;Íø¦ƒL_ßSâe‚Úìðø“ƒ|W¿ßlåçEYÍæfjjš ÂËu…ƒ´vàÄDp]dÉm¼ÀžÙv×(û)KÜhI€~EõG“ïy†çØI‹NëЛ(vwó.©šO8äª>B yÉá|'ÿ¢7Ùc�\Þ(ðš‚.ÅÏMžÇXsL-¹0690öc §ë?bøŒ¯`N˰Q>X# GÚq¹ ¾<ÚÆO*SA°ûåâŒ#49 þîïêÀC¨>ŒÐö>°=„.àâ©5åB8–·4ú�RʼYX1–³�–¹r]SÝ”yd˜�3w‹Ó˜.Ü TžéسBe¡§ç ï�·x)ªš¬Mûüè +}oyš³/}tDÛ«/ó‘SB)un$ËCŸNC±‹â6^ÙÿVjó«NÕ1j@%®wðó5ýmàV¤PЄJ~�¦½b+.òN„2¥Ô?çaJ'øiyþ]ñ˜Gn96VfrǨ“6ý0ΜºÉc¨"l1o¶&”=”�;Ѿ `íúwâWKœEÉ•çf;`Áñ,×ÐEGH Ãè¼â3ð20¢ç$Æö½<×~W'ïïÑÞ÷Å‚ˆÞ!"�ÕŒL,ÂtôáobæÙ1äHU÷À\ Õš¯ªH=„”–6äĹß!€aéâe:¾ i(àº­Ž”‘EF>¹ˆÜcKë*Æî#¼ø3¤L}C1b#PmðÔO˜“í\*I `½Vf¤­?«ÿj#• ¾æþÎxÝVT¡çŒ–ó_¦¿’¾Z7Š «…Ó/Œ ôú%ã·0 š3Rð¤ +Þ¶’Ò¬SÁý–@}%ùñÒ&Û7ÄÇ~�l8Dè«ð/|`vÓ”©µV7óìLLK‹äý�ªé‘ÑÓ\!}~5LDHMÖF…MÔ¯0ì_ôb€ýŸ.:ê%têíÚ—÷7>†GiÎ¥–ùñÔ žà“°CbR ë)¹™›hYpD„C¯þr€Õ—ï{�;2€~&Ø:mM€+^²9Ä/Üå¸Y]ü§szêé´#GE¿„JÞ“ÆTíµþª6 ŠsLWâG/ÚWdäÚ G•9ÌØ¹ioˆ¸WŒÚП2;!%»âotá;R²~Jǰ3‡më9SÉÒsåËa©á„:ø^@¢ÖàRâîÿ.—.ˆÕ¨94̶ܮ¥^h̆-0”WTÓ–,jûöó¬FvT3õ·§‘—ëž�3aìfinˆ· šÐÆ«0%·¿™2¼ƒQÔçPÒgÛ¤a±²ðk~¸Ðéø)RÞ± *¯Ö$î)váiÓÌÆþ�å  ySâüºØâ|¯B¶tÒœC1±¥<(5úØúhÏÝWC ofùг_ª¸ý® œÀæ ¢o.}þÐ`a*Û/qt�z²[f¿Íh;¶»ý´càKÖöör;¯ñúþ]ÞäU`ÇÐe¿@™Œ ºýbR¹¸g¿SµýJ´‡j"–/NVuB>å¶_ZÐku‰Š…š�eb!’K~ïO“ÉMRÙæ¦ä²eQû‹rû% +1UñðZfÉ~?À%èÔa¬‹¹ÖÜäÌ®ä(u²ïéÆãíwM"·Ø&”¶ÚSÔ’ýÆ šäJö 5¦©›êC¦M'ÜIŸ:JcÞ”ýQxûÝdLl6RÑj7{»|¶~²÷o¿¡µ|Z¿9OÄkûý‡ ó(‰g¿† Û¯û +Ú[}Éo¿…e¿Q—~’¸õ»f²?o¿°Úerd¿Úì}!Þ~»PC  ³_wûe)…’ê©Wò€·*ö£·&<ùXèâIÅ´ÎçC�°ývlTôÑ‹Í^fY©Na·_òH0+Òœ¯Îÿxß~‰ŠÄ´u—„X³_ó‚í×(‡Lb¯3ŽvläöûlE4?•Þ.Ê-Å"i²sÝ~sÎØ³’lÈö4TDÄÅÑì×›®Oÿ©cû="Ý'[2熗ö‡Ü~uÁé’ç~7C�ýò+RÖö;7ׯJwÒgÑMŸ ß~·n éêà +)R/Ø»¯›Pz¦“€ƒ ¾:뽂ÂuR9„yŸæe‹„.¦›d³ÖMqm/8⦌ÏK¼ý~ï­j pþÈEc6 ”7™Ç.%ÝÉWô“ò¾ýnù�#Hh8ìç|kö É3VÞöûo\„(œ&W˜Úº Gëx£'3+otÝDŽ'ˆ°íwÞdz”ݪpû½¦’þ„dÖ™îVo¦TBÌR*­/‡Ó�`šý>�em¿à}‚÷ýëI�§íWå;(ÇLsÖªÚ" d¿YÛ¥ü¢F¯ðü0DŰ¯ôÌ‚·_G¯™ÝhÌ¿e¿¹ì°y·ßå-©¼Ó‘“’·7o¿…ÌÙWýøÙ” h×Î8¼Yîg¿Û|Y— +‹îo¿™ÿì× Á;Ö´ýjn³ß�)\4޲Ûol|(ñf¾+ûMÚàCÝïËOfÕžýŽœî~s]ÐórŽá¼he�B,Ú¡[!­&8™¥{‹B[ä‹­õìàÐl‚‹_G'£�ÂwlÈ29&Žê‰jâ®Ë¼™¯}q¿Ý5@Ö9að +ÜkÁ×0ß©ƒò{#³äàé8ØL0ÉNŽô× +0Ë©DŠÄ`Úàö©¾¾z#¨™²Y9ûþû$gò ½‰{®k»Ôäø#;<+ooÛû>¯^-iUUoAIʵ°`ùËSàŸç.Yúìsõjç‡{+ŠÆ÷‡ƒZU ª± pýx€ûØ&†JÀ}/í·'š¿›l„(Œƒ_ÄÕ F)äJÂ�Î0Æ\¡7 ̈ªÑ€‚Úbú�R~_ˆ=ßâ G/C“—è&Ù$ªvM±}!¹ª3)©ŒXFȤGÜ›AUž%©³g_œH˜K®L¡K³S=ec‚3à!‘æ6ùüGdºð£H±þ|5PÛšx +™dX ‘¸X·vL%?A(VÓSí ™lvË•OÁƒ›B’È„¢˜!“\+ÂÑ0?‰=tË&oJ ¢_–�>ÖEÓ‚Ù5ºÛ3=‚¥IûfŶÃaÊ„ÓnûêÁëSP?;›&Áy„¬öKשÒþŽß‚‚ðU{OiN!¯Ò´jº·!: 8[Ò`Š P×´í +—¹‘àÅ-õ4ÈÚ³; ¹í,…�YÌ(7÷ç +»š:þg®lz} ¾ò}…_ŠeZ!Lø�¨°½&‡£‰†–äÐÁ„~™'ú‚ªcWÍ6¸z²×IÜ¥”žyN +u°„\bÉ�“áÛP!þb÷Ÿ«„3ëõ4ÖýUwÝäê_œSÓóµOIjYàdœŒè<÷Ôo†'Çh:¼ìÛ˜¹ný@@ß6ÏðìnÂñ§Ãp×kå$€ë7¿âÍÏ=;§?èØÝpû<]Éĉ5ýêÜŸŠÛÀ\†ŸO /?*ŠÝuÏ?ñ¨êÜ’y´ÒŽçóQC|ß;P Mc¦÷O. #TŸ|,¨AÄOETi>w$*€ªfð5e¬¾‹iEäÔ@RGA4-¯Íœÿ¥ÔE©qÇØHü½%%lâ‘­«‰‡%Çûp†@ìb²óO(Ø<š;à?,&Ý’¬Ò­øë¿C3r¨kò�Oi.pÓh�1 |dÔä�Ú¿W`íí¹:Ra,Ξ½ÔtãÊećè[Fõ.ðŒŒ  ùµ=µsí á+Í%QnÖiU@U½7µ*óˆ²÷4>’¾I�Ÿ-}ãll“¾(„å¸Ip6g)ÕQ¤r÷•çëßÅJäÈLŽ?²ðqÛHÐOˆÍ! *J¸å5½ ™(wé“_ʃsü¾ì+×Û{Åý~LéØÈ}VüÑ2Δ» ¦Ø–ÔK¸\0ÎÝÁ�Ä•+wõ�âœÃtB˜!òÉŒ¡ù•wΰ¥jNÿ«"foVââ>"39�³¸»ëƒÝýÂM”}�@µ&åh)øZöSŠÃÿðÒ÷ÿõÎÚ“iR‰0­ŸÉ‹ôotÃÜ„ñé”P�‹qõKòŸufà[åÛWŒR‘p“aM {³·”Ð-jiô²§-)ÿ4û«­)oÊÒ� ¶ª›þÇtÕŸUsm »#¿šf—³1»ÛËxøÔ]Àe9y§T¸º«=�iÿÛyÙ][· CÁ¿¿��8FØ"—Ã(fX$í–!VJS† +™‚!—à%= ²½#°îµ/eð®?Nm<ÙÛën$÷ÝxO#æs +RXtì}2^20_ŒÌôœ²cB©´CV:›R‚ôè´Øa�µg]ìcˆ£0$ŒŽ˜¼”>~Ú®ŽÆßéîÿר±}“ØŠœVØÁ�’Þ œ9 Ó4VVäP¤*µQQ×–;-® l®íxº]uI¤Ëµ…syºt‡($D]°±òCc*õtﲨ‹ÒOxÓuÅDÝ­?H\B;ìÓ­t¸u ê¶ù�}Ê +bqJL& 3“RvmzšÕ'“XËÏjÔuôcÛw-í¼‚,±kŒDrÔuEßáKaËKb�ºŒfUÂÙÄh�Êf¹¹[à1õ¨«éá"Åk–5‚ïʶÃGækpFÝE¬“�oÁùZsÔ�½ºs1N!%·Ù¨+p e‘vÝÓz¬ÈQw5½+o/ZÉÁ¼ônë1Y;) PËÂuÙ*}ÍkÔ��ÿf29ðt{¿ŒdGÝþ ËMã/—�ºîªò¨æMÝ®ŠG]d Yª*!¬u„|[£.¿~èÜ�u¿!DÝkŒ6™B*�ÍEÝ•¥Øéiõtÿ5펺֘iTíLp9ÿ‹G·•!º¼ùâ£.pgç+6ŽÅÔŽº=Uãåz!Ë 9©Æw�Q— ¼Úå3tíxºŒ¢nxynùÑgN¯Çº=c2fViá肌• ³‡ö~Wœ ´Ù¢Ã’a¤yj®ÞÇsÔ"û:˜çÖão €Rj‚è”/Q¨OäÂÀÈE3T9G»Š öãíE8EÓ÷oeW™¼(K6›\½ˆ^ä¬cw[ô?Qq­µ3hr|KA8-Ç …Þ•_°ÀážÛ°íPwÈÛ< endstream endobj 29 0 obj [28 0 R] endobj 39 0 obj <> endobj xref +0 40 +0000000004 65535 f +0000000016 00000 n +0000000147 00000 n +0000040637 00000 n +0000000000 00000 f +0000040688 00000 n +0000000000 00000 f +0000046158 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000046231 00000 n +0000046427 00000 n +0000047598 00000 n +0000113186 00000 n +0000178774 00000 n +0000000000 00000 f +0000043171 00000 n +0000215111 00000 n +0000041070 00000 n +0000043475 00000 n +0000043362 00000 n +0000042257 00000 n +0000042610 00000 n +0000042658 00000 n +0000043246 00000 n +0000043277 00000 n +0000043510 00000 n +0000215136 00000 n +trailer <]>> startxref 215352 %%EOF \ No newline at end of file diff --git a/okhttps-doc/docs/.vuepress/config.js b/okhttps-doc/docs/.vuepress/config.js new file mode 100644 index 00000000..16f328c7 --- /dev/null +++ b/okhttps-doc/docs/.vuepress/config.js @@ -0,0 +1,106 @@ +module.exports = { + title: 'OkHttps', + locales: { + '/': { + lang: 'zh-CN', + description: 'OkHttps 官网 比 Retrofit 更好用的网络框架ï¼� OkHttp3 è½»é‡�å°�装 ,开箱å�³ç”¨ã€�Java领域å‰�å�Žç«¯å¤„ç�† Http问题的新选择。', + }, + '/en/': { + lang: 'en-US', + description: 'A very lightweight and powerful HTTP client for Java and Android.', + } + }, + head: [ + ['link', { rel: 'icon', href: '/logo.png' }] + ], + themeConfig: { + logo: '/logo.png', + locales: { + '/': { + nav: [ + { + text: '教程', + ariaLabel: '版本', + items: [ + { text: 'v1.x', link: '/v1/' }, + { text: 'v2.x', link: '/v2/' } + ] + }, + { text: '历å�²ç‰ˆæœ¬', link: 'https://gitee.com/ejlchina-zhxu/okhttps/releases' }, + { text: 'Grails 中文', link: 'http://grails.ejlchina.com' }, + { text: 'ç �云', link: 'https://gitee.com/ejlchina-zhxu/okhttps' } + ], + sidebar: { + '/v1/': [ + '', + 'foundation', + 'configuration', + 'features', + 'updown', + 'android' + ], + '/v2/': [ + '', + 'getstart', + 'foundation', + 'configuration', + 'updownload', + 'websocket', + 'stomp', + 'android' + ] + }, + lastUpdated: '上次更新', + editLinkText: '在 GitHub 上编辑此页' + }, + '/en/': { + nav: [ + { text: 'Guide', link: '/en/v2/' }, + { text: 'History Verions', link: 'https://github.com/ejlchina/okhttps/releases' }, + { text: 'Grails Chinese', link: 'http://grails.ejlchina.com' }, + { text: 'Gitee', link: 'https://gitee.com/ejlchina-zhxu/okhttps' } + ], + sidebar: { + '/en/v2/': [ + '', + 'getstart', + 'foundation', + 'configuration', + 'updownload', + 'android' + ] + }, + lastUpdated: 'Last Updated', + editLinkText: 'Edit this page on GitHub', + } + }, + sidebarDepth: 2, + smoothScroll: true, + repo: 'ejlchina/okhttps', + repoLabel: 'Github', + docsBranch: 'dev', + docsDir: 'docs', + editLinks: true, + author: 'Troy Zhou' + }, + // 若全局使用 vuepress,back-to-top 就会失效 + plugins: [ + '@vuepress/back-to-top', + '@vuepress/medium-zoom', + 'baidu-autopush', 'seo', + ['baidu-tongji', {hm: '6eb41c0ab450d5a4ae8769682ecb0ab2'}], + ['@vssue/vuepress-plugin-vssue', { + // 设置 `platform` 而ä¸�是 `api` + platform: 'github-v4', + // å…¶ä»–çš„ Vssue é…�ç½® + owner: 'ejlchina', + repo: 'okhttps', + clientId: '4e308eb78d3a64abd2a8', + clientSecret: '17bf99c60a396b66e8ac841e3dc7a6256ab6cbcf', + autoCreateIssue: true + }] + ], + markdown: { + lineNumbers: true + } +} diff --git a/okhttps-doc/docs/.vuepress/public/code_temp.png b/okhttps-doc/docs/.vuepress/public/code_temp.png new file mode 100644 index 00000000..b6f96b5a Binary files /dev/null and b/okhttps-doc/docs/.vuepress/public/code_temp.png differ diff --git a/okhttps-doc/docs/.vuepress/public/logo.png b/okhttps-doc/docs/.vuepress/public/logo.png new file mode 100644 index 00000000..3b3b2845 Binary files /dev/null and b/okhttps-doc/docs/.vuepress/public/logo.png differ diff --git a/okhttps-doc/docs/.vuepress/public/spi_config.png b/okhttps-doc/docs/.vuepress/public/spi_config.png new file mode 100644 index 00000000..82db9123 Binary files /dev/null and b/okhttps-doc/docs/.vuepress/public/spi_config.png differ diff --git a/okhttps-doc/docs/.vuepress/public/wx_discuss.png b/okhttps-doc/docs/.vuepress/public/wx_discuss.png new file mode 100644 index 00000000..fb8d810e Binary files /dev/null and b/okhttps-doc/docs/.vuepress/public/wx_discuss.png differ diff --git a/okhttps-doc/docs/.vuepress/styles/index.styl b/okhttps-doc/docs/.vuepress/styles/index.styl new file mode 100644 index 00000000..bde6de32 --- /dev/null +++ b/okhttps-doc/docs/.vuepress/styles/index.styl @@ -0,0 +1,3 @@ +.home .hero img { + height: 200px; +} \ No newline at end of file diff --git a/okhttps-doc/docs/README.md b/okhttps-doc/docs/README.md new file mode 100644 index 00000000..05a0cb23 --- /dev/null +++ b/okhttps-doc/docs/README.md @@ -0,0 +1,60 @@ +--- +home: true +heroImage: /logo.png +heroText: OkHttps V2 +tagline: 强大轻é‡� 且 å‰�å�Žç«¯é€šç”¨çš„ HTTP 客户端 +actionText: æž�速上手 → +actionLink: /v2/getstart.html +features: +- title: è½»é‡�纯粹优雅 + details: OkHttps é�žå¸¸è½»é‡�,体积仅是 Retrofit 的一å�Šä¸�到,并且ä¸�ä¾�赖于特定平å�°ï¼ŒAPI 语义简æ´�舒适。 +- title: 开箱å�³ç”¨çš„功能 + details: 异步预处ç�†å™¨ã€�回调执行器ã€�全局监å�¬å™¨ã€�回调阻断机制ã€�文件上传下载ã€�过程控制ã€�进度监å�¬ã€‚ +- title: 更多实用特性 + details: URL å� ä½�符ã€�Lambda 回调ã€�JSON自动å°�装解æž�ã€�OkHttp 的特性:拦截器ã€�连接池ã€�CookieJar 等。 + +footer: Apache Licensed | Copyright © 2020-present ejlchina +--- + +###
如艺术一般优雅,� 1�2�3 一样简�
+ +```java +HTTP http = HTTP.builder() + .baseUrl("http://api.example.com") + .addMsgConvertor(new GsonMsgConvertor()); + .build(); + +// �步 HTTP +List users = http.sync("/users") + .get() // GET请求 + .getBody() // �应报文体 + .toList(User.class); // 自动��列化 List + +// 异步 HTTP +http.async("/users/1") + .setOnResponse((HttpResult res) -> { + // // 自动��列化 Bean + User user = res.getBody().toBean(User.class); + }) + .get(); // GET请求 + +// WebSocket +http.webSocket("/chat") + .setOnMessage((WebSocket ws, Message msg) -> { + // 从�务器接收消� + Chat chat = msg.toBean(Chat.class); + // ��务器��消� + ws.send(chat); + }) + .listen(); // �动监� +``` + +
+ +###
有问必答微信交�群
+ +
+ +
+ +### [
了解更多
](/v2/) diff --git a/okhttps-doc/docs/en/README.md b/okhttps-doc/docs/en/README.md new file mode 100644 index 00000000..70f91ba6 --- /dev/null +++ b/okhttps-doc/docs/en/README.md @@ -0,0 +1,52 @@ +--- +home: true +heroImage: /logo.png +heroText: OkHttps V2 +tagline: A lightweight and powerful HTTP client on top of OkHttp +actionText: Get Started → +actionLink: /en/v2/ +features: +- title: Lightweight Elegant Easy + details: OkHttps is very lightweight, less than half the size of Retrofit, and does not depend on a specific platform. The API semantics are simple and comfortable. +- title: Out of the box features + details: Asynchronous preprocessor, callback executor, global listener, callback prevent mechanism, file upload and download, process control, progress monitoring. +- title: More practical features + details: URL placeholder, lambda callback, JSON auto encapsulation parsing, okhttp features like interceptor, connection pool, cookiejar, etc. + +footer: Apache Licensed | Copyright © 2020-present ejlchina +--- + +####
As Elegant as Art, As Easy as 1, 2, 3
+ +```java +HTTP http = HTTP.builder() + .baseUrl("http://api.example.com") + .addMsgConvertor(new GsonMsgConvertor()); + .build(); + +// Synchronous HTTP +List users = http.sync("/users") + .get() // GET method + .getBody() // response body + .toList(User.class); // Automatic deserialization + +// Asynchronous HTTP +http.async("/users/1") + .setOnResponse((HttpResult res) -> { + // Automatic deserialization + User user = res.getBody().toBean(User.class); + }) + .get(); // GET method + +// WebSocket +http.webSocket("/chat") + .setOnMessage((WebSocket ws, Message msg) -> { + // Automatic deserialization + Chat chat = msg.toBean(Chat.class); + // Automatic Serialization + ws.send(chat); + }) + .listen(); // Start listening +``` + +[
Lean More
](/en/v2/) \ No newline at end of file diff --git a/okhttps-doc/docs/en/v2/README.md b/okhttps-doc/docs/en/v2/README.md new file mode 100644 index 00000000..a220001d --- /dev/null +++ b/okhttps-doc/docs/en/v2/README.md @@ -0,0 +1,50 @@ +--- +description: OkHttps 安装 构建实例 HTTP build �步请求 异步请求 sync async BaseUrl request webSocket gradle maven ejlchina +--- + +# 介� + +## OkHttps 简介 + +  OkHttps 是近期开�的对 OkHttp3 轻��装的框架,它独创的异步预处�器,特色的标签,�活的上传下载进度监�与过程控制功能,在轻�解决很多原本�人头疼问题的�时,设计上也力求纯粹与优雅。 + + * 链�调用,一点到底 + * BaseURL�URL��符�HTTP�WebSocket + * JSON�Xml 等自动�装与解�,且支�与任�格�的数�解�框架集� + * �步拦截器�异步预处�器�回调执行器�全局监��回调阻断 + * 文件上传下载(过程控制�进度监�) + * �方法回调,充分利用 Lambda 表达� + * TCP连接池�Http2 + +::: tip +* OkHttps �常轻�(77Kb,Retrofit:124Kb),除 Okhttp 无第三方�赖,并且更加的开箱�用,API 也更加自然和语义化。 +* OkHttps 是一个纯粹的 Java 网络开�包,并��赖 Android,这一点和 Retrofit �� +* OkHttps 用起�很优美,�以� RxJava 那样链�用,�比 RxJava 更简�。 +::: + +### 项目组� + +项目 | 最新版本 | �述 +-|-|- +okhttps | 2.0.0 | OkHttps 核心模� +okhttps-fastjson | 2.0.0 | 扩展包:与 fastjson 集� +okhttps-gson | 2.0.0 | 扩展包:与 gson 集� +okhttps-jackson | 2.0.0 | 扩展包:与 jackson 集� + +以上是官方维护的与三大 JSON 框架集�的案例,�续将�供 xml 和 protobuf 的集�。 + +## v2.x 的新特性 + +* HTTP 任务新增`patch()`方法,��起 PATCH 请求,目�直接支�的 HTTP 方法有:GET�POST�PUT�PATCH�DELETE,并且暴露了`request(String method)`方法,�自定义�起任何请求,如:HEAD�OPTIONS�TRACE�CONNECT 等; + +* HTTP 任务新增`skipPreproc()`和`skipSerialPreproc()`方法,具体请求�跳过 所有 或�跳过 串行 预处�器; + +* 新增`MsgConvertor`接�,实现 OkHttps 与 fastjson 解耦,且���赖�个具体 json 框架�甚至��赖 json,它�以与 **任何格�** 的数�解�框架集�,如:json �xml �protobuf 等; + +* 构建`HTTP`实例时支�注入多个`MsgConvertor`,�实现�一个`HTTP`实例下,既有 json 解�,�有 xml 解�等强大特性,�时还�以让表�(form)请求�数 � json�xml 一样,支��列化功能。 + +* `HTTP`接�新增`webSocket(String url)`方法,与`sync(String url)`和`async(String url)`一样,支� Lamda 编程�预处�器机制�消�数�自动�列化和��列化机制; + +* 新增`OkHttps`工具类,支� SPI 方�注入�置,`OkHttps`和`HttpUtils`默认自动以 SPI 方�寻找�赖中的`MsgConvertor`; + +* 新增�自定义默认编�(�自定义�然默认为 utf-8)�具体请求�指定特殊编�功能。 \ No newline at end of file diff --git a/okhttps-doc/docs/en/v2/android.md b/okhttps-doc/docs/en/v2/android.md new file mode 100644 index 00000000..123fcf11 --- /dev/null +++ b/okhttps-doc/docs/en/v2/android.md @@ -0,0 +1,181 @@ +--- +description: OkHttps 安� 生命周期 UI线程 主线程 IO线程 自由 �活 切� 生命周期绑定 �置预处�器 定义生命周期监视器 �置全局回调监� +--- + +# 安� + +## 回调线程切� + +  在 Android 开�中,�常会把�些代�放到特点的线程去执行,比如网络请求�应�的页�更新在主线程(UI线程)执行,而�存文件则在IO线程�作。OkHttps 为这类问题�供了良好的方案。 + +  在 **默认** 情况下,**所有回调** 函数都会 **在 IO 线程** 执行。为什么会设计如此呢?这是因为 OkHttps �是纯粹的 Java 领域 Http工具包,本身对 Android �会有任何�赖,因此也�知 Android 的 UI 线程为何物。这么设计也让它在 Android 之外有更多的�能性。 + +### �置 + +  但是在 Android 里使用 OkHttps 的�,UI线程的问题能�优雅的解决呢?当然�以�简�粗暴的方法就是�置一个 回调执行器: + + ```java +HTTP http = HTTP.builder() + .callbackExecutor((Runnable run) -> { + // 实际编�中�以� Handler �出�,�需��次执行回调都�新创建 + new Handler(Looper.getMainLooper()).post(run); // 在主线程执行 + }) + .build(); +``` + +### 用例 + +  上述代�便实现了让 **所有** 的 **回调函数** 都在 **主线程(UI线程)** 执行的目的,如: + +```java +http.async("/users") + .addBodyPara("name", "Jack") + .setOnProcess((Process process) -> { + // 在主线程执行 + }) + .setOnResponse((HttpResult result) -> { + // 在主线程执行 + }) + .setOnException((Exception e) -> { + // 在主线程执行 + }) + .setOnComplete((State state) -> { + // 在主线程执行 + }) + .post(); +``` +  但是,如果�时还想让�些回调放在IO线程,实现 **自由切�**,怎么办呢?OkHttps 给出了�常�活的方法,如下: + +```java +http.async("/users") + .addBodyPara("name", "Jack") + .setOnProcess((Process process) -> { + // 在主线程执行 + }) + .nextOnIO() // 指定下一个回调在 IO 线程执行 + .setOnResponse((HttpResult result) -> { + // 在 IO 线程执行 + }) + .setOnException((Exception e) -> { + // 在主线程执行(没有指明 nextOnIO 则在回调执行器里执行) + }) + .nextOnIO() // 指定下一个回调在 IO 线程执行 + .setOnComplete((State state) -> { + // 在 IO 线程执行 + }) + .post(); +``` +  无论是哪一个回调,都�以使用`nextOnIO()`方法自由切�。�样,对于文件下载也是一样: + +```java +http.sync("/download/test.zip") + .get() + .getBody() + .setOnProcess((Process process) -> { + // 在主线程执行 + }) + .toFolder("D:/download/") + .nextOnIO() // 指定下一个回调在 IO 线程执行 + .setOnSuccess((File file) -> { + // 在 IO 线程执行 + }) + .setOnFailure((Failure failure) -> { + // 在主线程执行 + }) + .start(); +``` + +## 生命周期绑定 + +  由于 OkHttps 并��赖于 Android,所以它并没有�供关于生命周期绑定的直接实现,但它的一些扩展机制让我们很容易就�以实现这个需求。在开始之�,我们首先��解何为生命周期绑定: + +::: tip HTTP 请求的生命周期绑定 +所谓的生命周期绑定:�是让 HTTP 任务感知其所属的 Activity 或 Fragment 的生命周期,当 Activity 或 Fragment 将被销�时,框架应自动的把由它们�起的但尚未完�的 HTTP 任务全部�消,以�导致程�出错� +::: + +  现在我们需�对`HTTP`实例进行�置,�置�的`HTTP`实例具有生命周期绑定的功能,在`Activity`或`Fragment`里,它的使用效果如下: + +```java +// 在 Activity 或 Fragment 内�起请求 +http.async("http://www.baidu.com") + .bind(getLifecycle()) // 绑定生命周期 + .setOnResponse((HttpResult result) -> { + Log.i("FirstFragment", "收到请求:" + result.toString()); + }) + .get(); +``` +  上述代�中的`getLifecycle()`是`Activity`或`Fragment`自带的方法,而`bind()`是`HttpTask`的现有方法。在�置好`HTTP`实例�,上述代��起的请求便�以感知`Activity`或`Fragment`的生命周期。 + +  那`HTTP`实例到底该如何�置呢? + +### 第一步:�置预处�器 + +```java +HTTP http = HTTP.builder() + ... // �略其它�置项 + .addPreprocessor((Preprocessor.PreChain chain) -> { + HttpTask task = chain.getTask(); + Object bound = task.getBound(); + // 判断 task 是�绑定了 Lifecycle 对象 + if (bound instanceof Lifecycle) { + // �新绑定一个 生命周期监视器(LCObserver)对象,它的定义�下一步 + task.bind(new LCObserver(task, (Lifecycle) bound)); + } + chain.proceed(); + }) + ... // �略其它�置项 + .build(); +``` + +### 第二步:定义生命周期监视器 + +```java +public class LCObserver implements LifecycleObserver { + + HttpTask task; + Lifecycle lifecycle; + + LCObserver(HttpTask task, Lifecycle lifecycle) { + this.task = task; + this.lifecycle = lifecycle; + lifecycle.addObserver(this); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + public void onStop() { + task.cancel(); // 在 ON_STOP 事件中,�消对应的 HTTP 任务 + } + + public void unbind() { + // 在请求完�之�移除监视器 + lifecycle.removeObserver(this); + } + +} +``` + +### 第三步:�置全局回调监� + +  以上两步其实已�实现了生命周期绑定的功能,但是在请求完�之�,我们需�在`lifecycle`中移除`LCObserver`对象: + +```java +HTTP http = HTTP.builder() + ... // �略其它�置项 + .completeListener((HttpTask task, HttpResult.State state) -> { + Object bound = task.getBound(); + // 判断 task 是�绑定了生命周期监视器(LCObserver)对象 + if (bound instanceof LCObserver) { + // 解绑监视器 + ((LCObserver) bound).unbind(); + } + return true; + }) + ... // �略其它�置项 + .build(); +``` + +**以上三步便在Android中实现了生命周期与HTTP请求绑定的功能** + +::: tip �示 +以上三步实现的生命周期绑定,�仅对 HTTP 请求有作用,对 WebSocket 连接也�样有效哦。 +::: diff --git a/okhttps-doc/docs/en/v2/configuration.md b/okhttps-doc/docs/en/v2/configuration.md new file mode 100644 index 00000000..8b136e9d --- /dev/null +++ b/okhttps-doc/docs/en/v2/configuration.md @@ -0,0 +1,382 @@ +--- +description: OkHttps �置 BaseUrl 回调执行器 主线程 UI线程 OkHttpClient 预处�器 TOKEN问题最佳解决方案 刷新TOKEN 全局 回调监� 下载监� 回调阻断 拦截器 CookieJar�SSL�缓存�代��事件监� +--- + +# �置 + +## 设置 BaseUrl + +```java +HTTP http = HTTP.builder() + .baseUrl("http://api.demo.com") // 设置 BaseUrl + .build(); +``` +  该�置全局生效,在�置了`BaseUrl`之�,具体的请求便�以�略`BaseUrl`部分,使得代�更加简�,例如: + +```java +http.sync("/users").get() // http://api.demo.com/users + +http.sync("/auth/signin") // http://api.demo.com/auth/signin + .addBodyPara("username", "Jackson") + .addBodyPara("password", "xxxxxx") + .post(); // POST请求 +``` +  在�置了`BaseUrl`之�,如有特殊请求任务,�然�以使用全路径的方�,一点都�妨�: + +```java +http.sync("https://www.baidu.com").get(); +``` + +::: tip 支�动� BaseUrl �? +有些�学�能在网上看到�些网络�装框架声明支� **动� BaseUrl** 或者说 **动�域�**,那 OkHttps 支��? + +答:OkHttps 是 **�会** �供在�一个`HTTP`实例上�以动�设置`BaseUrl`的功能的。 + +为什么呢? + +答:因为这完全是一�缺陷设计,我们都知�在 Java 中多线程开�很常�,如果 A 线程正兴高采烈的准备��个地��起请求时,而 B 线程�然修改了`BaseUrl`,那 A 线程�就悲剧了么。所以类似 Retrofit 这些设计�熟的框架都�会�供这�功能的。 + +那我的应用中确实�用到多个域�,该怎么办呢? + +答:�以构建多个`HTTP`实例,��的实例负责��的域�,如果�个域�的请求都比较少,就是域�多,那便�用一个`HTTP`实例,请求较少的域�使用全路径访问��。 +::: + +## 回调执行器 + +  OkHttps 默认所有回调都在 **IO线程** 执行,如何想改�执行回调的线程时,�以�置回调执行器。例如在Android里,让所有的回调函数都在UI线程执行,则�以在构建`HTTP`时�置如下: + +```java +HTTP http = HTTP.builder() + .callbackExecutor((Runnable run) -> { + runOnUiThread(run); // 在UI线程执行 + }) + .build(); +``` +  该�置默认 **影�所有回调**,更多实现细节��考 [安�-回调线程切�](/v2/android.html#回调线程切�) 章节。 + +## 预处�器 + +  预处�器(`Preprocessor`)�以让我们在请求�出之�对请求本身�一些改�,但与`OkHttp`的拦截器(`Interceptor`)��:预处�器�以让我们 **异步** 处�这些问题。 + +### 并行预处�器 + +  例如,当我们想为请求任务自动添加`Token`头信�,而`Token`�能通过异步方法`requestToken`获�时,这时使用`Interceptor`就很难处�了,但�以使用预处�器轻�解决: + +```java +HTTP http = HTTP.builder() + .addPreprocessor((PreChain chain) -> { + HttpTask task = chain.getTask();// 获得当�的HTTP任务 + if (!task.isTagged("Auth")) { // 根�标签判断该任务是�需�Token + chain.proceed(); + return; + } + requestToken((String token) -> { // 异步获� Token + task.addHeader("Token", token);// 为任务添加头信� + chain.proceed(); // 继续当�的任务 + }); + }) + .build(); +``` +  和`Interceptor`一样,`Preprocessor`也�以添加多个。他们之间的区别如下: + +::: tip 拦截器与预处�器的区别 +* 拦截器�能处��步�作,预处�器支�处�异步�作 +* 拦截器都是并行处�请求,预处�器支�串行处�(详�6.5章节) +* 拦截器处�时机在请求�和�应�,预处�器�在请求�,并且先于拦截器执行。关于�应�,OkHttps还�供了全局回调监�(详�6.6章节) +::: + +### 串行预处�器(TOKEN问题最佳解决方案) + +  普通预处�器都是�并行处�的,然而有时我们希望�个预处�器�时�处�一个任务。比如 当`Token`过期时我们需�去刷新获�新`Token`,而刷新`Token`这个�作�能有一个任务去执行,因为如果`n`个任务�时执行的�,那么必有`n-1`个任务刚刷新得到的`Token`�能就立马失效了,而这是我们所�希望的。 + +  为了解决这个问题,OkHttps �供了串行预处�器,它�以让 HTTP 任务排好队,一个一个地进入预处�器: + +```java +HTTP http = HTTP.builder() + .addSerialPreprocessor((PreChain chain) -> { + HttpTask task = chain.getTask(); + if (!task.isTagged("Auth")) { + chain.proceed(); + return; + } + // 检查过期,若需�则刷新Token + requestTokenAndRefreshIfExpired((String token) -> { + task.addHeader("Token", token); + chain.proceed(); // 调用此方法�,�会有其它任务进入该处�器 + }); + }) + .build(); +``` +  串行预处�器实现了让HTTP任务排队串行处�的功能,但值得一�的是:它并没有因此而阻塞任何线程� + +::: warning 注� +由于 串行预处�器 �能一个一个的处�任务,所以在上述中的`requestTokenAndRefreshIfExpired`方法里,若�起网络请求,一定�跳过串行预处�器 [�使用`skipSerialPreproc()`方法],例如: + +```java +http.async("/oauth/refresh-token") + .skipSerialPreproc() // 跳过串行预处�器 + .addBodyPara("refreshToken", "xxxxxx") + .setOnResponse((HttpResult result) -> { + + }) + .post(); +``` +�则就会�生两个 HTTP 任务相互等待�也执行�了的问题。 + +如果你使用的是 v1.x 的版本,则�以使用`HTTP`实例的`request(Request request)`方法�起原生请求,这样则��过任何预处�器。 +::: + +## 消�转�器 + +OkHttps 自 2.0 版本器支�自定义消�转�器,并且�以添加多个,例如: + +```java +HTTP http = HTTP.builder() + .addMsgConvertor(new MyJsonMsgConvertor()); + .addMsgConvertor(new MyXmlMsgConvertor()); + .build(); +``` + +�置了消�转�器�,`HTTP`实例便具有了�列化和��列化这些格�数�的能力。 + +### ��列化 + +例如下例中,无论该接��应的是 JSON 还是 XML,都�以��列化�功: + +```java +// 无论该接��应的是 JSON 还是 XML,都�以��列化�功 +List orders = http.sync('/orders') + .get().getBody().toList(Order.class); +``` + +在 Websocket 通讯中,也是如此: + +```java +http.webSocket("/redpacket/status") // 监�红包的领�状� + .setOnMessage((WebSocket ws,Message msg) -> { + // 无论该接��应的是 JSON 还是 XML,都�以��列化�功 + Status status = msg.toBean(Status.class); + }) + .listen(); +``` + +### 正�列化 + +�外,如果�在请求�出阶段正��列化�数,则需�指定`bodyType`�数告诉 OkHttps 你想使用哪一个消�转�器,如: + +```java +Order order = newOrder(); +http.async('/orders') // �交订� + .bodyType("json") + .setBodyPara(order) // 以 JSON 格��列化 Order 对象 + .post(); +``` + +或者 XML: + +```java +Order order = newOrder(); +http.async('/orders') // �交订� + .bodyType("xml") + .setBodyPara(order) // 以 XML 格��列化 Order 对象 + .post(); +``` + +在 Websocket 里,也是�样的方法: + +```java +http.webSocket("/chat") + .bodyType("json") + .setOnOpen((WebSocket ws,HttpResult res) -> { + Hello hello = getHello(); + ws.send(hello); // 以 JSON 格��列化 Hello 对象 + }) + .listen(); +``` + +但如果你在 Websocket 通讯到�一个阶段�,�然想��外一�格����数�了,你还�以这样: + +```java +http.webSocket("/chat") + .bodyType("json") + .setOnOpen((WebSocket ws,HttpResult res) -> { + Hello hello = getHello(); + ws.send(hello); // 以 JSON 格��列化 Hello 对象 + + ws.msgType("xml") // 切�为 XML 格� + ws.send(hello); // 以 XML 格��列化 Hello 对象 + }) + .listen(); +``` + +### 默认�列化类型 + +然而大多数情况下,我们都使用一�消�转�器,比如 json,这时候你�以为`bodyType`�置一个默认值: + +```java +HTTP http = HTTP.builder() + // 修改 bodyType 的默认值为 json,若�修改,则默认是 form + .bodyType("json"); + .addMsgConvertor(new MyJsonMsgConvertor()); + .build(); +``` +::: tip �示 +�有请求报文体的正��列化�会收到`bodyType`的影�,对于�应报文体的���列化��其影�。 +::: + +### 表��列化 + +OkHttps �但能� JSON 或 XML 的�列化,并且还能�表��数的�列化,并且自带了一个`FormConvertor`,它�以给任�`MsgConvertor`赋予表��列化的能力,例如你已�实现了一个`MyJsonMsgConvertor`,�以这么�: + +```java +// 这里并��求是一个 JSON 转�器,XML 也�以 +// 有啥就给啥,效果是一样的 +MsgConvertor convertor = new MyJsonMsgConvertor(); + +HTTP http = HTTP.builder() + .addMsgConvertor(new MsgConvertor.FormConvertor(convertor)); + .build(); +``` + +这样,你��表�请求时,就�以直接扔进一个对象,它将自动完��列化的事情: + +```java +Order order = newOrder(); +http.async('/orders') // �交订� + .bodyType("form") + .setBodyPara(order) // 以 表� 格��列化 Order 对象 + .post(); +``` + +一般情况下,如果你有实现了一个`MsgConvertor`,我们推�把它和`FormConvertor`都添加进去,例如: + +```java +MsgConvertor convertor = new MyJsonMsgConvertor(); + +HTTP http = HTTP.builder() + .addMsgConvertor(convertor); + .addMsgConvertor(new MsgConvertor.FormConvertor(convertor)); + .build(); +``` + +如果你添加了官方�供的`MsgConvertor`�赖包(例如:`okhttps-jackson`等),在构建实例时,还�以这样直接注入: + +```java +HTTP.Builder builder = HTTP.builder() +// 自动完��赖中的 MsgConvertor 和 FormConvertor 的注入 +ConvertProvider.inject(builder); +HTTP http = builder.build(); +``` + +::: tip +如果你使用的是[`OkHttps`或`HttpUtils`工具类](/v2/getstart.html#工具类),它们都会自动�置`MsgConvertor`和`FormConvertor`,无需手动�置 + +在 v2.0.0.RC 版本中`FormConvertor`的�字是`MsgConvertor.FormMsgConvertor` +::: + +## 全局监� + +### 全局回调监� + +  全局回调是实际开�中�常需�的功能,比如对�务器�应的状��进行统一处�等,�时 OkHttps 的全局回调还具有 **回调阻断** 的功能: + +```java +HTTP http = HTTP.builder() + .responseListener((HttpTask task, HttpResult result) -> { + // 所有请求�应�都会走这里 + + // 返回 true 表示继续执行 task 的 OnResponse 回调, + // 返回 false 则表示��执行,� 阻断 + return true; + }) + .completeListener((HttpTask task, State state) -> { + // 所有请求执行完都会走这里 + + // 返回 true 表示继续执行 task 的 OnComplete 回调, + // 返回 false 则表示��执行,� 阻断 + return true; + }) + .exceptionListener((HttpTask task, IOException error) -> { + // 所有请求�生异常都会走这里 + + // 返回 true 表示继续执行 task 的 OnException 回调, + // 返回 false 则表示��执行,� 阻断 + return true; + }) + .build(); +``` + +**回调阻断** 其实是日常开�中比较常�的需求,比如: + +�有当接��应的状��在 [200, 300) 之间时,应用��具体的业务处�,其它则一律�用户�示接�返回的错误信�,则�以这么�: + +```java +HTTP http = HTTP.builder() + .responseListener((HttpTask task, HttpResult result) -> { + if (result.isSuccessful()) { + return true; // 继续接�的业务处� + } + showApiMsgToUser(result); // �用户展示接�的错误信� + return false; // 阻断 + }) + .build(); +``` + +然�具体的请求,就�以专注于业务处�,而�需担心接�的状�出错,比如: + +```java +Order order = newOrder(); // 订�信� + +http.async('/orders') // �交订� + .setBodyPara(order) + .setOnResponse((HttpResult result) -> { + // 进入该回调,则表示订�已�交�功,就�以放心的�接下�的业务处� + // 比如去�起支付: + startPay(result.getBody().toBean(PayInfo.class)); + }) + .post(); +``` + + +::: tip 全局回调监�与拦截器的异�: +* 拦截器�以添加多个,全局回调监�分三�,��最多添加一个 +* 拦截器处的�时机在请求�和�应�,全局回调监��在�应�,并且晚于拦截器 +* 全局回调监��以 **阻断**(return false)�个请求的具体回调,而拦截器�能 +::: + +### 全局下载监� + +```java +HTTP http = HTTP.builder() + .downloadListener((HttpTask task, Download download) -> { + // 所有下载在开始之�都会先走这里 + Ctrl ctrl = download.getCtrl(); // 下载控制器 + + }) + .build(); +``` + +## �置 OkHttpClient + +  与其他�装 OkHttp3 的框架��,OkHttps 并�会�蔽 OkHttp3 本身就很好用的功能,如下: + +```java +HTTP http = HTTP.builder() + .config((OkHttpClient.Builder builder) -> { + // �置连接池 最�10个连接(��置默认为 5) + builder.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)); + // �置连接超时时间(默认10秒) + builder.connectTimeout(20, TimeUnit.SECONDS); + // �置 WebSocket 心跳间隔(默认没有心跳) + builder.pingInterval(60, TimeUnit.SECONDS); + // �置拦截器 + builder.addInterceptor((Chain chain) -> { + Request request = chain.request(); + // 必须�步返回,拦截器内无法执行异步�作 + return chain.proceed(request); + }); + // 其它�置: CookieJar�SSL�缓存�代��事件监�... + // 所有 OkHttp 能�置的,都�以在这里�置 + }) + .build(); +``` \ No newline at end of file diff --git a/okhttps-doc/docs/en/v2/foundation.md b/okhttps-doc/docs/en/v2/foundation.md new file mode 100644 index 00000000..f95778ab --- /dev/null +++ b/okhttps-doc/docs/en/v2/foundation.md @@ -0,0 +1,726 @@ +--- +description: OkHttps 请求方法 回调函数 HttpResult cache 多次 消费报文体 HttpCall 构建HTTP任务 请求�数 JSON 使用标签 �起请求 �消请求 +--- + +# 基础 + +## 请求方法 + +OkHttps 使用`sync()`和`async()`方��起的请求,支�的 HTTP 方法有: + +HTTP 请求方法 | 实现方法 | Restful 释义 | 起始版本 +-|-|-|- +GET | `get()` | 获�资� | 1.0.0 +HEAD | `head()` | 获�资�头信� | 2.0.0 +POST | `post()` | �交资� | 1.0.0 +PUT | `put()` | 更新资� | 1.0.0 +PATCH | `patch()` | 部分更新资� | 2.0.0.RC +DELETE | `delete()` | 删除资� | 1.0.0 +其它 | `request(String method)` | 任何 HTTP 请求方法 | 2.0.0.RC + +例如,修改 ID 为 100 的书的标题,�以这样请求: + +```java +http.async("/books/100") + .addBodyPara("title", "新标题") + .patch(); // �起 PATCH 请求 +``` + +�如,删除 ID 为 100 的书,�以�起这样的请求: + +```java +http.async("/books/100").delete(); // �起 DELETE 请求 +``` + +�如,使用 HEAD 请求获��务器上文件的大�: + +```java +HttpResult res = http.sync("/download/file.zip").head(); +// // HttpResult#getContentLength() 新增于 2.0.0 版本 +System.out.println("size = " + res.getContentLength()); +``` + +你还�以自定义一些请求方法,例如: + +```java +http.async("/somethings") + .setOnResponse((HttpResult result) -> { + System.out.println(result); + }) + .request("OPTIONS"); // �起 OPTIONS 请求 +``` + +�外,�步请求的所有这些方法都会返回一个[`HttpResult`](#httpresult),而异步请求则会返回一个[`HttpCall`](#httpcall): + +```java +HttpResult res1 = http.sync("/books").get(); // �步 GET +HttpResult res2 = http.sync("/books").post(); // �步 POST + +HttpCall call1 = http.async("/books").get(); // 异步 GET +HttpCall call2 = http.async("/books").post(); // 异步 POST +``` + +## 回调函数 + +OkHttps 的回调函数全部使用�方法模�,这样�以充分利用 Java8 或 Kotlin 中的 Lambda 表达�,使你的代�更加简�优雅: + +### 普通回调 + +```java +http.async("/users") // http://api.demo.com/users + .setOnResponse((HttpResult result) -> { + // �应回调 + }) + .setOnException((IOException e) -> { + // 异常回调 + }) + .setOnComplete((State state) -> { + // 完�回调,无论�功失败都会执行,并且在 �应|异常回调 之�执行 + // �以根� state 枚举判断执行状�: + // State.CANCELED` 请求被�消 + // State.RESPONSED` 已收到�应 + // State.TIMEOUT` 请求超时 + // State.NETWORK_ERROR` 网络错误 + // State.EXCEPTION` 其它请求异常 + }) + .get(); +``` + +OkHttps �时还支� **全局回调** 和 **回调阻断** 机制,详� [全局回调监�](/v2/configuration.html#全局回调监�)。 + +::: tip +* �有异步请求��以设置这三�(�应|异常|完�)回调 +* �步请求直接返回结果,无需使用回调 +::: + +### 进度回调 + +上传进度回调: + +```java +http.sync("/upload") + .addFilePara("avatar", "D:/image/avatar.jpg") + .setOnProcess((Process process) -> { + long doneBytes = process.getDoneBytes(); // 已上传字节数 + long totalBytes = process.getTotalBytes(); // 总共的字节数 + double rate = process.getRate(); // 已上传的比例 + boolean isDone = process.isDone(); // 是�上传完� + }) + .post(); +``` + +关于上传的更多细节请�考 [文件上传](/v2/updown.html#文件上传) 章节。 + +下载进度回调: + +```java +http.sync("/download/test.zip").get().getBody() + .setOnProcess((Process process) -> { + long doneBytes = process.getDoneBytes(); // 已下载字节数 + long totalBytes = process.getTotalBytes(); // 总共的字节数 + double rate = process.getRate(); // 已下载的比例 + boolean isDone = process.isDone(); // 是�下载完� + }) + .toFolder("D:/download/") // 指定下载的目录,文件�将根�下载信�自动生� + .start(); +``` + +#### 还有下载�功或失败的回调: + +```java +http.sync("/download/test.zip").get().getBody() + .toFile("D:/download/test.zip") // 下载到文件 + .setOnSuccess((File file) -> { + // 下载�功 + }) + .setOnFailure((Failure failure) -> { + // 下载失败 + }) + .start(); +``` + +关于下载的更多内容,请�考 [文件下载](/v2/updown.html#文件下载) 章节。 + +### WebSocket 回调 + +```java +http.webSocket("/websocket-endpoint") + .setOnOpen((WebSocket ws, HttpResult res) -> { + // WebSocket 连接建立回调 + }) + .setOnMessage((WebSocket ws, Message msg) -> { + // �务器下�消�回调 + }) + .setOnException((WebSocket ws, Throwable thr) -> { + // 连接�生异常回调 + }) + .setOnClosing((WebSocket ws, WebSocket.Close close) -> { + // 连接正在关闭回调 + }) + .setOnClosed((WebSocket ws, WebSocket.Close close) -> { + // 连接已关闭(v2.0.0 之�包�连接被�消 和 连接�生异常) + boolean isCanceled = close.isCanceled(); // from v2.0.0 + boolean isException = close.isException(); // from v2.0.0 + int code = close.getCode(); + String reason = close.getReason(); + }) + .listen(); +``` + +::: tip +OkHttps 的所有回调函数都默认在 IO 线程执行,若�切�默认线程(例如 Android 中切�到 UI 线程)请�� [回调线程切�](/v2/android.html#回调线程切�)。 +::: + +## HttpResult + +  `HttpResult`是HTTP请求执行完�的结果,它是�步请求方法`get()`�`post()`等的返回值,也是异步请求�应回调`OnResponse`的�数,它定义了如下方法: + +* `getState()` 得到请求执行状�枚举,它有以下�值: + * `State.CANCELED` 请求被�消 + * `State.RESPONSED` 已收到�应 + * `State.TIMEOUT` 请求超时 + * `State.NETWORK_ERROR` 网络错误 + * `State.EXCEPTION` 其它请求异常 +* `getStatus()` 得到 HTTP 状�� +* `isSuccessful()` 是��应�功,状��在 [200..300) 之间 +* `getHeaders()` 得到HTTP�应头 +* `getHeaders(String name)` 得到HTTP�应头 +* `getHeader(String name)` 得到HTTP�应头 +* `getContentLength();` 解��应头中的 Content-Length 长度信�(v2.0.0 新增) +* `getBody()` 得到�应报文体`Body`实例,它定义了如下方法(对�一个`Body`实例,以下的`toXXX()`类方法�能使用一个且仅能调用一次,除�先使用 cache 方法): + * `toBytes()` 报文体转�为字节数组 + * `toByteStream()` 报文体转�为字节输入� + * `toCharStream()` 报文体转�为字符输入� + * `toString()` 报文体转�为字符串文本 + * `toBean(Class type)` 报文体根�`type`自动��列化为 JavaBean(�赖`MsgConvertor`) + * `toList(Class type)` 报文体根�`type`自动��列化为 JavaBean 列表(�赖`MsgConvertor`) + * `toMapper()` 报文体自动��列化为映射结构的对象(�赖`MsgConvertor`,v2.0.0.RC 之�是`toJsonObject()`) + * `toArray()` 报文体自动��列化为数组结构的对象(�赖`MsgConvertor`,v2.0.0.RC 之�是`toJsonArray()`) + * `toFile(String filePath)` 下载到指定路径 + * `toFile(File file)` 下载到指定文件 + * `toFolder(String dirPath)` 下载到指定目录 + * `toFolder(File dir)` 下载到指定目录 + * `getType()` 返回报文体的媒体类型(v2.0.0 新增,��推�`getContentType()`方法) + * `getLength()` 返回报文体的字节长度(v2.0.0 新增,��推�`getContentLength()`方法) + * `cache()` 缓存报文体,开�缓存����使用`toXXX()`类方法 + * `close()` 关闭报文体,未对报文体�任何消费时使用,比如�读�报文头 +* `getError()` 执行中�生的异常,自动�获执行请求时�生的 网络超时�网络错误 和 其它IO异常 +* `close()` 关闭报文,未对报文体�任何消费时使用,比如�读�长度 + +示例,请求结果自动��列化为 Bean 和 List: + +```java +// 自动转Bean +Order order = http.sync("/orders/1").get() + .getBody().toBean(Order.class); + +// 自动转List +List orders = http.sync("/orders") + .get().getBody().toList(Order.class); +``` + +上例中,为了获�订�列表,必须先定义一个`Order`类,如果你�想定义它,�以直接使用`toMapper()`和`toArray()`方法: + +```java +Mapper order = http.sync("/orders/1") + .get().getBody().toMapper(); + +long orderId = order.getLong("id"); // 订�ID +long orderNo = order.getString("orderNo"); // 订�� + +Array orders = http.sync("/orders") + .get().getBody().toArray(); + +int size = orders.size(); // 订�个数 +Mapper o1 = orders.getMapper(0) // 第1个订� +``` + +关于 Mapper 和 Array 的更多信�,请�考 [Mapper 接�](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/Mapper.java) 和 [Array 接�](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/Array.java)。 + +示例,使用 `cache()` 方法: + +```java +Body body = http.sync("/orders").get().getBody().cache(); + +// 使用 cache �,�以多次使用 toXXX() 方法 +System.out.println(body.toString()); +System.out.println(body.toArray()); +System.out.println(body.toList(Order.class)); +``` + +示例,获�下载文件的大�: + +```java +long size = http.sync("/download/test.zip").get().getBody() + .close() // �是想获得文件大�,�消费报文体,所以直接关闭 + .getLength(); // 获得待下载文件的大� +// 由于未消费报文体,所以本次请求�会消耗下载报文体的时间和网络�� +System.out.println("size = " + size); +``` + +上述代�等�于: + +```java +long size = http.sync("/download/test.zip").get().close().getContentLength(); +System.out.println("size = " + size); +``` + +还等效于: + +```java +long size = http.sync("/download/test.zip").head().getContentLength(); +// 因为 HEAD 请求没有�应报文体,所以就�需�关闭啦 +System.out.println("size = " + size); +``` + +## HttpCall + +[`HttpCall`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/HttpCall.java)是异步请求方法`get()`�`post()`等的返回值,它是一个接�,继承自[`Cancelable`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/Cancelable.java),与`java`的`Future`接�很�,它有如下方法: + +* `cancel()` �消本次请求,返回�消结果 +* `isCanceled()` 返回请求是�被�消 +* `isDone()` 返回是�执行完�,包��消和失败 +* `getResult()` 返回执行结果`HttpResult`对象,若请求未执行完,则挂起当�线程直到执行完��返回 + +例如,�消一个异步请求: + +```java +HttpCall call = http.async("/users/1").get(); + +System.out.println(call.isCanceled()); // false + +boolean success = call.cancel(); // �消请求 + +System.out.println(success); // true +System.out.println(call.isCanceled()); // true +``` + +## 构建请求 + +  `HTTP`接�的`sync(String url)`�`async(String url)`和`webSocket(String url)`方法返回一个`HttpTask`对象,它实现了[`Cancelable`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/Cancelable.java)接�,�时也�供了�链�调用的`addXXX`与`setXXX`等系列方法用于构建请求报文。 + +### 请求头 + +* `addHeader(String name, String value)` 添加请求头 +* `addHeader(Map headers)` 添加请求头 + +### 路径�数 + +* `addPathPara(String name, Object value)` 添加路径�数:替� URL 里的`{name}`��符 +* `addPathPara(Map params)` 添加路径�数:替� URL 里的`{name}`��符 + +路径�数�以存在于 URL 中的任何�置,例如: + +```java +final String BOOKS_QUERY_URL = "/authors/{authorId}/books?type={type}"; + +http.async(BOOKS_QUERY_URL) // /authors/1/books?bookType=2 + .addPathPara("authorId", 1) + .addPathPara("type", 2) + .setOnResponse((HttpResult res) -> { + + }) + .get(); +``` + +### 查询�数 + +* `addUrlPara(String name, Object value)` 添加 URL �数:拼接在 URL 的`?`之�(查询�数) +* `addUrlPara(Map params)` 添加 URL �数:拼接在 URL 的`?`之�(查询�数) + +::: warning +方法`addXxxPara`在 v2.0.0.RC 之��为`addXxxParam`, v2.0.0.RC 之�推�使用简�版的方法,�方法将在 v2.1.0 中移除。 +::: + +### 报文体类型 + +* `bodyType(String type)` 结�报文体�数使用,用于设置本次的请求报文体类型,�以是`form`�`json`�`xml`�`protobuf`等,默认为`form`(表�类型),�以在构建`HTTP`实例时修改默认的类型 + +::: tip 解� +`bodyType`对应于`MsgConvertor`的`mediaType()`方法的返回值,OkHttps 会按照既定的规则 [`mediaType().contain(bodyType)`] 去匹�出对应的`MsgConvertor`去�列化报文体�数。 +::: + +### 报文体�数(表��JSON�XML等) + +OkHttps 信仰统一与一致更加优雅,所以自 v2.0.0.RC 开始�它便统一了报文体�数的 API,无论是表��还是 JSON�XML 或是 protobuf 等等,��这些�数是放在请求报文体中,那么就�以通过一套 API 轻��定� + +* `addBodyPara(String name, Object value)` 添加 Body �数 +* `addBodyPara(Map params)` 添加 Body �数 +* `setBodyPara(Object object)` 设置 Body �数体 + +::: warning 注� +添加报文体�数�,��使用 GET�HEAD 请求方法 + +方法`setBodyPara`在 v2.0.0.RC 之��为`setRequestJson`, v2.0.0.RC 之�推�使用统一 API,�方法将在 v2.1.0 中移除。 +::: + +#### 表�请求 + +默认的请求报文体类型就是`form`表�,所以�以直接使用`addBodyPara`方法: + +```java +http.async("/projects") + .addBodyPara("name", "OkHttps") + .addBodyPara("desc", "最好用的网络框架") + .post(); +``` + +或者是传入一个 Map 对象: + +```java +Map params = new HashMap<>(); +params.put("name", "OkHttps"); +params.put("desc", "最好用的网络框架"); + +http.async("/projects") + .addBodyPara(params) + .post(); +``` + +甚至�以用`setBodyPara`传入一个`String`: + +```java +http.async("/projects") + .setBodyPara("name=OkHttps&desc=最好用的网络框架") + .post(); +``` + +如果你�置了`FormConvertor`(请�考 [�置-表��列化](/v2/configuration.html#表��列化) 章节),那你还�以直接传入一个 POJO(自定义的一个 Java 类): + +```java +Proejct project = new Proejct(); +project.setName("OkHttps"); +project.setDesc("最好用的网络框架"); + +http.async("/projects") + .setBodyPara(project) // 将自动�列化为表�格� + .post(); +``` + +以上 **4** �方�具有相�的效果,但如果你修改了默认的报文体�列化类型(请�考 [默认�列化类型](/v2/configuration.html#默认�列化类型) 章节),那还需在请求时指定当�的请求报文体类型: + +```java +http.async("/projects") + .bodyType("form") // 指明请求体类型是表� + ... +``` + +或使用`OkHttps.FORM`常� + +```java +http.async("/projects") + .bodyType(OkHttps.FORM) // 指明请求体类型是表� + ... +``` + +#### JSON 请求 + +JSON 请求�求默认的请求`bodyType`为`json` 或者 在具体请求中显�指明`bodyType`为`json`,其它用法和表�请求一模一样。 + +�个添加 + +```java +http.async("/projects") + .addBodyPara("name", "OkHttps") + .addBodyPara("desc", "最好用的网络框架") + .post(); +``` + +Map 方�: + +```java +Map params = new HashMap<>(); +params.put("name", "OkHttps"); +params.put("desc", "最好用的网络框架"); + +http.async("/projects") + .addBodyPara(params) + .post(); +``` + +POJO 方�: + +```java +Proejct project = new Proejct(); +project.setName("OkHttps"); +project.setDesc("最好用的网络框架"); + +http.async("/projects") + .setBodyPara(project) // 自动�列化 + .post(); +``` + +字符串方�: + +```java +http.async("/projects") + .setBodyPara("{\"name\":\"OkHttps\",\"desc\":\"最好用的网络框架\"}") + .post(); +``` + +唯一的��是,如果默认的`bodyType`�是`json`,那需�显�指定当�请求的`bodyType`为`json`: + +```java +http.async("/projects") + .bodyType("json") // 指明请求体类型是 JSON + ... +``` + +或使用`OkHttps.JSON`常� + +```java +http.async("/projects") + .bodyType(OkHttps.JSON) // 指明请求体类型是 JSON + ... +``` + +#### XML 请求 + +若默认�是 XML,则显�指定当�请求的报文体类型 + +```java +http.async("/projects") + .bodyType("xml") // 指明请求体类型是 XML + ... +``` + +或使用`OkHttps.XML`常� + +```java +http.async("/projects") + .bodyType(OkHttps.XML) // 指明请求体类型是 XML + ... +``` + +其它与 表�和 JSON,完全一致,��赘述。 + +#### 其它数�格�的请求 + +�需�扩展了相应的`MsgConvertor`,OkHttps 支�任�数�格�的请求。 + +### 文件�数 + +文件�数是一个特殊的报文体�数,当使用以下方法上传文件时,OkHttps 会强制报文体使用`form`模�。 + +* `addFilePara(String name, String filePath)` 上传文件 +* `addFilePara(String name, File file)` 上传文件 +* `addFilePara(String name, String type, byte[] content)` 上传文件 +* `addFilePara(String name, String type, String fileName, byte[] content)` 上传文件 + +文件�数和报文体�数,还�以混用,例如: + +```java +http.sync("/upload") + .addBodyPara("title", "头�") + .addFilePara("image", "D:/image/avatar.jpg") + .post(); +``` + +::: warning +如果你使用的是 v2.0.0.RC 版本中,以上代�会报 **“方法 addFilePara �能使用 form 方�请求�** 错误,�以用以下方�解决该问题: +```java +http.async("/upload") + .bodyType("multipart/form") + .addBodyPara("title", "头�") + .addFilePara("test", "D:/image/avatar.jpg") + .post(); +``` +详� ISSUE: [https://gitee.com/ejlchina-zhxu/okhttps/issues/I1H8G9](https://gitee.com/ejlchina-zhxu/okhttps/issues/I1H8G9) +::: + +无论当�的默认`bodyType`是什么,和文件�数一起添加的`Body`�数都将以`form`(表�)模��交。 + +### 其它 + +* `tag(String tag)` 为 HTTP 任务添加标签 +* `setRange(long rangeStart)` 设置 Range 头信�,`addHeader`的便�方法,用于[断点续传](/v1/updown.html#实现断点续传) +* `setRange(long rangeStart, long rangeEnd)` 设置 Range 头信�,�用于[分�下载](/v1/updown.html#实现分�下载) +* `bind(Object object)` 绑定一个对象,�用于实现 Android 里的[生命周期绑定](/v1/android.html#生命周期绑定) +* `nothrow()` 一��生异常时的处�方�,请�考 [异常处�](/v2/features.html#异常处�) 章节 +* `skipPreproc()` 设置本次请求跳过所有预处�器,请�考 [预处�器](/v2/configuration.html#并行预处�器) 章节 +* `skipSerialPreproc()` 设置本次请求�跳过所有串行预处�器,请�考 [串行预处�器](/v2/configuration.html#串行预处�器(token问题最佳解决方案)) 章节 + +## 使用标签 + +有时候我们想对 HTTP 任务加以分类,这时候�以使用标签功能: + +```java +http.async("/users") //(1) + .tag("A") + .get(); + +http.async("/users") //(2) + .tag("A.B") + .get(); + +http.async("/users") //(3) + .tag("B") + .get(); + +http.async("/users") //(4) + .tag("B") + .tag("C") // 从 v1.0.4 标签将以追加模�添加,等效于 setTag("B.C") + .get(); + +http.async("/users") //(5) + .tag("C") + .get(); + +http.webSocket("/websocket") + .tag("B") // (6) 标签�样�以作用在 WebSocket 连接上 + .listen(); +``` + +当使用标签�,就�以按标签批�的对HTTP任务进行�消: + +```java +int count = http.cancel("B"); //(2)(3)(4)(6)被�消(�消标签包�"B"的任务) +System.out.println(count); // 输出 4 +``` + +标签除了�以用��消任务,在预处�器中它也�以�挥作用,请�� [并行预处�器](/v2/configuration.html#并行预处�器) 与 [串行预处�器(token问题最佳解决方案)](/v2/configuration.html#串行预处�器(token问题最佳解决方案))。 + +## 异常处� + +  使用 OkHttps 时,**异常处��是必须的**,但相比其它的 HTTP 开�包,它还�供一个特别的处�方法:`nothrow()`,以满足��的异常处�需求。 + +### �步请求的异常 + +  默认情况下,当�步请求执行异常时,会直接�外抛出,我们�以用 `try catch` ��获,例如: + +```java +try { + HttpResult result = http.sync("/users/1").get(); +} catch (HttpException e) { + Throwable cause = e.getCause(); // 得到异常原因 + if (cause instanceof ConnectException) { + // 当没网络时,会抛出连接异常 + } + if (cause instanceof SocketTimeoutException) { + // 当接�长时间未�应,会抛出超时异常 + } + if (cause instanceof UnknownHostException) { + // 当把域�或IP写错,会抛出 UnknownHost 异常 + } + // ... +} +``` +  这�传统的异常处�方�,当然�以解决问题,但 OkHttps 有更佳的方案: + +```java +// 方法 nothrow() 让异常�直接抛出 +HttpResult result = http.sync("/users/1").nothrow().get(); +// 判断执行状� +switch (result.getState()) { + case RESPONSED: // 请求已正常�应 + break; + case CANCELED: // 请求已被�消 + break; + case NETWORK_ERROR: // 网络错误,说明用户没网了 + break; + case TIMEOUT: // 请求超时 + break; + case EXCEPTION: // 其它异常 + break; +} +// 还�以获得具体的异常信� +IOException error = result.getError(); +``` + +### 异步请求的异常 + +  异步请求最常用的异常处�方�就是设置一个异常回调: + +```java +http.async("/users/1") + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .setOnException((IOException e) -> { + // 这里处�请求异常 + }) + .get(); +``` +  当然,还有一个全局异常监�(`ExceptionListener`,请�考 [全局回调监�](/v2/configuration.html#全局回调监�) 章节): + +```java +HTTP http = HTTP.builder() + .exceptionListener((HttpTask task, IOException error) -> { + // 所有请求执行完都会走这里 + + // 返回 true 表示继续执行 task 的 OnComplete 回调, + // 返回 false 则表示��执行,� 阻断 + return true; + }) + .build(); +``` +  如果�设置`OnException`回调,也没有`ExceptionListener`,�生异常时会在 **IO 线程** 中�上抛出,外层无法�获: + +```java +try { + http.async("/users/1") + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .get(); +} catch (HttpException e) { + // 这�方�是�获�到异常的������ +} +``` +  �使没有`OnException`回调,�生异常时,�然会走`OnComplete`回调,如果设置了的�: + +```java +http.async("/users/1") + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .setOnComplete((State state) -> { + // �生异常,会先执行这里,�以根� state 判断�生了什么 + // 但执行完��然会在IO线程中�上抛出 + }) + .get(); +``` +  如果就是想 **�处�异常,也��上抛出**,�生错误完全无视,�以�到�?�以�还是使用`nothrow()`方法: + +```java +http.async("/users/1") + .nothrow() // 告诉 OkHttps �生异常时��外抛出 + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .get(); +``` + +## �消请求 + +  在 OkHttps 里�消请求共有 **4 �** 方��选: + +**1�** 使用`HttpCall#cancel()`�消�个请求(适用于异步请求,[详� 3.3 章节](#33-httpcall)) + +**2�** 使用`HttpTask#cancel()`�消�个请求(适用于所有请求)(since v1.0.4) + +```java +HttpTask task = http.async("/users") + .setOnResponse((HttpResult result) -> { + // �应回调 + }); + +task.get(); // �起 GET 请求 + +// �消请求,并返回是��消�功 +boolean canceled = task.cancel(); +``` + +**3�** 使用`HTTP#cancel(String tag)`按标签批��消请求(适用于所有请求,[详�第 5 章节](#5-使用标签)) + +**4�** 使用`HTTP#cancelAll()`�消所有请求(适用于所有请求)(since v1.0.2) + +```java +http.cancelAll(); // �消所有请求 +``` + +::: tip �示 +以上四�方�都对所有类型的请求有效,包括:�步 HTTP�异步 HTTP 和 WebSocket 连接。 +::: + +除了以上的 4 �方�,OkHttps 里还�以实现自动�消,请�考 [安�-生命周期绑定](/v2/android.html#生命周期绑定) 章节。 diff --git a/okhttps-doc/docs/en/v2/getstart.md b/okhttps-doc/docs/en/v2/getstart.md new file mode 100644 index 00000000..5b9324e5 --- /dev/null +++ b/okhttps-doc/docs/en/v2/getstart.md @@ -0,0 +1,280 @@ +--- +description: OkHttps 安装 构建实例 HTTP build �步请求 异步请求 sync async BaseUrl request webSocket gradle maven ejlchina +--- + +# 起步 + +## 安装 + +### Maven + +#### �独使用 OkHttps + +```xml + + com.ejlchina + okhttps + 2.0.0 + +``` + +�独使用 OkHttps 需�自定义[`MsgConvertor`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/MsgConvertor.java),�则无法使用 **自动正��列化** 相关功能,�文会详细讲解哪些功能会�到此影�。 + +#### 与 fastjson 一起使用 + +```xml + + com.ejlchina + okhttps-fastjson + 2.0.0 + +``` + +#### 与 gson 一起使用 + +```xml + + com.ejlchina + okhttps-gson + 2.0.0 + +``` + +#### 与 jackson 一起使用 + +```xml + + com.ejlchina + okhttps-jackson + 2.0.0 + +``` + +::: tip +以上�赖四选一�� +::: + +### Gradle + +#### �独使用 OkHttps + +```groovy +implementation 'com.ejlchina:okhttps:2.0.0' +``` + +�独使用 OkHttps 需�自定义[`MsgConvertor`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/MsgConvertor.java),�则无法使用 **自动正��列化** 相关功能,�文会详细讲解哪些功能会�到此影�。 + +#### 与 fastjson 一起使用 + +```groovy +implementation 'com.ejlchina:okhttps-fastjson:2.0.0' +``` + +#### 与 gson 一起使用 + +```groovy +implementation 'com.ejlchina:okhttps-gson:2.0.0' +``` + +#### 与 jackson 一起使用 + +```groovy +implementation 'com.ejlchina:okhttps-jackson:2.0.0' +``` + +::: tip +以上�赖四选一�� +::: + + +#### JDK 版本 + +安�中使用需�把 JDK 版本调� 1.8,在 app 模�的 build.gradle 中加入以下�置��: + +```groovy +android { + // �略其它�置.. + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} +``` + +## 使用 + +### 构建 HTTP + +```java +HTTP http = HTTP.builder().build(); +``` + +以上代�构建了一个最简�的`HTTP`实例,它拥有以下方法: + +* `sync(String url)` 开始一个�步 HTTP 请求 +* `async(String url)` 开始一个异步 HTTP 请求 +* `webSocket(String url)` 开始一个 WebSocket 连接 +* `cancel(String tag)` 按标签�消(�步 | 异步 | WebSocket)连接 +* `cancelAll()` �消所有(�步 | 异步 | WebSocket)连接 +* `request(Request request)` OkHttp 原生 HTTP 请求 +* `webSocket(Request request, WebSocketListener listener)` OkHttp 原生 WebSocket 连接 +* `newBuilder()` 用于�新构建一个 HTTP 实例 + +为了使用方便,在构建的时候,我们更愿�指定一个`BaseUrl`和[`MsgConvertor`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/MsgConvertor.java)(详� [设置 BaseUrl](/v1/configuration.html#设置-baseurl) 和 [消�转�器](/v2/configuration.html#消�转�器)): + +```java +HTTP http = HTTP.builder() + .baseUrl("http://api.example.com") + .addMsgConvertor(new GsonMsgConvertor()); + .build(); +``` + +上例中的[`GsonMsgConvertor`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps-gson/src/main/java/com/ejlchina/okhttps/GsonMsgConvertor.java)�自[`okhttps-gson`](https://gitee.com/ejlchina-zhxu/okhttps/tree/master/okhttps-gson)。为了简化文档,下文中出现的`http`�是在构建时设置了`BaseUrl`和`MsgConvertor`的`HTTP`实例。 + + +### �步请求 + +  使用方法`sync(String url)`开始一个�步请求: + +```java +List users = http.sync("/users") // http://api.example.com/users + .get() // GET请求 + .getBody() // 获��应报文体 + .toList(User.class); // 得到目标数� +``` + +  方法`sync`返回一个�步`HttpTask`,�链�使用。 + +### 异步请求 + +  使用方法`async(String url)`开始一个异步请求: + +```java +http.async("/users/1") // http://api.example.com/users/1 + .setOnResponse((HttpResult result) -> { + // 得到目标数� + User user = result.getBody().toBean(User.class); + }) + .get(); // GET请求 +``` +  方法`async`返回一个异步`HttpTask`,�链�使用。 + +### WebSocket + +  使用方法`webSocket(String url)`开始一个 WebSocket 通讯: + +```java +http.webSocket("/chat") + .setOnOpen((WebSocket ws, HttpResult res) -> { + ws.send("��务器问好"); + }) + .setOnMessage((WebSocket ws,Message msg) -> { + // 从�务器接收消�(自动��列化) + Chat chat = msg.toBean(Chat.class); + // 相�的消���给�务器(自动�列化 Chat 对象) + ws.send(chat); + }) + .listen(); // �动监� +``` +  方法`webSocket`返回一个支� WebSocket 的`HttpTask`,也�链�使用。 + + +### 工具类 + +OkHttps �供了两个开箱�用的工具类,让你从此告别�装工具类的烦�: + +工具类 | 起始版本 | 功能特点 +-|-|- +`OkHttps` | `2.0.0.RC` | 支�自动注入`MsgConvertor`,支� [SPI 方�注入�置](#�置okhttps),推�用于主应用中的网络开� +`HttpUtils` | `1.0.0` | 自`2.0.0.RC`开始支�自动注入`MsgConvertor`,�建议��其它�置,推�用于第三方�赖包中的网络开� + +这两个工具都类会自动以 SPI 方�注入�赖中的`MsgConvertor`(在第一次使用的时候),��你的项目中添加了相关�赖(如:okhttps-fastjson 等)。并且它们和`HTTP`接�拥有几乎一样的方法,并且这些方法都是��的: + +* `sync(String url)` 开始一个�步 HTTP 请求 +* `async(String url)` 开始一个异步 HTTP 请求 +* `webSocket(String url)` 开始一个 WebSocket 连接 +* `cancel(String tag)` 按标签�消(�步 | 异步 | WebSocket)连接 +* `cancelAll()` �消所有(�步 | 异步 | WebSocket)连接 +* `request(Request request)` OkHttp 原生 HTTP 请求 +* `webSocket(Request request, WebSocketListener listener)` OkHttp 原生 WebSocket 连接 + +例如,使用`OkHttps`,无需考虑构造`HTTP`实例,你�以这样�起一个请求: + +```java +OkHttps.async("https://api.example.com/auth/login") + .addBodyPara("username", "jack") + .addBodyPara("password", "xxxx") + .setOnResponse((HttpResult result) -> { + // 得到返回数�,使用 Mapper ��去定义一个实体类 + Mapper mapper = result.getBody().toMapper(); + // 登录是��功 + boolean success = mapper.getBool("success"); + }) + .post(); +``` + +### �置`OkHttps` + +工具类`OkHttps`还支�以 SPI 方�注入自定义�置,分以下两步: + +#### 第一步�新建一个�置类,实现[`com.ejlchina.okhttps.Config`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/Config.java)接� + +例如: + +```java +package com.example.okhttps; + +import com.ejlchina.okhttps.Config; +import com.ejlchina.okhttps.HTTP; + +public class OkHttpsConfig implements Config { + + @Override + public void with(HTTP.Builder builder) { + // 在这里对 HTTP.Builder �一些自定义的�置 + builder.baseUrl("https://api.domo.com"); + // 如果项目中添加了 okhttps-fastjson 或 okhttps-gson 或 okhttps-jackson �赖 + // OkHttps 会自动注入它们�供的 MsgConvertor + // 所以这里就�需���置 MsgConvertor 了 (内部实现自动注入的原�也是 SPI) + // 但如果没有添加这些�赖,那还需�自定义一个 MsgConvertor + builder.addMsgConvertor(new MyMsgConvertor()); + } + +} +``` + +#### 第二步�在项目的`/src/main`目录下新建`resources/META-INF/services/com.ejlchina.okhttps.Config`文件,文件内容是上一步自定义的�置类的全� + +例如: + +![](/spi_config.png) + +以上两步就完�了对`OkHttps`工具类的自定义�置,接下�你便�以体验它神奇的能力了: + +```java +List users = OkHttps.sync("/users") // http://api.example.com/users + .get().getBody().toList(User.class); +``` + +#### 为什么�这样呢? + +有�学�能会疑问,为什么�这样�置呢,�能直接给`OkHttps`设置一个`HTTP`实例�,�这样: + +```java +HTTP http = HTTP.builder() + // 自定义�置... + .build(); + +OkHttps.set(http); +``` + +这样�在一般情况下确实�以,但是在�些 Java 虚拟机上(特别在 Android)中,有一个致命的缺陷:那就是系统在�些情况下会回收����,而`OkHttps`内部�有的`HTTP`实例一旦被回收�,上述方�自定义的�置就会丢失,这样在�续的逻辑处�中就�能会造�严�的问题� + +而 SPI 注入的方�则�以很好的解决这个问题:�使`OkHttps`内部的`HTTP`实例被回收,在下次使用时,`OkHttps`�会自动加载注入的�置�新构建一个`HTTP`实例,这样就��了用户自定义的�置�会丢失。 + +所以我们推�在 Android 应用开�中,直接使用`OkHttps`,自定义�置通过 SPI 方�注入。 + +而在开�具有网络请求需求的�赖项目时,直接使用`HttpUtils`并且��自定义的�置,如果一定�自定义�置,那就使用`HTTP.Builder`�新构建一个`HTTP`实例使用�。 + + +**至此,你已轻�学会了 OkHttps 95% 的常规用法�但别急,��还有更精彩的。** \ No newline at end of file diff --git a/okhttps-doc/docs/en/v2/updownload.md b/okhttps-doc/docs/en/v2/updownload.md new file mode 100644 index 00000000..eb528473 --- /dev/null +++ b/okhttps-doc/docs/en/v2/updownload.md @@ -0,0 +1,240 @@ +--- +description: OkHttps 文件 上传 下载 进度监� 过程控制 断点续传 分�下载 +--- + +# 上传下载 + +## 文件下载 + +  OkHttps 并没有把文件的下载排除在常规的请求之外,�一套API,它优雅的设计使得下载与常规请求��的毫无�和感,一个最简�的示例: + +```java +http.sync("/download/test.zip") + .get() // 使用 GET 方法(其它方法也�以,看�务器支�) + .getBody() // 得到报文体 + .toFile("D:/download/test.zip") // 下载到指定的路径 + .start(); // �动下载 + +http.sync("/download/test.zip").get().getBody() + .toFolder("D:/download") // 下载到指定的目录,文件�将根�下载信�自动生� + .start(); +``` +  或使用异步连接方�: + +```java +http.async("/download/test.zip") + .setOnResponse((HttpResult result) -> { + result.getBody().toFolder("D:/download").start(); + }) + .get(); +``` +  这里�说明一下:`sync`与`async`的区别在于连接�务器并得到�应这个过程的�步与异步(这个过程的耗时在大文件下载中�比��),而`start`方法�动的下载过程则都是异步的。 + +### 下载进度监� + +  就直接上代�啦,诸�一看便懂: + +```java +http.sync("/download/test.zip") + .get() + .getBody() + .stepBytes(1024) // 设置�接收 1024 个字节执行一次进度回调(�设置默认为 8192) + // .stepRate(0.01) // 设置�接收 1% 执行一次进度回调(�设置以 StepBytes 为准) + .setOnProcess((Process process) -> { // 下载进度回调 + long doneBytes = process.getDoneBytes(); // 已下载字节数 + long totalBytes = process.getTotalBytes(); // 总共的字节数 + double rate = process.getRate(); // 已下载的比例 + boolean isDone = process.isDone(); // 是�下载完� + }) + .toFolder("D:/download/") // 指定下载的目录,文件�将根�下载信�自动生� + // .toFile("D:/download/test.zip") // 指定下载的路径,若文件已存在则覆盖 + .setOnSuccess((File file) -> { // 下载�功回调 + + }) + .start(); +``` +  值得一�的是:由于 OkHttps 并没有把下载�的很特别,这里设置的进度回调��对下载文件起用作,�使对�应JSON的常规请求,��设置了进度回调,它也会告诉你报文接收的进度(��是�务器�应的报文有`Content-Length`头),例如: + +```java +List users = http.sync("/users") + .get() + .getBody() + .stepBytes(2) + .setOnProcess((Process process) -> { + System.out.println(process.getRate()); + }) + .toList(User.class); +``` + +::: warning +方法`stepBytes`和`stepRate`在 v2.0.0 正�版之��为`setStepBytes`和`setStepRate`, v2.0.0 之�推�使用简�版的方法,�方法将在 v2.1.0 中移除。 +::: + +### 下载过程控制 + +  过于简�:还是直接上代�: + +```java +Ctrl ctrl = http.sync("/download/test.zip") + .get() + .getBody() + .setOnProcess((Process process) -> { + System.out.println(process.getRate()); + }) + .toFolder("D:/download/") + .start(); // 该方法返回一个下载过程控制器 + +ctrl.status(); // 下载状� +ctrl.pause(); // 暂�下载 +ctrl.resume(); // ��下载 +ctrl.cancel(); // �消下载(�时会删除文件,����) +``` +  无论是�步还是异步�起的下载请求,都�以�以上的控制: + +```java +http.async("/download/test.zip") + .setOnResponse((HttpResult result) -> { + // 拿到下载控制器 + Ctrl ctrl = result.getBody().toFolder("D:/download/").start(); + }) + .get(); +``` + +### 实现断点续传 + +  OkHttps 对断点续传并没有��更高层次的�装,因为这是app该去�的事情,它在设计上使��网络问题的处��简�的�时力求纯粹。下�的例��以看到,OkHttps 通过一个失败回调拿到 **断点**,便将��的问题�得简�: + +```java +http.sync("/download/test.zip") + .get() + .getBody() + .toFolder("D:/download/") + .setOnFailure((Failure failure) -> { // 下载失败回调,以便接收诸如网络错误等失败信� + IOException e = failure.getException(); // 具体的异常信� + long doneBytes = failure.getDoneBytes(); // 已下载的字节数(断点),需��存,用于断点续传 + File file = failure.getFile(); // 下载生�的文件,需��存 ,用于断点续传(��存路径也�以) + }) + .start(); +``` +  下�代�实现续传: + +```java +long doneBytes = ... // 拿到�存的断点 +File file = ... // 待续传的文件 + +http.sync("/download/test.zip") + .setRange(doneBytes) // 设置断点(已下载的字节数) + .get() + .getBody() + .toFile(file) // 下载到�一个文件里 + .setAppended() // 开�文件追加模� + .setOnSuccess((File file) -> { + + }) + .setOnFailure((Failure failure) -> { + + }) + .start(); +``` + +### 实现分�下载 + +  当文件很大时,有时候我们会考虑分�下载,与断点续传的�路是一样的,示例代�: + +```java +static String url = "http://api.demo.com/download/test.zip" + +public static void main(String[] args) { + long totalSize = OkHttps.sync(url).get().getBody() + .close() // 因为这次请求�是为了获得文件大�,�消费报文体,所以直接关闭 + .getContentLength(); // 获得待下载文件的大�(由于未消费报文体,所以该请求�会消耗下载报文体的时间和网络��) + download(totalSize, 0); // 从第 0 �开始下载 + sleep(50000); // 等待下载完�(�然本例的主线程就结�啦) +} + +static void download(long totalSize, int index) { + long size = 3 * 1024 * 1024; // ��下载 3M + long start = index * size; + long end = Math.min(start + size, totalSize); + OkHttps.sync(url) + .setRange(start, end) // 设置本次下载的范围 + .get().getBody() + .toFile("D:/download/test.zip") // 下载到�一个文件里 + .setAppended() // 开�文件追加模� + .setOnSuccess((File file) -> { + if (end < totalSize) { // 若未下载完,则继续下载下一� + download(totalSize, index + 1); + } else { + System.out.println("下载完�"); + } + }) + .start(); +} +``` + +本例中用到了`OkHttps`工具类,在应用的开�者我们推�使用它,请�考 [工具类](/v2/getstart.html#工具类) 章节。 + +## 文件上传 + +  一个简�文件上传的示例: + +```java +http.sync("/upload") + .addFilePara("test", "D:/download/test.zip") + .post(); // 上传�法一般使用 POST 或 PUT,看�务器支� +``` +  异步上传也是完全一样: + +```java +http.async("/upload") + .addFilePara("test", "D:/download/test.zip") + .post(); +``` + +::: warning +如果你使用的是 v2.0.0.RC 版本中,以上代�会报 **“方法 addFilePara �能使用 form 方�请求�** 错误,�以用以下方�解决该问题: +```java +http.async("/upload") + .bodyType("multipart/form") + .addFilePara("test", "D:/download/test.zip") + .post(); +``` +详� ISSUE: [https://gitee.com/ejlchina-zhxu/okhttps/issues/I1H8G9](https://gitee.com/ejlchina-zhxu/okhttps/issues/I1H8G9) +::: + +### 上传进度监� + +  OkHttps 的上传进度监�,监�的是所有请求报文体的��进度,示例代�: + +```java +http.sync("/upload") + .addBodyPara("name", "Jack") + .addBodyPara("age", 20) + .addFilePara("avatar", "D:/image/avatar.jpg") + .stepBytes(1024) // 设置��� 1024 个字节执行一次进度回调(�设置默认为 8192) + // .stepRate(0.01) // 设置��� 1% 执行一次进度回调(�设置以 StepBytes 为准) + .setOnProcess((Process process) -> { // 上传进度回调 + long doneBytes = process.getDoneBytes(); // 已��字节数 + long totalBytes = process.getTotalBytes(); // 总共的字节数 + double rate = process.getRate(); // 已��的比例 + boolean isDone = process.isDone(); // 是���完� + }) + .post(); +``` +  咦�怎么感觉和下载的进度回调的一样?没错�OkHttps 还是使用�一套API处�上传和下载的进度回调,区别�在于上传是在`get/post`方法之�使用这些API,下载是在`getBody`方法之�使用。很好�解:`get/post`之�是准备��请求时段,有上传的�义,而`getBody`之�,已是报文�应的时段,当然是下载。 + +### 上传过程控制 + +  上传文件的过程控制就很简�,和常规请求一样,�有异步�起的上传�以�消: + +```java +HttpCall call = http.async("/upload") + .addFilePara("test", "D:/download/test.zip") + .setOnProcess((Process process) -> { + System.out.println(process.getRate()); + }) + .post(); + +call.cancel(); // �消上传 +``` +  上传就没有暂�和继续这个功能啦,应该没人有这个需求�? \ No newline at end of file diff --git a/okhttps-doc/docs/v1/README.md b/okhttps-doc/docs/v1/README.md new file mode 100644 index 00000000..84a45c42 --- /dev/null +++ b/okhttps-doc/docs/v1/README.md @@ -0,0 +1,103 @@ +--- +description: OkHttps 安装 构建实例 HTTP build �步请求 异步请求 sync async BaseUrl request webSocket gradle maven ejlchina +--- + +# 起步 + +  OkHttps 是近期开�的对 OkHttp3 轻��装的框架,它独创的异步预处�器,特色的标签,�活的上传下载进度监�与过程控制功能,在轻�解决很多原本�人头疼问题的�时,设计上也力求纯粹与优雅。 + + * 链�调用,一点到底 + * BaseURL�URL��符�JSON自动�装与解� + * �步拦截器�异步预处�器�回调执行器�全局监��回调阻断 + * 文件上传下载(过程控制�进度监�) + * �方法回调,充分利用 Lambda 表达� + * TCP连接池�Http2 + +::: tip +* OkHttps �常轻�(59Kb),仅是 Retrofit(124Kb)的一�,并且更加的开箱�用,API 也更加自然和语义化。 +* OkHttps 是一个纯粹的 Java 网络开�包,并��赖 Android,这一点和 Retrofit �� +* OkHttps 用起�很优美,�以� RxJava 那样链�用,�比 RxJava 更简�。 +::: + +## 安装 + +### Maven + +```xml + + com.ejlchina + okhttps + 1.0.5 + +``` +### Gradle + +```groovy +implementation 'com.ejlchina:okhttps:1.0.5' +``` + +安�中使用需�把 JDK 版本调� 1.8,在 app 模�的 build.gradle 中加入以下�置��: + +```groovy +android { + // �略其它�置.. + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} +``` + +## 使用 + +### 构建实例 + +```java +HTTP http = HTTP.builder().build(); +``` +  以上代�构建了一个最简�的`HTTP`实例,它拥有以下方法: + +* `sync(String url)` 开始一个�步请求 +* `async(String url)` 开始一个异步请求 +* `cancel(String tag)` 按标签�消请求 +* `cancelAll()` �消所有HTTP任务,包括�步和异步 +* `request(Request request)` OkHttp 原生请求 +* `webSocket(Request request, WebSocketListener listener)` WebSocket通讯 + +  为了使用方便,在构建的时候,我们更愿�指定一个`BaseUrl`(详� [设置 BaseUrl](/v1/configuration.html#设置-baseurl)): + +```java +HTTP http = HTTP.builder() + .baseUrl("http://api.demo.com") + .build(); +``` +  为了简化文档,下文中出现的`http`�是在构建时设置了`BaseUrl`的`HTTP`实例。 + +### �步请求 + +  使用方法`sync(String url)`开始一个�步请求: + +```java +List users = http.sync("/users") // http://api.demo.com/users + .get() // GET请求 + .getBody() // 获��应报文体 + .toList(User.class); // 得到目标数� +``` +  方法`sync`返回一个�步`HttpTask`,�链�使用。 + +### 异步请求 + +  使用方法`async(String url)`开始一个异步请求: + +```java +http.async("/users/1") // http://api.demo.com/users/1 + .setOnResponse((HttpResult result) -> { + // 得到目标数� + User user = result.getBody().toBean(User.class); + }) + .get(); // GET请求 +``` +  方法`async`返回一个异步`HttpTask`,�链�使用。 + + +**至此,你已轻�学会了 OkHttps 95% 的常规用法�但别急,��还有更精彩的。** \ No newline at end of file diff --git a/okhttps-doc/docs/v1/android.md b/okhttps-doc/docs/v1/android.md new file mode 100644 index 00000000..73c7fde5 --- /dev/null +++ b/okhttps-doc/docs/v1/android.md @@ -0,0 +1,181 @@ +--- +description: OkHttps 安� 生命周期 UI线程 主线程 IO线程 自由 �活 切� 生命周期绑定 �置预处�器 定义生命周期监视器 �置全局回调监� +--- + +# 安� + +## 回调线程切� + +  在 Android 开�中,�常会把�些代�放到特点的线程去执行,比如网络请求�应�的页�更新在主线程(UI线程)执行,而�存文件则在IO线程�作。OkHttps 为这类问题�供了良好的方案。 + +  在 **默认** 情况下,**所有回调** 函数都会 **在 IO 线程** 执行。为什么会设计如此呢?这是因为 OkHttps �是纯粹的 Java 领域 Http工具包,本身对 Android �会有任何�赖,因此也�知 Android 的 UI 线程为何物。这么设计也让它在 Android 之外有更多的�能性。 + +### �置 + +  但是在 Android 里使用 OkHttps 的�,UI线程的问题能�优雅的解决呢?当然�以�简�粗暴的方法就是�置一个 回调执行器: + + ```java +HTTP http = HTTP.builder() + .callbackExecutor((Runnable run) -> { + // 实际编�中�以� Handler �出�,�需��次执行回调都�新创建 + new Handler(Looper.getMainLooper()).post(run); // 在主线程执行 + }) + .build(); +``` + +### 用例 + +  上述代�便实现了让 **所有** 的 **回调函数** 都在 **主线程(UI线程)** 执行的目的,如: + +```java +http.async("/users") + .addBodyParam("name", "Jack") + .setOnProcess((Process process) -> { + // 在主线程执行 + }) + .setOnResponse((HttpResult result) -> { + // 在主线程执行 + }) + .setOnException((Exception e) -> { + // 在主线程执行 + }) + .setOnComplete((State state) -> { + // 在主线程执行 + }) + .post(); +``` +  但是,如果�时还想让�些回调放在IO线程,实现 **自由切�**,怎么办呢?OkHttps 给出了�常�活的方法,如下: + +```java +http.async("/users") + .addBodyParam("name", "Jack") + .setOnProcess((Process process) -> { + // 在主线程执行 + }) + .nextOnIO() // 指定下一个回调在 IO 线程执行 + .setOnResponse((HttpResult result) -> { + // 在 IO 线程执行 + }) + .setOnException((Exception e) -> { + // 在主线程执行(没有指明 nextOnIO 则在回调执行器里执行) + }) + .nextOnIO() // 指定下一个回调在 IO 线程执行 + .setOnComplete((State state) -> { + // 在 IO 线程执行 + }) + .post(); +``` +  无论是哪一个回调,都�以使用`nextOnIO()`方法自由切�。�样,对于文件下载也是一样: + +```java +http.sync("/download/test.zip") + .get() + .getBody() + .setOnProcess((Process process) -> { + // 在主线程执行 + }) + .toFolder("D:/download/") + .nextOnIO() // 指定下一个回调在 IO 线程执行 + .setOnSuccess((File file) -> { + // 在 IO 线程执行 + }) + .setOnFailure((Failure failure) -> { + // 在主线程执行 + }) + .start(); +``` + +## 生命周期绑定 + +  由于 OkHttps 并��赖于 Android,所以它并没有�供关于生命周期绑定的直接实现,但它的一些扩展机制让我们很容易就�以实现这个需求。在开始之�,我们首先��解何为生命周期绑定: + +::: tip HTTP 请求的生命周期绑定 +所谓的生命周期绑定:�是让 HTTP 任务感知其所属的 Activity 或 Fragment 的生命周期,当 Activity 或 Fragment 将被销�时,框架应自动的把由它们�起的但尚未完�的 HTTP 任务全部�消,以�导致程�出错� +::: + +  现在我们需�对`HTTP`实例进行�置,�置�的`HTTP`实例具有生命周期绑定的功能,在`Activity`或`Fragment`里,它的使用效果如下: + +```java +// 在 Activity 或 Fragment 内�起请求 +http.async("http://www.baidu.com") + .bind(getLifecycle()) // 绑定生命周期 + .setOnResponse((HttpResult result) -> { + Log.i("FirstFragment", "收到请求:" + result.toString()); + }) + .get(); +``` +  上述代�中的`getLifecycle()`是`Activity`或`Fragment`自带的方法,而`bind()`是`HttpTask`的现有方法。在�置好`HTTP`实例�,上述代��起的请求便�以感知`Activity`或`Fragment`的生命周期。 + +  那`HTTP`实例到底该如何�置呢? + +### 第一步:�置预处�器 + +```java +HTTP http = HTTP.builder() + ... // �略其它�置项 + .addPreprocessor((Preprocessor.PreChain chain) -> { + HttpTask task = chain.getTask(); + Object bound = task.getBound(); + // 判断 task 是�绑定了 Lifecycle 对象 + if (bound instanceof Lifecycle) { + // �新绑定一个 生命周期监视器(LCObserver)对象,它的定义�下一步 + task.bind(new LCObserver(task, (Lifecycle) bound)); + } + chain.proceed(); + }) + ... // �略其它�置项 + .build(); +``` + +### 第二步:定义生命周期监视器 + +```java +public class LCObserver implements LifecycleObserver { + + HttpTask task; + Lifecycle lifecycle; + + LCObserver(HttpTask task, Lifecycle lifecycle) { + this.task = task; + this.lifecycle = lifecycle; + lifecycle.addObserver(this); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + public void onStop() { + task.cancel(); // 在 ON_STOP 事件中,�消对应的 HTTP 任务 + } + + public void unbind() { + // 在请求完�之�移除监视器 + lifecycle.removeObserver(this); + } + +} +``` + +### 第三步:�置全局回调监� + +  以上两步其实已�实现了生命周期绑定的功能,但是在请求完�之�,我们需�在`lifecycle`中移除`LCObserver`对象: + +```java +HTTP http = HTTP.builder() + ... // �略其它�置项 + .completeListener((HttpTask task, HttpResult.State state) -> { + Object bound = task.getBound(); + // 判断 task 是�绑定了生命周期监视器(LCObserver)对象 + if (bound instanceof LCObserver) { + // 解绑监视器 + ((LCObserver) bound).unbind(); + } + return true; + }) + ... // �略其它�置项 + .build(); +``` + +**以上三步便在Android中实现了生命周期与HTTP请求绑定的功能** + +  �常简�,懒得敲代�的�学还�以 [点这里 OkHttps.java](https://gitee.com/ejlchina-zhxu/okhttps-android-demo/blob/master/app/src/main/java/com/flower/myapplication/http/OkHttps.java) 直接下载�装好的��,其中�仅编写了生命周期绑定的�置,还有在UI线程执行回调的�置。 + +  有需�的�学,�以直接下载下�使用,还�以基于它�次扩展,比如实现自动添加 TOKEN 的功能,具体�以�考[6.5 串行预处�器(TOKEN问题最佳解决方案)](#65-串行预处�器token问题最佳解决方案),�比如扩展实现生命周期与下载事件绑定的功能,都�常简�。 diff --git a/okhttps-doc/docs/v1/configuration.md b/okhttps-doc/docs/v1/configuration.md new file mode 100644 index 00000000..30bf60e9 --- /dev/null +++ b/okhttps-doc/docs/v1/configuration.md @@ -0,0 +1,158 @@ +--- +description: OkHttps �置 BaseUrl 回调执行器 主线程 UI线程 OkHttpClient 预处�器 TOKEN问题最佳解决方案 刷新TOKEN 全局 回调监� 下载监� 回调阻断 拦截器 CookieJar�SSL�缓存�代��事件监� +--- + +# �置 + +## 设置 BaseUrl + +```java +HTTP http = HTTP.builder() + .baseUrl("http://api.demo.com") // 设置 BaseUrl + .build(); +``` +  该�置全局生效,在�置了`BaseUrl`之�,具体的请求便�以�略`BaseUrl`部分,使得代�更加简�,例如: + +```java +http.sync("/users").get() // http://api.demo.com/users + +http.sync("/auth/signin") // http://api.demo.com/auth/signin + .addBodyParam("username", "Jackson") + .addBodyParam("password", "xxxxxx") + .post(); // POST请求 +``` +  在�置了`BaseUrl`之�,如有特殊请求任务,�然�以使用全路径的方�,一点都�妨�: + +```java +http.sync("https://www.baidu.com").get(); +``` + +## 回调执行器 + +  OkHttps 默认所有回调都在 **IO线程** 执行,如何想改�执行回调的线程时,�以�置回调执行器。例如在Android里,让所有的回调函数都在UI线程执行,则�以在构建`HTTP`时�置如下: + +```java +HTTP http = HTTP.builder() + .callbackExecutor((Runnable run) -> { + runOnUiThread(run); // 在UI线程执行 + }) + .build(); +``` +  该�置默认 **影�所有回调**。 + +## �置 OkHttpClient + +  与其他�装 OkHttp3 的框架��,OkHttps 并�会�蔽 OkHttp3 本身就很好用的功能,如下: + +```java +HTTP http = HTTP.builder() + .config((Builder builder) -> { + // �置连接池 最�10个连接(��置默认为 5) + builder.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)); + // �置连接超时时间(默认10秒) + builder.connectTimeout(20, TimeUnit.SECONDS); + // �置拦截器 + builder.addInterceptor((Chain chain) -> { + Request request = chain.request(); + // 必须�步返回,拦截器内无法执行异步�作 + return chain.proceed(request); + }); + // 其它�置: CookieJar�SSL�缓存�代��事件监�... + }) + .build(); +``` + +## 并行预处�器 + +  预处�器(`Preprocessor`)�以让我们在请求�出之�对请求本身�一些改�,但与`OkHttp`的拦截器(`Interceptor`)��:预处�器�以让我们 **异步** 处�这些问题。 + +  例如,当我们想为请求任务自动添加`Token`头信�,而`Token`�能通过异步方法`requestToken`获�时,这时使用`Interceptor`就很难处�了,但�以使用预处�器轻�解决: + +```java +HTTP http = HTTP.builder() + .addPreprocessor((PreChain chain) -> { + HttpTask task = chain.getTask();// 获得当�的HTTP任务 + if (!task.isTagged("Auth")) { // 根�标签判断该任务是�需�Token + chain.proceed(); + return; + } + requestToken((String token) -> { // 异步获� Token + task.addHeader("Token", token);// 为任务添加头信� + chain.proceed(); // 继续当�的任务 + }); + }) + .build(); +``` +  和`Interceptor`一样,`Preprocessor`也�以添加多个。他们之间的区别如下: + +::: tip 拦截器与预处�器的区别 +* 拦截器�能处��步�作,预处�器支�处�异步�作 +* 拦截器都是并行处�请求,预处�器支�串行处�(详�6.5章节) +* 拦截器处�时机在请求�和�应�,预处�器�在请求�,并且先于拦截器执行。关于�应�,OkHttps还�供了全局回调监�(详�6.6章节) +::: + +## 串行预处�器(TOKEN问题最佳解决方案) + +  普通预处�器都是�并行处�的,然而有时我们希望�个预处�器�时�处�一个任务。比如 当`Token`过期时我们需�去刷新获�新`Token`,而刷新`Token`这个�作�能有一个任务去执行,因为如果`n`个任务�时执行的�,那么必有`n-1`个任务刚刷新得到的`Token`�能就立马失效了,而这是我们所�希望的。 + +  为了解决这个问题,OkHttps �供了串行预处�器,它�以让 HTTP 任务排好队,一个一个地进入预处�器: + +```java +HTTP http = HTTP.builder() + .addSerialPreprocessor((PreChain chain) -> { + HttpTask task = chain.getTask(); + if (!task.isTagged("Auth")) { + chain.proceed(); + return; + } + // 检查过期,若需�则刷新Token + requestTokenAndRefreshIfExpired((String token) -> { + task.addHeader("Token", token); + chain.proceed(); // 调用此方法�,�会有其它任务进入该处�器 + }); + }) + .build(); +``` +  串行预处�器实现了让HTTP任务排队串行处�的功能,但值得一�的是:它并没有因此而阻塞任何线程� + +## 全局回调监� + +  全局回调是实际开�中�常需�的功能,比如对�务器�应的状��进行统一处�等,�时 OkHttps 的全局回调还具有 **回调阻断** 的功能: + +```java +HTTP http = HTTP.builder() + .responseListener((HttpTask task, HttpResult result) -> { + // 所有请求�应�都会走这里 + + return true; // 返回 true 表示继续执行 task 的 OnResponse 回调,false 表示��执行 + }) + .completeListener((HttpTask task, State state) -> { + // 所有请求执行完都会走这里 + + return true; // 返回 true 表示继续执行 task 的 OnComplete 回调,false 表示��执行 + }) + .exceptionListener((HttpTask task, IOException error) -> { + // 所有请求�生异常都会走这里 + + return true; // 返回 true 表示继续执行 task 的 OnException 回调,false 表示��执行 + }) + .build(); +``` + +::: tip 全局回调监�与拦截器的异�: +* 拦截器�以添加多个,全局回调监�分三�,��最多添加一个 +* 拦截器处的�时机在请求�和�应�,全局回调监��在�应�,并且晚于拦截器 +* 全局回调监��以 **阻断**(return false)�个请求的具体回调,而拦截器�能 +::: + +## 全局下载监� + +```java +HTTP http = HTTP.builder() + .downloadListener((HttpTask task, Download download) -> { + // 所有下载在开始之�都会先走这里 + Ctrl ctrl = download.getCtrl(); // 下载控制器 + + }) + .build(); +``` \ No newline at end of file diff --git a/okhttps-doc/docs/v1/features.md b/okhttps-doc/docs/v1/features.md new file mode 100644 index 00000000..56771574 --- /dev/null +++ b/okhttps-doc/docs/v1/features.md @@ -0,0 +1,170 @@ +--- +description: OkHttps 使用 HttpUtils 类 WebSocket 异常处� nothrow 异常回调 多�方� �消请求 标签 批��消 cancelAll +--- + +# 特色 + +## 使用 HttpUtils 类 + +  类`HttpUtils`本是 [�身 HttpUtils](https://gitee.com/ejlchina-zhxu/httputils) 的 1.x 版本里的最��的核心类,由于在��的版本里抽象出了`HTTP`接�,使得它的��性已�如往昔。但��的使用它,�然�以带��少便利,特别是在没有IOC容器的环境里,比如在Android开�和一些工具项目的开�中。 + +  类`HttpUtils`共定义了四个��方法: + +* `of(HTTP http)` �置`HttpUtils`�有的`HTTP`实例(�调用此方法�默认使用一个没有没有�过任何�置的`HTTP`懒实例) +* `sync(String url)` 开始一个�步请求 (内部通过一个`HTTP`�例实现) +* `async(String url)` 开始一个异步请求 (内部通过一个`HTTP`�例实现) +* `cancel(String tag)` 按标签�消请求(内部通过一个`HTTP`�例实现) +* `cancelAll()` �消所有HTTP任务,包括�步和异步(内部通过一个`HTTP`�例实现) +* `request(Request request)` OkHttp 原生请求 (该请求��过 预处�器) +* `webSocket(Request request, WebSocketListener listener)` WebSocket通讯 +  也就是说,能使用`http`实例的地方,都�以使用`HttpUtils`类,例如: + +```java +// 在�置HTTP实例之�,�能使用全路径方� +List roles = HttpUtils.sync("http://api.demo.com/roles") + .get().getBody().toList(Role.class); + +// �置HTTP实例,全局生效 +HttpUtils.of(HTTP.builder() + .baseUrl("http://api.demo.com") + .build()); + +// 内部使用新的HTTP实例 +List users = HttpUtils.sync("/users") + .get().getBody().toList(User.class); +``` + +## 异常处� + +  使用 OkHttps 时,**异常处��是必须的**,但相比其它的 HTTP 开�包,它还�供一个特别的处�方法:`nothrow()`,以满足��的异常处�需求。 + +### �步请求的异常 + +  默认情况下,当�步请求执行异常时,会直接�外抛出,我们�以用 `try catch` ��获,例如: + +```java +try { + HttpResult result = http.sync("/users/1").get(); +} catch (HttpException e) { + Throwable cause = e.getCause(); // 得到异常原因 + if (cause instanceof ConnectException) { + // 当没网络时,会抛出连接异常 + } + if (cause instanceof SocketTimeoutException) { + // 当接�长时间未�应,会抛出超时异常 + } + if (cause instanceof UnknownHostException) { + // 当把域�或IP写错,会抛出 UnknownHost 异常 + } + // ... +} +``` +  这�传统的异常处�方�,当然�以解决问题,但 OkHttps 有更佳的方案: + +```java +// 方法 nothrow() 让异常�直接抛出 +HttpResult result = http.sync("/users/1").nothrow().get(); +// 判断执行状� +switch (result.getState()) { + case RESPONSED: // 请求已正常�应 + break; + case CANCELED: // 请求已被�消 + break; + case NETWORK_ERROR: // 网络错误,说明用户没网了 + break; + case TIMEOUT: // 请求超时 + break; + case EXCEPTION: // 其它异常 + break; +} +// 还�以获得具体的异常信� +IOException error = result.getError(); +``` + +### 异步请求的异常 + +  异步请求最常用的异常处�方�就是设置一个异常回调: + +```java +http.async("/users/1") + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .setOnException((IOException e) -> { + // 这里处�请求异常 + }) + .get(); +``` +  当然,还有一个全局异常监�(`ExceptionListener`): + +```java +HTTP http = HTTP.builder() + .exceptionListener((HttpTask task, IOException error) -> { + // 所有请求�生异常都会走这里 + + return true; // 返回 true 表示继续执行 task 的 OnException 回调,false 表示��执行 + }) + .build(); +``` +  如果�设置`OnException`回调,也没有`ExceptionListener`,�生异常时会在 **IO 线程** 中�上抛出,外层无法�获: + +```java +try { + http.async("/users/1") + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .get(); +} catch (HttpException e) { + // 这�方�是�获�到异常的������ +} +``` +  �使没有`OnException`回调,�生异常时,�然会走`OnComplete`回调,如果设置了的�: + +```java +http.async("/users/1") + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .setOnComplete((State state) -> { + // �生异常,会先执行这里,�以根� state 判断�生了什么 + // 但执行完��然会在IO线程中�上抛出 + }) + .get(); +``` +  如果就是想 **�处�异常,也��上抛出**,�生错误完全无视,�以�到�?�以�还是使用`nothrow()`方法: + +```java +http.async("/users/1") + .nothrow() // 告诉 OkHttps �生异常时��外抛出 + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .get(); +``` + +## �消请求 + +  在 OkHttps 里�消请求共有 **4 �** 方��选: + +* 使用`HttpCall#cancel()`�消�个请求(适用于异步请求,[详� 3.3 章节](#33-httpcall)) +* 使用`HttpTask#cancel()`�消�个请求(适用于所有请求)(since v1.0.4) + +```java +HttpTask task = http.async("/users") + .setOnResponse((HttpResult result) -> { + // �应回调 + }); + +task.get(); // �起 GET 请求 + +// �消请求,并返回是��消�功 +boolean canceled = task.cancel(); +``` + +* 使用`HTTP#cancel(String tag)`按标签批��消请求(适用于所有请求,[详�第 5 章节](#5-使用标签)) +* 使用`HTTP#cancelAll()`�消所有请求(适用于所有请求)(since v1.0.2) + +```java +http.cancelAll(); // �消所有请求 +``` \ No newline at end of file diff --git a/okhttps-doc/docs/v1/foundation.md b/okhttps-doc/docs/v1/foundation.md new file mode 100644 index 00000000..e48cd8af --- /dev/null +++ b/okhttps-doc/docs/v1/foundation.md @@ -0,0 +1,209 @@ +--- +description: OkHttps 请求方法 回调函数 HttpResult cache 多次 消费报文体 HttpCall 构建HTTP任务 请求�数 JSON 使用标签 �起请求 �消请求 +--- + +# 基础 + +## 请求方法 + +  �步与异步的`HttpTask`都拥有`get`�`post`�`put`与`delete`方法。��的是:�步`HttpTask`的这些方法返回一个`HttpResult`,而异步`HttpTask`的这些方法返回一个`HttpCall`。 + +```java +HttpResult res1 = http.sync("/users").get(); // �步 GET +HttpResult res2 = http.sync("/users").post(); // �步 POST +HttpResult res3 = http.sync("/users/1").put(); // �步 PUT +HttpResult res4 = http.sync("/users/1").delete();// �步 DELETE +HttpCall call1 = http.async("/users").get(); // 异步 GET +HttpCall call2 = http.async("/users").post(); // 异步 POST +HttpCall call3 = http.async("/users/1").put(); // 异步 PUT +HttpCall call4 = http.async("/users/1").delete();// 异步 DELETE +``` + +## 回调函数 + +  OkHttps 的回调函数全部使用�方法模�,这样�以充分利用 Java8 或 Kotlin 中的 Lambda 表达�,使你的代�更加简�优雅: + +```java +http.async("/users/{id}") // http://api.demo.com/users/1 + .addPathParam("id", 1) + .setOnResponse((HttpResult result) -> { + // �应回调 + }) + .setOnException((IOException e) -> { + // 异常回调 + }) + .setOnComplete((State state) -> { + // 完�回调,无论�功失败都会执行 + // 并且在 �应|异常回调 之�执行 + }) + .get(); +``` + +  OkHttps �时还支� **全局回调** 和 **回调阻断** 机制,详� [全局回调监�](/v1/configuration.html#全局回调监�)。 + +::: tip +* �有异步请求��以设置这三�(�应|异常|完�)回调 +* �步请求直接返回结果,无需使用回调 +::: + +## HttpResult + +  `HttpResult`是HTTP请求执行完�的结果,它是�步请求方法( `get`�`post`�`put`�`delete`)的返回值,也是异步请求�应回调(`OnResponse`)的�数,它定义了如下方法: + +* `getState()` 得到请求执行状�枚举,它有以下�值: + * `State.CANCELED` 请求被�消 + * `State.RESPONSED` 已收到�应 + * `State.TIMEOUT` 请求超时 + * `State.NETWORK_ERROR` 网络错误 + * `State.EXCEPTION` 其它请求异常 +* `getStatus()` 得到HTTP状�� +* `isSuccessful()` 是��应�功,状��在 [200..300) 之间 +* `getHeaders()` 得到HTTP�应头 +* `getHeaders(String name)` 得到HTTP�应头 +* `getHeader(String name)` 得到HTTP�应头 +* `getBody()` 得到�应报文体`Body`实例,它定义了如下方法(对�一个`Body`实例,以下的`toXXX()`类方法�能使用一个且仅能调用一次,除�先使用 cache 方法): + * `toBytes()` 返回字节数组 + * `toByteStream()` 返回字节输入� + * `toCharStream()` 返回字符输入� + * `toString()` 返回字符串 + * `toJsonObject()` 返回Json对象 + * `toJsonArray()` 返回Json数组 + * `toBean(Class type)` 返回根�type自动json解��的JavaBean + * `toList(Class type)` 返回根�type自动json解��的JavaBean列表 + * `toFile(String filePath)` 下载到指定路径 + * `toFile(File file)` 下载到指定文件 + * `toFolder(String dirPath)` 下载到指定目录 + * `toFolder(File dir)` 下载到指定目录 + * `getContentType()` 返回报文体的媒体类型 + * `getContentLength()` 返回报文体的字节长度 + * `cache()` 缓存报文体,开�缓存����使用`toXXX()`类方法 + * `close()` 关闭报文体,未对报文体�任何消费时使用,比如�读�报文头 +* `getError()` 执行中�生的异常,自动�获执行请求时�生的 网络超时�网络错误 和 其它IO异常 +* `close()` 关闭报文,未对报文体�任何消费时使用,比如�读�长度 + +  示例,请求结果自动转Bean和List: + +```java +// 自动转Bean +Order order = http.sync("/orders/1") + .get().getBody().toBean(Order.class); + +// 自动转List +List orders = http.sync("/orders") + .get().getBody().toList(Order.class); +``` + +  示例,使用 cache 方法: + +```java +Body body = http.sync("/orders").get().getBody().cache(); + +// 使用 cache �,�以多次使用 toXXX() 方法 +System.out.println(body.toString()); +System.out.println(body.toJsonArray()); +System.out.println(body.toList(Order.class)); +``` + +  示例,获�下载文件的大�: + +```java +long size = http.sync("/download/test.zip") + .get().getBody() + .close() // �是想获得文件大�,�消费报文体,所以直接关闭 + .getContentLength(); // 获得待下载文件的大� + +// 由于未消费报文体,所以本次请求�会消耗下载报文体的时间和网络�� + +System.out.println("size = " + size); +``` + +## HttpCall + +  `HttpCall`对象是异步请求方法(`get`�`post`�`put`�`delete`)的返回值,与`java`的`Future`接�很�,它有如下方法: + +* `cancel()` �消本次请求,返回�消结果 +* `isCanceled()` 返回请求是�被�消 +* `isDone()` 返回是�执行完�,包��消和失败 +* `getResult()` 返回执行结果`HttpResult`对象,若请求未执行完,则挂起当�线程直到执行完��返回 + +  �消一个异步请求示例: + +```java +HttpCall call = http.async("/users/1").get(); + +System.out.println(call.isCanceled()); // false + +boolean success = call.cancel(); // �消请求 + +System.out.println(success); // true +System.out.println(call.isCanceled()); // true +``` + +## 构建HTTP任务 + +  `HTTP`对象的`sync`与`async`方法返回一个`HttpTask`对象,该对象�供了�链�调用的`addXXX`与`setXXX`等系列方法用于构建任务本身。 + +* `addHeader(String name, String value)` 添加请求头 +* `addHeader(Map headers)` 添加请求头 + +* `addPathParam(String name, Object value)` 添加路径�数:替�URL里的{name}��符 +* `addPathParam(Map params)` 添加路径�数:替�URL里的{name}��符 + +* `addUrlParam(String name, Object value)` 添加URL�数:拼接在URL的?之�(查询�数) +* `addUrlParam(Map params)` 添加URL�数:拼接在URL的?之�(查询�数) + +* `addBodyParam(String name, Object value)` 添加Body�数:以表�key=value&的形�放在报文体内(表��数) +* `addBodyParam(Map params)` 添加Body�数:以表�key=value&的形�放在报文体内(表��数) + +* `addJsonParam(String name, Object value)` 添加Json�数:请求体为Json(支�多层结构) +* `addJsonParam(Map params)` 添加Json�数:请求体为Json(支�多层结构) + +* `setRequestJson(Object json)` 设置请求体的Json字符串 或待转�为 Json的 JavaBean +* `setRequestJson(Object bean, String dateFormat)` 设置请求体的Json字符串 或待转�为 Json的 JavaBean + +* `addFileParam(String name, String filePath)` 上传文件 +* `addFileParam(String name, File file)` 上传文件 +* `addFileParam(String name, String type, InputStream inputStream)` 上传文件 +* `addFileParam(String name, String type, String fileName, InputStream input)` 上传文件 +* `addFileParam(String name, String type, byte[] content)` 上传文件 +* `addFileParam(String name, String type, String fileName, byte[] content)` 上传文件 + +* `setTag(String tag)` 为HTTP任务添加标签 +* `setRange(long rangeStart)` 设置Range头信�,用于[断点续传](/v1/updown.html#实现断点续传) +* `setRange(long rangeStart, long rangeEnd)` 设置Range头信�,�用于[分�下载](/v1/updown.html#实现分�下载) + +* `bind(Object object)` 绑定一个对象,�用于实现Android里的[生命周期绑定](/v1/android.html#生命周期绑定) + +## 使用标签 + +  有时候我们想对HTTP任务加以分类,这时候�以使用标签功能: + +```java +http.async("/users") //(1) + .setTag("A") + .get(); + +http.async("/users") //(2) + .setTag("A.B") + .get(); + +http.async("/users") //(3) + .setTag("B") + .get(); + +http.async("/users") //(4) + .setTag("B") + .setTag("C") // 从 v1.0.4 标签将以追加模�添加,等效于 setTag("B.C") + .get(); + +http.async("/users") //(5) + .setTag("C") + .get(); +``` +  当使用标签�,就�以按标签批�的对HTTP任务进行�消: + +```java +int count = http.cancel("B"); //(2)(3)(4)被�消(�消标签包�"B"的任务) +System.out.println(count); // 输出 3 +``` +  标签除了�以用��消任务,在预处�器中它也�以�挥作用,请�� [并行预处�器](/v1/configuration.html#并行预处�器) 与 [串行预处�器(token问题最佳解决方案)](/v1/configuration.html#串行预处�器(token问题最佳解决方案))。 diff --git a/okhttps-doc/docs/v1/updown.md b/okhttps-doc/docs/v1/updown.md new file mode 100644 index 00000000..73486bac --- /dev/null +++ b/okhttps-doc/docs/v1/updown.md @@ -0,0 +1,223 @@ +--- +description: OkHttps 文件 上传 下载 进度监� 过程控制 断点续传 分�下载 +--- + +# 上传下载 + +## 文件下载 + +  OkHttps 并没有把文件的下载排除在常规的请求之外,�一套API,它优雅的设计使得下载与常规请求��的毫无�和感,一个最简�的示例: + +```java +http.sync("/download/test.zip") + .get() // 使用 GET 方法(其它方法也�以,看�务器支�) + .getBody() // 得到报文体 + .toFile("D:/download/test.zip") // 下载到指定的路径 + .start(); // �动下载 + +http.sync("/download/test.zip").get().getBody() + .toFolder("D:/download") // 下载到指定的目录,文件�将根�下载信�自动生� + .start(); +``` +  或使用异步连接方�: + +```java +http.async("/download/test.zip") + .setOnResponse((HttpResult result) -> { + result.getBody().toFolder("D:/download").start(); + }) + .get(); +``` +  这里�说明一下:`sync`与`async`的区别在于连接�务器并得到�应这个过程的�步与异步(这个过程的耗时在大文件下载中�比��),而`start`方法�动的下载过程则都是异步的。 + +### 下载进度监� + +  就直接上代�啦,诸�一看便懂: + +```java +http.sync("/download/test.zip") + .get() + .getBody() + .setStepBytes(1024) // 设置�接收 1024 个字节执行一次进度回调(�设置默认为 8192) + // .setStepRate(0.01) // 设置�接收 1% 执行一次进度回调(�设置以 StepBytes 为准) + .setOnProcess((Process process) -> { // 下载进度回调 + long doneBytes = process.getDoneBytes(); // 已下载字节数 + long totalBytes = process.getTotalBytes(); // 总共的字节数 + double rate = process.getRate(); // 已下载的比例 + boolean isDone = process.isDone(); // 是�下载完� + }) + .toFolder("D:/download/") // 指定下载的目录,文件�将根�下载信�自动生� + // .toFile("D:/download/test.zip") // 指定下载的路径,若文件已存在则覆盖 + .setOnSuccess((File file) -> { // 下载�功回调 + + }) + .start(); +``` +  值得一�的是:由于 OkHttps 并没有把下载�的很特别,这里设置的进度回调��对下载文件起用作,�使对�应JSON的常规请求,��设置了进度回调,它也会告诉你报文接收的进度(��是�务器�应的报文有`Content-Length`头),例如: + +```java +List users = http.sync("/users") + .get() + .getBody() + .setStepBytes(2) + .setOnProcess((Process process) -> { + System.out.println(process.getRate()); + }) + .toList(User.class); +``` + +### 下载过程控制 + +  过于简�:还是直接上代�: + +```java +Ctrl ctrl = http.sync("/download/test.zip") + .get() + .getBody() + .setOnProcess((Process process) -> { + System.out.println(process.getRate()); + }) + .toFolder("D:/download/") + .start(); // 该方法返回一个下载过程控制器 + +ctrl.status(); // 下载状� +ctrl.pause(); // 暂�下载 +ctrl.resume(); // ��下载 +ctrl.cancel(); // �消下载(�时会删除文件,����) +``` +  无论是�步还是异步�起的下载请求,都�以�以上的控制: + +```java +http.async("/download/test.zip") + .setOnResponse((HttpResult result) -> { + // 拿到下载控制器 + Ctrl ctrl = result.getBody().toFolder("D:/download/").start(); + }) + .get(); +``` + +### 实现断点续传 + +  OkHttps 对断点续传并没有��更高层次的�装,因为这是app该去�的事情,它在设计上使��网络问题的处��简�的�时力求纯粹。下�的例��以看到,OkHttps 通过一个失败回调拿到 **断点**,便将��的问题�得简�: + +```java +http.sync("/download/test.zip") + .get() + .getBody() + .toFolder("D:/download/") + .setOnFailure((Failure failure) -> { // 下载失败回调,以便接收诸如网络错误等失败信� + IOException e = failure.getException(); // 具体的异常信� + long doneBytes = failure.getDoneBytes(); // 已下载的字节数(断点),需��存,用于断点续传 + File file = failure.getFile(); // 下载生�的文件,需��存 ,用于断点续传(��存路径也�以) + }) + .start(); +``` +  下�代�实现续传: + +```java +long doneBytes = ... // 拿到�存的断点 +File file = ... // 待续传的文件 + +http.sync("/download/test.zip") + .setRange(doneBytes) // 设置断点(已下载的字节数) + .get() + .getBody() + .toFile(file) // 下载到�一个文件里 + .setAppended() // 开�文件追加模� + .setOnSuccess((File file) -> { + + }) + .setOnFailure((Failure failure) -> { + + }) + .start(); +``` + +### 实现分�下载 + +  当文件很大时,有时候我们会考虑分�下载,与断点续传的�路是一样的,示例代�: + +```java +static String url = "http://api.demo.com/download/test.zip" + +public static void main(String[] args) { + long totalSize = HttpUtils.sync(url).get().getBody() + .close() // 因为这次请求�是为了获得文件大�,�消费报文体,所以直接关闭 + .getContentLength(); // 获得待下载文件的大�(由于未消费报文体,所以该请求�会消耗下载报文体的时间和网络��) + download(totalSize, 0); // 从第 0 �开始下载 + sleep(50000); // 等待下载完�(�然本例的主线程就结�啦) +} + +static void download(long totalSize, int index) { + long size = 3 * 1024 * 1024; // ��下载 3M + long start = index * size; + long end = Math.min(start + size, totalSize); + HttpUtils.sync(url) + .setRange(start, end) // 设置本次下载的范围 + .get().getBody() + .toFile("D:/download/test.zip") // 下载到�一个文件里 + .setAppended() // 开�文件追加模� + .setOnSuccess((File file) -> { + if (end < totalSize) { // 若未下载完,则继续下载下一� + download(totalSize, index + 1); + } else { + System.out.println("下载完�"); + } + }) + .start(); +} +``` + +## 文件上传 + +  一个简�文件上传的示例: + +```java +http.sync("/upload") + .addFileParam("test", "D:/download/test.zip") + .post(); // 上传�法一般使用 POST 或 PUT,看�务器支� +``` +  异步上传也是完全一样: + +```java +http.async("/upload") + .addFileParam("test", "D:/download/test.zip") + .post(); +``` + +### 上传进度监� + +  OkHttps 的上传进度监�,监�的是所有请求报文体的��进度,示例代�: + +```java +http.sync("/upload") + .addBodyParam("name", "Jack") + .addBodyParam("age", 20) + .addFileParam("avatar", "D:/image/avatar.jpg") + .setStepBytes(1024) // 设置��� 1024 个字节执行一次进度回调(�设置默认为 8192) + // .setStepRate(0.01) // 设置��� 1% 执行一次进度回调(�设置以 StepBytes 为准) + .setOnProcess((Process process) -> { // 上传进度回调 + long doneBytes = process.getDoneBytes(); // 已��字节数 + long totalBytes = process.getTotalBytes(); // 总共的字节数 + double rate = process.getRate(); // 已��的比例 + boolean isDone = process.isDone(); // 是���完� + }) + .post(); +``` +  咦�怎么感觉和下载的进度回调的一样?没错�OkHttps 还是使用�一套API处�上传和下载的进度回调,区别�在于上传是在`get/post`方法之�使用这些API,下载是在`getBody`方法之�使用。很好�解:`get/post`之�是准备��请求时段,有上传的�义,而`getBody`之�,已是报文�应的时段,当然是下载。 + +### 上传过程控制 + +  上传文件的过程控制就很简�,和常规请求一样,�有异步�起的上传�以�消: + +```java +HttpCall call = http.async("/upload") + .addFileParam("test", "D:/download/test.zip") + .setOnProcess((Process process) -> { + System.out.println(process.getRate()); + }) + .post(); + +call.cancel(); // �消上传 +``` +  上传就没有暂�和继续这个功能啦,应该没人有这个需求�? \ No newline at end of file diff --git a/okhttps-doc/docs/v2/README.md b/okhttps-doc/docs/v2/README.md new file mode 100644 index 00000000..ddbc1d71 --- /dev/null +++ b/okhttps-doc/docs/v2/README.md @@ -0,0 +1,84 @@ +--- +description: OkHttps 安装 构建实例 HTTP build �步请求 异步请求 sync async BaseUrl request webSocket gradle maven ejlchina +--- + +# 介� + +## OkHttps 简介 + +  OkHttps 是近期开�的对 OkHttp3 轻��装的框架,它独创的异步预处�器,特色的标签,�活的上传下载进度监�与过程控制功能,在轻�解决很多原本�人头疼问题的�时,设计上也力求纯粹与优雅。 + + * 链�调用,一点到底 + * BaseURL�URL��符�HTTP�WebSocket + * JSON�Xml 等自动�装与解�,且支�与任�格�的数�解�框架集� + * �步拦截器�异步预处�器�回调执行器�全局监��回调阻断 + * 文件上传下载(过程控制�进度监�) + * �方法回调,充分利用 Lambda 表达� + * TCP连接池�Http2 + +::: tip +* OkHttps �常轻�(77Kb,Retrofit:124Kb),除 Okhttp 无第三方�赖,并且更加的开箱�用,API 也更加自然和语义化。 +* OkHttps 是一个纯粹的 Java 网络开�包,并��赖 Android,这一点和 Retrofit �� +* OkHttps 用起�很优美,�以� RxJava 那样链�用,�比 RxJava 更简�。 +::: + +### 项目组� + +项目 | 最新版本 | �述 +-|-|- +okhttps | 2.3.0 | OkHttps 核心模� +[okhttps-fastjson](https://gitee.com/ejlchina-zhxu/okhttps/tree/master/okhttps-fastjson) | 2.3.0 | 与 fastjson 集� +[okhttps-gson](https://gitee.com/ejlchina-zhxu/okhttps/tree/master/okhttps-gson) | 2.3.0 | 与 gson 集� +[okhttps-jackson](https://gitee.com/ejlchina-zhxu/okhttps/tree/master/okhttps-jackson) | 2.3.0 | 与 jackson 集� +[okhttps-stomp](https://gitee.com/ejlchina-zhxu/okhttps-stomp) | 1.0.1 | 超级轻�的 Stomp 客户端 + + +## v2.3 的新特性 + +�新实现 WebSocket 心跳机制 +使用者�以选择使用 OkHttp 自带的新桃模�,也�以选择使用 OkHttps �供的增强型新增机制,它具有如下特性 + +1. 客户端��的任何消�都具有一次客户端心跳作用 +2. �务器��的任何消�都具有一次�务器心跳作用 +3. 若�务器超过 3 * pongSeconds 秒没有回�心跳,�判断心跳超时 + +## v2.2 的新特性 + +1. 增强泛型��列化,支���泛型 +2. HttpCall 接�新增 getTask 方法,�获�当�任务 +3. OkHttps 工具类 新增 newBuilder 方法,�用于�制 SPI 方�注入的�置信� +4. HttpTask 类 新增 isAsyncHttp 和 isSyncHttp 方法(v2.1.0 新增了 isWebsocket 方法)�用于判断 HttpTask 的任务类型 +5. 优化在 Android 端的异步请求性能 + +## v2.1 的新特性 + +* 对异步请求的�应�供了 6 �便�回调方法,在�关心具体状�时(与全局�应监�组�使用),使用�常方便; + - `setOnResBody` 在回调里直接�得`Body`对象,无需�使用`res.getBody()` + - `setOnResBean` 在回调里直接�得 Java Bean 对象,无需�使用`res.getBody().toBean(Class)` + - `setOnResList` 在回调里直接�得 Java List 列表,无需�使用`res.getBody().toList(Class)` + - `setOnResMapper` 在回调里直接�得 Mapper 对象,无需�使用`res.getBody().toMapper()` + - `setOnResArray` 在回调里直接�得 Array 对象,无需�使用`res.getBody().toArray()` + - `setOnResString` 在回调里直接�得 String 对象,无需�使用`res.getBody().toString()` + +* 优化性能:使用`HTTP#newBuilder()`方法克隆`HTTP`实例时,新实例与旧实例之间资�共享最大化。 + + +## v2.0 的新特性 + +* HTTP 任务新增`patch()`和`head()`方法,��起 PATCH 和 HEAD 请求,目�直接支�的 HTTP 方法有:GET�POST�PUT�PATCH�DELETE,并且暴露了`request(String method)`方法,�自定义�起任何请求,如:HEAD�OPTIONS�TRACE�CONNECT 等; + +* HTTP 任务新增`skipPreproc()`和`skipSerialPreproc()`方法,具体请求�跳过 所有 或�跳过 串行 预处�器; + +* 新增`MsgConvertor`接�,实现 OkHttps 与 fastjson 解耦,且���赖�个具体 json 框架�甚至��赖 json,它�以与 **任何格�** 的数�解�框架集�,如:json �xml �protobuf 等; + +* 构建`HTTP`实例时支�注入多个`MsgConvertor`,�实现�一个`HTTP`实例下,既有 json 解�,�有 xml 解�等强大特性,�时还�以让表�(form)请求�数 � json�xml 一样,支��列化功能。 + +* `HTTP`接�新增`webSocket(String url)`方法,与`sync(String url)`和`async(String url)`一样,支� Lamda 编程�预处�器机制�消�数�自动�列化和��列化机制; + +* 新增`OkHttps`工具类,支� SPI 方�注入�置,`OkHttps`和`HttpUtils`默认自动以 SPI 方�寻找�赖中的`MsgConvertor`; + +* 新增�自定义默认编�(�自定义�然默认为 utf-8)�具体请求�指定特殊编�功能。 + +
+ + diff --git a/okhttps-doc/docs/v2/android.md b/okhttps-doc/docs/v2/android.md new file mode 100644 index 00000000..41bb8d88 --- /dev/null +++ b/okhttps-doc/docs/v2/android.md @@ -0,0 +1,720 @@ +--- +description: OkHttps 安� 生命周期 UI线程 主线程 IO线程 自由 �活 切� 生命周期绑定 �置预处�器 定义生命周期监视器 �置全局回调监� +--- + +# 安� + +## 回调线程切� + +  在 Android 开�中,�常会把�些代�放到特点的线程去执行,比如网络请求�应�的页�更新在主线程(UI线程)执行,而�存文件则在IO线程�作。OkHttps 为这类问题�供了良好的方案。 + +  在 **默认** 情况下,**所有回调** 函数都会 **在 IO 线程** 执行。为什么会设计如此呢?这是因为 OkHttps �是纯粹的 Java 领域 Http工具包,本身对 Android �会有任何�赖,因此也�知 Android 的 UI 线程为何物。这么设计也让它在 Android 之外有更多的�能性。 + +### �置 + +  但是在 Android 里使用 OkHttps 的�,UI线程的问题能�优雅的解决呢?当然�以�简�粗暴的方法就是�置一个 回调执行器: + +```java +HTTP http = HTTP.builder() + .callbackExecutor((Runnable run) -> { + // 实际编�中�以� Handler �出�,�需��次执行回调都�新创建 + new Handler(Looper.getMainLooper()).post(run); // 在主线程执行 + }) + .build(); +``` + +::: warning 注� +在 Android 7+(SDK 24 以上)中使用 v2.0.0 �以�版本,当在主线程里消费报文体时(调用`Body#toXxx()`方法),会引�`android.os.NetworkOnMainThreadException`异常。�以通过添加一个拦截器�解决: + +```java +HTTP http = HTTP.builder() + .config( builder -> builder.addInterceptor(chain -> { + Response res = chain.proceed(chain.request()); + ResponseBody body = res.body(); + ResponseBody newBody = null; + if (body != null) { + newBody = ResponseBody.create(body.contentType(), body.bytes()); + } + return res.newBuilder().body(newBody).build(); + })) + // �略其它... + .build(); +``` +::: + +### 用例 + +  上述代�便实现了让 **所有** 的 **回调函数** 都在 **主线程(UI线程)** 执行的目的,如: + +```java +http.async("/users") + .addBodyPara("name", "Jack") + .setOnProcess((Process process) -> { + // 在主线程执行 + }) + .setOnResponse((HttpResult result) -> { + // 在主线程执行 + }) + .setOnException((Exception e) -> { + // 在主线程执行 + }) + .setOnComplete((State state) -> { + // 在主线程执行 + }) + .post(); +``` +  但是,如果�时还想让�些回调放在IO线程,实现 **自由切�**,怎么办呢?OkHttps 给出了�常�活的方法,如下: + +```java +http.async("/users") + .addBodyPara("name", "Jack") + .setOnProcess((Process process) -> { + // 在主线程执行 + }) + .nextOnIO() // 指定下一个回调在 IO 线程执行 + .setOnResponse((HttpResult result) -> { + // 在 IO 线程执行 + }) + .setOnException((Exception e) -> { + // 在主线程执行(没有指明 nextOnIO 则在回调执行器里执行) + }) + .nextOnIO() // 指定下一个回调在 IO 线程执行 + .setOnComplete((State state) -> { + // 在 IO 线程执行 + }) + .post(); +``` +  无论是哪一个回调,都�以使用`nextOnIO()`方法自由切�。�样,对于文件下载也是一样: + +```java +http.sync("/download/test.zip") + .get() + .getBody() + .setOnProcess((Process process) -> { + // 在主线程执行 + }) + .toFolder("D:/download/") + .nextOnIO() // 指定下一个回调在 IO 线程执行 + .setOnSuccess((File file) -> { + // 在 IO 线程执行 + }) + .setOnFailure((Failure failure) -> { + // 在主线程执行 + }) + .start(); +``` + +## 生命周期绑定 + +  由于 OkHttps 并��赖于 Android,所以它并没有�供关于生命周期绑定的直接实现,但它的一些扩展机制让我们很容易就�以实现这个需求。在开始之�,我们首先��解何为生命周期绑定: + +::: tip HTTP 请求的生命周期绑定 +所谓的生命周期绑定:�是让 HTTP 任务感知其所属的 Activity 或 Fragment 的生命周期,当 Activity 或 Fragment 将被销�时,框架应自动的把由它们�起的但尚未完�的 HTTP 任务全部�消,以�导致程�出错� +::: + +  现在我们需�对`HTTP`实例进行�置,�置�的`HTTP`实例具有生命周期绑定的功能,在`Activity`或`Fragment`里,它的使用效果如下: + +```java +// 在 Activity 或 Fragment 内�起请求 +http.async("http://www.baidu.com") + .bind(getLifecycle()) // 绑定生命周期 + .setOnResponse((HttpResult result) -> { + Log.i("FirstFragment", "收到请求:" + result.toString()); + }) + .get(); +``` +  上述代�中的`getLifecycle()`是`Activity`或`Fragment`自带的方法,而`bind()`是`HttpTask`的现有方法。在�置好`HTTP`实例�,上述代��起的请求便�以感知`Activity`或`Fragment`的生命周期。 + +  那`HTTP`实例到底该如何�置呢? + +### 第一步:�置预处�器 + +```java +HTTP http = HTTP.builder() + ... // �略其它�置项 + .addPreprocessor((Preprocessor.PreChain chain) -> { + HttpTask task = chain.getTask(); + Object bound = task.getBound(); + // 判断 task 是�绑定了 Lifecycle 对象 + if (bound instanceof Lifecycle) { + // �新绑定一个 生命周期监视器(LCObserver)对象,它的定义�下一步 + task.bind(new LCObserver(task, (Lifecycle) bound)); + } + chain.proceed(); + }) + ... // �略其它�置项 + .build(); +``` + +### 第二步:定义生命周期监视器 + +```java +public class LCObserver implements LifecycleObserver { + + HttpTask task; + Lifecycle lifecycle; + + LCObserver(HttpTask task, Lifecycle lifecycle) { + this.task = task; + this.lifecycle = lifecycle; + lifecycle.addObserver(this); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + public void onStop() { + task.cancel(); // 在 ON_STOP 事件中,�消对应的 HTTP 任务 + } + + public void unbind() { + // 在请求完�之�移除监视器 + lifecycle.removeObserver(this); + } + +} +``` + +### 第三步:�置全局回调监� + +  以上两步其实已�实现了生命周期绑定的功能,但是在请求完�之�,我们需�在`lifecycle`中移除`LCObserver`对象: + +```java +HTTP http = HTTP.builder() + ... // �略其它�置项 + .completeListener((HttpTask task, HttpResult.State state) -> { + Object bound = task.getBound(); + // 判断 task 是�绑定了生命周期监视器(LCObserver)对象 + if (bound instanceof LCObserver) { + // 解绑监视器 + ((LCObserver) bound).unbind(); + } + return true; + }) + ... // �略其它�置项 + .build(); +``` + +### 总结 + +上述实现生命周期绑定的过程,关键在于对`HttpTask`的`bind(Object object)`方法传入一个`LifeCycle`对象,然�在预处�器和全局监�里�了相关处�。实际上,我们还�以在调用`bind`方法时传入一个`Activity`或`Fragment`对象,这样在预处�器和全局监�里,就�仅�以得到`LifeCycle`,还�以得到`Context`对象,��考下文的 [安�-最佳实践](/v2/android.html#最佳实践)。 + +::: tip �示 +以上三步实现的生命周期绑定,�仅对 HTTP 请求有作用,对 WebSocket 连接也�样有效哦。 +::: + + +## 自动加载框 + +在调用一个耗时较长的接�时,我们往往需�显示一个加载框,以便让用户知�我们的 APP 正在工作而�是�死。 + +一般的�法,是这样实现: + +```java +showLoading(); // 在请求开始之�显示加载框 + +http.async('/api/...') + ... + .setOnComplete(state -> { + // 在请求结�(�功|失败|异常)之�关闭加载框 + hideLoading(); + }) + .post(); +``` + +这样当然�以解决问题,但是一个应用开�下�至少涉�到有三四�个接�请求,多的甚至到�百上�个,如果�个加载框的逻辑都这么写,会造�很多冗余代�,很�优雅。 + +那���以,在请求接�时,我们�给一个标签,如果有这个标签,请求时就自动显示加载框,结��就自动关闭加载框呢?�这样: + +```java +http.async('/api/...') + .tag("LOADING") // 自动显示和关闭加载框 + ... + .post(); +``` + +当然�以�OkHttps 扩展机制的强大,一��心�体现了。�,我们直接看实现代�� + +### 第一步:�置预处�器 + +预处�器是在网络请求开始之�执行,我们�以�这里显示加载框: + +```java +HTTP http = HTTP.builder() + ... // �略其它�置项 + .addPreprocessor(chain -> { + HttpTask task = chain.getTask(); + // 根�标签判断是�显示加载框 + if (task.isTagged("LOADING")) { + showLoading(context(task)); // 显示加载框 + } + chain.proceed(); + }) + ... // �略其它�置项 + .build(); +``` + +其中第9行使用了一个`context(HttpTask task)`方法,它的作用是从`HttpTask`的绑定对象中�得一个`Context`对象,用于创建加载框。 + +### 第二步:�置全局监� + +```java +HTTP http = HTTP.builder() + ... // �略其它�置项 + .completeListener((HttpTask task, HttpResult.State state) -> { + if (task.isTagged("LOADING")) { + hideLoading(); // 关闭加载框 + } + return true; + }); + ... // �略其它�置项 + .build(); +``` + +### 总结 + +以上两步�实现了网络请求时的自动显示与关闭加载框的功能,是�是很简�呢,全部代���考下文的 [安�-最佳实践](/v2/android.html#最佳实践)。 + +## 最佳实践 + +本章节,将以��的方�为你呈现使用 OkHttps 的正确姿势,包括: + +* 默认回调线程 +* 生命周期绑定 +* 加载框自动显示关闭 +* TOKEN 自动添加与刷新 +* 错误�统一处�与回调阻断 + +### 标签常� + +由于或多或少需�用到一些标签,我们最好把它定义�常�: + +```java +public class Tags { + + /** + * 用于标记�接�需� Token 头信� + * 如果没有办法得到�法的 Token,则跳转登录 + */ + public static final String TOKEN = "TOKEN"; + + /** + * 用于标记�接�是�开�自动加载框功能 + */ + public static final String LOADING = "LOADING"; + + // 若有其它的想法,还�以定义其它的标签 + // ... +} +``` + +### 接�路径常� + +由于�一个接��能会在多处使用,所以我们�以把它们定义为常�(这一步�选): + +```java +public class Urls { + + /** + * BaseUrl 还�以根� build.gradle 的�置�� + * 打出��环境的包,自动使用��的 BaseUrl,这里便��示例 + */ + public static final String BASE_URL = "https://api.example.com"; + + /** + * 当 Token 快过期时,调用该接��刷新 Token + */ + public static final String TOKEN_REFRESH = "/oauth/access-token"; + + // 其它接� ... +} +``` + +### �置 OkHttps + +由于我们推�在主应用中直接使用`OkHttps`类,所以我们在`OkHttpsConfig`中�置,��考 [起步-�置 OkHttps](/v2/getstart.html#�置okhttps) 章节。 + +#### 主干�置 + +```java +public class OkHttpsConfig implements Config { + + // 绑定到主线程的 Handler + private Handler mainHandler = new Handler(Looper.getMainLooper()); + + @Override + public void with(HTTP.Builder builder) { + + builder.baseUrl(Urls.BASE_URL) // �置 BaseURL + + // 如果默认请求体是JSON,则开�,�则默认为表� + //.bodyType("json") + + // �置默认回调在主线程执行 + .callbackExecutor(run -> mainHandler.post(run)) + + // 加载框预处�(在 TOKEN 处�之�) + .addPreprocessor(chain -> { + HttpTask task = chain.getTask(); + // 根�标签判断是�显示加载框 + if (task.isTagged(Tags.LOADING)) { + showLoading(context(task)); + } + chain.proceed(); + }) + + // 实现生命周期绑定 + .addPreprocessor(chain -> { + HttpTask task = chain.getTask(); + Object bound = task.getBound(); + task.bind(new BoundWrapper(task, bound)); + chain.proceed(); + }) + + // TOKEN 处�(串行预处�) + .addSerialPreprocessor(chain -> { + HttpTask task = chain.getTask(); + // 根�标签判断是�需� Token + if (!task.isTagged(Tags.TOKEN)) { + chain.proceed(); + return; + } + Context ctx = context(task); + requestTokenAndRefreshIfExpired(ctx, chain.getHttp(), + (String token) -> { + if (token != null) { + // 添加 Token 头信�,�字需�和�端商定 + task.addHeader("Access-Token", token); + } else if (ctx != null) { + // 若没有得到 Token, 则跳转登录页� + ctx.startActivity(new Intent(ctx, LoginActivity.class)); + } else { + Log.e("OkHttps", "没有 Context 无法跳转登录页��"); + } + // 无论如何,这行代�一定�执行到,�然�续接�会一直在排队中 + chain.proceed(); + }); + }) + + // 错误�统一处� + .responseListener((HttpTask task, HttpResult result) -> { + // 刷新 Token 的接��以例外 + if (task.getUrl().contains(Urls.TOKEN_REFRESH) + || result.isSuccessful()) { + // 这里演示的是 HTTP 状��的处�,如果有自定义的 code, 也�以进行深层次的判断 + return true; // 继续接�的业务处� + } + // �用户展示接�的错误信� + showMsgToUser(task, result.getBody().toString()); + return false; // 阻断 + }) + + // 生命周期绑定:第三步 + .completeListener((HttpTask task, HttpResult.State state) -> { + Object bound = task.getBound(); + if (bound instanceof BoundWrapper) { + ((BoundWrapper) bound).unbind(); + } + // 网络错误统一处� + switch (state) { + case TIMEOUT: + showMsgToUser(task, "网络连接超时"); + break; + case NETWORK_ERROR: + showMsgToUser(task, "网络错误,请检查WIFI或数�是�开�"); + break; + case EXCEPTION: + showMsgToUser(task, "接�请求异常: " + task.getUrl()); + break; + } + if (task.isTagged(Tags.LOADING)) { + hideLoading(); // 关闭加载框 + } + return true; + }); + } + + // 其它 ... +} +``` + +上述是一个主干�置,其中涉�到`showLoading`方法�`hideLoading`方法�`BoundWrapper`类�`context`方法�`showMsgToUser`方法 和 `requestTokenAndRefreshIfExpired`方法,下��次给出它们的实现: + +#### `showLoading`和`hideLoading`方法 + +``` java +public class OkHttpsConfig implements Config { + + // �略其它... + + private ProgressDialog loading = null; + private AtomicInteger loadings = new AtomicInteger(0); + + // 显示加载框 + private void showLoading(Context ctx) { + if (loading == null) { + // 这里就用 ProgressDialog �演示了,当然�以替��你喜爱的加载框 + loading = new ProgressDialog(ctx); + loading.setMessage("正在加载,请�等..."); + } + // 增加加载框显示计数 + loadings.incrementAndGet(); + loading.show(); + } + + // 关闭加载框 + private void hideLoading() { + // 判断是�所有显示加载框的接�都已完� + if (loadings.decrementAndGet() <= 0 + && loading != null) { + loading.dismiss(); + loading = null; + } + } + + // 其它 ... +} +``` + +#### `BoundWrapper`类 + +它�以�独�为一个类,也�以写�内部类: + +```java +public class OkHttpsConfig implements Config { + + // �略其它... + + static class BoundWrapper implements LifecycleObserver { + + HttpTask task; + Lifecycle lifecycle; + Object bound; + + BoundWrapper(HttpTask task, Object bound) { + this.task = task; + if (bound instanceof LifecycleOwner) { + lifecycle = ((LifecycleOwner) bound).getLifecycle(); + lifecycle.addObserver(this); + } + this.bound = bound; + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + public void onStop() { + task.cancel(); // 在 ON_STOP 事件中,�消对应的 HTTP 任务 + } + + void unbind() { + if (lifecycle != null) { + lifecycle.removeObserver(this); + } + } + + } + + // 其它 ... +} +``` + +#### `context`方法 + +```java +public class OkHttpsConfig implements Config { + + // �略其它... + + /** + * 获� Context 对象 + **/ + private Context context(HttpTask task) { + Object bound = task.getBound(); + if (bound instanceof BoundWrapper) { + bound = ((BoundWrapper) bound).bound; + } + if (bound instanceof Context) { + return (Context) bound; + } + if (bound instanceof Fragment) { + return ((Fragment) bound).getActivity(); + } + // 还�以添加更多获� Context 的策略,比如从全局 Application 里� + return null; + } + + // 其它 ... +} +``` + +#### `showMsgToUser`方法 + +```java +public class OkHttpsConfig implements Config { + + // �略其它... + + /** + * �用户展示信� + **/ + private void showMsgToUser(HttpTask task, String message) { + Context ctx = context(task); + if (ctx != null) { + // 这里就简�用 Toast 示例一下,有更高级的实现�以替� + Toast.makeText(ctx, message, Toast.LENGTH_LONG).show(); + } + } + + // 其它 ... +} +``` + +#### `requestTokenAndRefreshIfExpired`方法 + +```java +public class OkHttpsConfig implements Config { + + // �略其它... + + /** + * 获�TOKEN,若过期则刷新(代�中的字符串�以替�为常�) + **/ + private void requestTokenAndRefreshIfExpired(Context ctx, HTTP http, + OnCallback callback) { + if (ctx == null) { + callback.on(null); + return; + } + // 这里演示使用 Preference 存储,也�以使用数�库存储 + SharedPreferences token = ctx.getSharedPreferences("token", + Context.MODE_PRIVATE); + long now = System.currentTimeMillis(); + // 刷新令牌 + String refreshToken = token.getString("refreshToken", null); + // 判断有效期�以�� 60 秒,以防在接下�的网络延迟中过期了 + if (token.getLong("refreshTokenExpiresAt", 0) < now + 60000 + || refreshToken == null) { + // 刷新令牌已过期,说明长时间未使用,需��新登录 + callback.on(null); + return; + } + // 访问令牌 + String accessToken = token.getString("accessToken", null); + if (token.getLong("accessTokenExpiresAt", 0) > now + 60000 + && accessToken != null) { + // 访问令牌未过期,则回调令牌 + callback.on(accessToken); + return; + } + // 访问令牌已过期,刷新令牌未过期,则调接�刷新当�令牌 + http.async(Urls.TOKEN_REFRESH) + .skipPreproc() // 跳过所有预处�器 + .addBodyPara("refreshToken", refreshToken) + .setOnResponse(res -> { + if (!res.isSuccessful()) { + callback.on(null); + return; + } + Mapper mapper = res.getBody().toMapper(); + String newRefreshToken = mapper.getString("refreshToken"); + String newAccessToken = mapper.getString("accessToken"); + int refreshTokenExpiresIn = mapper.getInt("refreshTokenExpiresIn"); + int accessTokenExpiresIn = mapper.getInt("accessTokenExpiresIn"); + // 因为�生了请求,当�时间已��化,所有�新获�时间 + long now2 = System.currentTimeMillis(); + // �存到 SharedPreferences + token.edit() + .putString("refreshToken", newRefreshToken) + .putString("accessToken", newAccessToken) + .putLong("refreshTokenExpiresAt", refreshTokenExpiresIn * 1000 + now2) + .putLong("accessTokenExpiresAt", accessTokenExpiresIn * 1000 + now2) + .commit(); + // 回调令牌 + callback.on(newAccessToken); + }) + .setOnException(e -> callback.on(null)) + .post(); + } + +} +``` + +### 使用效果 + +#### 在 Activity 或 Fragment 中使用 + +```java +OkHttps.async(Urls.SOME_URL) + .bind(this) // 绑定(生命周期|Context获�) + .tag(Tags.TOKEN) // 自动添加 TOEKN + .tag(Tags.LOADING) // 自动显示加载框 + // 请求�数设置... + .setOnResponse(res -> { + // TODO: 正确�应处� + }) + .post(); +``` + +::: tip +上述中的`this`得指� Activity 或 Fragment 本身,如果是在按钮的点击事件中请求,需�指明为`YourActivity.this`或者`YourFragment.this` +::: + +#### 在其它能获得 Context 对象的环境里使用 + +```java +OkHttps.async(Urls.SOME_URL) + .bind(context) // 绑定(生命周期|Context获�) + .tag(Tags.TOKEN) // 自动添加 TOEKN + .tag(Tags.LOADING) // 自动显示加载框 + // 请求�数设置... + .setOnResponse(res -> { + // TODO: 正确�应处� + }) + .post(); +``` + +### 类 Retrofit 方� + +如果你是 Retrofit 用户,喜欢� Retrofit 那样把网络接�分类归到��的Java文件中,完全没有问题,比如你�以这样: + +#### 定义 UserService + +```java +public class UserService { + + private Context context; + + public UserService(Context context) { + this.context = context; + } + + /** + * 获�我的信� + **/ + public AsyncHttpTask updatePasswrod(String passwrod) { + return OkHttps.async("/mine/passwrod") + .bind(context) + .tag(Tags.TOKEN) // 自动添加 TOEKN + .tag(Tags.LOADING) // 自动显示加载框 + .addBodyPara("passwrod", passwrod); + } + + // 其它接�... +} +``` + +#### 使用 UserService + +```java +UserService userService = new UserService(this) + +String newPassword = "123456"; + +userService.updatePasswrod(newPassword) + .setOnResponse(res -> { + // 密�更新�功 + }) + .put(); + +``` + +
+ + \ No newline at end of file diff --git a/okhttps-doc/docs/v2/configuration.md b/okhttps-doc/docs/v2/configuration.md new file mode 100644 index 00000000..70320e33 --- /dev/null +++ b/okhttps-doc/docs/v2/configuration.md @@ -0,0 +1,414 @@ +--- +description: OkHttps �置 BaseUrl 回调执行器 主线程 UI线程 OkHttpClient 预处�器 TOKEN问题最佳解决方案 刷新TOKEN 全局 回调监� 下载监� 回调阻断 拦截器 CookieJar�SSL�缓存�代��事件监� +--- + +# �置 + +## 设置 BaseUrl + +```java +HTTP http = HTTP.builder() + .baseUrl("http://api.demo.com") // 设置 BaseUrl + .build(); +``` +  该�置全局生效,在�置了`BaseUrl`之�,具体的请求便�以�略`BaseUrl`部分,使得代�更加简�,例如: + +```java +http.sync("/users").get() // http://api.demo.com/users + +http.sync("/auth/signin") // http://api.demo.com/auth/signin + .addBodyPara("username", "Jackson") + .addBodyPara("password", "xxxxxx") + .post(); // POST请求 +``` +  在�置了`BaseUrl`之�,如有特殊请求任务,�然�以使用全路径的方�,一点都�妨�: + +```java +http.sync("https://www.baidu.com").get(); +``` + +::: tip 支�动� BaseUrl �? +有些�学�能在网上看到�些网络�装框架声明支� **动� BaseUrl** 或者说 **动�域�**,那 OkHttps 支��? + +答:OkHttps 是 **�会** �供在�一个`HTTP`实例上�以动�设置`BaseUrl`的功能的。 + +为什么呢? + +答:因为这完全是一�缺陷设计,我们都知�在 Java 中多线程开�很常�,如果 A 线程正兴高采烈的准备��个地��起请求时,而 B 线程�然修改了`BaseUrl`,那 A 线程�就悲剧了么。所以类似 Retrofit 这些设计�熟的框架都�会�供这�功能的。 + +那我的应用中确实�用到多个域�,该怎么办呢? + +答:�以构建多个`HTTP`实例,��的实例负责��的域�,如果�个域�的请求都比较少,就是域�多,那便�用一个`HTTP`实例,请求较少的域�使用全路径访问��。 +::: + +## 回调执行器 + +  OkHttps 默认所有回调都在 **IO线程** 执行,如何想改�执行回调的线程时,�以�置回调执行器。例如在Android里,让所有的回调函数都在 UI 线程执行,则�以在构建`HTTP`时�置如下: + +```java +HTTP http = HTTP.builder() + .callbackExecutor((Runnable run) -> { + runOnUiThread(run); // 在UI线程执行 + }) + .build(); +``` +  该�置默认 [**影�所有回调**](/v2/foundation.html#回调函数),� [**全局监�**](/v2/configuration.html#全局监�) ��此影�,更多实现细节��考 [安�-回调线程切�](/v2/android.html#回调线程切�) 章节。 + +::: warning 注� +在 Android 7+(SDK 24 以上)中使用 v2.0.0 �以�版本,当在主线程里消费报文体时(调用`Body#toXxx()`方法),会引�`android.os.NetworkOnMainThreadException`异常。�以通过添加一个拦截器�解决: + +```java +HTTP http = HTTP.builder() + .config( builder -> builder.addInterceptor(chain -> { + Response res = chain.proceed(chain.request()); + ResponseBody body = res.body(); + ResponseBody newBody = null; + if (body != null) { + newBody = ResponseBody.create(body.contentType(), body.bytes()); + } + return res.newBuilder().body(newBody).build(); + })) + // �略其它... + .build(); +``` +::: + +## 预处�器 + +  预处�器(`Preprocessor`)�以让我们在请求�出之�对请求本身�一些改�,但与`OkHttp`的拦截器(`Interceptor`)��:预处�器�以让我们 **异步** 处�这些问题。 + +### 并行预处�器 + +  例如,当我们想为请求任务自动添加`Token`头信�,而`Token`�能通过异步方法`requestToken`获�时,这时使用`Interceptor`就很难处�了,但�以使用预处�器轻�解决: + +```java +HTTP http = HTTP.builder() + .addPreprocessor((PreChain chain) -> { + HttpTask task = chain.getTask();// 获得当�的HTTP任务 + if (!task.isTagged("Auth")) { // 根�标签判断该任务是�需�Token + chain.proceed(); + return; + } + requestToken((String token) -> { // 异步获� Token + task.addHeader("Token", token);// 为任务添加头信� + chain.proceed(); // 继续当�的任务 + }); + }) + .build(); +``` + +和`Interceptor`一样,`Preprocessor`也�以添加多个。他们之间的区别如下: + +::: tip 拦截器与预处�器的区别 +* 拦截器�能处��步�作,预处�器支�处�异步�作 +* 拦截器都是并行处�请求,预处�器支�串行处�(详�6.5章节) +* 拦截器处�时机在请求�和�应�,预处�器�在请求�,并且先于拦截器执行。关于�应�,OkHttps还�供了全局回调监�(详�6.6章节) +::: + +并行预处�器还�以实现更多功能,比如 [安�-生命周期绑定](/v2/android.html#生命周期绑定)�[安�-自动加载框](/v2/android.html#自动加载框) 等。 + +### 串行预处�器(TOKEN问题最佳解决方案) + +  普通预处�器都是�并行处�的,然而有时我们希望�个预处�器�时�处�一个任务。比如 当`Token`过期时我们需�去刷新获�新`Token`,而刷新`Token`这个�作�能有一个任务去执行,因为如果`n`个任务�时执行的�,那么必有`n-1`个任务刚刷新得到的`Token`�能就立马失效了,而这是我们所�希望的。 + +  为了解决这个问题,OkHttps �供了串行预处�器,它�以让 HTTP 任务排好队,一个一个地进入预处�器: + +```java +HTTP http = HTTP.builder() + .addSerialPreprocessor((PreChain chain) -> { + HttpTask task = chain.getTask(); + if (!task.isTagged("Auth")) { + chain.proceed(); + return; + } + // 检查过期,若需�则刷新Token + requestTokenAndRefreshIfExpired((String token) -> { + task.addHeader("Token", token); + chain.proceed(); // 调用此方法�,�会有其它任务进入该处�器 + }); + }) + .build(); +``` + +串行预处�器实现了让HTTP任务排队串行处�的功能,但值得一�的是:它并没有因此而阻塞任何线程� + +::: warning 注� +由于 串行预处�器 �能一个一个的处�任务,所以在上述中的`requestTokenAndRefreshIfExpired`方法里,若�起网络请求,一定�跳过串行预处�器 [�使用`skipSerialPreproc()`方法],例如: + +```java +http.async("/oauth/refresh-token") + .skipSerialPreproc() // 跳过串行预处�器 + .addBodyPara("refreshToken", "xxxxxx") + .setOnResponse((HttpResult result) -> { + + }) + .post(); +``` +�则就会�生两个 HTTP 任务相互等待�也执行�了的问题。 + +如果你使用的是 v1.x 的版本,则�以使用`HTTP`实例的`request(Request request)`方法�起原生请求,这样也��过任何预处�器。 +::: + +关于 TOKEN 的更多处�细节,请�考 [安�-最佳实践](/v2/android.html#最佳实践) 章节。 + +## 消�转�器 + +OkHttps 自 2.0 版本器支�自定义消�转�器,并且�以添加多个,例如: + +```java +HTTP http = HTTP.builder() + .addMsgConvertor(new MyJsonMsgConvertor()); + .addMsgConvertor(new MyXmlMsgConvertor()); + .build(); +``` + +�置了消�转�器�,`HTTP`实例便具有了�列化和��列化这些格�数�的能力。 + +### ��列化 + +例如下例中,无论该接��应的是 JSON 还是 XML,都�以��列化�功: + +```java +// 无论该接��应的是 JSON 还是 XML,都�以��列化�功 +List orders = http.sync('/orders') + .get().getBody().toList(Order.class); +``` + +在 Websocket 通讯中,也是如此: + +```java +http.webSocket("/redpacket/status") // 监�红包的领�状� + .setOnMessage((WebSocket ws,Message msg) -> { + // 无论该接��应的是 JSON 还是 XML,都�以��列化�功 + Status status = msg.toBean(Status.class); + }) + .listen(); +``` + +### 正�列化 + +�外,如果�在请求�出阶段正��列化�数,则需�指定`bodyType`�数告诉 OkHttps 你想使用哪一个消�转�器,如: + +```java +Order order = newOrder(); +http.async('/orders') // �交订� + .bodyType("json") + .setBodyPara(order) // 以 JSON 格��列化 Order 对象 + .post(); +``` + +或者 XML: + +```java +Order order = newOrder(); +http.async('/orders') // �交订� + .bodyType("xml") + .setBodyPara(order) // 以 XML 格��列化 Order 对象 + .post(); +``` + +在 Websocket 里,也是�样的方法: + +```java +http.webSocket("/chat") + .bodyType("json") + .setOnOpen((WebSocket ws,HttpResult res) -> { + Hello hello = getHello(); + ws.send(hello); // 以 JSON 格��列化 Hello 对象 + }) + .listen(); +``` + +但如果你在 Websocket 通讯到�一个阶段�,�然想��外一�格����数�了,你还�以这样: + +```java +http.webSocket("/chat") + .bodyType("json") + .setOnOpen((WebSocket ws,HttpResult res) -> { + Hello hello = getHello(); + ws.send(hello); // 以 JSON 格��列化 Hello 对象 + + ws.msgType("xml") // 切�为 XML 格� + ws.send(hello); // 以 XML 格��列化 Hello 对象 + }) + .listen(); +``` + +### 默认�列化类型 + +然而大多数情况下,我们都使用一�消�转�器,比如 json,这时候你�以为`bodyType`�置一个默认值: + +```java +HTTP http = HTTP.builder() + // 修改 bodyType 的默认值为 json,若�修改,则默认是 form + .bodyType("json"); + .addMsgConvertor(new MyJsonMsgConvertor()); + .build(); +``` +::: tip �示 +�有请求报文体的正��列化�会收到`bodyType`的影�,对于�应报文体的���列化��其影�。 +::: + +### 表��列化 + +OkHttps �但能� JSON 或 XML 的�列化,并且还能�表��数的�列化,并且自带了一个`FormConvertor`,它�以给任�`MsgConvertor`赋予表��列化的能力,例如你已�实现了一个`MyJsonMsgConvertor`,�以这么�: + +```java +// 这里并��求是一个 JSON 转�器,XML 也�以 +// 有啥就给啥,效果是一样的 +MsgConvertor convertor = new MyJsonMsgConvertor(); + +HTTP http = HTTP.builder() + .addMsgConvertor(new MsgConvertor.FormConvertor(convertor)); + .build(); +``` + +这样,你��表�请求时,就�以直接扔进一个对象,它将自动完��列化的事情: + +```java +Order order = newOrder(); +http.async('/orders') // �交订� + .bodyType("form") + .setBodyPara(order) // 以 表� 格��列化 Order 对象 + .post(); +``` + +一般情况下,如果你有实现了一个`MsgConvertor`,我们推�把它和`FormConvertor`都添加进去,例如: + +```java +MsgConvertor convertor = new MyJsonMsgConvertor(); + +HTTP http = HTTP.builder() + .addMsgConvertor(convertor); + .addMsgConvertor(new MsgConvertor.FormConvertor(convertor)); + .build(); +``` + +如果你添加了官方�供的`MsgConvertor`�赖包(例如:`okhttps-jackson`等),在构建实例时,还�以这样直接注入: + +```java +HTTP.Builder builder = HTTP.builder() +// 自动完��赖中的 MsgConvertor 和 FormConvertor 的注入 +ConvertProvider.inject(builder); +HTTP http = builder.build(); +``` + +::: tip +如果你使用的是[`OkHttps`或`HttpUtils`工具类](/v2/getstart.html#工具类),它们都会自动�置`MsgConvertor`和`FormConvertor`,无需手动�置 + +在 v2.0.0.RC 版本中`FormConvertor`的�字是`MsgConvertor.FormMsgConvertor` +::: + +## 全局监� + +### 全局回调监� + +  全局回调是实际开�中�常需�的功能,比如对�务器�应的状��进行统一处�等,�时 OkHttps 的全局回调还具有 **回调阻断** 的功能: + +```java +HTTP http = HTTP.builder() + .responseListener((HttpTask task, HttpResult result) -> { + // 所有请求�应�都会走这里 + + // 返回 true 表示继续执行 task 的 OnResponse 回调, + // 返回 false 则表示��执行,� 阻断 + return true; + }) + .completeListener((HttpTask task, State state) -> { + // 所有请求执行完都会走这里 + + // 返回 true 表示继续执行 task 的 OnComplete 回调, + // 返回 false 则表示��执行,� 阻断 + return true; + }) + .exceptionListener((HttpTask task, IOException error) -> { + // 所有请求�生异常都会走这里 + + // 返回 true 表示继续执行 task 的 OnException 回调, + // 返回 false 则表示��执行,� 阻断 + return true; + }) + .build(); +``` + +**回调阻断** 其实是日常开�中比较常�的需求,比如: + +�有当接��应的状��在 [200, 300) 之间时,应用��具体的业务处�,其它则一律�用户�示接�返回的错误信�,则�以这么�: + +```java +HTTP http = HTTP.builder() + .responseListener((HttpTask task, HttpResult result) -> { + if (result.isSuccessful()) { + return true; // 继续接�的业务处� + } + showApiMsgToUser(result); // �用户展示接�的错误信� + return false; // 阻断 + }) + .build(); +``` + +然�具体的请求,就�以专注于业务处�,而�需担心接�的状�出错,比如: + +```java +Order order = newOrder(); // 订�信� + +http.async('/orders') // �交订� + .setBodyPara(order) + .setOnResponse((HttpResult result) -> { + // 进入该回调,则表示订�已�交�功,就�以放心的�接下�的业务处� + // 比如去�起支付: + startPay(result.getBody().toBean(PayInfo.class)); + }) + .post(); +``` + +::: tip 全局回调监�与拦截器的异�: +* 拦截器�以添加多个,全局回调监�分三�,��最多添加一个 +* 拦截器处的�时机在请求�和�应�,全局回调监��在�应�,并且晚于拦截器 +* 全局回调监��以 **阻断**(return false)�个请求的具体回调,而拦截器�能 +::: + +::: warning +如果你开�的是安�应用,我们强烈建议你添加 [全局异常监�](/v2/configuration.html#全局回调监�),这样当你在�个请求中忘记使用`OnException`或`nothrow`,而它��生了超时或网络异常时,�至于让程�崩溃。 +::: + +### 全局下载监� + +```java +HTTP http = HTTP.builder() + .downloadListener((HttpTask task, Download download) -> { + // 所有下载在开始之�都会先走这里 + Ctrl ctrl = download.getCtrl(); // 下载控制器 + + }) + .build(); +``` + +## �置 OkHttpClient + +  与其他�装 OkHttp3 的框架��,OkHttps 并�会�蔽 OkHttp3 本身就很好用的功能,如下: + +```java +HTTP http = HTTP.builder() + .config((OkHttpClient.Builder builder) -> { + // �置连接池 最�10个连接(��置默认为 5) + builder.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)); + // �置连接超时时间(默认10秒) + builder.connectTimeout(20, TimeUnit.SECONDS); + // �置 WebSocket 心跳间隔(默认没有心跳) + builder.pingInterval(60, TimeUnit.SECONDS); + // �置拦截器 + builder.addInterceptor((Chain chain) -> { + Request request = chain.request(); + // 必须�步返回,拦截器内无法执行异步�作 + return chain.proceed(request); + }); + // 其它�置: CookieJar�SSL�缓存�代��事件监�... + // 所有 OkHttp 能�置的,都�以在这里�置 + }) + .build(); +``` + +
+ + \ No newline at end of file diff --git a/okhttps-doc/docs/v2/discuss.md b/okhttps-doc/docs/v2/discuss.md new file mode 100644 index 00000000..e69de29b diff --git a/okhttps-doc/docs/v2/foundation.md b/okhttps-doc/docs/v2/foundation.md new file mode 100644 index 00000000..5e7ce347 --- /dev/null +++ b/okhttps-doc/docs/v2/foundation.md @@ -0,0 +1,847 @@ +--- +description: OkHttps 请求方法 回调函数 HttpResult cache 多次 消费报文体 HttpCall 构建HTTP任务 请求�数 JSON 使用标签 �起请求 �消请求 +--- + +# 基础 + +## 请求方法 + +OkHttps 使用`sync(String url)`和`async(String url)`方法�起 �步和异步的 HTTP 请求,它们支�的方法有: + +HTTP 请求方法 | 实现方法 | Restful 释义 | 起始版本 +-|-|-|- +GET | `get()` | 获�资� | 1.0.0 +HEAD | `head()` | 获�资�头信� | 2.0.0 +POST | `post()` | �交资� | 1.0.0 +PUT | `put()` | 更新资� | 1.0.0 +PATCH | `patch()` | 部分更新资� | 2.0.0.RC +DELETE | `delete()` | 删除资� | 1.0.0 +其它 | `request(String method)` | 任何 HTTP 请求方法 | 2.0.0.RC + +例如,修改 ID 为 100 的书的标题,�以这样请求: + +```java +http.async("/books/100") + .addBodyPara("title", "新标题") + .patch(); // �起 PATCH 请求 +``` + +�如,删除 ID 为 100 的书,�以�起这样的请求: + +```java +http.async("/books/100").delete(); // �起 DELETE 请求 +``` + +�如,使用 HEAD 请求获��务器上文件的大�: + +```java +HttpResult res = http.sync("/download/file.zip").head(); +// // HttpResult#getContentLength() 新增于 2.0.0 版本 +System.out.println("size = " + res.getContentLength()); +``` + +你还�以自定义一些请求方法,例如: + +```java +http.async("/somethings") + .setOnResponse((HttpResult result) -> { + System.out.println(result); + }) + .request("OPTIONS"); // �起 OPTIONS 请求 +``` + +�外,�步请求的所有这些方法都会返回一个[`HttpResult`](#httpresult),而异步请求则会返回一个[`HttpCall`](#httpcall): + +```java +HttpResult res1 = http.sync("/books").get(); // �步 GET +HttpResult res2 = http.sync("/books").post(); // �步 POST + +HttpCall call1 = http.async("/books").get(); // 异步 GET +HttpCall call2 = http.async("/books").post(); // 异步 POST +``` + +## 回调函数 + +OkHttps 的回调函数全部使用�方法模�,这样�以充分利用 Java8 或 Kotlin 中的 Lambda 表达�,使你的代�更加简�优雅: + +### 普通回调 + +```java +http.async("/users") // http://api.demo.com/users + .setOnResponse((HttpResult res) -> { + // �应回调 + int status = res.getStatus(); // 状�� + Headers headers = res.getHeaders(); // �应头 + Body body = res.getBody(); // 报文体 + }) + .setOnException((IOException e) -> { + // 异常回调 + }) + .setOnComplete((State state) -> { + // 完�回调,无论�功失败都会执行,并且在 �应|异常回调 之�执行 + // �以根� state 枚举判断执行状�: + // State.CANCELED` 请求被�消 + // State.RESPONSED` 已收到�应 + // State.TIMEOUT` 请求超时 + // State.NETWORK_ERROR` 网络错误 + // State.EXCEPTION` 其它请求异常 + }) + .get(); +``` + +OkHttps �时还支� **全局回调** 和 **回调阻断** 机制,详� [全局回调监�](/v2/configuration.html#全局回调监�)。 + +::: tip +* �有异步请求��以设置这三�(�应|异常|完�)回调 +* �步请求直接返回结果,无需使用回调 +::: + +### 便�回调 + +OkHttps 自 v2.1.0 起,对异步请求�供了 **6** �便�回调方法,它们都是`OnResponse`回调的简化版,当在具体请求中�关心状��和�应头信�时,使用起��常方便: + +```java +http.async("/users") // http://api.demo.com/users + .setOnResBody(Body body -> { + // 得到�应报文体 Body 对象 + }) + .setOnResMapper(Mapper mapper -> { + // 得到�应报文体��列化�的 Mapper 对象 + }) + .setOnResArray(Array array -> { + // 得到�应报文体��列化�的 Array 对象 + }) + .setOnResBean(Bean.class, User bean -> { + // 得到�应报文体根� Bean.class ��列化�的 Java Bean 对象 + }) + .setOnResBean(new TypeRef>(){}, Result result -> { + // 得到�应报文体根� TypeRef ��列化�的 ��泛型 对象 + }) + .setOnResList(Bean.class, List list -> { + // 得到�应报文体根� Bean.class ��列化�的 Java Bean 列表 + }) + .setOnResString(String str -> { + // 得到�应报文体的字符串 String 对象 + }) + .get(); +``` + +以上的便�回调里,无法�`OnResponse`那样获得�应的状��和头信�,所以最好和 [全局回调监�](/v2/configuration.html#全局回调监�) ��使用。在全局回调里对错误�先�统一处�。 + + +### 进度回调 + +上传进度回调: + +```java +http.sync("/upload") + .addFilePara("avatar", "D:/image/avatar.jpg") + .setOnProcess((Process process) -> { + long doneBytes = process.getDoneBytes(); // 已上传字节数 + long totalBytes = process.getTotalBytes(); // 总共的字节数 + double rate = process.getRate(); // 已上传的比例 + boolean isDone = process.isDone(); // 是�上传完� + }) + .post(); +``` + +关于上传的更多细节请�考 [文件上传](/v2/updown.html#文件上传) 章节。 + +下载进度回调: + +```java +http.sync("/download/test.zip").get().getBody() + .setOnProcess((Process process) -> { + long doneBytes = process.getDoneBytes(); // 已下载字节数 + long totalBytes = process.getTotalBytes(); // 总共的字节数 + double rate = process.getRate(); // 已下载的比例 + boolean isDone = process.isDone(); // 是�下载完� + }) + .toFolder("D:/download/") // 指定下载的目录,文件�将根�下载信�自动生� + .start(); +``` + +#### 还有下载�功或失败的回调: + +```java +http.sync("/download/test.zip").get().getBody() + .toFile("D:/download/test.zip") // 下载到文件 + .setOnSuccess((File file) -> { + // 下载�功 + }) + .setOnFailure((Failure failure) -> { + // 下载失败 + }) + .start(); +``` + +关于下载的更多内容,请�考 [文件下载](/v2/updown.html#文件下载) 章节。 + +### WebSocket 回调 + +```java +http.webSocket("/websocket-endpoint") + .setOnOpen((WebSocket ws, HttpResult res) -> { + // WebSocket 连接建立回调 + }) + .setOnMessage((WebSocket ws, Message msg) -> { + // �务器下�消�回调 + }) + .setOnException((WebSocket ws, Throwable thr) -> { + // 连接�生异常回调 + }) + .setOnClosing((WebSocket ws, WebSocket.Close close) -> { + // 连接正在关闭回调 + }) + .setOnClosed((WebSocket ws, WebSocket.Close close) -> { + // 连接已关闭(v2.0.0 之�包�连接被�消 和 连接�生异常) + boolean isCanceled = close.isCanceled(); // from v2.0.0 + boolean isException = close.isException(); // from v2.0.0 + int code = close.getCode(); + String reason = close.getReason(); + }) + .listen(); +``` + +::: tip +OkHttps 的所有回调函数都默认在 IO 线程执行,若�切�默认线程(例如 Android 中切�到 UI 线程)请�� [回调线程切�](/v2/android.html#回调线程切�)。 +::: + +## HttpResult + +  `HttpResult`是HTTP请求执行完�的结果,它是�步请求方法`get()`�`post()`等的返回值,也是异步请求�应回调`OnResponse`的�数,它定义了如下方法: + +* `getState()` 得到请求执行状�枚举,它有以下�值: + * `State.CANCELED` 请求被�消 + * `State.RESPONSED` 已收到�应 + * `State.TIMEOUT` 请求超时 + * `State.NETWORK_ERROR` 网络错误 + * `State.EXCEPTION` 其它请求异常 +* `getStatus()` 得到 HTTP 状�� +* `isSuccessful()` 是��应�功,状��在 [200..300) 之间 +* `getHeaders()` 得到HTTP�应头 +* `getHeaders(String name)` 得到HTTP�应头 +* `getHeader(String name)` 得到HTTP�应头 +* `getContentLength();` 解��应头中的 Content-Length 长度信�(v2.0.0 新增) +* `getBody()` 得到�应报文体`Body`实例,它定义了如下方法(对�一个`Body`实例,以下的`toXXX()`类方法�能使用一个且仅能调用一次,除�先使用 cache 方法): + * `toBytes()` 报文体转�为字节数组 + * `toByteStream()` 报文体转�为字节输入� + * `toCharStream()` 报文体转�为字符输入� + * `toString()` 报文体转�为字符串文本 + * `toBean(Class type)` 报文体根�`type`自动��列化为 JavaBean(�赖`MsgConvertor`) + * `toBean(Type type)` 报文体根�`type`自动��列化为 JavaBean(�赖`MsgConvertor`)(支���泛型) + * `toBean(TypeRef type)` 报文体根�`type`自动��列化为 JavaBean(�赖`MsgConvertor`)(支���泛型) + * `toList(Class type)` 报文体根�`type`自动��列化为 JavaBean 列表(�赖`MsgConvertor`) + * `toMapper()` 报文体自动��列化为映射结构的对象(�赖`MsgConvertor`,v2.0.0.RC 之�是`toJsonObject()`) + * `toArray()` 报文体自动��列化为数组结构的对象(�赖`MsgConvertor`,v2.0.0.RC 之�是`toJsonArray()`) + * `toFile(String filePath)` 下载到指定路径 + * `toFile(File file)` 下载到指定文件 + * `toFolder(String dirPath)` 下载到指定目录 + * `toFolder(File dir)` 下载到指定目录 + * `getType()` 返回报文体的媒体类型(v2.0.0 新增,��推�`getContentType()`方法) + * `getLength()` 返回报文体的字节长度(v2.0.0 新增,��推�`getContentLength()`方法) + * `cache()` 缓存报文体,开�缓存����使用`toXXX()`类方法 + * `close()` 关闭报文体,未对报文体�任何消费时使用,比如�读�报文头 +* `getError()` 执行中�生的异常,自动�获执行请求时�生的 网络超时�网络错误 和 其它IO异常 +* `close()` 关闭报文,未对报文体�任何消费时使用,比如�读�长度 + +示例,请求结果自动��列化为 Bean 和 List: + +```java +// 自动转Bean +Order order = http.sync("/orders/1").get() + .getBody().toBean(Order.class); + +// 自动转List +List orders = http.sync("/orders") + .get().getBody().toList(Order.class); +``` + +上例中,为了获�订�列表,必须先定义一个`Order`类,如果你�想定义它,�以直接使用`toMapper()`和`toArray()`方法: + +```java +Mapper order = http.sync("/orders/1") + .get().getBody().toMapper(); + +long orderId = order.getLong("id"); // 订�ID +long orderNo = order.getString("orderNo"); // 订�� + +Array orders = http.sync("/orders") + .get().getBody().toArray(); + +int size = orders.size(); // 订�个数 +Mapper o1 = orders.getMapper(0) // 第1个订� +``` + +关于 Mapper 和 Array 的更多信�,请�考 [Mapper 接�](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/Mapper.java) 和 [Array 接�](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/Array.java)。 + +示例,使用 `cache()` 方法: + +```java +Body body = http.sync("/orders").get().getBody().cache(); + +// 使用 cache �,�以多次使用 toXXX() 方法 +System.out.println(body.toString()); +System.out.println(body.toArray()); +System.out.println(body.toList(Order.class)); +``` + +示例,获�下载文件的大�: + +```java +long size = http.sync("/download/test.zip").get().getBody() + .close() // �是想获得文件大�,�消费报文体,所以直接关闭 + .getLength(); // 获得待下载文件的大� +// 由于未消费报文体,所以本次请求�会消耗下载报文体的时间和网络�� +System.out.println("size = " + size); +``` + +上述代�等�于: + +```java +long size = http.sync("/download/test.zip").get().close().getContentLength(); +System.out.println("size = " + size); +``` + +还等效于: + +```java +long size = http.sync("/download/test.zip").head().getContentLength(); +// 因为 HEAD 请求没有�应报文体,所以就�需�关闭啦 +System.out.println("size = " + size); +``` + +## HttpCall + +[`HttpCall`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/HttpCall.java)是异步请求方法`get()`�`post()`等的返回值,它是一个接�,继承自[`Cancelable`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/Cancelable.java),与`java`的`Future`接�很�,它有如下方法: + +* `cancel()` �消本次请求,返回�消结果 +* `isCanceled()` 返回请求是�被�消 +* `isDone()` 返回是�执行完�,包��消和失败 +* `getResult()` 返回执行结果`HttpResult`对象,若请求未执行完,则挂起当�线程直到执行完��返回 + +例如,�消一个异步请求: + +```java +HttpCall call = http.async("/users/1").get(); + +System.out.println(call.isCanceled()); // false + +boolean success = call.cancel(); // �消请求 + +System.out.println(success); // true +System.out.println(call.isCanceled()); // true +``` + +## 构建请求 + +  `HTTP`接�的`sync(String url)`�`async(String url)`和`webSocket(String url)`方法返回一个`HttpTask`对象,它实现了[`Cancelable`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/Cancelable.java)接�,�时也�供了�链�调用的`addXXX`与`setXXX`等系列方法用于构建请求报文。 + +### 请求头 + +* `addHeader(String name, String value)` 添加请求头 +* `addHeader(Map headers)` 添加请求头 + +### 路径�数 + +* `addPathPara(String name, Object value)` 添加路径�数:替� URL 里的`{name}`��符 +* `addPathPara(Map params)` 添加路径�数:替� URL 里的`{name}`��符 + +路径�数�以存在于 URL 中的任何�置,例如: + +```java +final String BOOKS_QUERY_URL = "/authors/{authorId}/books?type={type}"; + +http.async(BOOKS_QUERY_URL) // /authors/1/books?bookType=2 + .addPathPara("authorId", 1) + .addPathPara("type", 2) + .setOnResponse((HttpResult res) -> { + + }) + .get(); +``` + +### 查询�数 + +* `addUrlPara(String name, Object value)` 添加 URL �数:拼接在 URL 的`?`之�(查询�数) +* `addUrlPara(Map params)` 添加 URL �数:拼接在 URL 的`?`之�(查询�数) + +::: warning +方法`addXxxPara`在 v2.0.0.RC 之��为`addXxxParam`, v2.0.0.RC 之�推�使用简�版的方法,�方法将在 v2.1.0 中移除。 +::: + +### 报文体类型 + +* `bodyType(String type)` 结�报文体�数使用,用于设置本次的请求报文体类型,�以是`form`�`json`�`xml`�`protobuf`等,默认为`form`(表�类型),�以在构建`HTTP`实例时修改默认的类型 + +::: tip 解� +`bodyType`对应于`MsgConvertor`的`mediaType()`方法的返回值,OkHttps 会按照既定的规则 [`mediaType().contain(bodyType)`] 去匹�出对应的`MsgConvertor`去�列化报文体�数。 +::: + +### 报文体�数(表��JSON�XML等) + +OkHttps 信仰统一与一致更加优雅,所以自 v2.0.0.RC 开始�它便统一了报文体�数的 API,无论是表��还是 JSON�XML 或是 protobuf 等等,��这些�数是放在请求报文体中,那么就�以通过一套 API 轻��定� + +* `addBodyPara(String name, Object value)` 添加 Body �数 +* `addBodyPara(Map params)` 添加 Body �数 +* `setBodyPara(Object object)` 设置 Body �数体 + +::: warning 注� +添加报文体�数�,��使用 GET�HEAD 请求方法 + +方法`setBodyPara`在 v2.0.0.RC 之��为`setRequestJson`, v2.0.0.RC 之�推�使用统一 API,�方法将在 v2.1.0 中移除。 +::: + +#### 表�请求 + +默认的请求报文体类型就是`form`表�,所以�以直接使用`addBodyPara`方法: + +```java +http.async("/projects") + .addBodyPara("name", "OkHttps") + .addBodyPara("desc", "最好用的网络框架") + .post(); +``` + +或者是传入一个 Map 对象: + +```java +Map params = new HashMap<>(); +params.put("name", "OkHttps"); +params.put("desc", "最好用的网络框架"); + +http.async("/projects") + .addBodyPara(params) + .post(); +``` + +甚至�以用`setBodyPara`传入一个`String`: + +```java +http.async("/projects") + .setBodyPara("name=OkHttps&desc=最好用的网络框架") + .post(); +``` + +如果你�置了`FormConvertor`(请�考 [�置-表��列化](/v2/configuration.html#表��列化) 章节),那你还�以直接传入一个 POJO(自定义的一个 Java 类): + +```java +Proejct project = new Proejct(); +project.setName("OkHttps"); +project.setDesc("最好用的网络框架"); + +http.async("/projects") + .setBodyPara(project) // 将自动�列化为表�格� + .post(); +``` + +以上 **4** �方�具有相�的效果,但如果你修改了默认的报文体�列化类型(请�考 [默认�列化类型](/v2/configuration.html#默认�列化类型) 章节),那还需在请求时指定当�的请求报文体类型: + +```java +http.async("/projects") + .bodyType("form") // 指明请求体类型是表� + ... +``` + +或使用`OkHttps.FORM`常� + +```java +http.async("/projects") + .bodyType(OkHttps.FORM) // 指明请求体类型是表� + ... +``` + +#### JSON 请求 + +JSON 请求�求默认的请求`bodyType`为`json` 或者 在具体请求中显�指明`bodyType`为`json`,其它用法和表�请求一模一样。 + +�个添加 + +```java +http.async("/projects") + .addBodyPara("name", "OkHttps") + .addBodyPara("desc", "最好用的网络框架") + .post(); +``` + +Map 方�: + +```java +Map params = new HashMap<>(); +params.put("name", "OkHttps"); +params.put("desc", "最好用的网络框架"); + +http.async("/projects") + .addBodyPara(params) + .post(); +``` + +POJO 方�: + +```java +Proejct project = new Proejct(); +project.setName("OkHttps"); +project.setDesc("最好用的网络框架"); + +http.async("/projects") + .setBodyPara(project) // 自动�列化 + .post(); +``` + +字符串方�: + +```java +http.async("/projects") + .setBodyPara("{\"name\":\"OkHttps\",\"desc\":\"最好用的网络框架\"}") + .post(); +``` + +唯一的��是,如果默认的`bodyType`�是`json`,那需�显�指定当�请求的`bodyType`为`json`: + +```java +http.async("/projects") + .bodyType("json") // 指明请求体类型是 JSON + ... +``` + +或使用`OkHttps.JSON`常� + +```java +http.async("/projects") + .bodyType(OkHttps.JSON) // 指明请求体类型是 JSON + ... +``` + +#### XML 请求 + +若默认�是 XML,则显�指定当�请求的报文体类型 + +```java +http.async("/projects") + .bodyType("xml") // 指明请求体类型是 XML + ... +``` + +或使用`OkHttps.XML`常� + +```java +http.async("/projects") + .bodyType(OkHttps.XML) // 指明请求体类型是 XML + ... +``` + +其它与 表�和 JSON,完全一致,��赘述。 + +#### 其它数�格�的请求 + +�需�扩展了相应的`MsgConvertor`,OkHttps 支�任�数�格�的请求。 + +### 文件�数 + +文件�数是一个特殊的报文体�数,当使用以下方法上传文件时,OkHttps 会强制报文体使用`form`模�。 + +* `addFilePara(String name, String filePath)` 上传文件 +* `addFilePara(String name, File file)` 上传文件 +* `addFilePara(String name, String type, byte[] content)` 上传文件 +* `addFilePara(String name, String type, String fileName, byte[] content)` 上传文件 + +文件�数和报文体�数,还�以混用,例如: + +```java +http.sync("/upload") + .addBodyPara("title", "头�") + .addFilePara("image", "D:/image/avatar.jpg") + .post(); +``` + +::: warning +如果你使用的是 v2.0.0.RC 版本中,以上代�会报 **“方法 addFilePara �能使用 form 方�请求�** 错误,�以用以下方�解决该问题: +```java +http.async("/upload") + .bodyType("multipart/form") + .addBodyPara("title", "头�") + .addFilePara("test", "D:/image/avatar.jpg") + .post(); +``` +详� ISSUE: [https://gitee.com/ejlchina-zhxu/okhttps/issues/I1H8G9](https://gitee.com/ejlchina-zhxu/okhttps/issues/I1H8G9) +::: + +无论当�的默认`bodyType`是什么,和文件�数一起添加的`Body`�数都将以`form`(表�)模��交。 + +### 其它 + +* `tag(String tag)` 为 HTTP 任务添加标签 +* `setRange(long rangeStart)` 设置 Range 头信�,`addHeader`的便�方法,用于[断点续传](/v1/updown.html#实现断点续传) +* `setRange(long rangeStart, long rangeEnd)` 设置 Range 头信�,�用于[分�下载](/v1/updown.html#实现分�下载) +* `bind(Object object)` 绑定一个对象,�用于实现 Android 里的[生命周期绑定](/v1/android.html#生命周期绑定) +* `nothrow()` 一��生异常时的处�方�,请�考 [异常处�](/v2/features.html#异常处�) 章节 +* `skipPreproc()` 设置本次请求跳过所有预处�器,请�考 [预处�器](/v2/configuration.html#并行预处�器) 章节 +* `skipSerialPreproc()` 设置本次请求�跳过所有串行预处�器,请�考 [串行预处�器](/v2/configuration.html#串行预处�器(token问题最佳解决方案)) 章节 + +## 使用标签 + +有时候我们想对 HTTP 任务加以分类,这时候�以使用标签功能: + +```java +http.async("/users") //(1) + .tag("A") + .get(); + +http.async("/users") //(2) + .tag("A.B") + .get(); + +http.async("/users") //(3) + .tag("B") + .get(); + +http.async("/users") //(4) + .tag("B") + .tag("C") // 从 v1.0.4 标签将以追加模�添加,等效于 setTag("B.C") + .get(); + +http.async("/users") //(5) + .tag("C") + .get(); + +http.webSocket("/websocket") + .tag("B") // (6) 标签�样�以作用在 WebSocket 连接上 + .listen(); +``` + +### 预处�器中访问标签 + +在 [预处�器](/v2/configuration.html#并行预处�器) 中�以通过`chain`对象获得当�的请求任务`HttpTask`,然�使用`isTagged`或`getTag`方法获�标签信�: + +```java +HTTP http = HTTP.builder() + // 添加预处�器 + .addPreprocessor(chain -> { + + // 获得当�的HTTP任务 + HttpTask task = chain.getTask(); + // 判断该任务是�添加了 "A" 标签 + boolean tagged = task.isTagged("A"); + // 得到整个标签串 + String tag = task.getTag(); + + // ... + }) + // ... +``` + +在 [串行预处�器](/v2/configuration.html#串行预处�器(token问题最佳解决方案)) 中也是�样的方法访问标签。 + +### 全局监�中访问标签 + +在 [全局监�](/v2/configuration.html#全局监�) 中,访问标签就加简�一点,因为`HttpTask`正是形�之一,例如全局�应监�: + +```java +HTTP http = HTTP.builder() + // 全局�应监� + .responseListener((HttpTask task, HttpResult result) -> { + + // 判断该任务是�添加了 "A" 标签 + boolean tagged = task.isTagged("A"); + // 得到整个标签串 + String tag = task.getTag(); + + // ... + }) + // ... +``` + +其它类型的全局监�也是�样的方法访问标签。 + +### 拦截器中访问标签 + +自 v2.0.1 起,OkHttps 支�在拦截器内访问标签,�通过`Request`对象拿到整个标签串: + +```java +HTTP http = HTTP.builder() + // OkHttpClient 原生�置 + .config(b -> { + // 添加拦截器 + b.addInterceptor(chain -> { + + // 拿到 Request 对象 + Request request = chain.request(); + // 拿到整个标签串 + String tag = request.tag(String.class); + + // ... + }); + }) + // ... +``` + +拦截器的�置,��� [�置 OkHttpClient](/v2/configuration.html#�置-okhttpclient) 章节。 + +### 使用标签�消请求 + +当使用标签�,还�以按标签批�的对HTTP任务进行�消: + +```java +int count = http.cancel("B"); //(2)(3)(4)(6)被�消(�消标签包�"B"的任务) +System.out.println(count); // 输出 4 +``` + +�消请求任务,�是标签的一个附带功能。 + +标签 真正的强大之处在于:它�以和 [预处�器](/v2/configuration.html#并行预处�器) 和 [全局监�](/v2/configuration.html#全局监�) � 拦截器 ��使用,以此�扩展很多功能。��考 [串行预处�器(token问题最佳解决方案)](/v2/configuration.html#串行预处�器(token问题最佳解决方案))和 [安�-自动加载框](/v2/android.html#自动加载框) 等章节。 + +�外,请求任务的�消,还有更多的方�,��考 [�消请求](/v2/foundation.html#�消请求) 章节 + +## 异常处� + +  使用 OkHttps 时,**异常处��是必须的**,但相比其它的 HTTP 开�包,它还�供一个特别的处�方法:`nothrow()`,以满足��的异常处�需求。 + +### �步请求的异常 + +  默认情况下,当�步请求执行异常时,会直接�外抛出,我们�以用 `try catch` ��获,例如: + +```java +try { + HttpResult result = http.sync("/users/1").get(); +} catch (HttpException e) { + Throwable cause = e.getCause(); // 得到异常原因 + if (cause instanceof ConnectException) { + // 当没网络时,会抛出连接异常 + } + if (cause instanceof SocketTimeoutException) { + // 当接�长时间未�应,会抛出超时异常 + } + if (cause instanceof UnknownHostException) { + // 当把域�或IP写错,会抛出 UnknownHost 异常 + } + // ... +} +``` +  这�传统的异常处�方�,当然�以解决问题,但 OkHttps 有更佳的方案: + +```java +// 方法 nothrow() 让异常�直接抛出 +HttpResult result = http.sync("/users/1").nothrow().get(); +// 判断执行状� +switch (result.getState()) { + case RESPONSED: // 请求已正常�应 + break; + case CANCELED: // 请求已被�消 + break; + case NETWORK_ERROR: // 网络错误,说明用户没网了 + break; + case TIMEOUT: // 请求超时 + break; + case EXCEPTION: // 其它异常 + break; +} +// 还�以获得具体的异常信� +IOException error = result.getError(); +``` + +### 异步请求的异常 + +  异步请求最常用的异常处�方�就是设置一个异常回调: + +```java +http.async("/users/1") + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .setOnException((IOException e) -> { + // 这里处�请求异常 + }) + .get(); +``` +  当然,还有一个全局异常监�(`ExceptionListener`,请�考 [全局回调监�](/v2/configuration.html#全局回调监�) 章节): + +```java +HTTP http = HTTP.builder() + .exceptionListener((HttpTask task, IOException error) -> { + // 所有请求执行完都会走这里 + + // 返回 true 表示继续执行 task 的 OnComplete 回调, + // 返回 false 则表示��执行,� 阻断 + return true; + }) + .build(); +``` +  如果�设置`OnException`回调,也没有`ExceptionListener`,�生异常时会在 **IO 线程** 中�上抛出,外层无法�获: + +```java +try { + http.async("/users/1") + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .get(); +} catch (HttpException e) { + // 这�方�是�获�到异常的������ +} +``` +  �使没有`OnException`回调,�生异常时,�然会走`OnComplete`回调,如果设置了的�: + +```java +http.async("/users/1") + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .setOnComplete((State state) -> { + // �生异常,会先执行这里,�以根� state 判断�生了什么 + // 但执行完��然会在IO线程中�上抛出 + }) + .get(); +``` +  如果就是想 **�处�异常,也��上抛出**,�生错误完全无视,�以�到�?�以�还是使用`nothrow()`方法: + +```java +http.async("/users/1") + .nothrow() // 告诉 OkHttps �生异常时��外抛出 + .setOnResponse((HttpResult result) -> { + // 当�生异常时就�会走这里 + }) + .get(); +``` + +::: warning +如果你开�的是安�应用,我们强烈建议你添加 [全局异常监�](/v2/configuration.html#全局回调监�),这样当你在�个请求中忘记使用`OnException`或`nothrow`,而它��生了超时或网络异常时,�至于让程�崩溃。 +::: + + +## �消请求 + +  在 OkHttps 里�消请求共有 **4 �** 方��选: + +**1�** 使用`HttpCall#cancel()`�消�个请求(适用于异步请求,[详�`HttpCall`章节](/v2/foundation.html#httpcall)) + +**2�** 使用`HttpTask#cancel()`�消�个请求(适用于所有请求)(since v1.0.4) + +```java +HttpTask task = http.async("/users") + .setOnResponse((HttpResult result) -> { + // �应回调 + }); + +task.get(); // �起 GET 请求 + +// �消请求,并返回是��消�功 +boolean canceled = task.cancel(); +``` + +**3�** 使用`HTTP#cancel(String tag)`按标签批��消请求(适用于所有请求,[详� 标签 章节](/v2/foundation.html#使用标签)) + +**4�** 使用`HTTP#cancelAll()`�消所有请求(适用于所有请求)(since v1.0.2) + +```java +http.cancelAll(); // �消所有请求 +``` + +::: tip �示 +以上四�方�都对所有类型的请求有效,包括:�步 HTTP�异步 HTTP 和 WebSocket 连接。 +::: + +除了以上的 4 �方�,OkHttps 里还�以实现自动�消,请�考 [安�-生命周期绑定](/v2/android.html#生命周期绑定) 章节。 + +
+ + \ No newline at end of file diff --git a/okhttps-doc/docs/v2/getstart.md b/okhttps-doc/docs/v2/getstart.md new file mode 100644 index 00000000..58fc6efd --- /dev/null +++ b/okhttps-doc/docs/v2/getstart.md @@ -0,0 +1,339 @@ +--- +description: OkHttps 安装 构建实例 HTTP build �步请求 异步请求 sync async BaseUrl request webSocket gradle maven ejlchina +--- + +# 起步 + +## 安装 + +### Maven + +#### �独使用 OkHttps + +```xml + + com.ejlchina + okhttps + 2.3.0 + +``` + +�独使用 OkHttps 需�自定义[`MsgConvertor`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/MsgConvertor.java),�则无法使用 **自动正��列化** 相关功能,�文会详细讲解哪些功能会�到此影�。 + +#### 与 fastjson 一起使用 + +```xml + + com.ejlchina + okhttps-fastjson + 2.3.0 + +``` + +#### 与 gson 一起使用 + +```xml + + com.ejlchina + okhttps-gson + 2.3.0 + +``` + +#### 与 jackson 一起使用 + +```xml + + com.ejlchina + okhttps-jackson + 2.3.0 + +``` + +::: tip +以上�赖四选一�� +::: + +#### 使用 Stomp 客户端 + +```xml + + com.ejlchina + okhttps-stomp + 1.0.1 + +``` + +### Gradle + +#### �独使用 OkHttps + +```groovy +implementation 'com.ejlchina:okhttps:2.3.0' +``` + +�独使用 OkHttps 需�自定义[`MsgConvertor`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/MsgConvertor.java),�则无法使用 **自动正��列化** 相关功能,�文会详细讲解哪些功能会�到此影�。 + +#### 与 fastjson 一起使用 + +```groovy +implementation 'com.ejlchina:okhttps-fastjson:2.3.0' +``` + +#### 与 gson 一起使用 + +```groovy +implementation 'com.ejlchina:okhttps-gson:2.3.0' +``` + +#### 与 jackson 一起使用 + +```groovy +implementation 'com.ejlchina:okhttps-jackson:2.3.0' +``` + +::: tip +以上�赖四选一�� +::: + +#### 使用 Stomp 客户端 + +```groovy +implementation 'com.ejlchina:okhttps-stomp:1.0.1' +``` + +#### JDK 版本 + +安�中使用需�把 JDK 版本调� 1.8,在 app 模�的 build.gradle 中加入以下�置��: + +```groovy +android { + // �略其它�置.. + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} +``` + +## 使用 + +### 构建 HTTP + +```java +HTTP http = HTTP.builder().build(); +``` + +以上代�构建了一个最简�的`HTTP`实例,它拥有以下方法: + +* `sync(String url)` 开始一个�步 HTTP 请求 +* `async(String url)` 开始一个异步 HTTP 请求 +* `webSocket(String url)` 开始一个 WebSocket 连接 +* `cancel(String tag)` 按标签�消(�步 | 异步 | WebSocket)连接 +* `cancelAll()` �消所有(�步 | 异步 | WebSocket)连接 +* `request(Request request)` OkHttp 原生 HTTP 请求 +* `webSocket(Request request, WebSocketListener listener)` OkHttp 原生 WebSocket 连接 +* `newBuilder()` 用于�新构建一个 HTTP 实例 + +为了使用方便,在构建的时候,我们更愿�指定一个`BaseUrl`和[`MsgConvertor`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/MsgConvertor.java)(详� [设置 BaseUrl](/v1/configuration.html#设置-baseurl) 和 [消�转�器](/v2/configuration.html#消�转�器)): + +```java +HTTP http = HTTP.builder() + .baseUrl("http://api.example.com") + .addMsgConvertor(new GsonMsgConvertor()) + .build(); +``` + +上例中的[`GsonMsgConvertor`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps-gson/src/main/java/com/ejlchina/okhttps/GsonMsgConvertor.java)�自[`okhttps-gson`](https://gitee.com/ejlchina-zhxu/okhttps/tree/master/okhttps-gson)。为了简化文档,下文中出现的`http`�是在构建时设置了`BaseUrl`和`MsgConvertor`的`HTTP`实例。 + +### �步请求 + +  使用方法`sync(String url)`开始一个�步请求: + +```java +List users = http.sync("/users") // http://api.example.com/users + .get() // GET请求 + .getBody() // 获��应报文体 + .toList(User.class); // 得到目标数� +``` + +  方法`sync`返回一个�步`HttpTask`,�链�使用。 + +### 异步请求 + +  使用方法`async(String url)`开始一个异步请求: + +```java +http.async("/users/1") // http://api.example.com/users/1 + .setOnResponse((HttpResult res) -> { + // 得到目标数� + User user = res.getBody().toBean(User.class); + }) + .get(); // GET请求 +``` +  方法`async`返回一个异步`HttpTask`,�链�使用。 + +### WebSocket + +  使用方法`webSocket(String url)`开始一个 WebSocket 通讯: + +```java +http.webSocket("/chat") + .setOnOpen((WebSocket ws, HttpResult res) -> { + ws.send("��务器问好"); + }) + .setOnMessage((WebSocket ws,Message msg) -> { + // 从�务器接收消�(自动��列化) + Chat chat = msg.toBean(Chat.class); + // 相�的消���给�务器(自动�列化 Chat 对象) + ws.send(chat); + }) + .listen(); // �动监� +``` +  方法`webSocket`返回一个支� WebSocket 的`HttpTask`,也�链�使用。 + +### 请求三步曲 + +#### 第一步�确定请求方� + +�步 HTTP(`sync`)�异步 HTTP(`async`)或 WebSocket(`webSocket`) + +#### 第二步�构建请求任务 + +* `addXxxPara` - 添加请求�数 +* `setOnXxxx` - 设置回调函数 +* `tag` - 添加标签 +* ... + +#### 第三步�调用请求方法 + +HTTP 请求方法: + +* `get()` - GET 请求 +* `post()` - POST 请求 +* `put()` - PUT 请求 +* `delete()` - DELETE 请求 +* ... + +Websocket 方法: + +* `listen()` - �动监� + +#### 任�请求,都�循请求三部曲� + +### 工具类 + +OkHttps �供了两个开箱�用的工具类,让你从此告别�装工具类的烦�: + +工具类 | 起始版本 | 功能特点 +-|-|- +`OkHttps` | `2.0.0.RC` | 支�自动注入`MsgConvertor`,支� [SPI 方�注入�置](#�置okhttps),推�用于主应用中的网络开� +`HttpUtils` | `1.0.0` | 自`2.0.0.RC`开始支�自动注入`MsgConvertor`,�建议��其它�置,推�用于第三方�赖包中的网络开� + +这两个工具都类会自动以 SPI 方�注入�赖中的`MsgConvertor`(在第一次使用的时候),��你的项目中添加了相关�赖(如:okhttps-fastjson 等)。并且它们和`HTTP`接�拥有几乎一样的方法,并且这些方法都是��的: + +* `sync(String url)` 开始一个�步 HTTP 请求 +* `async(String url)` 开始一个异步 HTTP 请求 +* `webSocket(String url)` 开始一个 WebSocket 连接 +* `cancel(String tag)` 按标签�消(�步 | 异步 | WebSocket)连接 +* `cancelAll()` �消所有(�步 | 异步 | WebSocket)连接 +* `request(Request request)` OkHttp 原生 HTTP 请求 +* `webSocket(Request request, WebSocketListener listener)` OkHttp 原生 WebSocket 连接 + +例如,使用`OkHttps`,无需考虑构造`HTTP`实例,你�以这样�起一个请求: + +```java +OkHttps.async("https://api.example.com/auth/login") + .addBodyPara("username", "jack") + .addBodyPara("password", "xxxx") + .setOnResponse((HttpResult result) -> { + // 得到返回数�,使用 Mapper ��去定义一个实体类 + Mapper mapper = result.getBody().toMapper(); + // 登录是��功 + boolean success = mapper.getBool("success"); + }) + .post(); +``` + +### �置`OkHttps` + +工具类`OkHttps`还支�以 SPI 方�注入自定义�置,分以下两步: + +#### 第一步�新建一个�置类,实现[`com.ejlchina.okhttps.Config`](https://gitee.com/ejlchina-zhxu/okhttps/blob/master/okhttps/src/main/java/com/ejlchina/okhttps/Config.java)接� + +例如: + +```java +package com.example.okhttps; + +import com.ejlchina.okhttps.Config; +import com.ejlchina.okhttps.HTTP; + +public class OkHttpsConfig implements Config { + + @Override + public void with(HTTP.Builder builder) { + // 在这里对 HTTP.Builder �一些自定义的�置 + builder.baseUrl("https://api.domo.com"); + // 如果项目中添加了 okhttps-fastjson 或 okhttps-gson 或 okhttps-jackson �赖 + // OkHttps 会自动注入它们�供的 MsgConvertor + // 所以这里就�需���置 MsgConvertor 了 (内部实现自动注入的原�也是 SPI) + // 但如果没有添加这些�赖,那还需�自定义一个 MsgConvertor + builder.addMsgConvertor(new MyMsgConvertor()); + } + +} +``` + +以上�简�的演示,更多�置案例��考 [安�-最佳实践](/v2/android.html#最佳实践) 章节。 + +#### 第二步�在项目的`/src/main`目录下新建`resources/META-INF/services/com.ejlchina.okhttps.Config`文件,文件内容是上一步自定义的�置类的全� + +例如: + +![](/spi_config.png) + +以上两步就完�了对`OkHttps`工具类的自定义�置,接下�你便�以体验它神奇的能力了: + +```java +List users = OkHttps.sync("/users") // http://api.example.com/users + .get().getBody().toList(User.class); +``` + +#### 为什么�这样呢? + +有�学�能会疑问,为什么�这样�置呢,�能直接给`OkHttps`设置一个`HTTP`实例�,�这样: + +```java +HTTP http = HTTP.builder() + // 自定义�置... + .build(); + +OkHttps.set(http); +``` + +这样�在一般情况下确实�以,但是在�些 Java 虚拟机上(特别在 Android)中,有一个致命的缺陷:那就是系统在�些情况下会回收����,而`OkHttps`内部�有的`HTTP`实例一旦被回收�,上述方�自定义的�置就会丢失,这样在�续的逻辑处�中就�能会造�严�的问题� + +而 SPI 注入的方�则�以很好的解决这个问题:�使`OkHttps`内部的`HTTP`实例被回收,在下次使用时,`OkHttps`�会自动加载注入的�置�新构建一个`HTTP`实例,这样就��了用户自定义的�置�会丢失。 + +所以我们推�在 Android 应用开�中,直接使用`OkHttps`,自定义�置通过 SPI 方�注入。 + +而在开�具有网络请求需求的�赖项目时,直接使用`HttpUtils`并且��自定义的�置,如果一定�自定义�置,那就使用`HTTP.Builder`�新构建一个`HTTP`实例使用�。 + +### IDEA �技巧 + +由于 OkHttps �循请求三部曲,所以我们在 IDEA 中设置一个代�模�(Android Studio 一样设置) + +* �� File -> Settings 打开 Settings 设置框 +* 展开 Editor -> Live Templates + +![](/code_temp.png) + +如图设置好�,写代�时,输入`oks`,按回车键,就会自动生�模版,赶快�试一下�。 + + +**至此,你已轻�学会了 OkHttps 90% 的常规用法�但别急,��还有更加精彩的。** + +
+ + \ No newline at end of file diff --git a/okhttps-doc/docs/v2/stomp.md b/okhttps-doc/docs/v2/stomp.md new file mode 100644 index 00000000..932a7e1e --- /dev/null +++ b/okhttps-doc/docs/v2/stomp.md @@ -0,0 +1,144 @@ +--- +description: OkHttps WebSocket Stomp OkHttp +--- + +# Stomp + +项目 [OkHttps Stomp](https://gitee.com/ejlchina-zhxu/okhttps-stomp) 基于 OkHttps 的 [WebSocket](/v2/websocket.html) 功能实现了一个�常轻�的 Stomp 客户端�议,它�以让你轻�实现 **广播�布与订阅** 和 **队列�布与订阅** 的客户端功能。 + +## 添加�赖 + +�开� Stomp 功能,需�添加以下�赖: + +### Maven + +```xml + + com.ejlchina + okhttps-stomp + 1.0.1 + +``` + +### Gradle + +```groovy +implementation 'com.ejlchina:okhttps-stomp:1.0.1' +``` + +## 构建 Stomp 实例 + +类`Stomp`�供��方法`over`�构建`Stomp`实例: + +* `over(WebSocketTask task)` 基于 OkHttps 的 WebSocket 构建一个 Stomp 实例,并自动确认消� +* `over(WebSocketTask task, boolean autoAck)` 构建一个 Stomp 实例,并�指定是�自动确认消� + +例如: + +```java +// 使用一个 Websocket 连接构建一个 Stomp 实例,�时设置心跳间隔为 20 秒 +Stomp stomp = Stomp.over(OkHttps.webSocket("wss://...").heatbeat(20, 20)); +``` + +以上代�构建了一个简�的 Stomp 客户端,并默认在收到消�时会**自动确认**,如果需�收到确认,�以使用下�的方�: + +```java +Stomp stomp = Stomp.over( + OkHttps.webSocket("wss://...").heatbeat(20, 20), + false // �数设置 autoAck 为 false,将需�手动确认消� + ); +``` + +## 连接 Stomp �务 + +得到一个`Stomp`实例�,�使用以下两个方法连接 Stomp �务器: + +* `connect()` 直接连接 Stomp �务器 +* `connect(List
headers)` �带一些 Stomp 头信�连接 Stomp �务器 + +例如: + +```java +stomp.connect(); +``` + +如果�务器在连接时需�指令一些�外信�,比如连接 RabbitMQ 时需�指定`login`�`passcode` 和 `vhost`: + +```java +List
headers = new ArrayList<>(); +headers.add(new Header("login", "username")); +headers.add(new Header("passcode", "xxxxxxx")); +headers.add(new Header("host", "your_vhost")); + +stomp.connect(headers); +``` + +## 连接状�监� + +```java +Stomp.over(OkHttps.webSocket("wss://...").heatbeat(20, 20)) + .setOnConnected(stomp -> { + // �务器连接�功回调 + }) + .setOnDisconnected(close -> { + // 连接已断开回调 + }) + .connect(); +``` + +## 消�订阅与退订 + +### 订阅广播 + +```java +stomp.topic("/your-topic", (Message msg) -> { + // 得到消�负载 + String payload = msg.getPayload(); + + // 如果需�手动确认消�,调用此方法确认 + stomp.ack(msg) +}); +``` + +### 退订广播 + +```java +stomp.untopic("/your-topic"); +``` + +### 订阅队列 + +```java +stomp.queue("/your-queue", (Message msg) -> { + // 得到消�负载 + String payload = msg.getPayload(); + + // 如果需�手动确认消�,调用此方法确认 + stomp.ack(msg) +}); +``` + +### 退订队列 + +```java +stomp.unqueue("/your-queue"); +``` + +## ��消� + +```java +// ��到广播 +stomp.sendTo("/topic/your-topic", "Hello World"); +// ��到队列 +stomp.sendTo("/queue/your-queue", "Hello World"); +``` + +## 断开�务 + +```java +stomp.disconnect(); +``` + +
+ + \ No newline at end of file diff --git a/okhttps-doc/docs/v2/updownload.md b/okhttps-doc/docs/v2/updownload.md new file mode 100644 index 00000000..c2ebd739 --- /dev/null +++ b/okhttps-doc/docs/v2/updownload.md @@ -0,0 +1,244 @@ +--- +description: OkHttps 文件 上传 下载 进度监� 过程控制 断点续传 分�下载 +--- + +# 上传下载 + +## 文件下载 + +  OkHttps 并没有把文件的下载排除在常规的请求之外,�一套API,它优雅的设计使得下载与常规请求��的毫无�和感,一个最简�的示例: + +```java +http.sync("/download/test.zip") + .get() // 使用 GET 方法(其它方法也�以,看�务器支�) + .getBody() // 得到报文体 + .toFile("D:/download/test.zip") // 下载到指定的路径 + .start(); // �动下载 + +http.sync("/download/test.zip").get().getBody() + .toFolder("D:/download") // 下载到指定的目录,文件�将根�下载信�自动生� + .start(); +``` +  或使用异步连接方�: + +```java +http.async("/download/test.zip") + .setOnResponse((HttpResult result) -> { + result.getBody().toFolder("D:/download").start(); + }) + .get(); +``` +  这里�说明一下:`sync`与`async`的区别在于连接�务器并得到�应这个过程的�步与异步(这个过程的耗时在大文件下载中�比��),而`start`方法�动的下载过程则都是异步的。 + +### 下载进度监� + +  就直接上代�啦,诸�一看便懂: + +```java +http.sync("/download/test.zip") + .get() + .getBody() + .stepBytes(1024) // 设置�接收 1024 个字节执行一次进度回调(�设置默认为 8192) + // .stepRate(0.01) // 设置�接收 1% 执行一次进度回调(�设置以 StepBytes 为准) + .setOnProcess((Process process) -> { // 下载进度回调 + long doneBytes = process.getDoneBytes(); // 已下载字节数 + long totalBytes = process.getTotalBytes(); // 总共的字节数 + double rate = process.getRate(); // 已下载的比例 + boolean isDone = process.isDone(); // 是�下载完� + }) + .toFolder("D:/download/") // 指定下载的目录,文件�将根�下载信�自动生� + // .toFile("D:/download/test.zip") // 指定下载的路径,若文件已存在则覆盖 + .setOnSuccess((File file) -> { // 下载�功回调 + + }) + .start(); +``` +  值得一�的是:由于 OkHttps 并没有把下载�的很特别,这里设置的进度回调��对下载文件起用作,�使对�应JSON的常规请求,��设置了进度回调,它也会告诉你报文接收的进度(��是�务器�应的报文有`Content-Length`头),例如: + +```java +List users = http.sync("/users") + .get() + .getBody() + .stepBytes(2) + .setOnProcess((Process process) -> { + System.out.println(process.getRate()); + }) + .toList(User.class); +``` + +::: warning +方法`stepBytes`和`stepRate`在 v2.0.0 正�版之��为`setStepBytes`和`setStepRate`, v2.0.0 之�推�使用简�版的方法,�方法将在 v2.1.0 中移除。 +::: + +### 下载过程控制 + +  过于简�:还是直接上代�: + +```java +Ctrl ctrl = http.sync("/download/test.zip") + .get() + .getBody() + .setOnProcess((Process process) -> { + System.out.println(process.getRate()); + }) + .toFolder("D:/download/") + .start(); // 该方法返回一个下载过程控制器 + +ctrl.status(); // 下载状� +ctrl.pause(); // 暂�下载 +ctrl.resume(); // ��下载 +ctrl.cancel(); // �消下载(�时会删除文件,����) +``` +  无论是�步还是异步�起的下载请求,都�以�以上的控制: + +```java +http.async("/download/test.zip") + .setOnResponse((HttpResult result) -> { + // 拿到下载控制器 + Ctrl ctrl = result.getBody().toFolder("D:/download/").start(); + }) + .get(); +``` + +### 实现断点续传 + +  OkHttps 对断点续传并没有��更高层次的�装,因为这是app该去�的事情,它在设计上使��网络问题的处��简�的�时力求纯粹。下�的例��以看到,OkHttps 通过一个失败回调拿到 **断点**,便将��的问题�得简�: + +```java +http.sync("/download/test.zip") + .get() + .getBody() + .toFolder("D:/download/") + .setOnFailure((Failure failure) -> { // 下载失败回调,以便接收诸如网络错误等失败信� + IOException e = failure.getException(); // 具体的异常信� + long doneBytes = failure.getDoneBytes(); // 已下载的字节数(断点),需��存,用于断点续传 + File file = failure.getFile(); // 下载生�的文件,需��存 ,用于断点续传(��存路径也�以) + }) + .start(); +``` +  下�代�实现续传: + +```java +long doneBytes = ... // 拿到�存的断点 +File file = ... // 待续传的文件 + +http.sync("/download/test.zip") + .setRange(doneBytes) // 设置断点(已下载的字节数) + .get() + .getBody() + .toFile(file) // 下载到�一个文件里 + .setAppended() // 开�文件追加模� + .setOnSuccess((File file) -> { + + }) + .setOnFailure((Failure failure) -> { + + }) + .start(); +``` + +### 实现分�下载 + +  当文件很大时,有时候我们会考虑分�下载,与断点续传的�路是一样的,示例代�: + +```java +static String url = "http://api.demo.com/download/test.zip" + +public static void main(String[] args) { + long totalSize = OkHttps.sync(url).get().getBody() + .close() // 因为这次请求�是为了获得文件大�,�消费报文体,所以直接关闭 + .getContentLength(); // 获得待下载文件的大�(由于未消费报文体,所以该请求�会消耗下载报文体的时间和网络��) + download(totalSize, 0); // 从第 0 �开始下载 + sleep(50000); // 等待下载完�(�然本例的主线程就结�啦) +} + +static void download(long totalSize, int index) { + long size = 3 * 1024 * 1024; // ��下载 3M + long start = index * size; + long end = Math.min(start + size, totalSize); + OkHttps.sync(url) + .setRange(start, end) // 设置本次下载的范围 + .get().getBody() + .toFile("D:/download/test.zip") // 下载到�一个文件里 + .setAppended() // 开�文件追加模� + .setOnSuccess((File file) -> { + if (end < totalSize) { // 若未下载完,则继续下载下一� + download(totalSize, index + 1); + } else { + System.out.println("下载完�"); + } + }) + .start(); +} +``` + +本例中用到了`OkHttps`工具类,在应用的开�者我们推�使用它,请�考 [工具类](/v2/getstart.html#工具类) 章节。 + +## 文件上传 + +  一个简�文件上传的示例: + +```java +http.sync("/upload") + .addFilePara("test", "D:/download/test.zip") + .post(); // 上传�法一般使用 POST 或 PUT,看�务器支� +``` +  异步上传也是完全一样: + +```java +http.async("/upload") + .addFilePara("test", "D:/download/test.zip") + .post(); +``` + +::: warning +如果你使用的是 v2.0.0.RC 版本中,以上代�会报 **“方法 addFilePara �能使用 form 方�请求�** 错误,�以用以下方�解决该问题: +```java +http.async("/upload") + .bodyType("multipart/form") + .addFilePara("test", "D:/download/test.zip") + .post(); +``` +详� ISSUE: [https://gitee.com/ejlchina-zhxu/okhttps/issues/I1H8G9](https://gitee.com/ejlchina-zhxu/okhttps/issues/I1H8G9) +::: + +### 上传进度监� + +  OkHttps 的上传进度监�,监�的是所有请求报文体的��进度,示例代�: + +```java +http.sync("/upload") + .addBodyPara("name", "Jack") + .addBodyPara("age", 20) + .addFilePara("avatar", "D:/image/avatar.jpg") + .stepBytes(1024) // 设置��� 1024 个字节执行一次进度回调(�设置默认为 8192) + // .stepRate(0.01) // 设置��� 1% 执行一次进度回调(�设置以 StepBytes 为准) + .setOnProcess((Process process) -> { // 上传进度回调 + long doneBytes = process.getDoneBytes(); // 已��字节数 + long totalBytes = process.getTotalBytes(); // 总共的字节数 + double rate = process.getRate(); // 已��的比例 + boolean isDone = process.isDone(); // 是���完� + }) + .post(); +``` +  咦�怎么感觉和下载的进度回调的一样?没错�OkHttps 还是使用�一套API处�上传和下载的进度回调,区别�在于上传是在`get/post`方法之�使用这些API,下载是在`getBody`方法之�使用。很好�解:`get/post`之�是准备��请求时段,有上传的�义,而`getBody`之�,已是报文�应的时段,当然是下载。 + +### 上传过程控制 + +  上传文件的过程控制就很简�,和常规请求一样,�有异步�起的上传�以�消: + +```java +HttpCall call = http.async("/upload") + .addFilePara("test", "D:/download/test.zip") + .setOnProcess((Process process) -> { + System.out.println(process.getRate()); + }) + .post(); + +call.cancel(); // �消上传 +``` +  上传就没有暂�和继续这个功能啦,应该没人有这个需求�? + +
+ + \ No newline at end of file diff --git a/okhttps-doc/docs/v2/websocket.md b/okhttps-doc/docs/v2/websocket.md new file mode 100644 index 00000000..a749536c --- /dev/null +++ b/okhttps-doc/docs/v2/websocket.md @@ -0,0 +1,162 @@ +--- +description: OkHttps WebSocket Heatbeat 心跳 OkHttp +--- + +# WebSocket + +OkHttps 使用`webSocket(String url)`方法�起 WebSocket 连接,并使用`listen()`方法�动监�。 + +## 连接�数 + +如果在连接时需���务器传递�数,处�方�和 HTTP 请求一样,例如需�用户�和密��能连接 WebSocket �务: + +```java +http.webSocket("/chat") + .addUrlPara("username", "Jack") + .addUrlPara("password", "xxxxxxxx") + .setOnMessage((WebSocket ws,Message msg) -> { + + }) + .listen(); // �动监� +``` + +::: warning 注� +WebSocket 连接�能添加 **请求头**� **Url �数**(查询�数)和 **Path �数**, 报文体�数(Body)和 文件�数是��许添加的。 +::: + +## 心跳机制 + +OkHttps 自带两�心跳机制 + +### 全局心跳�置 + +在构建`HTTP`实例时,�以�置一个全局心跳时间间隔: + +```java +HTTP http = HTTP.builder() + .config((OkHttpClient.Builder builder) -> { + + // �置 WebSocket 心跳间隔(默认没有心跳) + builder.pingInterval(10, TimeUnit.SECONDS); + }) + .build(); +``` + +如上�置,当使用这个`HTTP`实例�起 WebSocket 连接时,客户端会�隔 10秒 ��务器��一次 PING 消�,�时�务器必须在客户端��心跳�的 10秒 内回� PONG 消�,�则就会触�`SocketTimeoutException`异常 + +### �次心跳�置(since V2.3.0) + +自 V2.3.0 起 OkHttps �供了�外一�心跳机制,它在�起具体的 WebSocket 连接时通过方法`heatbeat(int pingSeconds, int pongSeconds)`分别指定客户端与�务器的心跳时间间隔: + +```java +http.webSocket("/chat") + .heatbeat(10, 10) + .setOnMessage((WebSocket ws,Message msg) -> { + + }) + .listen(); // �动监� +``` + +如上�置,客户端�会�隔 10秒 ��务器��一次 PING 消�,并期望�务器回� PONG 消�的间隔也是 10 秒一次,但如果�务器或网络由于�些未知原因导致客户端未能正确收到 PONG 消�,客户端还会容�两次失败,当第三个 10 秒�还未收到�务器的任何消�时,则会触�`SocketTimeoutException`异常 + +::: tip OkHttps 的心跳机制相对于 OkHttp 主�有以下特点 +* 客户端��的任何消�都具有一次心跳作用 +* �务器��的任何消�都具有一次心跳作用 +* 若�务器超过 3 * pongSeconds 秒没有回�心跳,�触�心跳超时 +::: + +## 消�收� + +### 接收消� + +�起 WebSocket 连接是,设置一个`OnMessage`回调,便�接收到�务器的消�: + +```java +http.webSocket("/chat") + .setOnMessage((WebSocket ws,Message msg) -> { + // 接收到消� msg + }) + .listen(); // �动监� +``` + +在该回调内接收到一个`Message`类型的消�对象,它和`HttpReault.Body`都继承自`Toable`接�,他共有如下一些方法: + +* `isText()` 判断是文本消�还是二进制消� +* `toByteStream()` 消�体转字节� +* `toBytes()` 消�体转字数组 +* `toByteString()` 消�体转字数组 +* `toCharStream()` 消�体转字符� +* `toString()` 消�体转字符串 +* `toMapper()` 消�体转 Mapper 对象(�想定义 Java Bean 时使用) +* `toArray()` 消�体转 Array 数组(�想定义 Java Bean 时使用) +* `toBean(Class type)` 报文体Json文本转JavaBean +* `toBean(Type type)` 报文体Json文本转JavaBean +* `toBean(TypeRef type)` 报文体Json文本转JavaBean +* `toList(Class type)` 报文体Json文本转JavaBean列表 + +### ��消� + +��务器��消�,首先�获得一个`WebSocket`实例,该实例�以通过`listen()`方法的返回值或回调方法的�数获�,如: + +```java +WebSocket ws = http.webSocket("/chat").listen(); // �动监�,并返回一个 WebSocket 实例 +``` + +接�`WebSocket`继承自`Cancelable`,它共定义了如下方法: + +* `cancel()` �消连接(连接�功��以�消) +* `queueSize()` 排队待��消�的数� +* `send(Object object)` ��消�,�数是待��的对象,�以是 String | ByteString | byte[] | Java Bean +* `close(int code, String reason)` 关闭连接(连接�功��以关闭) +* `msgType(String type)` 设置消�传输类型,类似于`bodyType` + +一个��消�的例� + +```java +WebSocket ws = http.webSocket("/chat").listen(); +ws.send("Hello World!") // 该消�会先进入排队等待状�,当连接�功时��给�务器 +``` + +或者 + +```java +http.webSocket("/chat") + .setOnOpen((WebSocket ws, HttpResult res) -> { + // 当连接�功时��给�务器 + ws.send("Hello World!") + }) + .listen(); +``` + +## 回调方法 + +WebSocket 连接共�设置 **5** �回调方法: + +```java +http.webSocket("/websocket-endpoint") + .setOnOpen((WebSocket ws, HttpResult res) -> { + // WebSocket 连接�功时回调 + }) + .setOnMessage((WebSocket ws, Message msg) -> { + // 收到�务器下�的消�时回调 + }) + .setOnException((WebSocket ws, Throwable thr) -> { + // 连接�生异常时回调 + }) + .setOnClosing((WebSocket ws, WebSocket.Close close) -> { + // 连接正在关闭时回调 + }) + .setOnClosed((WebSocket ws, WebSocket.Close close) -> { + // 连接已关闭(v2.0.0 之�包�连接被�消 和 连接�生异常)时回调 + }) + .listen(); +``` + +::: tip 需�注�的是 +如果设置了 [全局回调监�](/v2/configuration.html#全局回调监�), 它们对 WebSocket 连接 �样起作用 +::: + + +
+ + \ No newline at end of file diff --git a/okhttps-doc/package.json b/okhttps-doc/package.json new file mode 100644 index 00000000..3e309bcb --- /dev/null +++ b/okhttps-doc/package.json @@ -0,0 +1,18 @@ +{ + "scripts": { + "dev": "vuepress dev docs", + "build": "vuepress build docs" + }, + "devDependencies": { + "@vuepress/plugin-back-to-top": "^1.5.0", + "@vuepress/plugin-medium-zoom": "^1.5.0", + "vuepress": "^1.5.0", + "vuepress-plugin-baidu-autopush": "^1.0.1", + "vuepress-plugin-baidu-tongji": "^1.0.1", + "vuepress-plugin-seo": "^0.1.2" + }, + "dependencies": { + "@vssue/api-github-v4": "^1.4.0", + "@vssue/vuepress-plugin-vssue": "^1.4.4" + } +} diff --git a/okhttps-fastjson/.gitignore b/okhttps-fastjson/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/okhttps-fastjson/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/okhttps-fastjson/README.md b/okhttps-fastjson/README.md deleted file mode 100644 index bbf641f0..00000000 --- a/okhttps-fastjson/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# OkHttps FastJson - -#### 介� - -基于 OkHttps 与 FastJson 集�扩展 - - -##### Maven - -```xml - - cn.zhxu - okhttps-fastjson - 3.5.3 - -``` - -##### Gradle - -```groovy -implementation 'cn.zhxu:okhttps-fastjson:3.5.3' -``` - -#### 更多请�阅 [https://ok.zhxu.cn/](https://ok.zhxu.cn/) diff --git a/okhttps-fastjson/pom.xml b/okhttps-fastjson/pom.xml index 219950e4..7c02e0bb 100644 --- a/okhttps-fastjson/pom.xml +++ b/okhttps-fastjson/pom.xml @@ -6,26 +6,26 @@ jar - cn.zhxu + com.ejlchina okhttps-parent - 4.1.0 + 2.3.0 - cn.zhxu + com.ejlchina okhttps - ${project.version} + ${okhttps.version} - cn.zhxu - data-fastjson - ${data.version} + com.alibaba + fastjson + ${fastjson.version} - cn.zhxu + com.ejlchina okhttps-test - ${project.version} + ${okhttps.version} test diff --git a/okhttps-fastjson/src/main/java/cn/zhxu/okhttps/fastjson/FastjsonMsgConvertor.java b/okhttps-fastjson/src/main/java/cn/zhxu/okhttps/fastjson/FastjsonMsgConvertor.java deleted file mode 100644 index 1ed65148..00000000 --- a/okhttps-fastjson/src/main/java/cn/zhxu/okhttps/fastjson/FastjsonMsgConvertor.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.zhxu.okhttps.fastjson; - -import cn.zhxu.data.fastjson.FastjsonDataConvertor; -import cn.zhxu.okhttps.ConvertProvider; -import cn.zhxu.okhttps.MsgConvertor; - -public class FastjsonMsgConvertor extends FastjsonDataConvertor implements MsgConvertor, ConvertProvider { - - @Override - public String mediaType() { - return "application/json"; - } - - @Override - public MsgConvertor getConvertor() { - return new FastjsonMsgConvertor(); - } - -} diff --git a/okhttps-fastjson/src/main/java/com/ejlchina/okhttps/FastjsonArray.java b/okhttps-fastjson/src/main/java/com/ejlchina/okhttps/FastjsonArray.java new file mode 100644 index 00000000..0755c484 --- /dev/null +++ b/okhttps-fastjson/src/main/java/com/ejlchina/okhttps/FastjsonArray.java @@ -0,0 +1,77 @@ +package com.ejlchina.okhttps; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; + +public class FastjsonArray implements Array { + + private JSONArray json; + + public FastjsonArray(JSONArray json) { + this.json = json; + } + + @Override + public int size() { + return json.size(); + } + + @Override + public boolean isEmpty() { + return json.isEmpty(); + } + + @Override + public Mapper getMapper(int index) { + JSONObject subJson = json.getJSONObject(index); + if (subJson != null) { + return new FastjsonMapper(subJson); + } + return null; + } + + @Override + public Array getArray(int index) { + JSONArray subJson = json.getJSONArray(index); + if (subJson != null) { + return new FastjsonArray(subJson); + } + return null; + } + + @Override + public boolean getBool(int index) { + return json.getBooleanValue(index); + } + + @Override + public int getInt(int index) { + return json.getIntValue(index); + } + + @Override + public long getLong(int index) { + return json.getLongValue(index); + } + + @Override + public float getFloat(int index) { + return json.getFloatValue(index); + } + + @Override + public double getDouble(int index) { + return json.getDoubleValue(index); + } + + @Override + public String getString(int index) { + return json.getString(index); + } + + @Override + public String toString() { + return json.toJSONString(); + } + +} diff --git a/okhttps-fastjson/src/main/java/com/ejlchina/okhttps/FastjsonMapper.java b/okhttps-fastjson/src/main/java/com/ejlchina/okhttps/FastjsonMapper.java new file mode 100644 index 00000000..7e5aa6d4 --- /dev/null +++ b/okhttps-fastjson/src/main/java/com/ejlchina/okhttps/FastjsonMapper.java @@ -0,0 +1,89 @@ +package com.ejlchina.okhttps; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; + +import java.util.Set; + +public class FastjsonMapper implements Mapper { + + private JSONObject json; + + public FastjsonMapper(JSONObject json) { + this.json = json; + } + + @Override + public int size() { + return json.size(); + } + + @Override + public boolean isEmpty() { + return json.isEmpty(); + } + + @Override + public Mapper getMapper(String key) { + JSONObject subJson = json.getJSONObject(key); + if (subJson != null) { + return new FastjsonMapper(subJson); + } + return null; + } + + @Override + public Array getArray(String key) { + JSONArray subJson = json.getJSONArray(key); + if (subJson != null) { + return new FastjsonArray(subJson); + } + return null; + } + + @Override + public boolean getBool(String key) { + return json.getBooleanValue(key); + } + + @Override + public int getInt(String key) { + return json.getIntValue(key); + } + + @Override + public long getLong(String key) { + return json.getLongValue(key); + } + + @Override + public float getFloat(String key) { + return json.getFloatValue(key); + } + + @Override + public double getDouble(String key) { + return json.getDoubleValue(key); + } + + @Override + public String getString(String key) { + return json.getString(key); + } + + @Override + public boolean has(String key) { + return json.containsKey(key); + } + + @Override + public Set keySet() { + return json.keySet(); + } + + @Override + public String toString() { + return json.toJSONString(); + } + +} diff --git a/okhttps-fastjson/src/main/java/com/ejlchina/okhttps/FastjsonMsgConvertor.java b/okhttps-fastjson/src/main/java/com/ejlchina/okhttps/FastjsonMsgConvertor.java new file mode 100644 index 00000000..d8040572 --- /dev/null +++ b/okhttps-fastjson/src/main/java/com/ejlchina/okhttps/FastjsonMsgConvertor.java @@ -0,0 +1,58 @@ +package com.ejlchina.okhttps; + +import com.alibaba.fastjson.JSON; +import com.ejlchina.okhttps.internal.HttpException; +import okio.Okio; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.List; + +public class FastjsonMsgConvertor implements MsgConvertor, ConvertProvider { + + @Override + public String mediaType() { + return "application/json"; + } + + @Override + public Mapper toMapper(InputStream in, Charset charset) { + return new FastjsonMapper(JSON.parseObject(toString(in, charset))); + } + + @Override + public Array toArray(InputStream in, Charset charset) { + return new FastjsonArray(JSON.parseArray(toString(in, charset))); + } + + @Override + public byte[] serialize(Object object, Charset charset) { + return JSON.toJSONString(object).getBytes(charset); + } + + @Override + public T toBean(Type type, InputStream in, Charset charset) { + return JSON.parseObject(toString(in, charset), type); + } + + @Override + public List toList(Class type, InputStream in, Charset charset) { + return JSON.parseArray(toString(in, charset), type); + } + + @Override + public MsgConvertor getConvertor() { + return new FastjsonMsgConvertor(); + } + + private String toString(InputStream in, Charset charset) { + try { + return Okio.buffer(Okio.source(in)).readString(charset); + } catch (IOException e) { + throw new HttpException("读�文本异常", e); + } + } + +} diff --git a/okhttps-fastjson/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider b/okhttps-fastjson/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider deleted file mode 100644 index 5ae917c7..00000000 --- a/okhttps-fastjson/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider +++ /dev/null @@ -1 +0,0 @@ -cn.zhxu.okhttps.fastjson.FastjsonMsgConvertor \ No newline at end of file diff --git a/okhttps-fastjson/src/main/resources/META-INF/services/com.ejlchina.okhttps.ConvertProvider b/okhttps-fastjson/src/main/resources/META-INF/services/com.ejlchina.okhttps.ConvertProvider new file mode 100644 index 00000000..88f3b11d --- /dev/null +++ b/okhttps-fastjson/src/main/resources/META-INF/services/com.ejlchina.okhttps.ConvertProvider @@ -0,0 +1 @@ +com.ejlchina.okhttps.FastjsonMsgConvertor \ No newline at end of file diff --git a/okhttps-fastjson/src/test/java/cn/zhxu/okhttps/fastjson/TestCase.java b/okhttps-fastjson/src/test/java/com/ejlchina/okhttps/TestCase.java similarity index 51% rename from okhttps-fastjson/src/test/java/cn/zhxu/okhttps/fastjson/TestCase.java rename to okhttps-fastjson/src/test/java/com/ejlchina/okhttps/TestCase.java index 2f0cc7e6..b47d6741 100644 --- a/okhttps-fastjson/src/test/java/cn/zhxu/okhttps/fastjson/TestCase.java +++ b/okhttps-fastjson/src/test/java/com/ejlchina/okhttps/TestCase.java @@ -1,15 +1,15 @@ -package cn.zhxu.okhttps.fastjson; +package com.ejlchina.okhttps; -import cn.zhxu.okhttps.MsgConvertor; -import cn.zhxu.okhttps.test.JsonTestCases; +import com.ejlchina.okhttps.test.JsonMsgConvertorTest; import org.junit.Test; + public class TestCase { @Test public void doTest() throws Exception { MsgConvertor msgConvertor = new FastjsonMsgConvertor(); - new JsonTestCases(msgConvertor).run(); + new JsonMsgConvertorTest(msgConvertor).run(); } } diff --git a/okhttps-fastjson2-jsonb/README.md b/okhttps-fastjson2-jsonb/README.md deleted file mode 100644 index 56d4f135..00000000 --- a/okhttps-fastjson2-jsonb/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# OkHttps FastJson - -#### 介� - -基于 OkHttps 与 Fastjson2Jsonb 集�扩展 - - -##### Maven - -```xml - - cn.zhxu - okhttps-fastjson2-jsonb - 3.5.3 - -``` - -##### Gradle - -```groovy -implementation 'cn.zhxu:okhttps-fastjson2-jsonb:3.5.3' -``` - -#### 更多请�阅 [https://ok.zhxu.cn/](https://ok.zhxu.cn/) diff --git a/okhttps-fastjson2-jsonb/pom.xml b/okhttps-fastjson2-jsonb/pom.xml deleted file mode 100644 index dc2a91c0..00000000 --- a/okhttps-fastjson2-jsonb/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - 4.0.0 - - okhttps-fastjson2-jsonb - OkHttps Fastjson2 JSONB - jar - - - cn.zhxu - okhttps-parent - 4.1.0 - - - - - cn.zhxu - okhttps - ${project.version} - - - cn.zhxu - data-fastjson2-jsonb - ${data.version} - - - cn.zhxu - okhttps-test - ${project.version} - test - - - - - okhttps-fastjson2-jsonb - - \ No newline at end of file diff --git a/okhttps-fastjson2-jsonb/src/main/java/cn/zhxu/okhttps/fastjson2/jsonb/Fastjson2JsonbMsgConvertor.java b/okhttps-fastjson2-jsonb/src/main/java/cn/zhxu/okhttps/fastjson2/jsonb/Fastjson2JsonbMsgConvertor.java deleted file mode 100644 index 17867cd8..00000000 --- a/okhttps-fastjson2-jsonb/src/main/java/cn/zhxu/okhttps/fastjson2/jsonb/Fastjson2JsonbMsgConvertor.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.zhxu.okhttps.fastjson2.jsonb; - -import cn.zhxu.data.fastjson2.jsonb.Fastjson2JsonbDataConvertor; -import cn.zhxu.okhttps.ConvertProvider; -import cn.zhxu.okhttps.MsgConvertor; - -public class Fastjson2JsonbMsgConvertor extends Fastjson2JsonbDataConvertor implements MsgConvertor, ConvertProvider { - - @Override - public String mediaType() { - return "application/jsonb"; - } - - @Override - public MsgConvertor getConvertor() { - return new Fastjson2JsonbMsgConvertor(); - } - -} diff --git a/okhttps-fastjson2-jsonb/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider b/okhttps-fastjson2-jsonb/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider deleted file mode 100644 index 3683502a..00000000 --- a/okhttps-fastjson2-jsonb/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider +++ /dev/null @@ -1 +0,0 @@ -cn.zhxu.okhttps.fastjson2.jsonb.Fastjson2JsonbMsgConvertor \ No newline at end of file diff --git a/okhttps-fastjson2-jsonb/src/test/java/cn/zhxu/okhttps/fastjson2/jsonb/TestCase.java b/okhttps-fastjson2-jsonb/src/test/java/cn/zhxu/okhttps/fastjson2/jsonb/TestCase.java deleted file mode 100644 index 835408ba..00000000 --- a/okhttps-fastjson2-jsonb/src/test/java/cn/zhxu/okhttps/fastjson2/jsonb/TestCase.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.zhxu.okhttps.fastjson2.jsonb; - -import cn.zhxu.okhttps.MsgConvertor; -import cn.zhxu.okhttps.test.JsonTestCases; -import org.junit.Test; - - -public class TestCase { - - @Test - public void doTest() throws Exception { - MsgConvertor msgConvertor = new Fastjson2JsonbMsgConvertor(); -// new JsonTestCases(msgConvertor).run(); - } - -} diff --git a/okhttps-fastjson2/README.md b/okhttps-fastjson2/README.md deleted file mode 100644 index cdaa2cf2..00000000 --- a/okhttps-fastjson2/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# OkHttps FastJson - -#### 介� - -基于 OkHttps 与 Fastjson2 集�扩展 - - -##### Maven - -```xml - - cn.zhxu - okhttps-fastjson2 - 3.5.3 - -``` - -##### Gradle - -```groovy -implementation 'cn.zhxu:okhttps-fastjson2:3.5.3' -``` - -#### 更多请�阅 [https://ok.zhxu.cn/](https://ok.zhxu.cn/) diff --git a/okhttps-fastjson2/pom.xml b/okhttps-fastjson2/pom.xml deleted file mode 100644 index dd971348..00000000 --- a/okhttps-fastjson2/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - 4.0.0 - - okhttps-fastjson2 - OkHttps Fastjson2 - jar - - - cn.zhxu - okhttps-parent - 4.1.0 - - - - - cn.zhxu - okhttps - ${project.version} - - - cn.zhxu - data-fastjson2 - ${data.version} - - - cn.zhxu - okhttps-test - ${project.version} - test - - - - - okhttps-fastjson2 - - \ No newline at end of file diff --git a/okhttps-fastjson2/src/main/java/cn/zhxu/okhttps/fastjson2/Fastjson2MsgConvertor.java b/okhttps-fastjson2/src/main/java/cn/zhxu/okhttps/fastjson2/Fastjson2MsgConvertor.java deleted file mode 100644 index a7199847..00000000 --- a/okhttps-fastjson2/src/main/java/cn/zhxu/okhttps/fastjson2/Fastjson2MsgConvertor.java +++ /dev/null @@ -1,19 +0,0 @@ -package cn.zhxu.okhttps.fastjson2; - -import cn.zhxu.data.fastjson2.Fastjson2DataConvertor; -import cn.zhxu.okhttps.ConvertProvider; -import cn.zhxu.okhttps.MsgConvertor; - -public class Fastjson2MsgConvertor extends Fastjson2DataConvertor implements MsgConvertor, ConvertProvider { - - @Override - public String mediaType() { - return "application/json"; - } - - @Override - public MsgConvertor getConvertor() { - return new Fastjson2MsgConvertor(); - } - -} diff --git a/okhttps-fastjson2/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider b/okhttps-fastjson2/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider deleted file mode 100644 index 37efd499..00000000 --- a/okhttps-fastjson2/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider +++ /dev/null @@ -1 +0,0 @@ -cn.zhxu.okhttps.fastjson2.Fastjson2MsgConvertor \ No newline at end of file diff --git a/okhttps-fastjson2/src/test/java/cn/zhxu/okhttps/fastjson2/TestCase.java b/okhttps-fastjson2/src/test/java/cn/zhxu/okhttps/fastjson2/TestCase.java deleted file mode 100644 index cc5051ad..00000000 --- a/okhttps-fastjson2/src/test/java/cn/zhxu/okhttps/fastjson2/TestCase.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.zhxu.okhttps.fastjson2; - -import cn.zhxu.okhttps.MsgConvertor; -import cn.zhxu.okhttps.test.JsonTestCases; -import org.junit.Test; - - -public class TestCase { - - @Test - public void doTest() throws Exception { - MsgConvertor msgConvertor = new Fastjson2MsgConvertor(); - new JsonTestCases(msgConvertor).run(); - } - -} diff --git a/okhttps-gson/.gitignore b/okhttps-gson/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/okhttps-gson/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/okhttps-gson/README.md b/okhttps-gson/README.md deleted file mode 100644 index 938f5181..00000000 --- a/okhttps-gson/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# OkHttps Gson - -#### 介� - -基于 OkHttps 与 Gson 集�扩展 - - -##### Maven - -```xml - - cn.zhxu - okhttps-gson - 3.5.3 - -``` - -##### Gradle - -```groovy -implementation 'cn.zhxu:okhttps-gson:3.5.3' -``` - -#### 更多请�阅 [https://ok.zhxu.cn/](https://ok.zhxu.cn/) diff --git a/okhttps-gson/pom.xml b/okhttps-gson/pom.xml index ef0d2ad7..659ea07a 100644 --- a/okhttps-gson/pom.xml +++ b/okhttps-gson/pom.xml @@ -6,26 +6,26 @@ jar - cn.zhxu + com.ejlchina okhttps-parent - 4.1.0 + 2.3.0 - cn.zhxu + com.ejlchina okhttps - ${project.version} + ${okhttps.version} + + com.google.code.gson + gson + ${gson.version} + - cn.zhxu - data-gson - ${data.version} - - - cn.zhxu + com.ejlchina okhttps-test - ${project.version} + ${okhttps.version} test diff --git a/okhttps-gson/src/main/java/cn/zhxu/okhttps/gson/GsonMsgConvertor.java b/okhttps-gson/src/main/java/cn/zhxu/okhttps/gson/GsonMsgConvertor.java deleted file mode 100644 index 15272f4b..00000000 --- a/okhttps-gson/src/main/java/cn/zhxu/okhttps/gson/GsonMsgConvertor.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.zhxu.okhttps.gson; - -import cn.zhxu.data.gson.GsonDataConvertor; -import cn.zhxu.okhttps.ConvertProvider; -import cn.zhxu.okhttps.MsgConvertor; -import com.google.gson.Gson; - -public class GsonMsgConvertor extends GsonDataConvertor implements MsgConvertor, ConvertProvider { - - public GsonMsgConvertor() { } - - public GsonMsgConvertor(Gson gson) { - super(gson); - } - - @Override - public String mediaType() { - return "application/json"; - } - - @Override - public MsgConvertor getConvertor() { - return new GsonMsgConvertor(); - } - -} - - - - - - - - - - - - - - diff --git a/okhttps-gson/src/main/java/com/ejlchina/okhttps/GsonArray.java b/okhttps-gson/src/main/java/com/ejlchina/okhttps/GsonArray.java new file mode 100644 index 00000000..99795e4b --- /dev/null +++ b/okhttps-gson/src/main/java/com/ejlchina/okhttps/GsonArray.java @@ -0,0 +1,101 @@ +package com.ejlchina.okhttps; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + +public class GsonArray implements Array { + + private JsonArray json; + + public GsonArray(JsonArray json) { + this.json = json; + } + + @Override + public int size() { + return json.size(); + } + + @Override + public boolean isEmpty() { + return json.size() == 0; + } + + @Override + public Mapper getMapper(int index) { + JsonElement subJson = json.get(index); + if (subJson != null) { + return new GsonMapper(subJson.getAsJsonObject()); + } + return null; + } + + @Override + public Array getArray(int index) { + JsonElement subJson = json.get(index); + if (subJson != null) { + return new GsonArray(subJson.getAsJsonArray()); + } + return null; + } + + @Override + public boolean getBool(int index) { + JsonElement val = json.get(index); + if (val != null) { + return val.getAsBoolean(); + } + return false; + } + + @Override + public int getInt(int index) { + JsonElement val = json.get(index); + if (val != null) { + return val.getAsInt(); + } + return 0; + } + + @Override + public long getLong(int index) { + JsonElement val = json.get(index); + if (val != null) { + return val.getAsLong(); + } + return 0; + } + + @Override + public float getFloat(int index) { + JsonElement val = json.get(index); + if (val != null) { + return val.getAsFloat(); + } + return 0; + } + + @Override + public double getDouble(int index) { + JsonElement val = json.get(index); + if (val != null) { + return val.getAsDouble(); + } + return 0; + } + + @Override + public String getString(int index) { + JsonElement val = json.get(index); + if (val != null) { + return val.getAsString(); + } + return null; + } + + @Override + public String toString() { + return json.toString(); + } + +} diff --git a/okhttps-gson/src/main/java/com/ejlchina/okhttps/GsonMapper.java b/okhttps-gson/src/main/java/com/ejlchina/okhttps/GsonMapper.java new file mode 100644 index 00000000..90ea3f90 --- /dev/null +++ b/okhttps-gson/src/main/java/com/ejlchina/okhttps/GsonMapper.java @@ -0,0 +1,113 @@ +package com.ejlchina.okhttps; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import java.util.Set; + +public class GsonMapper implements Mapper { + + private JsonObject json; + + public GsonMapper(JsonObject json) { + this.json = json; + } + + @Override + public int size() { + return json.size(); + } + + @Override + public boolean isEmpty() { + return json.size() == 0; + } + + @Override + public Mapper getMapper(String key) { + JsonElement subJson = json.get(key); + if (subJson != null) { + return new GsonMapper(subJson.getAsJsonObject()); + } + return null; + } + + @Override + public Array getArray(String key) { + JsonElement subJson = json.get(key); + if (subJson != null) { + return new GsonArray(subJson.getAsJsonArray()); + } + return null; + } + + @Override + public boolean getBool(String key) { + JsonElement val = json.get(key); + if (val != null) { + return val.getAsBoolean(); + } + return false; + } + + @Override + public int getInt(String key) { + JsonElement val = json.get(key); + if (val != null) { + return val.getAsInt(); + } + return 0; + } + + @Override + public long getLong(String key) { + JsonElement val = json.get(key); + if (val != null) { + return val.getAsLong(); + } + return 0; + } + + @Override + public float getFloat(String key) { + JsonElement val = json.get(key); + if (val != null) { + return val.getAsFloat(); + } + return 0; + } + + @Override + public double getDouble(String key) { + JsonElement val = json.get(key); + if (val != null) { + return val.getAsDouble(); + } + return 0; + } + + @Override + public String getString(String key) { + JsonElement val = json.get(key); + if (val != null) { + return val.getAsString(); + } + return null; + } + + @Override + public boolean has(String key) { + return json.has(key); + } + + @Override + public Set keySet() { + return json.keySet(); + } + + @Override + public String toString() { + return json.toString(); + } + +} diff --git a/okhttps-gson/src/main/java/com/ejlchina/okhttps/GsonMsgConvertor.java b/okhttps-gson/src/main/java/com/ejlchina/okhttps/GsonMsgConvertor.java new file mode 100644 index 00000000..99ebd955 --- /dev/null +++ b/okhttps-gson/src/main/java/com/ejlchina/okhttps/GsonMsgConvertor.java @@ -0,0 +1,88 @@ +package com.ejlchina.okhttps; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class GsonMsgConvertor implements MsgConvertor, ConvertProvider { + + private Gson gson; + + public GsonMsgConvertor() { + this(new Gson()); + } + + public GsonMsgConvertor(Gson gson) { + this.gson = gson; + } + + @Override + public String mediaType() { + return "application/json"; + } + + @Override + public Mapper toMapper(InputStream in, Charset charset) { + return new GsonMapper(gson.fromJson(new InputStreamReader(in), JsonObject.class)); + } + + @Override + public Array toArray(InputStream in, Charset charset) { + return new GsonArray(gson.fromJson(new InputStreamReader(in), JsonArray.class)); + } + + @Override + public byte[] serialize(Object object, Charset charset) { + return gson.toJson(object).getBytes(charset); + } + + @Override + public T toBean(Type type, InputStream in, Charset charset) { + return gson.fromJson(new InputStreamReader(in), type); + } + + @Override + public List toList(Class type, InputStream in, Charset charset) { + T[] beans = gson.fromJson(new InputStreamReader(in), TypeToken.getArray(type).getType()); + List list = new ArrayList<>(); + Collections.addAll(list, beans); + return list; + } + + @Override + public MsgConvertor getConvertor() { + return new GsonMsgConvertor(); + } + + public Gson getGson() { + return gson; + } + + public void setGson(Gson gson) { + this.gson = gson; + } + +} + + + + + + + + + + + + + + diff --git a/okhttps-gson/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider b/okhttps-gson/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider deleted file mode 100644 index 122472d4..00000000 --- a/okhttps-gson/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider +++ /dev/null @@ -1 +0,0 @@ -cn.zhxu.okhttps.gson.GsonMsgConvertor \ No newline at end of file diff --git a/okhttps-gson/src/main/resources/META-INF/services/com.ejlchina.okhttps.ConvertProvider b/okhttps-gson/src/main/resources/META-INF/services/com.ejlchina.okhttps.ConvertProvider new file mode 100644 index 00000000..5eb90888 --- /dev/null +++ b/okhttps-gson/src/main/resources/META-INF/services/com.ejlchina.okhttps.ConvertProvider @@ -0,0 +1 @@ +com.ejlchina.okhttps.GsonMsgConvertor \ No newline at end of file diff --git a/okhttps-gson/src/test/java/cn/zhxu/okhttps/gson/TestCase.java b/okhttps-gson/src/test/java/com/ejlchina/okhttps/TestCase.java similarity index 51% rename from okhttps-gson/src/test/java/cn/zhxu/okhttps/gson/TestCase.java rename to okhttps-gson/src/test/java/com/ejlchina/okhttps/TestCase.java index 71406abe..a1fa060c 100644 --- a/okhttps-gson/src/test/java/cn/zhxu/okhttps/gson/TestCase.java +++ b/okhttps-gson/src/test/java/com/ejlchina/okhttps/TestCase.java @@ -1,7 +1,6 @@ -package cn.zhxu.okhttps.gson; +package com.ejlchina.okhttps; -import cn.zhxu.okhttps.MsgConvertor; -import cn.zhxu.okhttps.test.JsonTestCases; +import com.ejlchina.okhttps.test.JsonMsgConvertorTest; import org.junit.Test; @@ -10,7 +9,7 @@ public class TestCase { @Test public void doTest() throws Exception { MsgConvertor msgConvertor = new GsonMsgConvertor(); - new JsonTestCases(msgConvertor).run(); + new JsonMsgConvertorTest(msgConvertor).run(); } } diff --git a/okhttps-jackson-xml/README.md b/okhttps-jackson-xml/README.md deleted file mode 100644 index e2505225..00000000 --- a/okhttps-jackson-xml/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# OkHttps Jackson - -#### 介� - -基于 OkHttps 与 JacksonXml 集�扩展 - - -##### Maven - -```xml - - cn.zhxu - okhttps-jackson-xml - 3.5.3 - -``` - -##### Gradle - -```groovy -implementation 'cn.zhxu:okhttps-jackson-xml:3.5.3' -``` - -#### 更多请�阅 [https://ok.zhxu.cn/](https://ok.zhxu.cn/) diff --git a/okhttps-jackson-xml/pom.xml b/okhttps-jackson-xml/pom.xml deleted file mode 100644 index 54aa1fce..00000000 --- a/okhttps-jackson-xml/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - 4.0.0 - - okhttps-jackson-xml - OkHttps Jackson Xml - jar - - - cn.zhxu - okhttps-parent - 4.1.0 - - - - - cn.zhxu - okhttps - ${project.version} - - - cn.zhxu - data-jackson-xml - ${data.version} - - - cn.zhxu - okhttps-test - ${project.version} - test - - - - - okhttps-jackson-xml - - \ No newline at end of file diff --git a/okhttps-jackson-xml/src/main/java/cn/zhxu/okhttps/jackson/xml/JacksonXmlMsgConvertor.java b/okhttps-jackson-xml/src/main/java/cn/zhxu/okhttps/jackson/xml/JacksonXmlMsgConvertor.java deleted file mode 100644 index 44a29fca..00000000 --- a/okhttps-jackson-xml/src/main/java/cn/zhxu/okhttps/jackson/xml/JacksonXmlMsgConvertor.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.zhxu.okhttps.jackson.xml; - -import cn.zhxu.data.jackson.xml.JacksonXmlDataConvertor; -import cn.zhxu.okhttps.ConvertProvider; -import cn.zhxu.okhttps.MsgConvertor; -import com.fasterxml.jackson.dataformat.xml.XmlMapper; - -public class JacksonXmlMsgConvertor extends JacksonXmlDataConvertor implements MsgConvertor, ConvertProvider { - - public JacksonXmlMsgConvertor() { } - - public JacksonXmlMsgConvertor(XmlMapper xmlMapper) { - super(xmlMapper); - } - - @Override - public String mediaType() { - return "application/xml"; - } - - @Override - public MsgConvertor getConvertor() { - return new JacksonXmlMsgConvertor(); - } - -} diff --git a/okhttps-jackson-xml/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider b/okhttps-jackson-xml/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider deleted file mode 100644 index 344c3d0b..00000000 --- a/okhttps-jackson-xml/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider +++ /dev/null @@ -1 +0,0 @@ -cn.zhxu.okhttps.jackson.xml.JacksonXmlMsgConvertor \ No newline at end of file diff --git a/okhttps-jackson-xml/src/test/java/cn/zhxu/okhttps/jackson/xml/TestCase.java b/okhttps-jackson-xml/src/test/java/cn/zhxu/okhttps/jackson/xml/TestCase.java deleted file mode 100644 index 20af4b4c..00000000 --- a/okhttps-jackson-xml/src/test/java/cn/zhxu/okhttps/jackson/xml/TestCase.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.zhxu.okhttps.jackson.xml; - -import cn.zhxu.okhttps.MsgConvertor; -import cn.zhxu.okhttps.test.XmlTestCases; -import org.junit.Test; - - -public class TestCase { - - @Test - public void doTest() throws Exception { - MsgConvertor convertor = new JacksonXmlMsgConvertor(); -// new XmlTestCases(convertor).run(); - } - -} diff --git a/okhttps-jackson-yaml/README.md b/okhttps-jackson-yaml/README.md deleted file mode 100644 index dbd3b23f..00000000 --- a/okhttps-jackson-yaml/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# OkHttps Jackson - -#### 介� - -基于 OkHttps 与 JacksonYamls 集�扩展 - - -##### Maven - -```xml - - cn.zhxu - okhttps-jackson-yaml - 3.5.3 - -``` - -##### Gradle - -```groovy -implementation 'cn.zhxu:okhttps-jackson-yaml:3.5.3' -``` - -#### 更多请�阅 [https://ok.zhxu.cn/](https://ok.zhxu.cn/) diff --git a/okhttps-jackson-yaml/pom.xml b/okhttps-jackson-yaml/pom.xml deleted file mode 100644 index c550aa50..00000000 --- a/okhttps-jackson-yaml/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - 4.0.0 - - okhttps-jackson-yaml - OkHttps Jackson Yaml - jar - - - cn.zhxu - okhttps-parent - 4.1.0 - - - - - cn.zhxu - okhttps - ${project.version} - - - cn.zhxu - data-jackson-yaml - ${data.version} - - - cn.zhxu - okhttps-test - ${project.version} - test - - - - - okhttps-jackson-yaml - - \ No newline at end of file diff --git a/okhttps-jackson-yaml/src/main/java/cn/zhxu/okhttps/jackson/yaml/JacksonYamlMsgConvertor.java b/okhttps-jackson-yaml/src/main/java/cn/zhxu/okhttps/jackson/yaml/JacksonYamlMsgConvertor.java deleted file mode 100644 index e0dc08e1..00000000 --- a/okhttps-jackson-yaml/src/main/java/cn/zhxu/okhttps/jackson/yaml/JacksonYamlMsgConvertor.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.zhxu.okhttps.jackson.yaml; - -import cn.zhxu.data.jackson.yaml.JacksonYamlDataConvertor; -import cn.zhxu.okhttps.ConvertProvider; -import cn.zhxu.okhttps.MsgConvertor; -import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; - -public class JacksonYamlMsgConvertor extends JacksonYamlDataConvertor implements MsgConvertor, ConvertProvider { - - public JacksonYamlMsgConvertor() { } - - public JacksonYamlMsgConvertor(YAMLMapper yamlMapper) { - super(yamlMapper); - } - - @Override - public String mediaType() { - return "application/yaml"; - } - - @Override - public MsgConvertor getConvertor() { - return new JacksonYamlMsgConvertor(); - } - -} diff --git a/okhttps-jackson-yaml/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider b/okhttps-jackson-yaml/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider deleted file mode 100644 index 8b8befc7..00000000 --- a/okhttps-jackson-yaml/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider +++ /dev/null @@ -1 +0,0 @@ -cn.zhxu.okhttps.jackson.yaml.JacksonYamlMsgConvertor \ No newline at end of file diff --git a/okhttps-jackson-yaml/src/test/java/cn/zhxu/okhttps/jackson/yaml/TestCase.java b/okhttps-jackson-yaml/src/test/java/cn/zhxu/okhttps/jackson/yaml/TestCase.java deleted file mode 100644 index 9f781a72..00000000 --- a/okhttps-jackson-yaml/src/test/java/cn/zhxu/okhttps/jackson/yaml/TestCase.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.zhxu.okhttps.jackson.yaml; - -import cn.zhxu.okhttps.MsgConvertor; -import cn.zhxu.okhttps.test.JsonTestCases; -import org.junit.Test; - - -public class TestCase { - - @Test - public void doTest() throws Exception { - MsgConvertor convertor = new JacksonYamlMsgConvertor(); -// new JsonTestCases(convertor).run(); - } - -} diff --git a/okhttps-jackson/.gitignore b/okhttps-jackson/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/okhttps-jackson/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/okhttps-jackson/README.md b/okhttps-jackson/README.md deleted file mode 100644 index 54c1cf5e..00000000 --- a/okhttps-jackson/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# OkHttps Jackson - -#### 介� - -基于 OkHttps 与 Jackson 集�扩展 - - -##### Maven - -```xml - - cn.zhxu - okhttps-jackson - 3.5.3 - -``` - -##### Gradle - -```groovy -implementation 'cn.zhxu:okhttps-jackson:3.5.3' -``` - -#### 更多请�阅 [https://ok.zhxu.cn/](https://ok.zhxu.cn/) diff --git a/okhttps-jackson/pom.xml b/okhttps-jackson/pom.xml index d9cef0e0..7494f9e5 100644 --- a/okhttps-jackson/pom.xml +++ b/okhttps-jackson/pom.xml @@ -6,26 +6,26 @@ jar - cn.zhxu + com.ejlchina okhttps-parent - 4.1.0 + 2.3.0 - cn.zhxu + com.ejlchina okhttps - ${project.version} + ${okhttps.version} + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + - cn.zhxu - data-jackson - ${data.version} - - - cn.zhxu + com.ejlchina okhttps-test - ${project.version} + ${okhttps.version} test diff --git a/okhttps-jackson/src/main/java/cn/zhxu/okhttps/jackson/JacksonMsgConvertor.java b/okhttps-jackson/src/main/java/cn/zhxu/okhttps/jackson/JacksonMsgConvertor.java deleted file mode 100644 index 1c0e561d..00000000 --- a/okhttps-jackson/src/main/java/cn/zhxu/okhttps/jackson/JacksonMsgConvertor.java +++ /dev/null @@ -1,26 +0,0 @@ -package cn.zhxu.okhttps.jackson; - -import cn.zhxu.data.jackson.JacksonDataConvertor; -import cn.zhxu.okhttps.ConvertProvider; -import cn.zhxu.okhttps.MsgConvertor; -import com.fasterxml.jackson.databind.ObjectMapper; - -public class JacksonMsgConvertor extends JacksonDataConvertor implements MsgConvertor, ConvertProvider { - - public JacksonMsgConvertor() { } - - public JacksonMsgConvertor(ObjectMapper objectMapper) { - super(objectMapper); - } - - @Override - public String mediaType() { - return "application/json"; - } - - @Override - public MsgConvertor getConvertor() { - return new JacksonMsgConvertor(); - } - -} diff --git a/okhttps-jackson/src/main/java/com/ejlchina/okhttps/JacksonArray.java b/okhttps-jackson/src/main/java/com/ejlchina/okhttps/JacksonArray.java new file mode 100644 index 00000000..a42b88b1 --- /dev/null +++ b/okhttps-jackson/src/main/java/com/ejlchina/okhttps/JacksonArray.java @@ -0,0 +1,105 @@ +package com.ejlchina.okhttps; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +public class JacksonArray implements Array { + + private ArrayNode json; + + public JacksonArray(ArrayNode json) { + this.json = json; + } + + @Override + public int size() { + return json.size(); + } + + @Override + public boolean isEmpty() { + return json.isEmpty(); + } + + @Override + public Mapper getMapper(int index) { + JsonNode subJson = json.get(index); + if (subJson != null && subJson.isObject()) { + return new JacksonMapper((ObjectNode) subJson); + } + return null; + } + + @Override + public Array getArray(int index) { + JsonNode subJson = json.get(index); + if (subJson != null && subJson.isArray()) { + return new JacksonArray((ArrayNode) subJson); + } + return null; + } + + @Override + public boolean getBool(int index) { + JsonNode subJson = json.get(index); + if (subJson != null) { + return subJson.asBoolean(false); + } + return false; + } + + @Override + public int getInt(int index) { + JsonNode subJson = json.get(index); + if (subJson != null) { + return subJson.asInt(0); + } + return 0; + } + + @Override + public long getLong(int index) { + JsonNode subJson = json.get(index); + if (subJson != null) { + return subJson.asLong(0); + } + return 0; + } + + @Override + public float getFloat(int index) { + JsonNode subJson = json.get(index); + if (subJson != null && subJson.isNumber()) { + return subJson.floatValue(); + } + if (subJson != null) { + return (float) subJson.asDouble(0); + } + return 0; + } + + @Override + public double getDouble(int index) { + JsonNode subJson = json.get(index); + if (subJson != null) { + return subJson.asDouble(0); + } + return 0; + } + + @Override + public String getString(int index) { + JsonNode subJson = json.get(index); + if (subJson != null) { + return subJson.asText(); + } + return null; + } + + @Override + public String toString() { + return json.toString(); + } + +} diff --git a/okhttps-jackson/src/main/java/com/ejlchina/okhttps/JacksonMapper.java b/okhttps-jackson/src/main/java/com/ejlchina/okhttps/JacksonMapper.java new file mode 100644 index 00000000..c0a2c180 --- /dev/null +++ b/okhttps-jackson/src/main/java/com/ejlchina/okhttps/JacksonMapper.java @@ -0,0 +1,124 @@ +package com.ejlchina.okhttps; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +public class JacksonMapper implements Mapper { + + private ObjectNode json; + + public JacksonMapper(ObjectNode json) { + this.json = json; + } + + @Override + public int size() { + return json.size(); + } + + @Override + public boolean isEmpty() { + return json.isEmpty(); + } + + @Override + public Mapper getMapper(String key) { + JsonNode subJson = json.get(key); + if (subJson != null && subJson.isObject()) { + return new JacksonMapper((ObjectNode) subJson); + } + return null; + } + + @Override + public Array getArray(String key) { + JsonNode subJson = json.get(key); + if (subJson != null && subJson.isArray()) { + return new JacksonArray((ArrayNode) subJson); + } + return null; + } + + @Override + public boolean getBool(String key) { + JsonNode subJson = json.get(key); + if (subJson != null) { + return subJson.asBoolean(false); + } + return false; + } + + @Override + public int getInt(String key) { + JsonNode subJson = json.get(key); + if (subJson != null) { + return subJson.asInt(0); + } + return 0; + } + + @Override + public long getLong(String key) { + JsonNode subJson = json.get(key); + if (subJson != null) { + return subJson.asLong(0); + } + return 0; + } + + @Override + public float getFloat(String key) { + JsonNode subJson = json.get(key); + if (subJson != null && subJson.isNumber()) { + return subJson.floatValue(); + } + if (subJson != null) { + return (float) subJson.asDouble(0); + } + return 0; + } + + @Override + public double getDouble(String key) { + JsonNode subJson = json.get(key); + if (subJson != null) { + return subJson.asDouble(0); + } + return 0; + } + + @Override + public String getString(String key) { + JsonNode subJson = json.get(key); + if (subJson != null) { + return subJson.asText(); + } + return null; + } + + @Override + public boolean has(String key) { + return json.has(key); + } + + @Override + public Set keySet() { + Iterator it = json.fieldNames(); + Set set = new HashSet<>(); + while (it.hasNext()) { + set.add(it.next()); + } + return set; + } + + @Override + public String toString() { + return json.toString(); + } + +} diff --git a/okhttps-jackson/src/main/java/com/ejlchina/okhttps/JacksonMsgConvertor.java b/okhttps-jackson/src/main/java/com/ejlchina/okhttps/JacksonMsgConvertor.java new file mode 100644 index 00000000..95863559 --- /dev/null +++ b/okhttps-jackson/src/main/java/com/ejlchina/okhttps/JacksonMsgConvertor.java @@ -0,0 +1,132 @@ +package com.ejlchina.okhttps; + +import com.ejlchina.okhttps.internal.HttpException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.type.CollectionType; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JacksonMsgConvertor implements MsgConvertor, ConvertProvider { + + private ObjectMapper objectMapper; + + private final Map> cache = new HashMap<>(); + + + public JacksonMsgConvertor() { + this(new ObjectMapper()); + } + + public JacksonMsgConvertor(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public String mediaType() { + return "application/json"; + } + + @Override + public Mapper toMapper(InputStream in, Charset charset) { + try { + JsonNode json = objectMapper.readTree(in); + if (json.isObject()) { + return new JacksonMapper((ObjectNode) json); + } + if (json.isNull() || json.isMissingNode()) { + return null; + } + throw new HttpException("�是 一个 json 对象:" + json); + } catch (IOException e) { + throw new HttpException("Jackson 解�异常", e); + } + } + + @Override + public Array toArray(InputStream in, Charset charset) { + try { + JsonNode json = objectMapper.readTree(in); + if (json.isArray()) { + return new JacksonArray((ArrayNode) json); + } + if (json.isNull() || json.isMissingNode()) { + return null; + } + throw new HttpException("�是 一个 json 数组:" + json); + } catch (IOException e) { + throw new HttpException("Jackson 解�异常", e); + } + } + + @Override + public byte[] serialize(Object object, Charset charset) { + try { + return objectMapper.writeValueAsString(object).getBytes(charset); + } catch (JsonProcessingException e) { + throw new HttpException("Java Bean [" + object + "] Jackson �列化异常", e); + } + } + + private TypeReference toTypeRef(Type type) { + TypeReference typeRef; + synchronized (cache) { + //noinspection unchecked + typeRef = (TypeReference) cache.get(type); + if (typeRef == null) { + typeRef = new TypeReference() { + @Override + public Type getType() { + return type; + } + }; + cache.put(type, typeRef); + } + } + return typeRef; + } + + @Override + public T toBean(Type type, InputStream in, Charset charset) { + try { + return objectMapper.readValue(in, toTypeRef(type)); + } catch (IOException e) { + throw new HttpException("Jackson 解�异常", e); + } + } + + @Override + public List toList(Class type, InputStream in, Charset charset) { + CollectionType javaType = objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, type); + try { + return objectMapper.readValue(in, javaType); + } catch (IOException e) { + throw new HttpException("Jackson 解�异常", e); + } + } + + @Override + public MsgConvertor getConvertor() { + return new JacksonMsgConvertor(); + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + public void setObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + +} diff --git a/okhttps-jackson/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider b/okhttps-jackson/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider deleted file mode 100644 index dd5ee602..00000000 --- a/okhttps-jackson/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider +++ /dev/null @@ -1 +0,0 @@ -cn.zhxu.okhttps.jackson.JacksonMsgConvertor \ No newline at end of file diff --git a/okhttps-jackson/src/main/resources/META-INF/services/com.ejlchina.okhttps.ConvertProvider b/okhttps-jackson/src/main/resources/META-INF/services/com.ejlchina.okhttps.ConvertProvider new file mode 100644 index 00000000..ddfa7bde --- /dev/null +++ b/okhttps-jackson/src/main/resources/META-INF/services/com.ejlchina.okhttps.ConvertProvider @@ -0,0 +1 @@ +com.ejlchina.okhttps.JacksonMsgConvertor \ No newline at end of file diff --git a/okhttps-jackson/src/test/java/cn/zhxu/okhttps/jackson/TestCase.java b/okhttps-jackson/src/test/java/cn/zhxu/okhttps/jackson/TestCase.java deleted file mode 100644 index 74ea88f9..00000000 --- a/okhttps-jackson/src/test/java/cn/zhxu/okhttps/jackson/TestCase.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.zhxu.okhttps.jackson; - -import cn.zhxu.okhttps.MsgConvertor; -import cn.zhxu.okhttps.test.JsonTestCases; -import org.junit.Test; - - -public class TestCase { - - @Test - public void doTest() throws Exception { - MsgConvertor convertor = new JacksonMsgConvertor(); - new JsonTestCases(convertor).run(); - } - -} diff --git a/okhttps-jackson/src/test/java/com/ejlchina/okhttps/TestCase.java b/okhttps-jackson/src/test/java/com/ejlchina/okhttps/TestCase.java new file mode 100644 index 00000000..d9783489 --- /dev/null +++ b/okhttps-jackson/src/test/java/com/ejlchina/okhttps/TestCase.java @@ -0,0 +1,15 @@ +package com.ejlchina.okhttps; + +import com.ejlchina.okhttps.test.JsonMsgConvertorTest; +import org.junit.Test; + + +public class TestCase { + + @Test + public void doTest() throws Exception { + MsgConvertor msgConvertor = new JacksonMsgConvertor(); + new JsonMsgConvertorTest(msgConvertor).run(); + } + +} diff --git a/okhttps-snack3/README.md b/okhttps-snack3/README.md deleted file mode 100644 index 22af945b..00000000 --- a/okhttps-snack3/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# OkHttps Gson - -#### 介� - -基于 OkHttps 与 Gson 集�扩展 - - -##### Maven - -```xml - - cn.zhxu - okhttps-snack3 - 3.5.3 - -``` - -##### Gradle - -```groovy -implementation 'cn.zhxu:okhttps-snack3:3.5.3' -``` - -#### 更多请�阅 [https://ok.zhxu.cn/](https://ok.zhxu.cn/) diff --git a/okhttps-snack3/pom.xml b/okhttps-snack3/pom.xml deleted file mode 100644 index 364fdeae..00000000 --- a/okhttps-snack3/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - 4.0.0 - - okhttps-snack3 - OkHttps Snack3 - jar - - - cn.zhxu - okhttps-parent - 4.1.0 - - - - - cn.zhxu - okhttps - ${project.version} - - - cn.zhxu - data-snack3 - ${data.version} - - - cn.zhxu - okhttps-test - ${project.version} - test - - - - - okhttps-snack3 - - \ No newline at end of file diff --git a/okhttps-snack3/src/main/java/cn/zhxu/okhttps/snack3/Snack3MsgConvertor.java b/okhttps-snack3/src/main/java/cn/zhxu/okhttps/snack3/Snack3MsgConvertor.java deleted file mode 100644 index 2c56dd55..00000000 --- a/okhttps-snack3/src/main/java/cn/zhxu/okhttps/snack3/Snack3MsgConvertor.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.zhxu.okhttps.snack3; - -import cn.zhxu.data.snack3.Snack3DataConvertor; -import cn.zhxu.okhttps.ConvertProvider; -import cn.zhxu.okhttps.MsgConvertor; - -public class Snack3MsgConvertor extends Snack3DataConvertor implements MsgConvertor, ConvertProvider { - - @Override - public String mediaType() { - return "application/json"; - } - - @Override - public MsgConvertor getConvertor() { - return new Snack3MsgConvertor(); - } - -} - - - - - - - - - - - - - - diff --git a/okhttps-snack3/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider b/okhttps-snack3/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider deleted file mode 100644 index a546ae29..00000000 --- a/okhttps-snack3/src/main/resources/META-INF/services/cn.zhxu.okhttps.ConvertProvider +++ /dev/null @@ -1 +0,0 @@ -cn.zhxu.okhttps.snack3.Snack3MsgConvertor \ No newline at end of file diff --git a/okhttps-snack3/src/test/java/cn/zhxu/okhttps/snack3/TestCase.java b/okhttps-snack3/src/test/java/cn/zhxu/okhttps/snack3/TestCase.java deleted file mode 100644 index 9ebb192f..00000000 --- a/okhttps-snack3/src/test/java/cn/zhxu/okhttps/snack3/TestCase.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.zhxu.okhttps.snack3; - -import cn.zhxu.okhttps.MsgConvertor; -import cn.zhxu.okhttps.test.JsonTestCases; -import org.junit.Test; - - -public class TestCase { - - @Test - public void doTest() throws Exception { - MsgConvertor msgConvertor = new Snack3MsgConvertor(); - new JsonTestCases(msgConvertor).run(); - } - -} diff --git a/okhttps-stomp/README.md b/okhttps-stomp/README.md deleted file mode 100644 index ccc3ce2c..00000000 --- a/okhttps-stomp/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# OkHttps Stomp - -#### 介� - -基于 OkHttps 的 Stomp �议实现 - - -##### Maven - -```xml - - cn.zhxu - okhttps-stomp - 3.5.3 - -``` - -##### Gradle - -```groovy -implementation 'cn.zhxu:okhttps-stomp:3.5.3' -``` - -#### 更多请�阅 [https://ok.zhxu.cn/v2/stomp.html](https://ok.zhxu.cn/v2/stomp.html) diff --git a/okhttps-stomp/pom.xml b/okhttps-stomp/pom.xml deleted file mode 100644 index 3ccaa125..00000000 --- a/okhttps-stomp/pom.xml +++ /dev/null @@ -1,38 +0,0 @@ - - 4.0.0 - - okhttps-stomp - OkHttps Stomp - jar - - - cn.zhxu - okhttps-parent - 4.1.0 - - - - - cn.zhxu - okhttps - ${project.version} - - - cn.zhxu - okhttps-test - ${project.version} - test - - - org.jetbrains - annotations - 13.0 - compile - - - - - okhttps-stomp - - - \ No newline at end of file diff --git a/okhttps-stomp/src/main/java/cn/zhxu/stomp/Commands.java b/okhttps-stomp/src/main/java/cn/zhxu/stomp/Commands.java deleted file mode 100644 index 9a59bb4c..00000000 --- a/okhttps-stomp/src/main/java/cn/zhxu/stomp/Commands.java +++ /dev/null @@ -1,63 +0,0 @@ -package cn.zhxu.stomp; - -/** - * 命令 - */ -public class Commands { - - /** - * �起连接 - */ - public static final String CONNECT = "CONNECT"; - - /** - * 已连接 - */ - public static final String CONNECTED = "CONNECTED"; - - /** - * 断开连接 - */ - public static final String DISCONNECT = "DISCONNECT"; - - /** - * ��消� - */ - public static final String SEND = "SEND"; - - /** - * 收到消� - */ - public static final String MESSAGE = "MESSAGE"; - - /** - * 收到凭� - */ - public static final String RECEIPT = "RECEIPT"; - - /** - * �起订阅 - */ - public static final String SUBSCRIBE = "SUBSCRIBE"; - - /** - * �起退订 - */ - public static final String UNSUBSCRIBE = "UNSUBSCRIBE"; - - /** - * 确认消� - */ - public static final String ACK = "ACK"; - - /** - * 收到错误 - */ - public static final String ERROR = "ERROR"; - - /** - * 未知命令 - */ - public static final String UNKNOWN = "UNKNOWN"; - -} diff --git a/okhttps-stomp/src/main/java/cn/zhxu/stomp/Header.java b/okhttps-stomp/src/main/java/cn/zhxu/stomp/Header.java deleted file mode 100644 index e3441c96..00000000 --- a/okhttps-stomp/src/main/java/cn/zhxu/stomp/Header.java +++ /dev/null @@ -1,78 +0,0 @@ -package cn.zhxu.stomp; - -public class Header { - - /** - * 订阅者 ID - */ - public static final String ID = "id"; - - /** - * 接�版本 - */ - public static final String VERSION = "accept-version"; - - /** - * 心跳 - */ - public static final String HEART_BEAT = "heart-beat"; - - /** - * 目的地 - */ - public static final String DESTINATION = "destination"; - - /** - * 内容类型 - */ - public static final String CONTENT_TYPE = "content-type"; - - /** - * 消� ID - */ - public static final String MESSAGE_ID = "message-id"; - - /** - * 凭� - */ - public static final String RECEIPT = "receipt"; - - /** - * 凭� ID - */ - public static final String RECEIPT_ID = "receipt-id"; - - /** - * 订阅者 - */ - public static final String SUBSCRIPTION = "subscription"; - - /** - * 确认 - */ - public static final String ACK = "ack"; - - - private final String key; - private final String value; - - - public Header(String key, String value) { - this.key = key; - this.value = value; - } - - public String getKey() { - return key; - } - - public String getValue() { - return value; - } - - @Override - public String toString() { - return key + ':' + value; - } - -} diff --git a/okhttps-stomp/src/main/java/cn/zhxu/stomp/Message.java b/okhttps-stomp/src/main/java/cn/zhxu/stomp/Message.java deleted file mode 100644 index 6df8a05a..00000000 --- a/okhttps-stomp/src/main/java/cn/zhxu/stomp/Message.java +++ /dev/null @@ -1,57 +0,0 @@ -package cn.zhxu.stomp; - -import java.util.List; - - -public class Message { - - private final String command; - private final List
headers; - private final String payload; - - public Message(String command, List
headers) { - this(command, headers, null); - } - - public Message(String command, List
headers, String payload) { - this.command = command; - this.headers = headers; - this.payload = payload; - } - - public List
getHeaders() { - return headers; - } - - public String getPayload() { - return payload; - } - - public String getCommand() { - return command; - } - - - public String headerValue(String key) { - Header header = header(key); - if (header != null) { - return header.getValue(); - } - return null; - } - - public Header header(String key) { - if (headers != null) { - for (Header header : headers) { - if (header.getKey().equals(key)) return header; - } - } - return null; - } - - @Override - public String toString() { - return "Message {command='" + command + "', headers=" + headers +", payload='" + payload + "'}"; - } - -} diff --git a/okhttps-stomp/src/main/java/cn/zhxu/stomp/MsgCodec.java b/okhttps-stomp/src/main/java/cn/zhxu/stomp/MsgCodec.java deleted file mode 100644 index 1f83addd..00000000 --- a/okhttps-stomp/src/main/java/cn/zhxu/stomp/MsgCodec.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.zhxu.stomp; - -import java.util.function.Consumer; - -/** - * Stomp 消�编解�器 - * @since v3.1.0 - */ -public interface MsgCodec { - - /** - * 编� - * @param input Stomp 消� - * @return 编��的文本 - */ - String encode(Message input); - - /** - * 解� - * @param input 输入 - * @param out 输出 - */ - void decode(String input, Consumer out); - -} diff --git a/okhttps-stomp/src/main/java/cn/zhxu/stomp/MsgCodecImpl.java b/okhttps-stomp/src/main/java/cn/zhxu/stomp/MsgCodecImpl.java deleted file mode 100644 index bcdb6fa3..00000000 --- a/okhttps-stomp/src/main/java/cn/zhxu/stomp/MsgCodecImpl.java +++ /dev/null @@ -1,222 +0,0 @@ -package cn.zhxu.stomp; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -/** - * Stomp 消�编解�器 - * @since v3.1.0 - */ -public class MsgCodecImpl implements MsgCodec { - - /** - * Command 结�符 - */ - private String commandEnd = "\n"; - - /** - * Headers 结�符 - */ - private String headersEnd = "\n\n"; - - /** - * Header 之间的分隔符 - */ - private String headerDelimiter = "\n"; - - /** - * Header 键值 间的分隔符 - */ - private String headerKvDelimiter = ":"; - - /** - * Body 结�符 - */ - private String bodyEnd = "\u0000"; - - /** - * 待解�的 Stomp 报文容器 - */ - private final StringBuilder pending = new StringBuilder(); - - @Override - public String encode(Message input) { - StringBuilder sb = new StringBuilder(); - sb.append(input.getCommand()).append(commandEnd); - List
headers = msgHeaders(input); - int hCnt = headers.size(); - for (int index = 0; index < hCnt; index++) { - Header header = headers.get(index); - sb.append(header.getKey()) - .append(headerKvDelimiter) - .append(header.getValue()); - if (index < hCnt - 1) { - sb.append(headerDelimiter); - } - } - sb.append(headersEnd); - String payload = input.getPayload(); - if (payload != null) { - sb.append(payload); - } - sb.append(bodyEnd); - return sb.toString(); - } - - protected List
msgHeaders(Message input) { - List
headers = input.getHeaders(); - return new ArrayList<>(headers); - } - - @Override - public synchronized void decode(String input, Consumer out) { - if (input == null || input.isEmpty()) { - return; - } - // 装入待解�容器 - pending.append(input); - // 开始解� - decode(out, 0); - } - - /** - * @param out 输出 - * @param start 开始解�的问题 - */ - protected void decode(Consumer out, int start) { - cleanPendingStartData(start); - // Body 结尾符下标 - int bEndIdx = pending.indexOf(bodyEnd); - if (bEndIdx < 0) { - // 数�包尚未接收完毕,直接返回 - return; - } - // Command 结尾符下标 - int cEndIdx = pending.indexOf(commandEnd); - // Headers 结尾符下标 - int hEndIdx = pending.indexOf(headersEnd); - if (cEndIdx <= 0 || hEndIdx <= cEndIdx || bEndIdx <= hEndIdx) { - // �法数�包,跳过一个字符�新解� - decode(out, 1); - return; - } - // 解� Command - String command = pending.substring(0, cEndIdx).trim(); - if (!isCommand(command)) { - // �法数�包,跳过一个字符�新解� - decode(out, 1); - return; - } - // 解� Headers - List
headers = decodeHeaders(cEndIdx, hEndIdx); - // 解� Body - String payload = pending.substring(hEndIdx + headersEnd.length(), bEndIdx); - // 输出解�结果 - out.accept(createMessage(command, headers, payload)); - // �新解� bodyEnd 之�的数� - decode(out, bEndIdx + bodyEnd.length()); - } - - protected void cleanPendingStartData(int start) { - if (start > 0) { - // 清空 start 之�的数� - pending.delete(0, start); - } - // Command 开始符下标 - int index = 0; - for (int i = 0; i < pending.length(); i++) { - char c = pending.charAt(i); - if (isCommandChar(c)) { - index = i; - break; - } - } - if (index > 0) { - // 清空 Command 之�的数� - pending.delete(0, index); - } - } - - /** - * 解� Headers - * @param cEndIdx Command 结尾符下标 - * @param hEndIdx Headers 结尾符下标 - * @return Headers - */ - protected List
decodeHeaders(int cEndIdx, int hEndIdx) { - String[] strHeaders = pending.substring(cEndIdx + commandEnd.length(), hEndIdx) - .split(headerDelimiter); - List
headers = new ArrayList<>(strHeaders.length); - for (String header : strHeaders) { - String[] hv = header.split(headerKvDelimiter); - if (hv.length == 2) { - headers.add(new Header(hv[0], hv[1])); - } - } - return headers; - } - - protected boolean isCommand(String command) { - return !command.isEmpty() && command.matches("[A-Z]+"); - } - - protected boolean isCommandChar(char c) { - return c >= 'A' && c <= 'Z'; - } - - protected Message createMessage(String command, List
headers, String payload) { - return new Message(command, headers, payload); - } - - public String getCommandEnd() { - return commandEnd; - } - - public void setCommandEnd(String commandEnd) { - if (commandEnd != null) { - this.commandEnd = commandEnd; - } - } - - public String getHeadersEnd() { - return headersEnd; - } - - public void setHeadersEnd(String headersEnd) { - if (headersEnd != null) { - this.headersEnd = headersEnd; - } - } - - public String getBodyEnd() { - return bodyEnd; - } - - public void setBodyEnd(String bodyEnd) { - if (bodyEnd != null) { - this.bodyEnd = bodyEnd; - } - } - - public String getHeaderDelimiter() { - return headerDelimiter; - } - - public void setHeaderDelimiter(String headerDelimiter) { - if (headerDelimiter != null) { - this.headerDelimiter = headerDelimiter; - } - } - - public String getHeaderKvDelimiter() { - return headerKvDelimiter; - } - - public void setHeaderKvDelimiter(String headerKvDelimiter) { - if (headerKvDelimiter != null) { - this.headerKvDelimiter = headerKvDelimiter; - } - } - -} diff --git a/okhttps-stomp/src/main/java/cn/zhxu/stomp/Stomp.java b/okhttps-stomp/src/main/java/cn/zhxu/stomp/Stomp.java deleted file mode 100644 index f8555564..00000000 --- a/okhttps-stomp/src/main/java/cn/zhxu/stomp/Stomp.java +++ /dev/null @@ -1,470 +0,0 @@ -package cn.zhxu.stomp; - -import cn.zhxu.okhttps.Platform; -import cn.zhxu.okhttps.WHttpTask; -import cn.zhxu.okhttps.WebSocket; - -import java.util.*; -import java.util.function.Consumer; - -/** - * 基于 OkHttps websockt 的 Stomp 客户端 - */ -public class Stomp { - - private static final String TOPIC = "/topic"; - private static final String QUEUE = "/queue"; - - public static final String SUPPORTED_VERSIONS = "1.1,1.2"; - public static final String AUTO_ACK = "auto"; - public static final String CLIENT_ACK = "client"; - - private final boolean autoAck; - private boolean connected = false; // 是�已连接 - private boolean connecting = false; // 是�连接中 - private boolean disconnecting = false; // 是�断开连接中 - private final WHttpTask task; - private WebSocket websocket; - - private final List subscribers; - - private Consumer onConnected; - private Consumer onDisconnected; - private Consumer onException; - private Consumer onError; - - private final String disReceipt; - - private MsgCodec msgCodec = new MsgCodecImpl(); - - - private Stomp(WHttpTask task, boolean autoAck) { - this.task = task; - this.autoAck = autoAck; - this.subscribers = Collections.synchronizedList(new ArrayList<>()); - this.disReceipt = UUID.randomUUID().toString(); - } - - /** - * 构建 Stomp 客户端(自动确定消�) - * @param task 底层的 WebSocket 连接 - * @return Stomp - */ - public static Stomp over(WHttpTask task) { - return over(task, true); - } - - /** - * 构建 Stomp 客户端 - * @param task 底层的 WebSocket 连接 - * @param autoAck 是�自动确定消� - * @return Stomp - */ - public static Stomp over(WHttpTask task, boolean autoAck) { - return new Stomp(task, autoAck); - } - - /** - * @since 2.5.0 - * @return 是�自动确认消� - */ - public boolean isAutoAck() { - return autoAck; - } - - /** - * 连接 Stomp �务器 - * @return Stomp - */ - public Stomp connect() { - return connect(null); - } - - /** - * 连接 Stomp �务器 - * @param headers Stomp 头信� - * @return Stomp - */ - public synchronized Stomp connect(List
headers) { - if (connected || connecting) { - return this; - } - websocket = task - .setOnOpen((ws, res) -> doOnOpened(headers)) - .setOnMessage((ws, msg) -> msgCodec.decode(msg.toString(), this::receive)) - .setOnException((ws, e) -> doOnException(e)) - .setOnClosed((ws, close) -> doOnClosed(close)) - .listen(); - connecting = true; - disconnecting = false; - return this; - } - - private synchronized void doOnOpened(List
headers) { - if (websocket != null) { - int pingSecs = task.pingSeconds(); - int pongSecs = task.pongSeconds(); - List
cHeaders = new ArrayList<>(); - cHeaders.add(new Header(Header.VERSION, SUPPORTED_VERSIONS)); - if (pingSecs > 0 && pongSecs > 0) { - cHeaders.add(new Header(Header.HEART_BEAT, pingSecs * 1000 + "," + pongSecs * 1000)); - } - if (headers != null) { - cHeaders.addAll(headers); - } - send(new Message(Commands.CONNECT, cHeaders, null)); - } - } - - private synchronized void doOnException(Throwable throwable) { - Consumer listener = onException; - if (listener != null) { - listener.accept(throwable); - } - disconnecting = false; - connecting = false; - } - - private synchronized void doOnClosed(WebSocket.Close close) { - connected = false; - connecting = false; - disconnecting = false; - websocket = null; - for (Subscriber subscriber : subscribers) { - subscriber.resetStatus(); - } - Consumer listener = onDisconnected; - if (listener != null) { - listener.accept(close); - } - } - - /** - * @since 2.5.0 - * @return 是�已连接 - */ - public boolean isConnected() { - return connected && websocket != null; - } - - /** - * @since 3.1.0 - * @return 是�正在连接 - */ - public boolean isConnecting() { - return connecting && websocket != null; - } - - /** - * @since 3.1.0 - * @return 是�正在断开连接 - */ - public boolean isDisconnecting() { - return disconnecting && websocket != null; - } - - /** - * 断开连接,将先�� DISCONNECT 消�给�务器,�务器回��断开连接 - * 默认等待�务器为 10 秒,10秒�自动关闭 - */ - public void disconnect() { - disconnect(10); - } - - /** - * @since v3.1.0 - * 断开连接,将先�� DISCONNECT 消�给�务器,�务器回��断开连接 - * @param maxWaitSeconds 最大等待�务器回�时间,超出�自动关闭 - */ - public void disconnect(int maxWaitSeconds) { - new Timer().schedule(new TimerTask() { - @Override - public void run() { - disconnect(true); - } - }, 1000L * maxWaitSeconds); - Header header = new Header(Header.RECEIPT, disReceipt); - List
headers = Collections.singletonList(header); - send(new Message(Commands.DISCONNECT, headers)); - disconnecting = true; - connecting = false; - } - - /** - * 断开连接 - * @param immediate 是�立�断开 - * @since v3.1.0 - */ - public synchronized void disconnect(boolean immediate) { - if (immediate) { - WebSocket ws = websocket; - if (ws != null) { - ws.close(1000, "disconnect by user"); - websocket = null; - } - } else { - disconnect(10); - } - } - - /** - * 连接�功回调 - * @param onConnected 连接�功回调 - * @return Stomp - */ - public Stomp setOnConnected(Consumer onConnected) { - this.onConnected = onConnected; - return this; - } - - /** - * 连接断开回调 - * @param onDisconnected 断开连接回调 - * @return Stomp - */ - public Stomp setOnDisconnected(Consumer onDisconnected) { - this.onDisconnected = onDisconnected; - return this; - } - - /** - * 错误回调(底层连接异常) - * @since v3.1.1 - * @param onException 异常回调 - * @return Stomp - */ - public Stomp setOnException(Consumer onException) { - this.onException = onException; - return this; - } - - /** - * 错误回调(�务器返回的错误信�) - * @param onError 错误回调 - * @return Stomp - */ - public Stomp setOnError(Consumer onError) { - this.onError = onError; - return this; - } - - /** - * @since 2.5.0 - * ��消�到主题 - * @param destination 目的地 - * @param data 消� - */ - public void sendToTopic(String destination, String data) { - sendTo(TOPIC + destination, data); - } - - /** - * @since 2.5.0 - * ��消�到队列 - * @param destination 目的地 - * @param data 消� - */ - public void sendToQueue(String destination, String data) { - sendTo(QUEUE + destination, data); - } - - /** - * ��消�到指定目的地 - * @param destination 目的地 - * @param data 消� - */ - public void sendTo(String destination, String data) { - send(new Message(Commands.SEND, Collections.singletonList(new Header(Header.DESTINATION, destination)), data)); - } - - /** - * ��消�给�务器 - * @param message 消� - */ - public void send(Message message) { - WebSocket ws = websocket; - if (ws == null) { - throw new IllegalArgumentException("You must call connect before send"); - } - ws.send(msgCodec.encode(message)); - } - - /** - * 监�主题消� - * @param destination 监�地� - * @param callback 消�回调 - * @return Stomp - */ - public Stomp topic(String destination, Consumer callback) { - return topic(destination, null, callback); - } - - /** - * 监�主题消� - * @param destination 监�地� - * @param headers 附加头信� - * @param callback 消�回调 - * @return Stomp - */ - public Stomp topic(String destination, List
headers, Consumer callback) { - return subscribe(TOPIC + destination, headers, callback); - } - - /** - * 监�队列消� - * @param destination 监�地� - * @param callback 消�回调 - * @return Stomp - */ - public Stomp queue(String destination, Consumer callback) { - return queue(destination, null, callback); - } - - /** - * 监�队列消� - * @param destination 监�地� - * @param headers 附加头信� - * @param callback 消�回调 - * @return Stomp - */ - public Stomp queue(String destination, List
headers, Consumer callback) { - return subscribe(QUEUE + destination, headers, callback); - } - - /** - * 订阅消� - * @param destination 订阅地� - * @param headers 附加头信� - * @param callback 消�回调 - * @return Stomp - */ - public synchronized Stomp subscribe(String destination, List
headers, Consumer callback) { - if (destination == null || destination.isEmpty()) { - throw new IllegalArgumentException("destination can not be empty!"); - } - for (Subscriber s: subscribers) { - if (s.destinationEqual(destination)) { - Platform.logError("The destination [" + destination + "] has already been subscribed!"); - return this; - } - } - Subscriber subscriber = new Subscriber(this, destination, callback, headers); - subscribers.add(subscriber); - subscriber.subscribe(); - return this; - } - - /** - * 确认收到��消� - * @param message �务器�过�的消� - */ - public void ack(Message message) { - Header subscription = message.header(Header.SUBSCRIPTION); - Header msgId = message.header(Header.MESSAGE_ID); - if (subscription != null || msgId != null) { - List
headers = new ArrayList<>(); - headers.add(subscription); - headers.add(msgId); - send(new Message(Commands.ACK, headers, null)); - } else { - Platform.logError("subscription and message-id not found in " + message + ", so it can not be ack!"); - } - } - - /** - * �消主题监� - * @param destination 监�地� - */ - public void untopic(String destination) { - unsubscribe(TOPIC + destination); - } - - /** - * �消队列监� - * @param destination 监�地� - */ - public void unqueue(String destination) { - unsubscribe(QUEUE + destination); - } - - /** - * �消订阅 - * @param destination 订阅地� - */ - public synchronized void unsubscribe(String destination) { - Iterator it = subscribers.iterator(); - while (it.hasNext()) { - Subscriber s = it.next(); - if (s.destinationEqual(destination)) { - s.unsubscribe(); - it.remove(); - break; - } - } - } - - private synchronized void receive(Message msg) { - String command = msg.getCommand(); - if (Commands.CONNECTED.equals(command)) { - String hbHeader = msg.headerValue(Header.HEART_BEAT); - synchronized (this) { - connected = true; - connecting = false; - onConnectedFrameReceived(hbHeader); - } - } else if (Commands.MESSAGE.equals(command)) { - String id = msg.headerValue(Header.SUBSCRIPTION); - if (id != null) { - for (Subscriber s: subscribers) { - if (s.tryCallback(id, msg)) { - break; - } - } - } - } else if (Commands.RECEIPT.equals(command)) { - if (disReceipt.equals(msg.headerValue(Header.RECEIPT_ID))) { - // 断开连接 - disconnect(true); - } - } else if (Commands.ERROR.equals(command)) { - Consumer listener = onError; - if (listener != null) { - listener.accept(msg); - } - connecting = false; - } - } - - private void onConnectedFrameReceived(String hbHeader) { - int pingSecs = task.pingSeconds(); - int pongSecs = task.pongSeconds(); - if (hbHeader != null && (pingSecs > 0 || pongSecs > 0)) { - String[] heartbeats = hbHeader.split(","); - int pingSeconds = Integer.parseInt(heartbeats[1]) / 1000; - int pongSeconds = Integer.parseInt(heartbeats[0]) / 1000; - if (pingSeconds > 0 || pongSeconds > 0) { - if (task.pingSupplier() == null) { - task.pingSupplier(() -> "\n"); - } - task.heatbeat(Math.max(pingSeconds, pingSecs), Math.max(pongSeconds, pongSecs)); - } - } - Consumer listener = onConnected; - if (listener != null) { - listener.accept(this); - } - // 为兼容 Android 低版本,这里�使用 JDK8 的 List.forEach(..) 方法 - for (Subscriber subscriber: subscribers) { - subscriber.subscribe(); - } - } - - public MsgCodec getMsgCodec() { - return msgCodec; - } - - public void setMsgCodec(MsgCodec msgCodec) { - this.msgCodec = msgCodec; - } - -} diff --git a/okhttps-stomp/src/main/java/cn/zhxu/stomp/Subscriber.java b/okhttps-stomp/src/main/java/cn/zhxu/stomp/Subscriber.java deleted file mode 100644 index 0378012a..00000000 --- a/okhttps-stomp/src/main/java/cn/zhxu/stomp/Subscriber.java +++ /dev/null @@ -1,87 +0,0 @@ -package cn.zhxu.stomp; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; -import java.util.function.Consumer; - - -public class Subscriber { - - private final Stomp stomp; - private final String id; - private final String destination; - private final Consumer callback; - private final List
headers; - private boolean subscribed; - - public Subscriber(Stomp stomp, String destination, Consumer callback, List
headers) { - this.stomp = stomp; - this.id = UUID.randomUUID().toString(); - this.destination = destination; - this.callback = callback; - this.headers = headers; - } - - public void subscribe() { - synchronized (stomp) { - if (subscribed) { - return; - } - if (stomp.isConnected()) { - doSubscribe(); - } - } - } - - private void doSubscribe() { - List
headers = new ArrayList<>(); - headers.add(new Header(Header.ID, id)); - headers.add(new Header(Header.DESTINATION, destination)); - boolean ackNotAdded = true; - if (this.headers != null) { - for (Header header : this.headers) { - if (Header.ACK.equals(header.getKey())) { - ackNotAdded = false; - } - String key = header.getKey(); - if (!Header.ID.equals(key) && !Header.DESTINATION.equals(key)) { - headers.add(header); - } - } - } - if (ackNotAdded) { - headers.add(new Header(Header.ACK, stomp.isAutoAck() ? Stomp.AUTO_ACK : Stomp.CLIENT_ACK)); - } - stomp.send(new Message(Commands.SUBSCRIBE, headers, null)); - subscribed = true; - } - - public void unsubscribe() { - synchronized (stomp) { - if (stomp.isConnected()) { - List
headers = Collections.singletonList(new Header(Header.ID, id)); - stomp.send(new Message(Commands.UNSUBSCRIBE, headers, null)); - } - } - resetStatus(); - } - - public void resetStatus() { - subscribed = false; - } - - public boolean destinationEqual(String destination) { - return destination != null && destination.equals(this.destination); - } - - public boolean tryCallback(String id, Message msg) { - if (this.id.equals(id)) { - callback.accept(msg); - return true; - } - return false; - } - -} diff --git a/okhttps-stomp/src/test/java/cn/zhxu/stomp/MsgCodecTestCases.java b/okhttps-stomp/src/test/java/cn/zhxu/stomp/MsgCodecTestCases.java deleted file mode 100644 index 86eff6c8..00000000 --- a/okhttps-stomp/src/test/java/cn/zhxu/stomp/MsgCodecTestCases.java +++ /dev/null @@ -1,78 +0,0 @@ -package cn.zhxu.stomp; - -import org.junit.Assert; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - - -public class MsgCodecTestCases { - - static class DecodeCallback implements Consumer { - - private final List list = new ArrayList<>(); - - @Override - public void accept(Message data) { - list.add(data); - } - - public List getList() { - return list; - } - } - - - @Test - public void testMsgCodecImplDecode1() { - MsgCodec msgCodec = new MsgCodecImpl(); - DecodeCallback callback = new DecodeCallback(); - msgCodec.decode("MESSAGE\nk1:v1\nk2:v2\n\n123456\u0000", callback); - List list = callback.getList(); - Assert.assertEquals(1, list.size()); - Message message = list.get(0); - Assert.assertEquals("MESSAGE", message.getCommand()); - Assert.assertEquals("123456", message.getPayload()); - Assert.assertEquals("v1", message.headerValue("k1")); - Assert.assertEquals("v2", message.headerValue("k2")); - } - - @Test - public void testMsgCodecImplDecode2() { - MsgCodec msgCodec = new MsgCodecImpl(); - DecodeCallback callback = new DecodeCallback(); - msgCodec.decode(" \n MESSAGE\nk1:v1\nk2:v2\n\n123456\u0000", callback); - List list = callback.getList(); - Assert.assertEquals(1, list.size()); - Message message = list.get(0); - Assert.assertEquals("MESSAGE", message.getCommand()); - Assert.assertEquals("123456", message.getPayload()); - Assert.assertEquals("v1", message.headerValue("k1")); - Assert.assertEquals("v2", message.headerValue("k2")); - } - - @Test - public void testMsgCodecImplDecode3() { - MsgCodec msgCodec = new MsgCodecImpl(); - DecodeCallback callback = new DecodeCallback(); - msgCodec.decode(" \n ", callback); - msgCodec.decode(" MESSAGE\nk1:", callback); - msgCodec.decode("v1\nk2:v2\n\n12345", callback); - msgCodec.decode("6\u0000", callback); - msgCodec.decode("\n\nMS", callback); - msgCodec.decode("\n\nMES", callback); - msgCodec.decode("SAGE\nk1:", callback); - msgCodec.decode("v1\nk2:v2\n\n12345", callback); - msgCodec.decode("6\u0000", callback); - List list = callback.getList(); - Assert.assertEquals(2, list.size()); - Message message = list.get(1); - Assert.assertEquals("MESSAGE", message.getCommand()); - Assert.assertEquals("123456", message.getPayload()); - Assert.assertEquals("v1", message.headerValue("k1")); - Assert.assertEquals("v2", message.headerValue("k2")); - } - -} diff --git a/okhttps-stomp/src/test/java/cn/zhxu/stomp/Test.java b/okhttps-stomp/src/test/java/cn/zhxu/stomp/Test.java deleted file mode 100644 index 0564b09e..00000000 --- a/okhttps-stomp/src/test/java/cn/zhxu/stomp/Test.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.zhxu.stomp; - -import cn.zhxu.okhttps.OkHttps; - -import java.util.ArrayList; -import java.util.List; - -public class Test { - - public static void main(String[] args) throws InterruptedException { - - List
headers = new ArrayList<>(); - headers.add(new Header("login", "xxx")); - headers.add(new Header("passcode", "xxx")); - headers.add(new Header("host", "test")); - - Stomp stomp = Stomp.over(OkHttps.webSocket("ws://172.31.0.202:15674/ws").heatbeat(10, 10)) - .setOnConnected(s -> { - System.out.println("OnConnected"); - }) - .connect(headers); - - stomp.topic("/aaa", msg -> { - System.out.println("收到消�:" + msg); - System.out.println("内容:" + msg.getPayload()); - }); - - Thread.sleep(1000000); - - } - -} diff --git a/okhttps-test/.gitignore b/okhttps-test/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/okhttps-test/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/okhttps-test/pom.xml b/okhttps-test/pom.xml index cddf23cb..abaf915a 100644 --- a/okhttps-test/pom.xml +++ b/okhttps-test/pom.xml @@ -6,34 +6,22 @@ jar - cn.zhxu + com.ejlchina okhttps-parent - 4.1.0 + 2.3.0 - cn.zhxu + com.ejlchina okhttps - ${project.version} - - - com.squareup.okhttp3 - mockwebserver - ${okhttp.version} + ${okhttps.version} junit junit ${junit.version} - - - com.sun.xml.bind - jaxb-impl - 2.3.5 - - diff --git a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/BaseTest.java b/okhttps-test/src/main/java/cn/zhxu/okhttps/test/BaseTest.java deleted file mode 100644 index 9b218884..00000000 --- a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/BaseTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.zhxu.okhttps.test; - -import java.text.SimpleDateFormat; -import java.util.Date; - -import okhttp3.mockwebserver.MockWebServer; - -public class BaseTest { - - protected MockWebServer server = new MockWebServer(); - - protected String mockUrl = "http://" + server.getHostName() + ":" + server.getPort(); - - - public static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - public static void println() { - System.out.println(); - } - - public static void println(Object x) { - System.out.println(x); - } - - public static void println(long t0, String str) { - System.out.println(now() - t0 + "\t" + str); - } - - - public static void log(String str) { - SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); - System.out.println(sdf.format(new Date()) + "\t" + str); - } - - public static long now() { - return System.currentTimeMillis(); - } - -} diff --git a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/BaseTestCases.java b/okhttps-test/src/main/java/cn/zhxu/okhttps/test/BaseTestCases.java deleted file mode 100644 index ad49040c..00000000 --- a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/BaseTestCases.java +++ /dev/null @@ -1,347 +0,0 @@ -package cn.zhxu.okhttps.test; - -import cn.zhxu.data.Array; -import cn.zhxu.data.Mapper; -import cn.zhxu.data.TypeRef; -import cn.zhxu.okhttps.HTTP; -import cn.zhxu.okhttps.MsgConvertor; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.Assert; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -public abstract class BaseTestCases { - - final MockWebServer server = new MockWebServer(); - - final HTTP http; - - final MsgConvertor msgConvertor; - - final User user1 = new User(1, "Jack"); - final User user2 = new User(1, "Tom"); - - public BaseTestCases(MsgConvertor msgConvertor) { - this.http = HTTP.builder() - .baseUrl("http://" + server.getHostName() + ":" + server.getPort()) - .addMsgConvertor(msgConvertor) - .addMsgConvertor(new MsgConvertor.FormConvertor(msgConvertor)) - .build(); - this.msgConvertor = msgConvertor; - } - - public void run() throws Exception { - test_01_toMapper(); - test_02_toArray(); - test_03_serializeBean(); - test_04_toBean(); - test_05_toResult(); - test_06_toList(); - test_07_httpOnResponse(); - test_08_httpOnResBody(); - test_09_httpOnResMapper(); - test_10_httpOnResArray(); - test_11_httpOnResBean(); - test_12_httpOnResList(); - test_13_httpOnResString(); - test_14_httpMultiCallback(); - test_15_httpMultiCallback(); - testHttpSync(); - } - - abstract String getUser1Str(); - - abstract String getUser1ResultStr(); - - abstract String getUserListStr(); - - void assertUser1(User user) { - Assert.assertEquals(1, user.getId()); - Assert.assertEquals("Jack", user.getName()); - } - - void assertUser2(User user) { - Assert.assertEquals(2, user.getId()); - Assert.assertEquals("Tom", user.getName()); - } - - void assertMapper(Mapper mapper, int id, String name) { - Assert.assertEquals(2, mapper.size()); - mapper.forEach((key, data) -> { - if ("id".equals(key)) { - Assert.assertEquals(id, data.toInt()); - } - if ("name".equals(key)) { - Assert.assertEquals(name, data.toString()); - } - }); - Set keys = mapper.keySet(); - Assert.assertEquals(2, keys.size()); - Assert.assertTrue(keys.contains("id")); - Assert.assertTrue(keys.contains("name")); - Assert.assertFalse(mapper.isEmpty()); - Assert.assertEquals(2, mapper.size()); - Assert.assertEquals(id, mapper.getInt("id")); - Assert.assertFalse(mapper.has("age")); - Assert.assertEquals(0, mapper.getInt("age")); - Assert.assertEquals(name, mapper.getString("name")); - } - - void assertMapper1(Mapper mapper) { - assertMapper(mapper, 1, "Jack"); - assertUser1(mapper.toBean(User.class)); - } - - void assertMapper2(Mapper mapper) { - assertMapper(mapper, 2, "Tom"); - assertUser2(mapper.toBean(User.class)); - } - - void assertList(List list) { - Assert.assertEquals(2, list.size()); - assertUser1(list.get(0)); - assertUser2(list.get(1)); - } - - void assertArray(Array array) { - Assert.assertEquals(2, array.size()); - array.forEach((index, data) -> { - if (index == 0) { - assertMapper1(data.toMapper()); - } - if (index == 1) { - assertMapper2(data.toMapper()); - } - }); - assertMapper1(array.getMapper(0)); - assertMapper2(array.getMapper(1)); - assertList(array.toList(User.class)); - } - - void assertUser1Result(Result result) { - Assert.assertEquals(200, result.getCode()); - Assert.assertEquals("ok", result.getMsg()); - assertUser1(result.getData()); - } - - void test_01_toMapper() { - InputStream in = new ByteArrayInputStream(getUser1Str().getBytes(StandardCharsets.UTF_8)); - assertMapper1(msgConvertor.toMapper(in, StandardCharsets.UTF_8)); - assertMapper1(msgConvertor.toMapper(getUser1Str())); - System.out.println("case 01 passed!"); - } - - void test_02_toArray() { - InputStream in = new ByteArrayInputStream(getUserListStr().getBytes(StandardCharsets.UTF_8)); - assertArray(msgConvertor.toArray(in, StandardCharsets.UTF_8)); - assertArray(msgConvertor.toArray(getUserListStr())); - System.out.println("case 02 passed!"); - } - - void test_03_serializeBean() { - byte[] data = msgConvertor.serialize(user1, StandardCharsets.UTF_8); - Assert.assertEquals(getUser1Str(), new String(data, StandardCharsets.UTF_8)); - Assert.assertEquals(getUser1Str(), msgConvertor.serialize(user1)); - System.out.println("case 03 passed!"); - } - - void test_04_toBean() { - String json = getUser1Str(); - InputStream in = new ByteArrayInputStream(json.getBytes()); - assertUser1(msgConvertor.toBean(User.class, in, StandardCharsets.UTF_8)); - assertUser1(msgConvertor.toBean(User.class, json)); - System.out.println("case 04 passed!"); - } - - void test_05_toResult() { - String json = getUser1ResultStr(); - InputStream in = new ByteArrayInputStream(json.getBytes()); - Type type = new TypeRef>() {}.getType(); - assertUser1Result(msgConvertor.toBean(type, in, StandardCharsets.UTF_8)); - assertUser1Result(msgConvertor.toBean(type, json)); - System.out.println("case 05 passed!"); - } - - void test_06_toList() { - String json = getUserListStr(); - InputStream in = new ByteArrayInputStream(json.getBytes()); - assertList(msgConvertor.toList(User.class, in, StandardCharsets.UTF_8)); - assertList(msgConvertor.toList(User.class, json)); - System.out.println("case 06 passed!"); - } - - void test_07_httpOnResponse() throws InterruptedException { - server.enqueue(new MockResponse().setBody(getUser1Str())); - CountDownLatch latch = new CountDownLatch(1); - http.async("/") - .setOnResponse(res -> { - assertUser1(res.getBody().toBean(User.class)); - latch.countDown(); - }) - .setOnException(Throwable::printStackTrace) - .get(); - Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); - System.out.println("case 07 passed!"); - } - - void test_08_httpOnResBody() throws InterruptedException { - server.enqueue(new MockResponse().setBody(getUser1Str())); - CountDownLatch latch = new CountDownLatch(1); - http.async("/") - .setOnResBody(body -> { - assertUser1(body.toBean(User.class)); - latch.countDown(); - }) - .setOnException(Throwable::printStackTrace) - .get(); - Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); - System.out.println("case 08 passed!"); - } - - void test_09_httpOnResMapper() throws InterruptedException { - server.enqueue(new MockResponse().setBody(getUser1Str())); - CountDownLatch latch = new CountDownLatch(1); - http.async("/") - .setOnResMapper(mapper -> { - assertMapper1(mapper); - latch.countDown(); - }) - .setOnException(Throwable::printStackTrace) - .get(); - Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); - System.out.println("case 09 passed!"); - } - - void test_10_httpOnResArray() throws InterruptedException { - server.enqueue(new MockResponse().setBody(getUserListStr())); - CountDownLatch latch = new CountDownLatch(1); - http.async("/") - .setOnResArray(array -> { - assertArray(array); - latch.countDown(); - }) - .setOnException(Throwable::printStackTrace) - .get(); - Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); - System.out.println("case 10 passed!"); - } - - void test_11_httpOnResBean() throws InterruptedException { - server.enqueue(new MockResponse().setBody(getUser1Str())); - CountDownLatch latch = new CountDownLatch(1); - http.async("/") - .setOnResBean(User.class, user -> { - assertUser1(user); - latch.countDown(); - }) - .setOnException(Throwable::printStackTrace) - .get(); - Assert.assertTrue(latch.await(1000, TimeUnit.SECONDS)); - System.out.println("case 11 passed!"); - } - - void test_12_httpOnResList() throws InterruptedException { - server.enqueue(new MockResponse().setBody(getUserListStr())); - CountDownLatch latch = new CountDownLatch(1); - http.async("/") - .setOnResList(User.class, list -> { - assertList(list); - latch.countDown(); - }) - .setOnException(Throwable::printStackTrace) - .get(); - Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); - System.out.println("case 12 passed!"); - } - - void test_13_httpOnResString() throws InterruptedException { - String body = getUser1Str(); - server.enqueue(new MockResponse().setBody(body)); - CountDownLatch latch = new CountDownLatch(1); - http.async("/") - .setOnResString(str -> { - Assert.assertEquals(body, str); - latch.countDown(); - }) - .setOnException(Throwable::printStackTrace) - .get(); - Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); - System.out.println("case 13 passed!"); - } - - void test_14_httpMultiCallback() throws InterruptedException { - String user1Str = getUser1Str(); - server.enqueue(new MockResponse().setBody(user1Str)); - CountDownLatch latch = new CountDownLatch(5); - http.async("/") - .setOnResponse(res -> { - assertUser1(res.getBody().toBean(User.class)); - latch.countDown(); - }) - .setOnResBody(body -> { - assertMapper1(body.toMapper()); - latch.countDown(); - }) - .setOnResMapper(mapper -> { - assertMapper1(mapper); - latch.countDown(); - }) - .setOnResBean(User.class, user -> { - assertUser1(user); - latch.countDown(); - }) - .setOnResString(str -> { - Assert.assertEquals(user1Str, str); - latch.countDown(); - }) - .setOnException(Throwable::printStackTrace) - .get(); - Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); - System.out.println("case 14 passed!"); - } - - - void test_15_httpMultiCallback() throws InterruptedException { - String userListStr = getUserListStr(); - server.enqueue(new MockResponse().setBody(userListStr)); - CountDownLatch latch = new CountDownLatch(5); - http.async("/") - .setOnResponse(res -> { - assertList(res.getBody().toList(User.class)); - latch.countDown(); - }) - .setOnResBody(body -> { - assertArray(body.toArray()); - latch.countDown(); - }) - .setOnResList(User.class ,list -> { - assertList(list); - latch.countDown(); - }) - .setOnResArray(array -> { - assertArray(array); - latch.countDown(); - }) - .setOnResString(str -> { - Assert.assertEquals(userListStr, str); - latch.countDown(); - }) - .setOnException(Throwable::printStackTrace) - .get(); - Assert.assertTrue(latch.await(1, TimeUnit.SECONDS)); - System.out.println("case 15 passed!"); - } - - void testHttpSync() { - - } - -} diff --git a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/JsonTestCases.java b/okhttps-test/src/main/java/cn/zhxu/okhttps/test/JsonTestCases.java deleted file mode 100644 index 72019c51..00000000 --- a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/JsonTestCases.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.zhxu.okhttps.test; - -import cn.zhxu.okhttps.MsgConvertor; -import org.junit.Assert; - -import java.nio.charset.StandardCharsets; - - -public class JsonTestCases extends BaseTestCases { - - public JsonTestCases(MsgConvertor msgConvertor) { - super(msgConvertor); - } - - @Override - public void run() throws Exception { - super.run(); - testSerializeList(); - } - - @Override - String getUser1Str() { - return "{\"id\":1,\"name\":\"Jack\"}"; - } - - @Override - String getUser1ResultStr() { - return "{\"code\": 200, \"data\": {\"id\":1,\"name\":\"Jack\"}, \"msg\": \"ok\"}"; - } - - @Override - String getUserListStr() { - return "[{\"id\":1,\"name\":\"Jack\"},{\"id\":2,\"name\":\"Tom\"}]"; - } - - void testSerializeList() { - User u1 = new User(1, "Jack"); - User u2 = new User(2, "Tom"); - User[] list = new User[] {u1, u2}; - byte[] data = msgConvertor.serialize(list, StandardCharsets.UTF_8); - String json = new String(data, StandardCharsets.UTF_8); - Assert.assertEquals(getUserListStr(), json); - System.out.println("case 4 passed!"); - } - -} diff --git a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/XmlTestCases.java b/okhttps-test/src/main/java/cn/zhxu/okhttps/test/XmlTestCases.java deleted file mode 100644 index 399a311d..00000000 --- a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/XmlTestCases.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.zhxu.okhttps.test; - -import cn.zhxu.okhttps.MsgConvertor; - - -public class XmlTestCases extends BaseTestCases { - - public XmlTestCases(MsgConvertor msgConvertor) { - super(msgConvertor); - } - - @Override - String getUser1Str() { - return "1Jack"; - } - - @Override - String getUser1ResultStr() { - return "\n" + - "\t200\n" + - "\tok\n" + - "\t\n" + - "\t\t1Jack\n" + - "\t\n" + - ""; - } - - @Override - String getUserListStr() { - return "1Jack2Tom"; - } - -} diff --git a/okhttps-test/src/main/java/com/ejlchina/okhttps/test/BaseMsgConvertorTest.java b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/BaseMsgConvertorTest.java new file mode 100644 index 00000000..4078bb69 --- /dev/null +++ b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/BaseMsgConvertorTest.java @@ -0,0 +1,125 @@ +package com.ejlchina.okhttps.test; + +import com.ejlchina.okhttps.Array; +import com.ejlchina.okhttps.Mapper; +import com.ejlchina.okhttps.MsgConvertor; +import com.ejlchina.okhttps.TypeRef; +import org.junit.Assert; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Set; + +public abstract class BaseMsgConvertorTest { + + MsgConvertor msgConvertor; + + public BaseMsgConvertorTest(MsgConvertor msgConvertor) { + this.msgConvertor = msgConvertor; + } + + public void run() throws Exception { + testToMapper(); + testToArray(); + testSerializeBean(); + testSerializeList(); + testToBean(); + testToResult(); + testToList(); + } + + abstract String getUserObjectStr(); + + abstract String getResultUserObjectStr(); + + abstract String getUserListStr(); + + void testToMapper() { + String json = getUserObjectStr(); + InputStream in = new ByteArrayInputStream(json.getBytes()); + Mapper mapper = msgConvertor.toMapper(in, StandardCharsets.UTF_8); + Set keys = mapper.keySet(); + Assert.assertEquals(2, keys.size()); + Assert.assertTrue(keys.contains("id")); + Assert.assertTrue(keys.contains("name")); + Assert.assertFalse(mapper.isEmpty()); + Assert.assertEquals(2, mapper.size()); + Assert.assertEquals(1, mapper.getInt("id")); + Assert.assertFalse(mapper.has("age")); + Assert.assertEquals(0, mapper.getInt("age")); + Assert.assertEquals("Jack", mapper.getString("name")); + System.out.println("case 1 passed!"); + } + + void testToArray() { + String json = getUserListStr(); + InputStream in = new ByteArrayInputStream(json.getBytes()); + Array jsonObj = msgConvertor.toArray(in, StandardCharsets.UTF_8); + Assert.assertFalse(jsonObj.isEmpty()); + Assert.assertEquals(2, jsonObj.size()); + Mapper json1 = jsonObj.getMapper(0); + Mapper json2 = jsonObj.getMapper(1); + Assert.assertEquals(1, json1.getInt("id")); + Assert.assertEquals("Jack", json1.getString("name")); + Assert.assertEquals(2, json2.getInt("id")); + Assert.assertEquals("Tom", json2.getString("name")); + System.out.println("case 2 passed!"); + } + + void testSerializeBean() { + byte[] data = msgConvertor.serialize(new User(1, "Jack"), StandardCharsets.UTF_8); + String json = new String(data, StandardCharsets.UTF_8); + Assert.assertEquals(getUserObjectStr(), json); + System.out.println("case 3 passed!"); + } + + void testSerializeList() { + User u1 = new User(1, "Jack"); + User u2 = new User(2, "Tom"); + User[] list = new User[] {u1, u2}; + byte[] data = msgConvertor.serialize(list, StandardCharsets.UTF_8); + String json = new String(data, StandardCharsets.UTF_8); + Assert.assertEquals(getUserListStr(), json); + System.out.println("case 4 passed!"); + } + + void testToBean() { + String json = getUserObjectStr(); + InputStream in = new ByteArrayInputStream(json.getBytes()); + User user = msgConvertor.toBean(User.class, in, StandardCharsets.UTF_8); + Assert.assertEquals(1, user.getId()); + Assert.assertEquals("Jack", user.getName()); + System.out.println("case 6 passed!"); + } + + void testToResult() { + String json = getResultUserObjectStr(); + InputStream in = new ByteArrayInputStream(json.getBytes()); + Result result = msgConvertor.toBean(new TypeRef>(){}.getType(), in, StandardCharsets.UTF_8); + Assert.assertEquals(200, result.getCode()); + Assert.assertEquals("ok", result.getMsg()); + User user = result.getData(); + Assert.assertEquals(1, user.getId()); + Assert.assertEquals("Jack", user.getName()); + System.out.println("case 7 passed!"); + } + + void testToList() { + String json = getUserListStr(); + InputStream in = new ByteArrayInputStream(json.getBytes()); + List users = msgConvertor.toList(User.class, in, StandardCharsets.UTF_8); + Assert.assertEquals(2, users.size()); + User u1 = users.get(0); + User u2 = users.get(1); + Assert.assertEquals(1, u1.getId()); + Assert.assertEquals("Jack", u1.getName()); + Assert.assertEquals(2, u2.getId()); + Assert.assertEquals("Tom", u2.getName()); + System.out.println("case 8 passed!"); + } + +} diff --git a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/DateBean.java b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/DateBean.java similarity index 93% rename from okhttps-test/src/main/java/cn/zhxu/okhttps/test/DateBean.java rename to okhttps-test/src/main/java/com/ejlchina/okhttps/test/DateBean.java index 9030919d..34cfdb53 100644 --- a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/DateBean.java +++ b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/DateBean.java @@ -1,4 +1,4 @@ -package cn.zhxu.okhttps.test; +package com.ejlchina.okhttps.test; import java.util.Date; diff --git a/okhttps-test/src/main/java/com/ejlchina/okhttps/test/JsonMsgConvertorTest.java b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/JsonMsgConvertorTest.java new file mode 100644 index 00000000..279792f5 --- /dev/null +++ b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/JsonMsgConvertorTest.java @@ -0,0 +1,27 @@ +package com.ejlchina.okhttps.test; + +import com.ejlchina.okhttps.MsgConvertor; + + +public class JsonMsgConvertorTest extends BaseMsgConvertorTest { + + public JsonMsgConvertorTest(MsgConvertor msgConvertor) { + super(msgConvertor); + } + + @Override + String getUserObjectStr() { + return "{\"id\":1,\"name\":\"Jack\"}"; + } + + @Override + String getResultUserObjectStr() { + return "{\"code\": 200, \"data\": {\"id\":1,\"name\":\"Jack\"}, \"msg\": \"ok\"}"; + } + + @Override + String getUserListStr() { + return "[{\"id\":1,\"name\":\"Jack\"},{\"id\":2,\"name\":\"Tom\"}]"; + } + +} diff --git a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/Result.java b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/Result.java similarity index 82% rename from okhttps-test/src/main/java/cn/zhxu/okhttps/test/Result.java rename to okhttps-test/src/main/java/com/ejlchina/okhttps/test/Result.java index 42232d56..01123c47 100644 --- a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/Result.java +++ b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/Result.java @@ -1,8 +1,5 @@ -package cn.zhxu.okhttps.test; +package com.ejlchina.okhttps.test; -import javax.xml.bind.annotation.XmlRootElement; - -@XmlRootElement public class Result { int code; diff --git a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/User.java b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/User.java similarity index 83% rename from okhttps-test/src/main/java/cn/zhxu/okhttps/test/User.java rename to okhttps-test/src/main/java/com/ejlchina/okhttps/test/User.java index e76583f1..deb7c6dd 100644 --- a/okhttps-test/src/main/java/cn/zhxu/okhttps/test/User.java +++ b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/User.java @@ -1,13 +1,10 @@ -package cn.zhxu.okhttps.test; +package com.ejlchina.okhttps.test; - -import javax.xml.bind.annotation.XmlRootElement; - -@XmlRootElement public class User { private int id; private String name; + public User() { } @@ -34,12 +31,9 @@ public String getName() { } @Override - public String toString() { - return "User{" + - "id=" + id + - ", name='" + name + '\'' + - '}'; - } + public String toString() { + return "User [id=" + id + ", name=" + name + "]"; + } @Override public int hashCode() { diff --git a/okhttps-test/src/main/java/com/ejlchina/okhttps/test/XmlMsgConvertorTest.java b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/XmlMsgConvertorTest.java new file mode 100644 index 00000000..689c7493 --- /dev/null +++ b/okhttps-test/src/main/java/com/ejlchina/okhttps/test/XmlMsgConvertorTest.java @@ -0,0 +1,27 @@ +package com.ejlchina.okhttps.test; + +import com.ejlchina.okhttps.MsgConvertor; + + +public class XmlMsgConvertorTest extends BaseMsgConvertorTest { + + public XmlMsgConvertorTest(MsgConvertor msgConvertor) { + super(msgConvertor); + } + + @Override + String getUserObjectStr() { + return "1Jack"; + } + + @Override + String getResultUserObjectStr() { + return null; + } + + @Override + String getUserListStr() { + return "1Jack2Tom"; + } + +} diff --git a/okhttps/.gitignore b/okhttps/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/okhttps/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/okhttps/README.md b/okhttps/README.md deleted file mode 100644 index 4950d947..00000000 --- a/okhttps/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# OkHttps - -#### 介� - -基于 OkHttps 核心模� - - -##### Maven - -```xml - - cn.zhxu - okhttps - 3.5.3 - -``` - -##### Gradle - -```groovy -implementation 'cn.zhxu:okhttps:3.5.3' -``` - -#### 更多请�阅 [https://ok.zhxu.cn/](https://ok.zhxu.cn/) diff --git a/okhttps/pom.xml b/okhttps/pom.xml index be91b92b..d21e2207 100644 --- a/okhttps/pom.xml +++ b/okhttps/pom.xml @@ -6,32 +6,16 @@ jar - cn.zhxu + com.ejlchina okhttps-parent - 4.1.0 + 2.3.0 - - cn.zhxu - data-core - ${data.version} - com.squareup.okhttp3 okhttp ${okhttp.version} - - - okio - com.squareup.okio - - - - - com.squareup.okio - okio - ${okio.version} com.squareup.okhttp3 diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/BodyPara.java b/okhttps/src/main/java/cn/zhxu/okhttps/BodyPara.java deleted file mode 100644 index ca88bd81..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/BodyPara.java +++ /dev/null @@ -1,25 +0,0 @@ -package cn.zhxu.okhttps; - -/** - * 请求体表��数 - * @since v4.1.0 - */ -public class BodyPara { - - private final String type; - private final Object value; - - public BodyPara(String type, Object value) { - this.type = type; - this.value = value; - } - - public String getType() { - return type; - } - - public Object getValue() { - return value; - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/Download.java b/okhttps/src/main/java/cn/zhxu/okhttps/Download.java deleted file mode 100644 index fe622132..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/Download.java +++ /dev/null @@ -1,368 +0,0 @@ -package cn.zhxu.okhttps; - -import java.io.*; -import java.util.function.Consumer; - -/** - * 文件下载 - * @author Troy.Zhou - */ -public class Download { - - private final File file; - private final InputStream input; - private final TaskExecutor taskExecutor; - private final Ctrl ctrl; - - private Consumer onSuccess; - private Consumer onFailure; - private Consumer onComplete; - private long doneBytes; - private int buffSize = 0; - private long seekBytes = 0; - private boolean appended; - private volatile Status status; - private final Object lock = new Object(); - - protected boolean nextOnIO = false; - private boolean sOnIO; - private boolean fOnIO; - private boolean cOnIO; - - public Download(File file, InputStream input, TaskExecutor taskExecutor, long skipBytes) { - this.file = file; - this.input = input; - this.taskExecutor = taskExecutor; - this.seekBytes = skipBytes; - this.ctrl = new Ctrl(); - } - - /** - * 设置缓冲区大�,默认 2K(2048) - * @param buffSize 缓冲区大�(��:字节) - * @return Download - */ - public Download setBuffSize(int buffSize) { - if (buffSize > 0) { - this.buffSize = buffSize; - } - return this; - } - - /** - * 设置文件追加模� - * 用预断点续传和分�下载 - * @return Download - */ - public Download setAppended() { - this.appended = true; - return this; - } - - /** - * 设置文件指针,从文件的 seekBytes �置追加内容 - * �有�� setAppended() 方法一起�会有作用 - * @param seekBytes 跨越的字节数 - * @return Download - */ - public Download setFilePointer(long seekBytes) { - this.seekBytes = seekBytes; - return this; - } - - /** - * 在IO线程执行 - * @return Download - */ - public Download nextOnIO() { - nextOnIO = true; - return this; - } - - /** - * 设置下载�功回调 - * @param onSuccess �功回调函数 - * @return Download - */ - public Download setOnSuccess(Consumer onSuccess) { - this.onSuccess = onSuccess; - sOnIO = nextOnIO; - nextOnIO = false; - return this; - } - - /** - * 设置下载失败回调(�消�执行) - * @param onFailure 失败回调函数 - * @return Download - */ - public Download setOnFailure(Consumer onFailure) { - this.onFailure = onFailure; - fOnIO = nextOnIO; - nextOnIO = false; - return this; - } - - /** - * @since 3.2.0 - * 设置下载结�回调(�功�失败��消都执行) - * @param onComplete 结�回调函数 - * @return Download - */ - public Download setOnComplete(Consumer onComplete) { - this.onComplete = onComplete; - cOnIO = nextOnIO; - nextOnIO = false; - return this; - } - - /** - * 开始下载 - * @return 下载控制器 - */ - public Ctrl start() { - if (buffSize == 0) { - buffSize = Process.DEFAULT_STEP_BYTES; - } - RandomAccessFile raFile = randomAccessFile(); - if (raFile != null) { - status = Status.DOWNLOADING; - taskExecutor.execute(() -> doDownload(raFile), true); - } - return ctrl; - } - - /** - * 获�下载控制器 - * @return Ctrl - */ - public Ctrl getCtrl() { - return ctrl; - } - - /** - * 下载状� - * @since v3.2.0 - */ - public enum Status { - - /** - * 已�消 - */ - CANCELED(-1), - - /** - * 下载中 - */ - DOWNLOADING(1), - - /** - * 已暂� - */ - PAUSED(2), - - /** - * �功下载完� - */ - DONE(3), - - /** - * ��错误 - */ - ERROR(4); - - private final int value; - - Status(int value) { - this.value = value; - } - - public int value() { - return value; - } - - } - - /** - * 下载控制器 - */ - public class Ctrl { - - /** - * @return 下载状�(v3.2.0 之�方法 返回整型常�,v3.2.0 开始修改为枚举) - */ - public Status status() { - return status; - } - - /** - * 暂�下载任务(�有处于下载中状��能暂��功) - * @return 是�暂��功 - */ - public boolean pause() { - synchronized (lock) { - if (status == Status.DOWNLOADING) { - status = Status.PAUSED; - return true; - } - } - return false; - } - - /** - * 继续下载任务(�有处于暂�状��能���功) - * @return 是����功 - */ - public boolean resume() { - synchronized (lock) { - if (status == Status.PAUSED) { - status = Status.DOWNLOADING; - return true; - } - } - return false; - } - - /** - * �消下载任务(�有处于暂�或下载中状��能�消�功) - * @return 是��消�功 - */ - public boolean cancel() { - synchronized (lock) { - if (status == Status.PAUSED || status == Status.DOWNLOADING) { - status = Status.CANCELED; - return true; - } - } - return false; - } - - } - - public class Failure { - - private final IOException exception; - - Failure(IOException exception) { - this.exception = exception; - } - - /** - * @return 下载文件 - */ - public File getFile() { - return file; - } - - /** - * @return 本次已下载字节数 - */ - public long getDoneBytes() { - return doneBytes; - } - - /** - * @return 异常信� - */ - public IOException getException() { - return exception; - } - - } - - private RandomAccessFile randomAccessFile() { - try { - return new RandomAccessFile(file, "rw"); - } catch (FileNotFoundException e) { - status = Status.ERROR; - closeQuietly(input); - fireOnComplete(); - fireOnFailure(e); - } - return null; - } - - private void doDownload(RandomAccessFile raFile) { - try { - if (appended && seekBytes > 0) { - // 使支�并行下载到�一个文件 - raFile.seek(seekBytes); - } - while (status != Status.CANCELED && status != Status.DONE) { - if (status == Status.DOWNLOADING) { - byte[] buff = new byte[buffSize]; - int len; - while ((len = input.read(buff)) != -1) { - raFile.write(buff, 0, len); - doneBytes += len; - if (status == Status.CANCELED - || status == Status.PAUSED) { - break; - } - } - if (len == -1) { - synchronized (lock) { - if (status != Status.CANCELED) { - status = Status.DONE; - } - } - } - } - } - } catch (IOException e) { - synchronized (lock) { - if (status != Status.CANCELED) { - status = Status.ERROR; - } - } - if (status == Status.ERROR) { - fireOnFailure(e); - } - } finally { - closeQuietly(raFile); - closeQuietly(input); - if (status == Status.CANCELED && !file.delete()) { - Platform.logError("can not delete canceled file: " + file); - } - fireOnComplete(); - } - if (status == Status.DONE) { - fireOnSuccess(); - } - } - - private void fireOnComplete() { - Consumer onComplete = this.onComplete; - if (onComplete != null) { - taskExecutor.execute(() -> onComplete.accept(status), cOnIO); - } - } - - private void fireOnSuccess() { - Consumer onSuccess = this.onSuccess; - if (onSuccess != null) { - taskExecutor.execute(() -> onSuccess.accept(file), sOnIO); - } - } - - private void fireOnFailure(IOException e) { - Consumer onFailure = this.onFailure; - if (onFailure != null) { - taskExecutor.execute(() -> onFailure.accept(new Failure(e)), fOnIO); - } else { - throw new OkHttpsException("Download failed: ", e); - } - } - - public static void closeQuietly(Closeable closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (RuntimeException rethrown) { - throw rethrown; - } catch (Exception ignored) { - } - } - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/DownloadHelper.java b/okhttps/src/main/java/cn/zhxu/okhttps/DownloadHelper.java deleted file mode 100644 index 37dfac6d..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/DownloadHelper.java +++ /dev/null @@ -1,135 +0,0 @@ -package cn.zhxu.okhttps; - -import java.io.File; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - -/** - * 下载助手,用户解�下载路径与文件� - */ -public class DownloadHelper { - - private Map extMappings = new HashMap<>(); - - - public DownloadHelper() { - extMappings.put("text/plain", "txt"); - extMappings.put("audio/mpeg", "mp3"); - extMappings.put("video/mpeg4", "mp4"); - extMappings.put("video/quicktime", "mov"); - extMappings.put("application/x-genesis-rom", "md"); - extMappings.put("application/x-xls", "xls"); - extMappings.put("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx"); - extMappings.put("application/vnd.android.package-archive", "apk"); - extMappings.put("application/msword", "doc"); - extMappings.put("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx"); - extMappings.put("application/x-java-archive", "jar"); - extMappings.put("application/x-zip-compressed", "zip"); - extMappings.put("application/x-compressed-tar", "tar.gz"); - extMappings.put("application/x-rar", "rar"); - } - - /** - * 解�下载文件� - * @param result HTTP �应结果 - * @return 文件� - */ - public String resolveFileName(HttpResult result) { - String contentDisposition = result.getHeader("Content-Disposition"); - // 通过 Content-Disposition 获�文件�,这点跟�务器有关,需��活�通 - if (contentDisposition == null || contentDisposition.length() < 1) { - String urlPath = result.getTask().getUrl(); - String urlName = urlPath.substring(urlPath.lastIndexOf("/") + 1); - return toFileName(urlName, result).replaceAll("[\"\\\\]", ""); - } - try { - String filename = URLDecoder.decode(contentDisposition.substring( - contentDisposition.indexOf("filename=") + 9), StandardCharsets.UTF_8.name()); - // 有些文件�会被包�在""里�,所以�去掉,�然无法读�文件�缀 - // �时去掉 / 与 \ 符,���生目录穿越问题:https://github.com/troyzhxu/okhttps/issues/89 - return filename.replaceAll("[\"/\\\\]", ""); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } - } - - public String toFileName(String name, HttpResult result) { - int dotIdx = name.lastIndexOf('.'); - if (dotIdx > -1 && dotIdx < name.length() - 1) { - return name; - } - // 获�文件扩展� - String ext = getExt(result.getHeader("Content-Type")); - if (ext != null) { - if (dotIdx == -1) { - return name + "." + ext; - } - return name + ext; - } - return name; - } - - - public String getExt(String contentType) { - String ext = extMappings.get(contentType); - if (ext == null) { - int i = contentType.indexOf('/'); - if (i > 0 && i < contentType.length() - 1) { - return contentType.substring(i + 1); - } - } - return ext; - } - - /** - * 解�文件路径 - * @param dirPath 目录 - * @param fileName 文件� - * @return 文件路径 - */ - public String resolveFilePath(String dirPath, String fileName) { - if (dirPath.endsWith("\\") || dirPath.endsWith("/")) { - return dirPath + fileName; - } - return dirPath + File.separator + fileName; - } - - /** - * 当文件已存在时,根�该方法生�一个新的文件� - * @param fileName 原文件� - * @param index ��次数 - * @return 新的文件� - */ - public String indexFileName(String fileName, int index) { - int i = fileName.lastIndexOf('.'); - if (i < 0) { - return fileName + "(" + index + ")"; - } - String ext = fileName.substring(i); - if (i > 0) { - String name = fileName.substring(0, i); - return name + "(" + index + ")" + ext; - } - return "(" + index + ")" + ext; - } - - public Map getExtMappings() { - return extMappings; - } - - public void setExtMappings(Map extMappings) { - this.extMappings = extMappings; - } - - public void addExtMappings(Map extMappings) { - this.extMappings.putAll(extMappings); - } - - public void addExtMapping(String mediaType, String ext) { - extMappings.put(mediaType, ext); - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/FilePara.java b/okhttps/src/main/java/cn/zhxu/okhttps/FilePara.java deleted file mode 100644 index 6c8443eb..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/FilePara.java +++ /dev/null @@ -1,86 +0,0 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.okhttps.internal.StreamRequestBody; -import okhttp3.MediaType; -import okhttp3.RequestBody; - -import java.io.File; -import java.io.InputStream; -import java.util.Arrays; - -/** - * @since 2.5.0 从 HttpTask 中分离 - */ -public class FilePara { - - private final String type; - private final String fileName; - private final byte[] content; - private final File file; - private final InputStream stream; - - public FilePara(String type, String fileName, byte[] content) { - this(type, fileName, null, content, null); - } - - public FilePara(String type, String fileName, File file) { - this(type, fileName, file, null, null); - } - - public FilePara(String type, String fileName, InputStream stream) { - this(type, fileName, null, null, stream); - } - - private FilePara(String type, String fileName, File file, byte[] content, InputStream stream) { - this.type = type; - this.fileName = fileName; - this.file = file; - this.content = content; - this.stream = stream; - } - - public RequestBody toRequestBody(MediaType contentType) { - if (file != null) { - return RequestBody.create(contentType, file); - } - if (content != null) { - return RequestBody.create(contentType, content); - } - if (stream != null) { - return new StreamRequestBody(contentType, stream); - } - throw new IllegalStateException("Invalid FilePara"); - } - - public String getType() { - return type; - } - - public String getFileName() { - return fileName; - } - - public byte[] getContent() { - return content; - } - - public File getFile() { - return file; - } - - public InputStream getStream() { - return stream; - } - - @Override - public String toString() { - return "FilePara{" + - "type='" + type + '\'' + - ", fileName='" + fileName + '\'' + - ", content=" + Arrays.toString(content) + - ", file=" + file + - ", stream=" + stream + - '}'; - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/HTTP.java b/okhttps/src/main/java/cn/zhxu/okhttps/HTTP.java deleted file mode 100644 index 910d87f0..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/HTTP.java +++ /dev/null @@ -1,335 +0,0 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.okhttps.okhttp.OkHttpBuilderImpl; -import okhttp3.WebSocket; -import okhttp3.*; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executor; - -/** - * HTTP 客户端接� - * - * @author 15735 - */ -public interface HTTP { - - String GET = "GET"; - String HEAD = "HEAD"; - String POST = "POST"; - String PUT = "PUT"; - String PATCH = "PATCH"; - String DELETE = "DELETE"; - - /** - * �步请求 - * @param url 请求地� - * @return �步HTTP任务 - */ - SHttpTask sync(String url); - - /** - * 异步请求 - * @param url 请求地� - * @return 异步HTTP任务 - */ - AHttpTask async(String url); - - /** - * Websocket 连接 - * @param url 连接地� - * @return WebSocket 任务 - */ - WHttpTask webSocket(String url); - - /** - * 根�标签�消HTTP任务,��任务的标签包�指定的Tag就会被�消 - * 从 v1.0.2 开始支��消�步请求 - * @param tag 标签 - * @return 被�消的任务数� - */ - int cancel(String tag); - - /** - * @since 1.0.2 - * �消所有HTTP任务,包括�步和异步 - */ - void cancelAll(); - - /** - * OkHttp 的 HTTP 原生请求 (该请求��过 预处�器) - * @param request 请求 - * @return Call - */ - Call request(Request request); - - /** - * OkHttp 的 Websocket 原生请求(该请求��过 预处�器) - * @param request 请求 - * @param listener 监�器 - * @return Websocket - */ - WebSocket webSocket(Request request, WebSocketListener listener); - - /** - * 获�任务执行器 - * @return TaskExecutor - */ - TaskExecutor executor(); - - /** - * 新的构建器 - * @return Builder - */ - Builder newBuilder(); - - /** - * HTTP 构建器 - * 自 v3.5.0 起�通过系统环境�� 切�该方法返回的 构建器实现类 - * @return HTTP 构建器 - */ - static Builder builder() { - String className = System.getProperty(Builder.class.getName()); - if (Platform.isNotBlank(className)) { - try { - Class dClass = Class.forName(className); - if (Builder.class.isAssignableFrom(dClass)) { - return (Builder) dClass.getDeclaredConstructor().newInstance(); - } else { - throw new OkHttpsException("The implementation class [" + className + "] you specified is not a subclass of " + Builder.class.getName()); - } - } catch (ClassNotFoundException e) { - throw new OkHttpsException("The implementation class [" + className + "] you specified can not be found", e); - } catch (NoSuchMethodException e) { - throw new OkHttpsException("There is none default constructor in [" + className + "] you specified", e); - } catch (ReflectiveOperationException e) { - throw new OkHttpsException("[" + className + "] can not be instanced", e); - } - } - return new OkHttpBuilderImpl(); // 默认构建器 - } - - /** - * Http �置器 - * - */ - interface OkConfig { - - /** - * 使用 builder �置 HttpClient - * @param builder OkHttpClient 构建器 - */ - void config(OkHttpClient.Builder builder); - - } - - /** - * HTTP 构建器 - * 自 v3.3.0 开始接�化,在以�版本中将�步与 okhttp3 解耦 - */ - interface Builder { - - /** - * 自 v3.2.0 ��以多次调用 - * �置 OkHttpClient - * @param config �置器 - * @return Builder - */ - Builder config(OkConfig config); - - /** - * 设置 baseUrl - * @param baseUrl 全局URL�缀 - * @return Builder - */ - Builder baseUrl(String baseUrl); - - String baseUrl(); - - /** - * �置媒体类型 - * @param mediaTypes 媒体类型 - * @return Builder - */ - Builder mediaTypes(Map mediaTypes); - - /** - * �置媒体类型 - * @param key 媒体类型KEY - * @param value 媒体类型VALUE - * @return Builder - */ - Builder mediaTypes(String key, String value); - - Map getMediaTypes(); - - /** - * 清空 ContentType - * @return Builder - * @since v3.5.0 - */ - HTTP.Builder clearContentTypes(); - - /** - * �置支�的报文体类型 - * @param contentTypes 报文体类型列表 - * @return Builder - */ - Builder contentTypes(List contentTypes); - - /** - * �置支�的报文体类型 - * @param contentType 报文体类型 - * @return Builder - */ - Builder contentTypes(String contentType); - - String[] contentTypes(); - - /** - * 设置回调执行器,例如实现切�线程功能,�对异步请求有效 - * @param executor 回调执行器 - * @return Builder - */ - Builder callbackExecutor(Executor executor); - - Executor mainExecutor(); - - /** - * �置 任务调度器,�用的调度由 {@link WHttpTask#heatbeat(int, int) } 指定的心跳任务 - * 若��置,则生�一个 线程容�为 1 的 ScheduledThreadPoolExecutor 调度器 - * @since v2.3.0 - * @param scheduler 调度器 - * @return Builder - */ - Builder taskScheduler(Scheduler scheduler); - - Scheduler taskScheduler(); - - /** - * 添加�并行处�请求任务的预处�器 - * @param preprocessor 预处�器 - * @return Builder - */ - Builder addPreprocessor(Preprocessor preprocessor); - - /** - * 添加串行预处�器 - * @param preprocessor 预处�器 - * @return Builder - */ - Builder addSerialPreprocessor(Preprocessor preprocessor); - - /** - * 清空预处�器(包括串行预处�器) - * @since v2.5.0 - * @return Builder - */ - Builder clearPreprocessors(); - - Preprocessor[] preprocessors(); - - /** - * 最大预处�时间(�数,相当普通请求的超时时间) - * @param times 普通超时时间的�数,默认为 10 - * @return Builder - */ - Builder preprocTimeoutTimes(int times); - - int preprocTimeoutTimes(); - - /** - * 设置全局�应监� - * @param listener 监�器 - * @return Builder - */ - Builder responseListener(TaskListener listener); - - TaskListener responseListener(); - - /** - * 设置全局异常监� - * @param listener 监�器 - * @return Builder - */ - Builder exceptionListener(TaskListener listener); - - TaskListener exceptionListener(); - - /** - * 设置全局完�监� - * @param listener 监�器 - * @return Builder - */ - Builder completeListener(TaskListener listener); - - TaskListener completeListener(); - - /** - * 设置下载监�器 - * @param listener 监�器 - * @return Builder - */ - Builder downloadListener(DownListener listener); - - DownListener downloadListener(); - - /** - * @since v2.0.0 - * 添加消�转�器 - * @param msgConvertor JSON �务 - * @return Builder - */ - Builder addMsgConvertor(MsgConvertor msgConvertor); - - /** - * 清空消�转�器 - * @since v2.5.0 - * @return Builder - */ - Builder clearMsgConvertors(); - - MsgConvertor[] msgConvertors(); - - /** - * @since v2.0.0 - * 设置默认编�格� - * @param charset 编� - * @return Builder - */ - Builder charset(Charset charset); - - Charset charset(); - - /** - * @since v2.0.0 - * 设置默认请求体类型 - * @param bodyType 请求体类型 - * @return Builder - */ - Builder bodyType(String bodyType); - - String bodyType(); - - /** - * @since v3.4.2 - * 设置下载文件�解�器 - * @param resolver 解�器 - * @return Builder - */ - Builder downloadHelper(DownloadHelper resolver); - - DownloadHelper downloadHelper(); - - /** - * 构建 HTTP 实例 - * @return HTTP - */ - HTTP build(); - - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/HttpTask.java b/okhttps/src/main/java/cn/zhxu/okhttps/HttpTask.java deleted file mode 100644 index d7381c22..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/HttpTask.java +++ /dev/null @@ -1,984 +0,0 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.data.ArrayListMap; -import cn.zhxu.data.ListMap; -import cn.zhxu.okhttps.HttpResult.State; -import cn.zhxu.okhttps.internal.*; -import cn.zhxu.okhttps.internal.AbstractHttpClient.TagTask; -import okhttp3.*; -import okhttp3.internal.http.HttpMethod; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.ConnectException; -import java.net.SocketException; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Map; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -/** - * Created by 周旭(Troy.Zhou) on 2020/3/11. - */ -@SuppressWarnings("unchecked") -public abstract class HttpTask> implements Cancelable { - - private static final String DOT = "."; - private static final String MULTIPART = "multipart/"; - private static final String FORM = "x-www-form-urlencoded"; - - protected final AbstractHttpClient httpClient; - protected boolean nothrow; - protected boolean nextOnIO = false; - - private final String urlPath; - private String tag; - - private ListMap headers; - private ListMap pathParams; - private ListMap urlParams; - private ListMap bodyParams; - private ListMap files; - - private Object requestBody; - private String bodyType; // 都是�写形� - private String boundary; // MultipartBody 的 边界符 - private Consumer onProcess; - private boolean processOnIO; - private long stepBytes = 0; - private double stepRate = -1; - - private Object object; - - private TagTask tagTask; - private Cancelable canceler; - private Charset charset; - - protected boolean skipPreproc = false; - protected boolean skipSerialPreproc = false; - - - public HttpTask(AbstractHttpClient httpClient, String urlPath) { - this.httpClient = httpClient; - this.charset = httpClient.charset(); - this.bodyType = httpClient.bodyType(); - this.urlPath = urlPath; - } - - /** - * 获�请求任务的URL地� - * @return URL地� - */ - public String getUrl() { - return urlPath; - } - - /** - * @return 是�是 Websocket 通讯 - */ - public boolean isWebsocket() { - return false; - } - - /** - * @since 2.2.0 - * @return 是�是 �步 Http 请求 - */ - public boolean isSyncHttp() { - return false; - } - - /** - * @since 2.2.0 - * @return 是�是 异步 Http 请求 - */ - public boolean isAsyncHttp() { - return false; - } - - /** - * 获�请求任务的标签 - * @return 标签 - */ - public String getTag() { - return tag; - } - - public String getBodyType() { - return bodyType; - } - - /** - * 标签匹� - * 判断任务标签与指定的标签是�匹�(包�指定的标签) - * @param tag 标签 - * @return 是�匹� - */ - public boolean isTagged(String tag) { - String theTag = this.tag; - if (theTag != null && tag != null) { - return theTag.equals(tag) || theTag.startsWith(tag + DOT) || theTag.endsWith(DOT + tag) - || theTag.contains(DOT + tag + DOT); - } - return false; - } - - /** - * 获�请求任务的头信� - * @return 头信� - */ - public ListMap getHeaders() { - return headers; - } - - /** - * @since 2.4.0 - * @return 路径�数 - */ - public ListMap getPathParas() { - return pathParams; - } - - /** - * @since 2.4.0 - * @return URL�数(查询�数) - */ - public ListMap getUrlParas() { - return urlParams; - } - - /** - * @since 2.4.0 - * @return 报文体�数 - */ - public ListMap getBodyParas() { - return bodyParams; - } - - /** - * @since 2.4.0 - * @return 文件�数 - */ - public ListMap getFileParas() { - return files; - } - - /** - * @since 2.4.0 - * @return 报文体 - */ - public Object getRequestBody() { - return requestBody; - } - - /** - * 获得被绑定的对象 - * @return Object - */ - public Object getBound() { - return object; - } - - /** - * 设置在�生异常时��上抛出,设置�: - * 异步请求�以在异常回调内�获异常,�步请求在返回结果中找到该异常 - * @return HttpTask 实例 - */ - public C nothrow() { - this.nothrow = true; - return (C) this; - } - - /** - * 指定该请求跳过任何预处�器(包括串行和并行) - * @return HttpTask 实例 - */ - public C skipPreproc() { - this.skipPreproc = true; - return (C) this; - } - - /** - * 指定该请求跳过任何串行预处�器 - * @return HttpTask 实例 - */ - public C skipSerialPreproc() { - this.skipSerialPreproc = true; - return (C) this; - } - - /** - * @since 2.0.0.RC - * 为请求任务添加标签 - * @param tag 标签 - * @return HttpTask 实例 - */ - public C tag(String tag) { - if (tag != null) { - if (this.tag != null) { - this.tag = this.tag + DOT + tag; - } else { - this.tag = tag; - } - updateTagTask(); - } - return (C) this; - } - - /** - * @since 2.0.0 - * 设置该请求的编�格� - * @param charset 编�格� - * @return HttpTask 实例 - */ - public C charset(Charset charset) { - if (charset != null) { - this.charset = charset; - } - return (C) this; - } - - /** - * @since 2.0.0 - * 设置请求体的类型,如:form�json�xml�protobuf 等 - * @param type 请求类型 - * @return HttpTask 实例 - */ - public C bodyType(String type) { - if (type != null) { - this.bodyType = type.toLowerCase(); - } - return (C) this; - } - - /** - * 下一个回调在IO线程执行 - * @return HttpTask 实例 - */ - public C nextOnIO() { - nextOnIO = true; - return (C) this; - } - - /** - * 绑定一个对象 - * @param object 对象 - * @return HttpTask 实例 - */ - public C bind(Object object) { - this.object = object; - return (C) this; - } - - /** - * Basic Auth 认� - * @param username 用户� - * @param password 密� - * @return HttpTask 实例 - * @since v3.5.0 - */ - public C basicAuth(String username, String password) { - byte[] authData = (username + ':' + password).getBytes(StandardCharsets.UTF_8); - byte[] authBytes = Base64.getEncoder().encode(authData); - String authStr = new String(authBytes, StandardCharsets.UTF_8); - return addHeader("Authorization", "Basic " + authStr); - } - - /** - * Bearer Auth 认� - * @param token 令牌 - * @return HttpTask 实例 - * @since v3.5.0 - */ - public C bearerAuth(String token) { - return addHeader("Authorization", "Bearer " + token); - } - - /** - * 添加请求头 - * @param name 请求头� - * @param value 请求头值 - * @return HttpTask 实例 - */ - public C addHeader(String name, String value) { - if (name != null && value != null) { - if (headers == null) { - headers = new ArrayListMap<>(); - } - headers.put(name, value); - } - return (C) this; - } - - /** - * 添加请求头 - * @param headers 请求头集� - * @return HttpTask 实例 - */ - public C addHeader(Map headers) { - if (headers != null) { - if (this.headers == null) { - this.headers = new ArrayListMap<>(); - } - this.headers.putAll(headers); - } - return (C) this; - } - - /** - * 设置Range头信� - * 表示接收报文体时跳过的字节数,用于断点续传 - * @param rangeStart 表示从 rangeStart 个字节处开始接收,通常是已�下载的字节数,�上次的断点) - * @return HttpTask 实例 - */ - public C setRange(long rangeStart) { - return addHeader("Range", "bytes=" + rangeStart + "-"); - } - - /** - * 设置 Range 头信� - * 设置接收报文体时接收的范围,用于分�下载 - * @param rangeStart 表示从 rangeStart 个字节处开始接收 - * @param rangeEnd 表示接收到 rangeEnd 个字节处 - * @return HttpTask 实例 - */ - public C setRange(long rangeStart, long rangeEnd) { - return addHeader("Range", "bytes=" + rangeStart + "-" + rangeEnd); - } - - /** - * 设置报文体��进度回调 - * @param onProcess 进度回调函数 - * @return HttpTask 实例 - */ - public C setOnProcess(Consumer onProcess) { - this.onProcess = onProcess; - processOnIO = nextOnIO; - nextOnIO = false; - return (C) this; - } - - /** - * 设置进度回调的步进字节,默认 8K(8192) - * 表示�接收 stepBytes 个字节,执行一次进度回调 - * @param stepBytes 步进字节 - * @return HttpTask 实例 - */ - public C stepBytes(long stepBytes) { - this.stepBytes = stepBytes; - return (C) this; - } - - /** - * 设置进度回调的步进比例 - * 表示�接收 stepRate 比例,执行一次进度回调 - * @param stepRate 步进比例 - * @return HttpTask 实例 - */ - public C stepRate(double stepRate) { - this.stepRate = stepRate; - return (C) this; - } - - /** - * 路径�数:替�URL里的{name} - * @param name �数� - * @param value �数值 - * @return HttpTask 实例 - **/ - public C addPathPara(String name, Object value) { - if (name != null && value != null) { - if (pathParams == null) { - pathParams = new ArrayListMap<>(); - } - pathParams.put(name, value.toString()); - } - return (C) this; - } - - /** - * 路径�数:替�URL里的{name} - * @param params �数集� - * @return HttpTask 实例 - **/ - public C addPathPara(Map params) { - if (pathParams == null) { - pathParams = new ArrayListMap<>(); - } - if (params != null) { - pathParams.putAll(params); - } - return (C) this; - } - - /** - * URL�数:拼接在URL�的�数 - * @param name �数� - * @param value �数值 - * @return HttpTask 实例 - **/ - public C addUrlPara(String name, Object value) { - if (name != null && value != null) { - if (urlParams == null) { - urlParams = new ArrayListMap<>(); - } - urlParams.put(name, value.toString()); - } - return (C) this; - } - - /** - * URL�数:拼接在URL�的�数 - * @param params �数集� - * @return HttpTask 实例 - **/ - public C addUrlPara(Map params) { - if (urlParams == null) { - urlParams = new ArrayListMap<>(); - } - if (params != null) { - urlParams.putAll(params); - } - return (C) this; - } - - /** - * Body�数:放在Body里的�数 - * @param name �数� - * @param value �数值 - * @return HttpTask 实例 - **/ - public C addBodyPara(String name, Object value) { - if (name != null && value != null) { - if (bodyParams == null) { - bodyParams = new ArrayListMap<>(); - } - bodyParams.put(name, value); - } - return (C) this; - } - - /** - * Body�数:放在 Body 里的�数(该方法�适�表��交方�) - * @param name �数� - * @param type 媒体类型: 如 txt�json�xml 等,�考 {@link HTTP.Builder#getMediaTypes() } - * @param value �数值 - * @return HttpTask 实例 - * @since v4.1.0 - **/ - public C addBodyPara(String name, String type, Object value) { - if (name != null && value != null) { - if (bodyParams == null) { - bodyParams = new ArrayListMap<>(); - } - if (type == null) { - bodyParams.put(name, value); - } else { - bodyParams.put(name, new BodyPara(type, value)); - } - } - return (C) this; - } - - /** - * Body�数:放在Body里的�数 - * @param params �数集� - * @return HttpTask 实例 - **/ - public C addBodyPara(Map params) { - if (bodyParams == null) { - bodyParams = new ArrayListMap<>(); - } - if (params != null) { - bodyParams.putAll(params); - } - return (C) this; - } - - /** - * 设置 请求报文体 - * @param body 请求报文体,�以是: - *
-     *  byte[] - 字节数组(直接作为报文体) 
- * String - 字符串(比如:JSON 字符串�键值对字符串,也是直接作为报文体)
- * POJO - 普通 Java 数�对象(由 {@link MsgConvertor } ��列化)
- * InputStream - 输入�(v3.5.0 开始支�) - *
- * @return HttpTask 实例 - **/ - public C setBodyPara(Object body) { - this.requestBody = body; - return (C) this; - } - - /** - * 添加文件�数(以 multipart/form-data 形�上传) - * @param name �数� - * @param filePath 文件路径 - * @return HttpTask 实例 - */ - public C addFilePara(String name, String filePath) { - return addFilePara(name, new File(filePath)); - } - - /** - * 添加文件�数(以 multipart/form-data 形�上传) - * @param name �数� - * @param type 文件类型/扩展�: 如 txt�png�jpg�doc 等,�考 {@link HTTP.Builder#getMediaTypes() } - * @param filePath 文件路径 - * @return HttpTask 实例 - */ - public C addFilePara(String name, String type, String filePath) { - return addFilePara(name, type, new File(filePath)); - } - - /** - * 添加文件�数(以 multipart/form-data 形�上传) - * @param name �数� - * @param file 文件 - * @return HttpTask 实例 - */ - public C addFilePara(String name, File file) { - if (file != null && file.exists()) { - String fileName = file.getName(); - String type = fileName.substring(fileName.lastIndexOf(DOT) + 1); - return addFilePara(name, type, file); - } - return (C) this; - } - - /** - * 添加文件�数(以 multipart/form-data 形�上传) - * @param name �数� - * @param type 文件类型/扩展�: 如 txt�png�jpg�doc 等 - * @param file 文件 - * @return HttpTask 实例 - */ - public C addFilePara(String name, String type, File file) { - if (name != null && file != null && file.exists()) { - if (files == null) { - files = new ArrayListMap<>(); - } - files.put(name, new FilePara(type, file.getName(), file)); - } - return (C) this; - } - - /** - * 添加文件�数(以 multipart/form-data 形�上传) - * @param name �数� - * @param type 文件类型/扩展�: 如 txt�png�jpg�doc 等 - * @param content 文件内容 - * @return HttpTask 实例 - */ - public C addFilePara(String name, String type, byte[] content) { - return addFilePara(name, type, name + DOT + type, content); - } - - /** - * 添加文件�数(以 multipart/form-data 形�上传) - * @param name �数� - * @param content 文件内容 - * @param fileName 文件�: 如 xxx.txt�xxx.png�xxx.doc 等 - * @return HttpTask 实例 - * @since v3.5.1 - */ - public C addFilePara(String name, byte[] content, String fileName) { - if (fileName != null) { - int dotIdx = fileName.indexOf(DOT); - if (dotIdx >= 0 && dotIdx < fileName.length() - 1) { - String type = fileName.substring(dotIdx + 1); - return addFilePara(name, type, fileName, content); - } - return addFilePara(name, null, fileName, content); - } - return (C) this; - } - - /** - * 添加文件�数(以 multipart/form-data 形�上传) - * @param name �数� - * @param type 文件类型/扩展�: 如 txt�png�jpg�doc 等 - * @param fileName 文件� - * @param content 文件内容 - * @return HttpTask 实例 - */ - public C addFilePara(String name, String type, String fileName, byte[] content) { - if (name != null && content != null) { - if (files == null) { - files = new ArrayListMap<>(); - } - files.put(name, new FilePara(type, fileName, content)); - } - return (C) this; - } - - /** - * 添加文件�数(以 multipart/form-data 形�上传) - * @param name �数� - * @param type 文件类型/扩展�: 如 txt�png�jpg�doc 等 - * @param stream 文件输入� - * @return HttpTask 实例 - * @since v3.5.0 - */ - public C addFilePara(String name, String type, InputStream stream) { - return addFilePara(name, type, name + DOT + type, stream); - } - - /** - * 添加文件�数(以 multipart/form-data 形�上传) - * @param name �数� - * @param stream 文件输入� - * @param fileName 文件�: 如 xxx.txt�xxx.png�xxx.doc 等 - * @return HttpTask 实例 - * @since v3.5.1 - */ - public C addFilePara(String name, InputStream stream, String fileName) { - if (fileName != null) { - int dotIdx = fileName.indexOf(DOT); - if (dotIdx >= 0 && dotIdx < fileName.length() - 1) { - String type = fileName.substring(dotIdx + 1); - return addFilePara(name, type, fileName, stream); - } - return addFilePara(name, null, fileName, stream); - } - return (C) this; - } - - /** - * 添加文件�数(以 multipart/form-data 形�上传) - * @param name �数� - * @param type 文件类型/扩展�: 如 txt�png�jpg�doc 等 - * @param fileName 文件� - * @param stream 文件输入� - * @return HttpTask 实例 - * @since v3.5.0 - */ - public C addFilePara(String name, String type, String fileName, InputStream stream) { - if (name != null && stream != null) { - if (files == null) { - files = new ArrayListMap<>(); - } - files.put(name, new FilePara(type, fileName, stream)); - } - return (C) this; - } - - /** - * @since v3.4.2 - * @return MultipartBody 的边界符 - */ - public String boundary() { - return boundary; - } - - /** - * 设置 MultipartBody 的边界符 - * @since v3.4.2 - * @param boundary 边界符 - * @return this - */ - public C boundary(String boundary) { - this.boundary = boundary; - return (C) this; - } - - @Override - public boolean cancel() { - if (canceler != null) { - return canceler.cancel(); - } - return false; - } - - protected void registeTagTask(Cancelable canceler) { - if (tag != null && tagTask == null) { - tagTask = httpClient.addTagTask(tag, canceler, this); - } - this.canceler = canceler; - } - - private void updateTagTask() { - if (tagTask != null) { - tagTask.setTag(tag); - } else - if (canceler != null) { - registeTagTask(canceler); - } - } - - protected void removeTagTask() { - if (tag != null) { - httpClient.removeTagTask(this); - } - } - - protected Call prepareCall(String method) { - Request request = prepareRequest(method.toUpperCase()); - return httpClient.request(request); - } - - protected Request prepareRequest(String method) { - boolean bodyCanUsed = HttpMethod.permitsRequestBody(method); - assertNotConflict(!bodyCanUsed); - Request.Builder builder = new Request.Builder() - .url(buildUrlPath()); - buildHeaders(builder); - if (bodyCanUsed) { - RequestBody reqBody = buildRequestBody(); - if (onProcess != null) { - long contentLength = contentLength(reqBody); - if (stepRate > 0 && stepRate <= 1) { - stepBytes = (long) (contentLength * stepRate); - } - if (stepBytes <= 0) { - stepBytes = Process.DEFAULT_STEP_BYTES; - } - reqBody = new ProcessRequestBody(reqBody, onProcess, - httpClient.executor().getExecutor(processOnIO), - contentLength, stepBytes); - } else { - reqBody = new FixedRequestBody(reqBody); - } - builder.method(method, reqBody); - } else { - builder.method(method, null); - } - if (tag != null) { - builder.tag(String.class, tag); - } - return builder.build(); - } - - private long contentLength(RequestBody reqBody) { - try { - return reqBody.contentLength(); - } catch (IOException e) { - throw new OkHttpsException("无法获�请求体长度", e); - } - } - - private void buildHeaders(Request.Builder builder) { - if (headers != null) { - headers.forEach((name, value) -> { - if (value == null) return; - builder.addHeader(name, value); - }); - } - } - - protected State toState(IOException e) { - if (e instanceof SocketTimeoutException) { - return State.TIMEOUT; - } else if (e instanceof UnknownHostException || e instanceof ConnectException) { - return State.NETWORK_ERROR; - } - String msg = e.getMessage(); - if (msg != null && ("Canceled".equals(msg) || e instanceof SocketException - && (msg.startsWith("Socket operation on nonsocket") || "Socket closed".equals(msg)))) { - return State.CANCELED; - } - return State.EXCEPTION; - } - - private RequestBody buildRequestBody() { - if (bodyParams != null && (OkHttps.FORM_DATA.equals(bodyType) || bodyType.startsWith(MULTIPART)) - || files != null) { - MultipartBody.Builder builder = multipartBodyBuilder(); - if (bodyParams != null) { - bodyParams.forEach((key, value) -> { - if (value == null) return; - MediaType contentType = null; - String bodyValue; - if (value instanceof BodyPara) { - BodyPara para = (BodyPara) value; - contentType = httpClient.mediaType(para.getType()); - bodyValue = para.getValue().toString(); - } else { - bodyValue = value.toString(); - } - byte[] content = bodyValue.getBytes(charset); - RequestBody body = RequestBody.create(contentType, content); - builder.addPart(MultipartBody.Part.createFormData(key, null, body)); - }); - } - if (files != null) { - files.forEach((name, file) -> { - MediaType type = httpClient.mediaType(file.getType()); - builder.addFormDataPart( - name, - file.getFileName(), - file.toRequestBody(type) - ); - }); - } - return builder.build(); - } - if (requestBody != null) { - return toRequestBody(requestBody); - } - if (bodyParams == null) { - return emptyRequestBody(); - } - if (OkHttps.FORM.equals(bodyType) || bodyType.endsWith(FORM)) { - FormBody.Builder builder = new FormBody.Builder(charset); - bodyParams.forEach((key, value) -> { - if (value == null) return; - builder.add(key, value.toString()); - }); - return builder.build(); - } - return toRequestBody(bodyParams); - } - - private MultipartBody.Builder multipartBodyBuilder() { - MultipartBody.Builder builder; - if (boundary != null) { - builder = new MultipartBody.Builder(boundary); - } else { - builder = new MultipartBody.Builder(); - } - if (bodyType.startsWith(MULTIPART)) { - try { - builder.setType(MediaType.get(bodyType)); - } catch (IllegalArgumentException ignore) { } - } else { - builder.setType(MultipartBody.FORM); - } - return builder; - } - - private RequestBody emptyRequestBody() { - if (OkHttps.FORM_DATA.equalsIgnoreCase(bodyType)) { - return new MultipartBody.Builder().setType(MultipartBody.FORM).build(); - } - return RequestBody.create(mediaType(), new byte[]{}); - } - - private MediaType mediaType() { - return httpClient.executor().doMsgConvert(bodyType, null).mediaType(charset); - } - - private RequestBody toRequestBody(Object bodyObj) { - if (bodyObj instanceof byte[] || bodyObj instanceof String) { - byte[] body = bodyObj instanceof byte[] ? (byte[]) bodyObj : ((String) bodyObj).getBytes(charset); - return RequestBody.create(mediaType(), body); - } - if (bodyObj instanceof InputStream) { - return new StreamRequestBody(mediaType(), (InputStream) bodyObj); - } - TaskExecutor.Data data = httpClient.executor().doMsgConvert(bodyType, c -> c.serialize(bodyObj, charset)); - return RequestBody.create(data.mediaType(charset), data.data); - } - - private String buildUrlPath() { - if (Platform.isBlank(urlPath)) { - throw new OkHttpsException("url �能为空�"); - } - StringBuilder sb = new StringBuilder(urlPath); - if (pathParams != null) { - pathParams.forEach((name, value) -> { - String target = "{" + name + "}"; - int start = sb.indexOf(target); - if (start >= 0) { - String newValue = value != null ? value.toString() : ""; - sb.replace(start, start + target.length(), newValue); - } else { - throw new OkHttpsException("PathPara [ " + name + " ] �存在于 url [ " + urlPath + " ]"); - } - }); - } - if (urlParams != null) { - if (sb.indexOf("?") >= 0) { // contains("?") - int lastIndex = sb.length() - 1; - if (sb.lastIndexOf("?") < lastIndex) { // !endsWith("?") - if (sb.lastIndexOf("=") < sb.lastIndexOf("?") + 2) { - throw new OkHttpsException("url 格�错误,'?' �没有�现 '='"); - } - if (sb.lastIndexOf("&") < lastIndex) { // !endsWith("&") - sb.append('&'); - } - } - } else { - sb.append('?'); - } - urlParams.forEach((name, value) -> { - if (value == null) return; - sb.append(name).append('=').append(value).append('&'); - }); - sb.delete(sb.length() - 1, sb.length()); - } - return sb.toString(); - } - - /** - * �数冲�校验 - * @param bodyCantUsed 报文体是���用 - */ - protected void assertNotConflict(boolean bodyCantUsed) { - if (bodyCantUsed) { - if (requestBody != null) { - throw new OkHttpsException("GET | HEAD request can not call setBodyPara(..) method!"); - } - if (isNotEmpty(bodyParams)) { - throw new OkHttpsException("GET | HEAD request can not call addBodyPara(..) method!"); - } - if (isNotEmpty(files)) { - throw new OkHttpsException("GET | HEAD request can not call addFilePara(..) method!"); - } - } - if (requestBody != null) { - if (isNotEmpty(bodyParams)) { - throw new OkHttpsException("can not call addBodyPara(..) and setBodyPara(..) at the same time!"); - } - if (isNotEmpty(files)) { - throw new OkHttpsException("can not call addFilePara(..) and setBodyPara(..) at the same time!"); - } - } - } - - private static boolean isNotEmpty(Map map) { - return map != null && !map.isEmpty(); - } - - /** - * @param latch CountDownLatch - * @return true 表示已超时:false 表示未超时 - */ - protected boolean timeoutAwait(CountDownLatch latch) { - try { - return !latch.await(httpClient.preprocTimeoutMillis(), - TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - throw new OkHttpsException("execute timeout: " + urlPath, e); - } - } - - protected HttpResult timeoutResult() { - if (nothrow) { - return new RealHttpResult(this, State.TIMEOUT); - } - throw new OkHttpsException(State.TIMEOUT, "execute timeout: " + urlPath); - } - - public Charset charset(Response response) { - ResponseBody b = response.body(); - MediaType type = b != null ? b.contentType() : null; - return type != null ? type.charset(charset) : charset; - } - - protected void execute(Runnable command, boolean onIo) { - httpClient.executor().execute(command, onIo); - } - - public AbstractHttpClient httpClient() { - return httpClient; - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/MsgConvertor.java b/okhttps/src/main/java/cn/zhxu/okhttps/MsgConvertor.java deleted file mode 100644 index 090660f7..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/MsgConvertor.java +++ /dev/null @@ -1,108 +0,0 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.data.Array; -import cn.zhxu.data.DataConvertor; -import cn.zhxu.data.DataSet; -import cn.zhxu.data.Mapper; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.Type; -import java.net.URLEncoder; -import java.nio.charset.Charset; -import java.util.List; - -/** - * 消�转�器接� - */ -public interface MsgConvertor extends DataConvertor { - - /** - * 消�的媒体类型 - * @return 媒体类型 - */ - String mediaType(); - - /** - * 表�转�器,�用于自动�列化表��数 - */ - class FormConvertor implements MsgConvertor { - - private final DataConvertor convertor; - private final boolean urlEncoded; - - public FormConvertor(DataConvertor convertor) { - this(convertor, false); - } - - /** - * @param convertor DataConvertor - * @param urlEncoded 是�进行 URLEncode 编� - */ - public FormConvertor(DataConvertor convertor, boolean urlEncoded) { - this.convertor = convertor; - this.urlEncoded = urlEncoded; - } - - @Override - public String mediaType() { - return "application/x-www-form-urlencoded"; - } - - @Override - public Mapper toMapper(InputStream in, Charset charset) { - return convertor.toMapper(in, charset); - } - - @Override - public Array toArray(InputStream in, Charset charset) { - return convertor.toArray(in, charset); - } - - @Override - public byte[] serialize(Object object, Charset charset, boolean pretty) { - byte[] result = convertor.serialize(object, charset); - Mapper mapper = convertor.toMapper(new ByteArrayInputStream(result), charset); - StringBuilder sb = new StringBuilder(); - mapper.forEach((key, data) -> { - String value = encodeValue(data, charset); - sb.append(key).append('=').append(value).append('&'); - }); - int endIndex = sb.length() > 1 ? sb.length() - 1 : sb.length(); - return sb.substring(0, endIndex).getBytes(charset); - } - - private String encodeValue(DataSet.Data data, Charset charset) { - String value = data.toString(); - if (urlEncoded) { - try { - return URLEncoder.encode(value, charset.name()); - } catch (UnsupportedEncodingException e) { - throw new OkHttpsException("UnsupportedEncoding: " + charset.name(), e); - } - } - return value; - } - - @Override - public T toBean(Type type, InputStream in, Charset charset) { - return convertor.toBean(type, in, charset); - } - - @Override - public List toList(Class type, InputStream in, Charset charset) { - return convertor.toList(type, in, charset); - } - - public DataConvertor getConvertor() { - return convertor; - } - - public boolean isUrlEncoded() { - return urlEncoded; - } - - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/Platform.java b/okhttps/src/main/java/cn/zhxu/okhttps/Platform.java deleted file mode 100644 index 1766bb0e..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/Platform.java +++ /dev/null @@ -1,133 +0,0 @@ -package cn.zhxu.okhttps; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ConcurrentModificationException; -import java.util.Map; -import java.util.Objects; -import java.util.function.BiConsumer; - -public class Platform { - - /** - * Android 的 SDK 版本,若�是 Android 平�,则为 0 - */ - public static final int ANDROID_SDK_INT = getAndroidSdkInt(); - - - private static int getAndroidSdkInt() { - try { - Class versionClass = Class.forName("android.os.Build$VERSION"); - Field field = versionClass.getDeclaredField("SDK_INT"); - return field.getInt(field); - } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { - return 0; - } - } - - public static void logInfo(String message) { - doLog(okhttp3.internal.platform.Platform.INFO, message, null); - } - - public static void logError(String message) { - logError(message, null); - } - - public static void logError(String message, Throwable t) { - doLog(okhttp3.internal.platform.Platform.WARN, message, t); - } - - private static Method logMethod = null; - - private static void doLog(int level, String message, Throwable t) { - okhttp3.internal.platform.Platform platform = okhttp3.internal.platform.Platform.get(); - if (isOkHttpVersionLessThan4()) { - platform.log(level, message, t); - return; - } - try { - synchronized (Platform.class) { - if (logMethod == null) { - logMethod = platform.getClass().getMethod("log", String.class, int.class, Throwable.class); - } - } - logMethod.invoke(platform, message, level, t); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - e.printStackTrace(); - } - } - - private static String okHttpVersion; - - private static boolean isOkHttpVersionLessThan4() { - if (okHttpVersion == null) { - try { - Class versionClass = Class.forName("okhttp3.internal.Version"); - Method method = versionClass.getDeclaredMethod("userAgent"); - okHttpVersion = (String) method.invoke(null); - } catch (Exception e) { - okHttpVersion = "4.x"; - } - } - return !okHttpVersion.startsWith("4"); - } - - // 该方法是为兼容 Android 低版本 - public static void forEach(Map map, BiConsumer action) { - Objects.requireNonNull(action); - for (Map.Entry entry : map.entrySet()) { - K k; - V v; - try { - k = entry.getKey(); - v = entry.getValue(); - } catch (IllegalStateException ise) { - // this usually means the entry is no longer in the map. - throw new ConcurrentModificationException(ise); - } - action.accept(k, v); - } - } - - /** - * 判断字符串是�为 null 或 空 - * @param str 待判断的字符串 - * @return str 是�是空白字符串 - * @since v3.5.0 - */ - public static boolean isBlank(String str) { - if (str == null) { - return true; - } - int len = str.length(); - if (len == 0) { - return true; - } - for (int i = 0; i < len; i++) { - switch (str.charAt(i)) { - case ' ': - case '\t': - case '\n': - case '\r': - // case '\b': - // case '\f': - break; - default: - return false; - } - } - return true; - } - - /** - * 判断字符串是��空 - * @param str 待判断的字符串 - * @return str 是�是�空字符串 - * @since v3.5.0 - */ - public static boolean isNotBlank(String str) { - return !isBlank(str); - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/Process.java b/okhttps/src/main/java/cn/zhxu/okhttps/Process.java deleted file mode 100644 index 65c8b73a..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/Process.java +++ /dev/null @@ -1,34 +0,0 @@ -package cn.zhxu.okhttps; - -/** - * 进度(上传或下载) - */ -public interface Process { - - int DEFAULT_STEP_BYTES = 8192; - - /** - * 当请求体大�未知(��上传)时,该方法始终返回 -1 - * 当请求体大�为 0 时,该方法始终返回 1 - * @return 完�比例 - */ - double getRate(); - - /** - * 当请求体大�未知(��上传)时,该方法返回 -1 - * @return 总字节数 - */ - long getTotalBytes(); - - /** - * @return 已完�字节数 - */ - long getDoneBytes(); - - /** - * �有当 当请求体大�已知 的情况下,该方法��能返回 true - * @return 任务是�完� - */ - boolean isDone(); - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/FixedRequestBody.java b/okhttps/src/main/java/cn/zhxu/okhttps/internal/FixedRequestBody.java deleted file mode 100644 index 89eb7d34..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/FixedRequestBody.java +++ /dev/null @@ -1,53 +0,0 @@ -package cn.zhxu.okhttps.internal; - -import okhttp3.MediaType; -import okhttp3.RequestBody; -import okio.BufferedSink; - -import java.io.IOException; - -/** - * 修�报文体在写出过程中会�生无法被异常回调�获的� IO 异常的问题 - * @author Troy.Zhou @ 2021-09-01 - * @since v3.1.5 - */ -public class FixedRequestBody extends RequestBody { - - private final RequestBody requestBody; - - public FixedRequestBody(RequestBody requestBody) { - this.requestBody = requestBody; - } - - @Override - public long contentLength() throws IOException { - return requestBody.contentLength(); - } - - @Override - public boolean isDuplex() { - return requestBody.isDuplex(); - } - - @Override - public boolean isOneShot() { - return requestBody.isOneShot(); - } - - @Override - public MediaType contentType() { - return requestBody.contentType(); - } - - @Override - public void writeTo(@SuppressWarnings("NullableProblems") BufferedSink sink) throws IOException { - try { - requestBody.writeTo(sink); - } catch (IOException e) { - throw e; - } catch (Exception e) { - throw new IOException("请求体写出异常", e); - } - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/ProcessInputStream.java b/okhttps/src/main/java/cn/zhxu/okhttps/internal/ProcessInputStream.java deleted file mode 100644 index ec828e5b..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/ProcessInputStream.java +++ /dev/null @@ -1,87 +0,0 @@ -package cn.zhxu.okhttps.internal; - -import cn.zhxu.okhttps.Process; - -import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.Executor; -import java.util.function.Consumer; - -public class ProcessInputStream extends InputStream { - - private final InputStream input; - private final Consumer onProcess; - private final Executor callbackExecutor; - private final long stepBytes; - private final RealProcess process; - private boolean doneCalled = false; - private long step; - - public ProcessInputStream(InputStream input, Consumer onProcess, long totalBytes, long stepBytes, - long doneBytes, Executor callbackExecutor) { - this.input = input; - this.onProcess = onProcess; - this.stepBytes = stepBytes; - this.callbackExecutor = callbackExecutor; - this.process = new RealProcess(totalBytes, doneBytes); - this.step = doneBytes / stepBytes; - } - - @Override - public int read() throws IOException { - byte[] buf = new byte[1]; - int count = read(buf, 0, 1); - if (count > 0) { - return buf[0]; - } - return -1; - } - - @Override - public int read(@SuppressWarnings("NullableProblems") byte[] buf, int off, int len) throws IOException { - int total = 0; - while (total < len) { - // 一次读�长度 - int length = Math.min(len - total, (int) stepBytes); - int read = input.read(buf, off + total, length); - if (read == -1) { - // 已�读完 - if (total == 0) { - return read; - } - break; - } - updateProcess(read); - total += read; - } - return total; - } - - private void updateProcess(long count) { - process.addDoneBytes(count); - if (process.isUndoneAndUnreached(step * stepBytes)) { - return; - } - if (process.isDone()) { - if (doneCalled) { - return; - } - doneCalled = true; - } - step = (process.getDoneBytes() - 1) / stepBytes + 1; - // 因为 process 一直被更新,所有此处应克隆一个新的对象用于回调 - Process p = process.newProcess(); - callbackExecutor.execute(() -> onProcess.accept(p)); - } - - @Override - public int available() throws IOException { - return input.available(); - } - - @Override - public void close() throws IOException { - input.close(); - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/ProcessRequestBody.java b/okhttps/src/main/java/cn/zhxu/okhttps/internal/ProcessRequestBody.java deleted file mode 100644 index c7533d4b..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/ProcessRequestBody.java +++ /dev/null @@ -1,74 +0,0 @@ -package cn.zhxu.okhttps.internal; - -import cn.zhxu.okhttps.Process; -import okhttp3.RequestBody; -import okio.*; - -import java.io.IOException; -import java.util.concurrent.Executor; -import java.util.function.Consumer; - -public class ProcessRequestBody extends FixedRequestBody { - - private final Consumer onProcess; - private final Executor callbackExecutor; - private final RealProcess process; - private final long stepBytes; - - public ProcessRequestBody(RequestBody requestBody, Consumer onProcess, Executor callbackExecutor, - long contentLength, long stepBytes) { - super(requestBody); - this.onProcess = onProcess; - this.callbackExecutor = callbackExecutor; - this.stepBytes = stepBytes; - this.process = new RealProcess(contentLength, 0); - } - - class ProcessableSink extends ForwardingSink { - - private long step = 0; - private boolean doneCalled = false; - - public ProcessableSink(Sink delegate) { - super(delegate); - } - - @Override - public void write(Buffer source, long byteCount) throws IOException { - // byteCount �能很大,也�能很� - long written = 0; - while (written < byteCount) { - long count = Math.min(stepBytes, byteCount - written); - super.write(source, count); - updateProcess(count); - written += count; - } - } - - private void updateProcess(long count) { - process.addDoneBytes(count); - if (process.isUndoneAndUnreached(step * stepBytes)) { - return; - } - if (process.isDone()) { - if (doneCalled) { - return; - } - doneCalled = true; - } - step = (process.getDoneBytes() - 1) / stepBytes + 1; - // 因为 process 一直被更新,所有此处应克隆一个新的对象用于回调 - Process p = process.newProcess(); - callbackExecutor.execute(() -> onProcess.accept(p)); - } - - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - BufferedSink buffer = Okio.buffer(new ProcessableSink(sink)); - super.writeTo(buffer); - buffer.flush(); - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/RealProcess.java b/okhttps/src/main/java/cn/zhxu/okhttps/internal/RealProcess.java deleted file mode 100644 index c9a925ed..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/RealProcess.java +++ /dev/null @@ -1,60 +0,0 @@ -package cn.zhxu.okhttps.internal; - -import cn.zhxu.okhttps.Process; - -public class RealProcess implements Process { - - // 总字节数(��上传时该字段为 -1) - private final long totalBytes; - // 已�完�字节数 - private long doneBytes; - - public RealProcess(long totalBytes, long doneBytes) { - this.totalBytes = totalBytes; - this.doneBytes = doneBytes; - } - - public Process newProcess() { - return new RealProcess(totalBytes, doneBytes); - } - - @Override - public double getRate() { - if (totalBytes == 0) { - return 1; - } - if (totalBytes < 0) { - return -1; - } - return (double) doneBytes / totalBytes; - } - - @Override - public long getTotalBytes() { - return totalBytes; - } - - @Override - public long getDoneBytes() { - return doneBytes; - } - - @Override - public boolean isDone() { - return doneBytes >= totalBytes && totalBytes >= 0; - } - - public void addDoneBytes(long delt) { - doneBytes += delt; - } - - public boolean isUndoneAndUnreached(long bytes) { - return doneBytes < bytes && (totalBytes < 0 || doneBytes < totalBytes); - } - - @Override - public String toString() { - return "Process[" + doneBytes + " / " + totalBytes + " | " + getRate() + ']'; - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/SerialPreprocessor.java b/okhttps/src/main/java/cn/zhxu/okhttps/internal/SerialPreprocessor.java deleted file mode 100644 index 388a4cb8..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/SerialPreprocessor.java +++ /dev/null @@ -1,56 +0,0 @@ -package cn.zhxu.okhttps.internal; - -import cn.zhxu.okhttps.Preprocessor; - -import java.util.LinkedList; -import java.util.Queue; - -/** - * 串行预处�器 - * @author Troy.Zhou - */ -public class SerialPreprocessor implements Preprocessor { - - // 预处�器 - final Preprocessor preprocessor; - // 待处�的任务队列 - final Queue pendings; - // 是�有任务正在执行 - boolean running = false; - - public SerialPreprocessor(Preprocessor preprocessor) { - this.preprocessor = preprocessor; - this.pendings = new LinkedList<>(); - } - - @Override - public void doProcess(Preprocessor.PreChain chain) { - boolean should = true; - synchronized (this) { - if (running) { - pendings.add(chain); - should = false; - } else { - running = true; - } - } - if (should) { - preprocessor.doProcess(chain); - } - } - - public void afterProcess() { - Preprocessor.PreChain chain = null; - synchronized (this) { - if (pendings.size() > 0) { - chain = pendings.poll(); - } else { - running = false; - } - } - if (chain != null) { - preprocessor.doProcess(chain); - } - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/StreamRequestBody.java b/okhttps/src/main/java/cn/zhxu/okhttps/internal/StreamRequestBody.java deleted file mode 100644 index 2e17e12b..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/StreamRequestBody.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.zhxu.okhttps.internal; - -import okhttp3.MediaType; -import okhttp3.RequestBody; -import okio.BufferedSink; -import okio.Okio; - -import java.io.IOException; -import java.io.InputStream; - -/** - * ��请求体 - * @author Troy.Zhou @ 2021-09-01 - * @since v3.5.0 - */ -public class StreamRequestBody extends RequestBody { - - private final MediaType contentType; - private final InputStream inputStream; - - public StreamRequestBody(MediaType contentType, InputStream inputStream) { - this.contentType = contentType; - this.inputStream = inputStream; - } - - @Override - public MediaType contentType() { - return contentType; - } - - @Override - public void writeTo(BufferedSink sink) throws IOException { - try { - sink.writeAll(Okio.source(inputStream)); - } finally { - inputStream.close(); - } - } - - @Override - public boolean isOneShot() { - return true; - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/okhttp/OkHttpBuilderImpl.java b/okhttps/src/main/java/cn/zhxu/okhttps/okhttp/OkHttpBuilderImpl.java deleted file mode 100644 index 4d691ec7..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/okhttp/OkHttpBuilderImpl.java +++ /dev/null @@ -1,335 +0,0 @@ -package cn.zhxu.okhttps.okhttp; - -import cn.zhxu.okhttps.*; -import cn.zhxu.okhttps.internal.CopyInterceptor; -import cn.zhxu.okhttps.internal.SerialPreprocessor; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.Executor; - - -public class OkHttpBuilderImpl implements HTTP.Builder { - - private OkHttpClient okClient; - - private String baseUrl; - - private final List configs; - - private final Map mediaTypes; - - private final List contentTypes; - - private final List msgConvertors; - - private final List preprocessors; - - private int preprocTimeoutTimes = 10; - - private Executor mainExecutor; - - private Scheduler taskScheduler; - - private DownListener downloadListener; - - private TaskListener responseListener; - - private TaskListener exceptionListener; - - private TaskListener completeListener; - - private Charset charset = StandardCharsets.UTF_8; - - private String bodyType = OkHttps.FORM; - - private DownloadHelper downloadHelper; - - public OkHttpBuilderImpl() { - mediaTypes = new HashMap<>(); - mediaTypes.put("png", "image/png"); - mediaTypes.put("jpg", "image/jpeg"); - mediaTypes.put("jpeg", "image/jpeg"); - mediaTypes.put("wav", "audio/wav"); - mediaTypes.put("mp3", "audio/mp3"); - mediaTypes.put("mp4", "video/mp4"); - mediaTypes.put("txt", "text/plain"); - mediaTypes.put("xls", "application/x-xls"); - mediaTypes.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); - mediaTypes.put("xml", "text/xml"); - mediaTypes.put("apk", "application/vnd.android.package-archive"); - mediaTypes.put("doc", "application/msword"); - mediaTypes.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); - mediaTypes.put("html", "text/html"); - mediaTypes.put("rar", "application/x-rar"); - mediaTypes.put("jar", "application/x-java-archive"); - contentTypes = new ArrayList<>(); - contentTypes.add("application/x-www-form-urlencoded"); - contentTypes.add("application/json"); - contentTypes.add("application/xml"); - contentTypes.add("application/protobuf"); - contentTypes.add("application/msgpack"); - preprocessors = new ArrayList<>(); - msgConvertors = new ArrayList<>(); - configs = new ArrayList<>(); - downloadHelper = new DownloadHelper(); - } - - public OkHttpBuilderImpl(OkHttpClientWrapper hc) { - okClient = hc.okClient(); - baseUrl = hc.baseUrl(); - mediaTypes = hc.mediaTypes(); - preprocessors = new ArrayList<>(); - Collections.addAll(preprocessors, hc.preprocessors()); - TaskExecutor executor = hc.executor(); - contentTypes = new ArrayList<>(); - Collections.addAll(contentTypes, executor.getContentTypes()); - mainExecutor = executor.getMainExecutor(); - taskScheduler = executor.getTaskScheduler(); - downloadListener = executor.getDownloadListener(); - responseListener = executor.getResponseListener(); - exceptionListener = executor.getExceptionListener(); - completeListener = executor.getCompleteListener(); - msgConvertors = new ArrayList<>(); - Collections.addAll(msgConvertors, executor.getMsgConvertors()); - preprocTimeoutTimes = hc.preprocTimeoutTimes(); - configs = new ArrayList<>(); - bodyType = hc.bodyType(); - charset = hc.charset(); - downloadHelper = hc.downloadHelper(); - } - - - public HTTP.Builder config(HTTP.OkConfig config) { - if (config != null) { - configs.add(config); - } - return this; - } - - public HTTP.Builder baseUrl(String baseUrl) { - if (baseUrl != null) { - this.baseUrl = baseUrl.trim(); - } - return this; - } - - public HTTP.Builder mediaTypes(Map mediaTypes) { - if (mediaTypes != null) { - this.mediaTypes.putAll(mediaTypes); - } - return this; - } - - public HTTP.Builder mediaTypes(String key, String value) { - if (key != null && value != null) { - this.mediaTypes.put(key, value); - } - return this; - } - - public HTTP.Builder clearContentTypes() { - contentTypes.clear(); - return this; - } - - public HTTP.Builder contentTypes(List contentTypes) { - if (contentTypes != null) { - this.contentTypes.addAll(contentTypes); - } - return this; - } - - public HTTP.Builder contentTypes(String contentType) { - if (contentType != null) { - this.contentTypes.add(contentType); - } - return this; - } - - public HTTP.Builder callbackExecutor(Executor executor) { - this.mainExecutor = executor; - return this; - } - - public HTTP.Builder taskScheduler(Scheduler scheduler) { - this.taskScheduler = scheduler; - return this; - } - - public HTTP.Builder addPreprocessor(Preprocessor preprocessor) { - if (preprocessor != null) { - preprocessors.add(preprocessor); - } - return this; - } - - public HTTP.Builder addSerialPreprocessor(Preprocessor preprocessor) { - if (preprocessor != null) { - preprocessors.add(new SerialPreprocessor(preprocessor)); - } - return this; - } - - public HTTP.Builder clearPreprocessors() { - preprocessors.clear(); - return this; - } - - public HTTP.Builder preprocTimeoutTimes(int times) { - if (times > 0) { - preprocTimeoutTimes = times; - } - return this; - } - - public HTTP.Builder responseListener(TaskListener listener) { - responseListener = listener; - return this; - } - - public HTTP.Builder exceptionListener(TaskListener listener) { - exceptionListener = listener; - return this; - } - - public HTTP.Builder completeListener(TaskListener listener) { - completeListener = listener; - return this; - } - - public HTTP.Builder downloadListener(DownListener listener) { - downloadListener = listener; - return this; - } - - public HTTP.Builder addMsgConvertor(MsgConvertor msgConvertor) { - if (msgConvertor != null) { - msgConvertors.add(msgConvertor); - } - return this; - } - - public HTTP.Builder clearMsgConvertors() { - msgConvertors.clear(); - return this; - } - - public HTTP.Builder charset(Charset charset) { - if (charset != null) { - this.charset = charset; - } - return this; - } - - public HTTP.Builder bodyType(String bodyType) { - if (bodyType != null) { - this.bodyType = bodyType.toLowerCase(); - } - return this; - } - - public HTTP build() { - if (configs.size() > 0 || okClient == null) { - OkHttpClient.Builder builder; - if (okClient != null) { - builder = okClient.newBuilder(); - } else { - builder = new OkHttpClient.Builder(); - } - for (HTTP.OkConfig config: configs) { - config.config(builder); - } - // fix issue: https://github.com/ejlchina/okhttps/issues/8 - if (needCopyInterceptor(builder.interceptors())) { - builder.addInterceptor(new CopyInterceptor()); - } - okClient = builder.build(); - } else if (needCopyInterceptor(okClient.interceptors())) { - okClient = okClient.newBuilder() - .addInterceptor(new CopyInterceptor()) - .build(); - } - return new OkHttpClientWrapper(this); - } - - private boolean needCopyInterceptor(List list) { - return mainExecutor != null && Platform.ANDROID_SDK_INT > 24 && CopyInterceptor.notIn(list); - } - - public OkHttpClient okClient() { - return okClient; - } - - public String baseUrl() { - return baseUrl; - } - - public Map getMediaTypes() { - return mediaTypes; - } - - public Executor mainExecutor() { - return mainExecutor; - } - - public Preprocessor[] preprocessors() { - return preprocessors.toArray(new Preprocessor[0]); - } - - public DownListener downloadListener() { - return downloadListener; - } - - public TaskListener responseListener() { - return responseListener; - } - - public TaskListener exceptionListener() { - return exceptionListener; - } - - public TaskListener completeListener() { - return completeListener; - } - - public MsgConvertor[] msgConvertors() { - return msgConvertors.toArray(new MsgConvertor[0]); - } - - public Scheduler taskScheduler() { - return taskScheduler; - } - - public String[] contentTypes() { - return contentTypes.toArray(new String[0]); - } - - public int preprocTimeoutTimes() { - return preprocTimeoutTimes; - } - - public Charset charset() { - return charset; - } - - public String bodyType() { - return bodyType; - } - - @Override - public HTTP.Builder downloadHelper(DownloadHelper downloadHelper) { - this.downloadHelper = downloadHelper; - return this; - } - - @Override - public DownloadHelper downloadHelper() { - return downloadHelper; - } - -} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/okhttp/OkHttpClientWrapper.java b/okhttps/src/main/java/cn/zhxu/okhttps/okhttp/OkHttpClientWrapper.java deleted file mode 100644 index 14489c93..00000000 --- a/okhttps/src/main/java/cn/zhxu/okhttps/okhttp/OkHttpClientWrapper.java +++ /dev/null @@ -1,55 +0,0 @@ -package cn.zhxu.okhttps.okhttp; - -import cn.zhxu.okhttps.HTTP; -import cn.zhxu.okhttps.internal.AbstractHttpClient; -import okhttp3.*; - -import java.util.concurrent.Executor; - - -public class OkHttpClientWrapper extends AbstractHttpClient { - - // OkHttpClient - final OkHttpClient okClient; - - public OkHttpClientWrapper(OkHttpBuilderImpl builder) { - super(builder); - this.okClient = builder.okClient(); - } - - @Override - public Executor ioExecutor(HTTP.Builder builder) { - OkHttpBuilderImpl builderImpl = (OkHttpBuilderImpl) builder; - return builderImpl.okClient().dispatcher().executorService(); - } - - @Override - public void doCancelAll() { - okClient.dispatcher().cancelAll(); - } - - @Override - public Call request(Request request) { - return okClient.newCall(request); - } - - @Override - public WebSocket webSocket(Request request, WebSocketListener listener) { - return okClient.newWebSocket(request, listener); - } - - public OkHttpClient okClient() { - return okClient; - } - - @Override - public int totalTimeoutMillis() { - return okClient.connectTimeoutMillis() + okClient.writeTimeoutMillis() + okClient.readTimeoutMillis(); - } - - @Override - public HTTP.Builder newBuilder() { - return new OkHttpBuilderImpl(this); - } - -} diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/Array.java b/okhttps/src/main/java/com/ejlchina/okhttps/Array.java new file mode 100644 index 00000000..99b897c2 --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/Array.java @@ -0,0 +1,68 @@ +package com.ejlchina.okhttps; + +/** + * @since 2.0.0 + * Array 接� 类似于 JsonArray + * 但为什么���为 json 呢,因它�止是 json,它还�以是 xml�yml�protobuf 等任何一�格�的数� + */ +public interface Array { + + /** + * @return JSON 的键值对数� + */ + int size(); + + /** + * @return 是�为空 + */ + boolean isEmpty(); + + /** + * @param index 元素下标 + * @return � JsonObj + */ + Mapper getMapper(int index); + + /** + * @param index 元素下标 + * @return � JsonArr + */ + Array getArray(int index); + + /** + * @param index 元素下标 + * @return boolean 值 + */ + boolean getBool(int index); + + /** + * @param index 元素下标 + * @return int 值 + */ + int getInt(int index); + + /** + * @param index 元素下标 + * @return long 值 + */ + long getLong(int index); + + /** + * @param index 元素下标 + * @return float 值 + */ + float getFloat(int index); + + /** + * @param index 元素下标 + * @return double 值 + */ + double getDouble(int index); + + /** + * @param index 元素下标 + * @return String 值 + */ + String getString(int index); + +} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/Cancelable.java b/okhttps/src/main/java/com/ejlchina/okhttps/Cancelable.java similarity index 80% rename from okhttps/src/main/java/cn/zhxu/okhttps/Cancelable.java rename to okhttps/src/main/java/com/ejlchina/okhttps/Cancelable.java index e6b3db84..0c81a7ac 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/Cancelable.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/Cancelable.java @@ -1,4 +1,4 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; public interface Cancelable { diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/Config.java b/okhttps/src/main/java/com/ejlchina/okhttps/Config.java similarity index 67% rename from okhttps/src/main/java/cn/zhxu/okhttps/Config.java rename to okhttps/src/main/java/com/ejlchina/okhttps/Config.java index c2eeb36d..71a803c7 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/Config.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/Config.java @@ -1,4 +1,4 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; import java.util.ServiceLoader; @@ -7,10 +7,6 @@ */ public interface Config { - /** - * �置 {@link OkHttps } 内部的 {@link HTTP } 实例 - * @param builder HTTP.Builder - */ void with(HTTP.Builder builder); static void config(HTTP.Builder builder) { diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/ConvertProvider.java b/okhttps/src/main/java/com/ejlchina/okhttps/ConvertProvider.java similarity index 94% rename from okhttps/src/main/java/cn/zhxu/okhttps/ConvertProvider.java rename to okhttps/src/main/java/com/ejlchina/okhttps/ConvertProvider.java index 0ce5d1ad..aa1600b9 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/ConvertProvider.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/ConvertProvider.java @@ -1,4 +1,4 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; import java.util.ServiceLoader; diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/DownListener.java b/okhttps/src/main/java/com/ejlchina/okhttps/DownListener.java similarity index 89% rename from okhttps/src/main/java/cn/zhxu/okhttps/DownListener.java rename to okhttps/src/main/java/com/ejlchina/okhttps/DownListener.java index a4f4735b..53cbebed 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/DownListener.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/DownListener.java @@ -1,4 +1,4 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; /** * 下载监�接� diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/Download.java b/okhttps/src/main/java/com/ejlchina/okhttps/Download.java new file mode 100644 index 00000000..15392056 --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/Download.java @@ -0,0 +1,304 @@ +package com.ejlchina.okhttps; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; + +import com.ejlchina.okhttps.internal.HttpException; +import com.ejlchina.okhttps.internal.TaskExecutor; + +import okhttp3.internal.Util; + +/** + * 文件下载 + * @author Troy.Zhou + */ +public class Download { + + private File file; + private InputStream input; + private OnCallback onSuccess; + private OnCallback onFailure; + private TaskExecutor taskExecutor; + private long doneBytes; + private int buffSize = 0; + private long seekBytes = 0; + private boolean appended; + private volatile int status; + private final Object lock = new Object(); + + protected boolean nextOnIO = false; + private boolean sOnIO; + private boolean fOnIO; + + private Ctrl ctrl; + + public Download(File file, InputStream input, TaskExecutor taskExecutor, long skipBytes) { + this.file = file; + this.input = input; + this.taskExecutor = taskExecutor; + this.seekBytes = skipBytes; + this.ctrl = new Ctrl(); + } + + /** + * 设置缓冲区大�,默认 2K(2048) + * @param buffSize 缓冲区大�(��:字节) + * @return Download + */ + public Download setBuffSize(int buffSize) { + if (buffSize > 0) { + this.buffSize = buffSize; + } + return this; + } + + /** + * 设置文件追加模� + * 用预断点续传和分�下载 + * @return Download + */ + public Download setAppended() { + this.appended = true; + return this; + } + + /** + * 设置文件指针,从文件的 seekBytes �置追加内容 + * @param seekBytes 跨越的字节数 + * @return Download + */ + public Download setFilePointer(long seekBytes) { + this.seekBytes = seekBytes; + return this; + } + + /** + * 在IO线程执行 + * @return Download + */ + public Download nextOnIO() { + nextOnIO = true; + return this; + } + + /** + * 设置下载�功回调 + * @param onSuccess �功回调函数 + * @return Download + */ + public Download setOnSuccess(OnCallback onSuccess) { + this.onSuccess = onSuccess; + sOnIO = nextOnIO; + nextOnIO = false; + return this; + } + + /** + * 设置下载失败回调 + * @param onFailure 失败回调函数 + * @return Download + */ + public Download setOnFailure(OnCallback onFailure) { + this.onFailure = onFailure; + fOnIO = nextOnIO; + nextOnIO = false; + return this; + } + + /** + * 开始下载 + * @return 下载控制器 + */ + public Ctrl start() { + if (buffSize == 0) { + buffSize = Process.DEFAULT_STEP_BYTES; + } + RandomAccessFile raFile = randomAccessFile(); + status = Ctrl.STATUS__DOWNLOADING; + taskExecutor.execute(() -> doDownload(raFile), true); + return ctrl; + } + + /** + * 获�下载控制器 + * @return Ctrl + */ + public Ctrl getCtrl() { + return ctrl; + } + + public class Ctrl { + + /** + * 已�消 + */ + public static final int STATUS__CANCELED = -1; + + /** + * 下载中 + */ + public static final int STATUS__DOWNLOADING = 1; + + /** + * 已暂� + */ + public static final int STATUS__PAUSED = 2; + + /** + * 已完� + */ + public static final int STATUS__DONE = 3; + + /** + * 错误 + */ + public static final int STATUS__ERROR = 4; + + /** + * @see #STATUS__CANCELED + * @see #STATUS__DOWNLOADING + * @see #STATUS__PAUSED + * @see #STATUS__DONE + * @return 下载状� + */ + public int status() { + return status; + } + + /** + * 暂�下载任务 + */ + public void pause() { + synchronized (lock) { + if (status == STATUS__DOWNLOADING) { + status = STATUS__PAUSED; + } + } + } + + /** + * 继续下载任务 + */ + public void resume() { + synchronized (lock) { + if (status == STATUS__PAUSED) { + status = STATUS__DOWNLOADING; + } + } + } + + /** + * �消下载任务 + */ + public void cancel() { + synchronized (lock) { + if (status == STATUS__PAUSED || status == STATUS__DOWNLOADING) { + status = STATUS__CANCELED; + } + } + } + + } + + public class Failure { + + private IOException exception; + + Failure(IOException exception) { + this.exception = exception; + } + + /** + * @return 下载文件 + */ + public File getFile() { + return file; + } + + /** + * @return 已下载字节数 + */ + public long getDoneBytes() { + return doneBytes; + } + + /** + * @return 异常信� + */ + public IOException getException() { + return exception; + } + + } + + private RandomAccessFile randomAccessFile() { + try { + return new RandomAccessFile(file, "rw"); + } catch (FileNotFoundException e) { + status = Ctrl.STATUS__ERROR; + Util.closeQuietly(input); + throw new HttpException("无法获�文件[" + file.getAbsolutePath() + "]的输入�", e); + } + } + + private void doDownload(RandomAccessFile raFile) { + try { + if (appended && seekBytes > 0) { + long length = raFile.length(); + if (seekBytes <= length) { + raFile.seek(seekBytes); + doneBytes = seekBytes; + } else { + raFile.seek(length); + doneBytes = length; + } + } + while (status != Ctrl.STATUS__CANCELED && status != Ctrl.STATUS__DONE) { + if (status == Ctrl.STATUS__DOWNLOADING) { + byte[] buff = new byte[buffSize]; + int len; + while ((len = input.read(buff)) != -1) { + raFile.write(buff, 0, len); + doneBytes += len; + if (status == Ctrl.STATUS__CANCELED + || status == Ctrl.STATUS__PAUSED) { + break; + } + } + if (len == -1) { + synchronized (lock) { + status = Ctrl.STATUS__DONE; + } + } + } + } + } catch (IOException e) { + synchronized (lock) { + status = Ctrl.STATUS__ERROR; + } + if (onFailure != null) { + taskExecutor.execute(() -> { + onFailure.on(new Failure(e)); + }, fOnIO); + } else { + throw new HttpException("�传输失败", e); + } + } finally { + Util.closeQuietly(raFile); + Util.closeQuietly(input); + if (status == Ctrl.STATUS__CANCELED) { + file.delete(); + } + } + if (status == Ctrl.STATUS__DONE + && onSuccess != null) { + taskExecutor.execute(() -> { + onSuccess.on(file); + }, sOnIO); + } + } + +} diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/HTTP.java b/okhttps/src/main/java/com/ejlchina/okhttps/HTTP.java new file mode 100644 index 00000000..bf0fc809 --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/HTTP.java @@ -0,0 +1,471 @@ +package com.ejlchina.okhttps; + +import com.ejlchina.okhttps.internal.*; + +import okhttp3.*; +import okhttp3.WebSocket; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.Executor; + +/** + * HTTP 客户端接� + * + * @author 15735 + */ +public interface HTTP { + + String GET = "GET"; + String HEAD = "HEAD"; + String POST = "POST"; + String PUT = "PUT"; + String PATCH = "PATCH"; + String DELETE = "DELETE"; + + /** + * �步请求 + * @param url 请求地� + * @return �步HTTP任务 + */ + SyncHttpTask sync(String url); + + /** + * 异步请求 + * @param url 请求地� + * @return 异步HTTP任务 + */ + AsyncHttpTask async(String url); + + /** + * Websocket 连接 + * @param url 连接地� + * @return WebSocket 任务 + */ + WebSocketTask webSocket(String url); + + /** + * 根�标签�消HTTP任务,��任务的标签包�指定的Tag就会被�消 + * 从 v1.0.2 开始支��消�步请求 + * @param tag 标签 + * @return 被�消的任务数� + */ + int cancel(String tag); + + /** + * @since 1.0.2 + * �消所有HTTP任务,包括�步和异步 + */ + void cancelAll(); + + /** + * OkHttp 的 HTTP 原生请求 (该请求��过 预处�器) + * @param request 请求 + * @return Call + */ + Call request(Request request); + + /** + * OkHttp 的 Websocket 原生请求(该请求��过 预处�器) + * + * @param request 请求 + * @param listener 监�器 + * @return Websocket + */ + WebSocket webSocket(Request request, WebSocketListener listener); + + /** + * 获�任务执行器 + * @return TaskExecutor + */ + TaskExecutor executor(); + + /** + * 新的构建器 + * @return Builder + */ + Builder newBuilder(); + + /** + * HTTP 构建器 + * @return HTTP 构建器 + */ + static Builder builder() { + return new Builder(); + } + + + /** + * Http �置器 + * + */ + interface OkConfig { + + /** + * 使用 builder �置 HttpClient + * @param builder OkHttpClient 构建器 + */ + void config(OkHttpClient.Builder builder); + + } + + + class Builder { + + private OkHttpClient okClient; + + private String baseUrl; + + private Map mediaTypes; + + private OkConfig config; + + private Executor mainExecutor; + + private Scheduler taskScheduler; + + private List preprocessors; + + private DownListener downloadListener; + + private TaskListener responseListener; + + private TaskListener exceptionListener; + + private TaskListener completeListener; + + private List msgConvertors; + + private int preprocTimeoutTimes = 10; + + private Charset charset = StandardCharsets.UTF_8; + + private String bodyType = OkHttps.FORM; + + public Builder() { + mediaTypes = new HashMap<>(); + mediaTypes.put("*", "application/octet-stream"); + mediaTypes.put("png", "image/png"); + mediaTypes.put("jpg", "image/jpeg"); + mediaTypes.put("jpeg", "image/jpeg"); + mediaTypes.put("wav", "audio/wav"); + mediaTypes.put("mp3", "audio/mp3"); + mediaTypes.put("mp4", "video/mpeg4"); + mediaTypes.put("txt", "text/plain"); + mediaTypes.put("xls", "application/x-xls"); + mediaTypes.put("xml", "text/xml"); + mediaTypes.put("apk", "application/vnd.android.package-archive"); + mediaTypes.put("doc", "application/msword"); + mediaTypes.put("pdf", "application/pdf"); + mediaTypes.put("html", "text/html"); + preprocessors = new ArrayList<>(); + msgConvertors = new ArrayList<>(); + } + + public Builder(HttpClient hc) { + this.okClient = hc.okClient(); + this.baseUrl = hc.baseUrl(); + this.mediaTypes = hc.mediaTypes(); + this.preprocessors = new ArrayList<>(); + Collections.addAll(this.preprocessors, hc.preprocessors()); + TaskExecutor executor = hc.executor(); + this.mainExecutor = executor.getMainExecutor(); + this.taskScheduler = executor.getTaskScheduler(); + this.downloadListener = executor.getDownloadListener(); + this.responseListener = executor.getResponseListener(); + this.exceptionListener = executor.getExceptionListener(); + this.completeListener = executor.getCompleteListener(); + this.msgConvertors = new ArrayList<>(); + Collections.addAll(this.msgConvertors, executor.getMsgConvertors()); + this.preprocTimeoutTimes = hc.preprocTimeoutTimes(); + this.charset = hc.charset(); + this.bodyType = hc.bodyType(); + } + + /** + * �置 OkHttpClient + * + * @param config �置器 + * @return Builder + */ + public Builder config(OkConfig config) { + this.config = config; + return this; + } + + /** + * 设置 baseUrl + * + * @param baseUrl 全局URL�缀 + * @return Builder + */ + public Builder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + /** + * �置媒体类型 + * + * @param mediaTypes 媒体类型 + * @return Builder + */ + public Builder mediaTypes(Map mediaTypes) { + if (mediaTypes != null) { + this.mediaTypes.putAll(mediaTypes); + } + return this; + } + + /** + * �置媒体类型 + * + * @param key 媒体类型KEY + * @param value 媒体类型VALUE + * @return Builder + */ + public Builder mediaTypes(String key, String value) { + if (key != null && value != null) { + this.mediaTypes.put(key, value); + } + return this; + } + + /** + * 设置回调执行器,例如实现切�线程功能,�对异步请求有效 + * + * @param executor 回调执行器 + * @return Builder + */ + public Builder callbackExecutor(Executor executor) { + this.mainExecutor = executor; + return this; + } + + /** + * �置 任务调度器,�用的调度由 {@link WebSocketTask#heatbeat(int, int) } 指定的心跳任务 + * 若��置,则生�一个 线程容�为 1 的 ScheduledThreadPoolExecutor 调度器 + * @param scheduler 调度器 + * @return Builder + */ + public Builder taskScheduler(Scheduler scheduler) { + this.taskScheduler = scheduler; + return this; + } + + /** + * 添加�并行处�请求任务的预处�器 + * + * @param preprocessor 预处�器 + * @return Builder + */ + public Builder addPreprocessor(Preprocessor preprocessor) { + if (preprocessor != null) { + preprocessors.add(preprocessor); + } + return this; + } + + /** + * 添加预处�器 + * + * @param preprocessor 预处�器 + * @return Builder + */ + public Builder addSerialPreprocessor(Preprocessor preprocessor) { + if (preprocessor != null) { + preprocessors.add(new HttpClient.SerialPreprocessor(preprocessor)); + } + return this; + } + + /** + * 最大预处�时间(�数,相当普通请求的超时时间) + * + * @param times 普通超时时间的�数,默认为 10 + * @return Builder + */ + public Builder preprocTimeoutTimes(int times) { + if (times > 0) { + this.preprocTimeoutTimes = times; + } + return this; + } + + /** + * 设置全局�应监� + * + * @param listener 监�器 + * @return Builder + */ + public Builder responseListener(TaskListener listener) { + this.responseListener = listener; + return this; + } + + /** + * 设置全局异常监� + * + * @param listener 监�器 + * @return Builder + */ + public Builder exceptionListener(TaskListener listener) { + this.exceptionListener = listener; + return this; + } + + /** + * 设置全局完�监� + * + * @param listener 监�器 + * @return Builder + */ + public Builder completeListener(TaskListener listener) { + this.completeListener = listener; + return this; + } + + /** + * 设置下载监�器 + * + * @param listener 监�器 + * @return Builder + */ + public Builder downloadListener(DownListener listener) { + this.downloadListener = listener; + return this; + } + + /** + * @since 2.0.0 + * 添加消�转�器 + * @param msgConvertor JSON �务 + * @return Builder + */ + public Builder addMsgConvertor(MsgConvertor msgConvertor) { + if (msgConvertor != null) { + this.msgConvertors.add(msgConvertor); + } + return this; + } + + /** + * @since 2.0.0 + * 设置默认编�格� + * @param charset 编� + * @return Builder + */ + public Builder charset(Charset charset) { + if (charset != null) { + this.charset = charset; + } + return this; + } + + /** + * @since 2.0.0 + * 设置默认请求体类型 + * @param bodyType 请求体类型 + * @return Builder + */ + public Builder bodyType(String bodyType) { + if (bodyType != null) { + this.bodyType = bodyType; + } + return this; + } + + /** + * 构建 HTTP 实例 + * @return HTTP + */ + public HTTP build() { + if (config != null || okClient == null) { + OkHttpClient.Builder builder; + if (okClient != null) { + builder = okClient.newBuilder(); + } else { + builder = new OkHttpClient.Builder(); + } + if (config != null) { + config.config(builder); + } + // fix issue: https://github.com/ejlchina/okhttps/issues/8 + if (needCopyInterceptor(builder.interceptors())) { + builder.addInterceptor(new CopyInterceptor()); + } + okClient = builder.build(); + } else if (needCopyInterceptor(okClient.interceptors())) { + okClient = okClient.newBuilder() + .addInterceptor(new CopyInterceptor()) + .build(); + } + return new HttpClient(this); + } + + private boolean needCopyInterceptor(List list) { + return mainExecutor != null && Platform.ANDROID_SDK_INT > 24 + && CopyInterceptor.notIn(list); + } + + public OkHttpClient okClient() { + return okClient; + } + + public String baseUrl() { + return baseUrl; + } + + public Map getMediaTypes() { + return mediaTypes; + } + + public Executor mainExecutor() { + return mainExecutor; + } + + public Preprocessor[] preprocessors() { + return preprocessors.toArray(new Preprocessor[0]); + } + + public DownListener downloadListener() { + return downloadListener; + } + + public TaskListener responseListener() { + return responseListener; + } + + public TaskListener exceptionListener() { + return exceptionListener; + } + + public TaskListener completeListener() { + return completeListener; + } + + public MsgConvertor[] msgConvertors() { + return msgConvertors.toArray(new MsgConvertor[0]); + } + + public Scheduler taskScheduler() { + return taskScheduler; + } + + public int preprocTimeoutTimes() { + return preprocTimeoutTimes; + } + + public Charset charset() { + return charset; + } + + public String bodyType() { + return bodyType; + } + + } + +} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/HttpCall.java b/okhttps/src/main/java/com/ejlchina/okhttps/HttpCall.java similarity index 82% rename from okhttps/src/main/java/cn/zhxu/okhttps/HttpCall.java rename to okhttps/src/main/java/com/ejlchina/okhttps/HttpCall.java index 00c15fa8..cf692cb1 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/HttpCall.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/HttpCall.java @@ -1,4 +1,6 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; + +import com.ejlchina.okhttps.internal.AsyncHttpTask; public interface HttpCall extends Cancelable { @@ -20,10 +22,9 @@ public interface HttpCall extends Cancelable { HttpResult getResult(); /** - * 获�所属的请求任务 * @since 2.2.0 * @return 当�的异步请求任务 */ - AHttpTask getTask(); + AsyncHttpTask getTask(); } diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/HttpResult.java b/okhttps/src/main/java/com/ejlchina/okhttps/HttpResult.java similarity index 81% rename from okhttps/src/main/java/cn/zhxu/okhttps/HttpResult.java rename to okhttps/src/main/java/com/ejlchina/okhttps/HttpResult.java index 8294c861..00098df7 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/HttpResult.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/HttpResult.java @@ -1,15 +1,15 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.data.ListMap; -import cn.zhxu.okhttps.internal.RealHttpResult; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.Response; +package com.ejlchina.okhttps; import java.io.File; import java.io.IOException; import java.util.List; -import java.util.function.Consumer; + +import com.ejlchina.okhttps.internal.RealHttpResult; +import com.ejlchina.okhttps.internal.TaskExecutor; + +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.Response; /** @@ -41,22 +41,7 @@ static HttpResult of(Response response, TaskExecutor taskExecutor) { throw new IllegalArgumentException("response �能为空"); } - /** - * 构造一个 HttpResult - * @param task HttpTask - * @param response Response - * @return HttpResult - */ - static HttpResult of(HttpTask task, Response response) { - if (task != null && response != null) { - return new RealHttpResult(task, response, task.httpClient.executor()); - } - throw new IllegalArgumentException("task 与 response �能为空"); - } - - /** - * HTTP 任务执行状� - */ + enum State { /** @@ -92,14 +77,11 @@ enum State { interface Body extends Toable { /** - * will be removed in next version * @return 媒体类型 */ - @Deprecated MediaType getType(); /** - * �赖于 Content-Length �应头 * @return 报文体字节长度 */ long getLength(); @@ -115,7 +97,7 @@ interface Body extends Toable { * @param onProcess 进度回调函数 * @return Body */ - Body setOnProcess(Consumer onProcess); + Body setOnProcess(OnCallback onProcess); /** * 设置进度回调的步进字节,默认 8K(8192) @@ -197,25 +179,15 @@ interface Body extends Toable { int getStatus(); /** - * WebSocket 返回 101 也返回 true (since v3.5.1) * @return 是��应�功,状��在 [200..300) 之间 */ boolean isSuccessful(); /** - * will be removed in next version * @return �应头 - * @see #allHeaders() */ - @Deprecated Headers getHeaders(); - /** - * @return 所有�应头 - * @since v3.5.0 - */ - ListMap allHeaders(); - /** * @param name 头�称 * @return �应头 @@ -239,14 +211,7 @@ interface Body extends Toable { * @return �应报文体 */ Body getBody(); - - /** - * 获�所属的请求任务 - * @since 2.5.0 - * @return HttpTask - */ - HttpTask getTask(); - + /** * @return 执行中�生的异常 */ diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/HttpTask.java b/okhttps/src/main/java/com/ejlchina/okhttps/HttpTask.java new file mode 100644 index 00000000..22c51b98 --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/HttpTask.java @@ -0,0 +1,745 @@ +package com.ejlchina.okhttps; + +import com.ejlchina.okhttps.HttpResult.State; +import com.ejlchina.okhttps.internal.*; +import com.ejlchina.okhttps.internal.HttpClient.TagTask; +import okhttp3.*; +import okhttp3.internal.http.HttpMethod; + +import java.io.File; +import java.io.IOException; +import java.net.ConnectException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Created by 周旭(Troy.Zhou) on 2020/3/11. + */ +@SuppressWarnings("unchecked") +public abstract class HttpTask> implements Cancelable { + + private static final String PATH_PARAM_REGEX = "[A-Za-z0-9_\\-/]*\\{[A-Za-z0-9_\\-]+\\}[A-Za-z0-9_\\-/]*"; + + protected HttpClient httpClient; + protected boolean nothrow; + protected boolean nextOnIO = false; + + private String urlPath; + private String tag; + private Map headers; + private Map pathParams; + private Map urlParams; + private Map bodyParams; + private Map files; + private Object requestBody; + private String bodyType; + private OnCallback onProcess; + private boolean processOnIO; + private long stepBytes = 0; + private double stepRate = -1; + + private Object object; + + private TagTask tagTask; + private Cancelable canceler; + private Charset charset; + + protected boolean skipPreproc = false; + protected boolean skipSerialPreproc = false; + + + public HttpTask(HttpClient httpClient, String url) { + this.urlPath = url; + this.httpClient = httpClient; + this.charset = httpClient.charset(); + this.bodyType = httpClient.bodyType(); + } + + /** + * 获�请求任务的URL地� + * @return URL地� + */ + public String getUrl() { + return urlPath; + } + + /** + * @return 是�是 Websocket 通讯 + */ + public boolean isWebsocket() { + return false; + } + + /** + * @since 2.2.0 + * @return 是�是 �步 Http 请求 + */ + public boolean isSyncHttp() { + return false; + } + + /** + * @since 2.2.0 + * @return 是�是 异步 Http 请求 + */ + public boolean isAsyncHttp() { + return false; + } + + /** + * 获�请求任务的标签 + * @return 标签 + */ + public String getTag() { + return tag; + } + + public String getBodyType() { + return bodyType; + } + + /** + * 标签匹� + * 判断任务标签与指定的标签是�匹�(包�指定的标签) + * @param tag 标签 + * @return 是�匹� + */ + public boolean isTagged(String tag) { + if (this.tag != null && tag != null) { + return this.tag.contains(tag); + } + return false; + } + + /** + * 获�请求任务的头信� + * @return 头信� + */ + public Map getHeaders() { + return headers; + } + + /** + * 获得被绑定的对象 + * @return Object + */ + public Object getBound() { + return object; + } + + /** + * 设置在�生异常时��上抛出,设置�: + * 异步请求�以在异常回调内�获异常,�步请求在返回结果中找到该异常 + * @return HttpTask 实例 + */ + public C nothrow() { + this.nothrow = true; + return (C) this; + } + + /** + * 指定该请求跳过任何预处�器(包括串行和并行) + * @return HttpTask 实例 + */ + public C skipPreproc() { + this.skipPreproc = true; + return (C) this; + } + + /** + * 指定该请求跳过任何串行预处�器 + * @return HttpTask 实例 + */ + public C skipSerialPreproc() { + this.skipSerialPreproc = true; + return (C) this; + } + + /** + * @since 2.0.0.RC + * 为请求任务添加标签 + * @param tag 标签 + * @return HttpTask 实例 + */ + public C tag(String tag) { + if (tag != null) { + if (this.tag != null) { + this.tag = this.tag + "." + tag; + } else { + this.tag = tag; + } + updateTagTask(); + } + return (C) this; + } + + /** + * @since 2.0.0 + * 设置该请求的编�格� + * @param charset 编�格� + * @return HttpTask 实例 + */ + public C charset(Charset charset) { + if (charset != null) { + this.charset = charset; + } + return (C) this; + } + + /** + * @since 2.0.0 + * 设置请求体的类型,如:form�json�xml�protobuf 等 + * @param type 请求类型 + * @return HttpTask 实例 + */ + public C bodyType(String type) { + if (type != null) { + this.bodyType = type; + } + return (C) this; + } + + /** + * 下一个回调在IO线程执行 + * @return HttpTask 实例 + */ + public C nextOnIO() { + nextOnIO = true; + return (C) this; + } + + /** + * 绑定一个对象 + * @param object 对象 + * @return HttpTask 实例 + */ + public C bind(Object object) { + this.object = object; + return (C) this; + } + + /** + * 添加请求头 + * @param name 请求头� + * @param value 请求头值 + * @return HttpTask 实例 + */ + public C addHeader(String name, String value) { + if (name != null && value != null) { + if (headers == null) { + headers = new HashMap<>(); + } + headers.put(name, value); + } + return (C) this; + } + + /** + * 添加请求头 + * @param headers 请求头集� + * @return HttpTask 实例 + */ + public C addHeader(Map headers) { + if (headers != null) { + if (this.headers == null) { + this.headers = new HashMap<>(); + } + this.headers.putAll(headers); + } + return (C) this; + } + + /** + * 设置Range头信� + * 表示接收报文体时跳过的字节数,用于断点续传 + * @param rangeStart 表示从 rangeStart 个字节处开始接收,通常是已�下载的字节数,�上次的断点) + * @return HttpTask 实例 + */ + public C setRange(long rangeStart) { + return addHeader("Range", "bytes=" + rangeStart + "-"); + } + + /** + * 设置Range头信� + * 设置接收报文体时接收的范围,用于分�下载 + * @param rangeStart 表示从 rangeStart 个字节处开始接收 + * @param rangeEnd 表示接收到 rangeEnd 个字节处 + * @return HttpTask 实例 + */ + public C setRange(long rangeStart, long rangeEnd) { + return addHeader("Range", "bytes=" + rangeStart + "-" + rangeEnd); + } + + /** + * 设置报文体��进度回调 + * @param onProcess 进度回调函数 + * @return HttpTask 实例 + */ + public C setOnProcess(OnCallback onProcess) { + this.onProcess = onProcess; + processOnIO = nextOnIO; + nextOnIO = false; + return (C) this; + } + + /** + * 设置进度回调的步进字节,默认 8K(8192) + * 表示�接收 stepBytes 个字节,执行一次进度回调 + * @param stepBytes 步进字节 + * @return HttpTask 实例 + */ + public C stepBytes(long stepBytes) { + this.stepBytes = stepBytes; + return (C) this; + } + + /** + * 设置进度回调的步进比例 + * 表示�接收 stepRate 比例,执行一次进度回调 + * @param stepRate 步进比例 + * @return HttpTask 实例 + */ + public C stepRate(double stepRate) { + this.stepRate = stepRate; + return (C) this; + } + + /** + * 路径�数:替�URL里的{name} + * @param name �数� + * @param value �数值 + * @return HttpTask 实例 + **/ + public C addPathPara(String name, Object value) { + if (name != null && value != null) { + if (pathParams == null) { + pathParams = new HashMap<>(); + } + pathParams.put(name, value.toString()); + } + return (C) this; + } + + /** + * 路径�数:替�URL里的{name} + * @param params �数集� + * @return HttpTask 实例 + **/ + public C addPathPara(Map params) { + if (pathParams == null) { + pathParams = new HashMap<>(); + } + doAddParams(pathParams, params); + return (C) this; + } + + /** + * URL�数:拼接在URL�的�数 + * @param name �数� + * @param value �数值 + * @return HttpTask 实例 + **/ + public C addUrlPara(String name, Object value) { + if (name != null && value != null) { + if (urlParams == null) { + urlParams = new HashMap<>(); + } + urlParams.put(name, value.toString()); + } + return (C) this; + } + + /** + * URL�数:拼接在URL�的�数 + * @param params �数集� + * @return HttpTask 实例 + **/ + public C addUrlPara(Map params) { + if (urlParams == null) { + urlParams = new HashMap<>(); + } + doAddParams(urlParams, params); + return (C) this; + } + + /** + * Body�数:放在Body里的�数 + * @param name �数� + * @param value �数值 + * @return HttpTask 实例 + **/ + public C addBodyPara(String name, Object value) { + if (name != null && value != null) { + if (bodyParams == null) { + bodyParams = new HashMap<>(); + } + bodyParams.put(name, value.toString()); + } + return (C) this; + } + + /** + * Body�数:放在Body里的�数 + * @param params �数集� + * @return HttpTask 实例 + **/ + public C addBodyPara(Map params) { + if (bodyParams == null) { + bodyParams = new HashMap<>(); + } + doAddParams(bodyParams, params); + return (C) this; + } + + private void doAddParams(Map taskParams, Map params) { + if (params != null) { + for (String name : params.keySet()) { + Object value = params.get(name); + if (name != null && value != null) { + taskParams.put(name, value.toString()); + } + } + } + } + + /** + * 设置 请求报文体 + * @param body 请求体,字节数组�字符串 或 Java对象(由 MsgConvertor ��列化) + * @return HttpTask 实例 + **/ + public C setBodyPara(Object body) { + this.requestBody = body; + return (C) this; + } + + /** + * 添加文件�数 + * @param name �数� + * @param filePath 文件路径 + * @return HttpTask 实例 + */ + public C addFilePara(String name, String filePath) { + return addFilePara(name, new File(filePath)); + } + + /** + * 添加文件�数 + * @param name �数� + * @param file 文件 + * @return HttpTask 实例 + */ + public C addFilePara(String name, File file) { + if (name != null && file != null && file.exists()) { + String fileName = file.getName(); + String type = fileName.substring(fileName.lastIndexOf(".") + 1); + if (files == null) { + files = new HashMap<>(); + } + files.put(name, new FilePara(type, fileName, file)); + } + return (C) this; + } + + /** + * 添加文件�数 + * @param name �数� + * @param type 文件类型: 如 png�jpg�jpeg 等 + * @param content 文件内容 + * @return HttpTask 实例 + */ + public C addFilePara(String name, String type, byte[] content) { + return addFilePara(name, type, null, content); + } + + /** + * 添加文件�数 + * @param name �数� + * @param type 文件类型: 如 png�jpg�jpeg 等 + * @param fileName 文件� + * @param content 文件内容 + * @return HttpTask 实例 + */ + public C addFilePara(String name, String type, String fileName, byte[] content) { + if (name != null && content != null) { + if (files == null) { + files = new HashMap<>(); + } + files.put(name, new FilePara(type, fileName, content)); + } + return (C) this; + } + + @Override + public boolean cancel() { + if (canceler != null) { + return canceler.cancel(); + } + return false; + } + + static class FilePara { + + String type; + String fileName; + byte[] content; + File file; + + FilePara(String type, String fileName, byte[] content) { + this.type = type; + this.fileName = fileName; + this.content = content; + } + + FilePara(String type, String fileName, File file) { + this.type = type; + this.fileName = fileName; + this.file = file; + } + + } + + protected void registeTagTask(Cancelable canceler) { + if (tag != null && tagTask == null) { + tagTask = httpClient.addTagTask(tag, canceler, this); + } + this.canceler = canceler; + } + + private void updateTagTask() { + if (tagTask != null) { + tagTask.setTag(tag); + } else + if (canceler != null) { + registeTagTask(canceler); + } + } + + protected void removeTagTask() { + if (tag != null) { + httpClient.removeTagTask(this); + } + } + + protected Call prepareCall(String method) { + Request request = prepareRequest(method); + return httpClient.request(request); + } + + protected Request prepareRequest(String method) { + boolean bodyCanUsed = HttpMethod.permitsRequestBody(method); + assertNotConflict(!bodyCanUsed); + Request.Builder builder = new Request.Builder() + .url(buildUrlPath()); + buildHeaders(builder); + if (bodyCanUsed) { + RequestBody reqBody = buildRequestBody(); + if (onProcess != null) { + long contentLength = contentLength(reqBody); + if (stepRate > 0 && stepRate <= 1) { + stepBytes = (long) (contentLength * stepRate); + } + if (stepBytes <= 0) { + stepBytes = Process.DEFAULT_STEP_BYTES; + } + reqBody = new ProcessRequestBody(reqBody, onProcess, + httpClient.executor().getExecutor(processOnIO), + contentLength, stepBytes); + } + builder.method(method, reqBody); + } else { + builder.method(method, null); + } + if (tag != null) { + builder.tag(String.class, tag); + } + return builder.build(); + } + + private long contentLength(RequestBody reqBody) { + try { + return reqBody.contentLength(); + } catch (IOException e) { + throw new HttpException("无法获�请求体长度", e); + } + } + + private void buildHeaders(Request.Builder builder) { + if (headers != null) { + for (String name : headers.keySet()) { + String value = headers.get(name); + if (value != null) { + builder.addHeader(name, value); + } + } + } + } + + protected State toState(IOException e) { + if (e instanceof SocketTimeoutException) { + return State.TIMEOUT; + } else if (e instanceof UnknownHostException || e instanceof ConnectException) { + return State.NETWORK_ERROR; + } + String msg = e.getMessage(); + if (msg != null && ("Canceled".equals(msg) || e instanceof SocketException + && (msg.startsWith("Socket operation on nonsocket") || "Socket closed".equals(msg)))) { + return State.CANCELED; + } + return State.EXCEPTION; + } + + private RequestBody buildRequestBody() { + if (files != null) { + MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM); + if (bodyParams != null) { + for (String name : bodyParams.keySet()) { + byte[] value = bodyParams.get(name).getBytes(charset); + RequestBody body = RequestBody.create(null, value); + builder.addPart(MultipartBody.Part.createFormData(name, null, body)); + } + } + for (String name : files.keySet()) { + FilePara file = files.get(name); + MediaType type = httpClient.mediaType(file.type); + RequestBody bodyPart; + if (file.file != null) { + bodyPart = RequestBody.create(type, file.file); + } else { + bodyPart = RequestBody.create(type, file.content); + } + builder.addFormDataPart(name, file.fileName, bodyPart); + } + return builder.build(); + } + if (requestBody != null) { + return toRequestBody(requestBody); + } + if (bodyParams == null) { + return new FormBody.Builder(charset).build(); + } + if (OkHttps.FORM.equalsIgnoreCase(bodyType)) { + FormBody.Builder builder = new FormBody.Builder(charset); + for (String name : bodyParams.keySet()) { + String value = bodyParams.get(name); + builder.add(name, value); + } + return builder.build(); + } + return toRequestBody(bodyParams); + } + + private RequestBody toRequestBody(Object object) { + if (object instanceof byte[] || object instanceof String) { + String mediaType = httpClient.executor().doMsgConvert(bodyType, null).mediaType; + byte[] body = object instanceof byte[] ? (byte[]) object : ((String) object).getBytes(charset); + return RequestBody.create(MediaType.parse(mediaType + "; charset=" + charset.name()), body); + } + TaskExecutor.Data data = httpClient.executor() + .doMsgConvert(bodyType, (MsgConvertor c) -> c.serialize(object, charset)); + return RequestBody.create(MediaType.parse(data.mediaType + "; charset=" + charset.name()), data.data); + } + + private String buildUrlPath() { + String url = urlPath; + if (url == null || url.trim().isEmpty()) { + throw new HttpException("url �能为空�"); + } + if (pathParams != null) { + for (String name : pathParams.keySet()) { + String target = "{" + name + "}"; + if (url.contains(target)) { + url = url.replace(target, pathParams.get(name)); + } else { + throw new HttpException("pathParameter [ " + name + " ] �存在于 url [ " + urlPath + " ]"); + } + } + } + if (url.matches(PATH_PARAM_REGEX)) { + throw new HttpException("url 里有 pathParameter 没有设置,你必须先调用 addPathParam 为其设置�"); + } + if (urlParams != null) { + url = buildUrl(url.trim()); + } + return url; + } + + private String buildUrl(String url) { + StringBuilder sb = new StringBuilder(url); + if (url.contains("?")) { + if (!url.endsWith("?")) { + if (url.lastIndexOf("=") < url.lastIndexOf("?") + 2) { + throw new HttpException("url 格�错误,'?' �没有�现 '='"); + } + if (!url.endsWith("&")) { + sb.append('&'); + } + } + } else { + sb.append('?'); + } + for (String name : urlParams.keySet()) { + sb.append(name).append('=').append(urlParams.get(name)).append('&'); + } + sb.delete(sb.length() - 1, sb.length()); + return sb.toString(); + } + + protected void assertNotConflict(boolean bodyCantUsed) { + if (bodyCantUsed) { + if (requestBody != null) { + throw new HttpException("GET | HEAD 请求 �能调用 setBodyPara 方法�"); + } + if (bodyParams != null) { + throw new HttpException("GET | HEAD 请求 �能调用 addBodyPara 方法�"); + } + if (files != null) { + throw new HttpException("GET | HEAD 请求 �能调用 addFilePara 方法�"); + } + } + if (requestBody != null) { + if (bodyParams != null) { + throw new HttpException("方法 addBodyPara 与 setBodyPara �能�时调用�"); + } + if (files != null) { + throw new HttpException("方法 addFilePara 与 setBodyPara �能�时调用�"); + } + } + } + + /** + * @param latch CountDownLatch + * @return 是�未超时:false 表示已超时 + */ + protected boolean timeoutAwait(CountDownLatch latch) { + try { + return latch.await(httpClient.preprocTimeoutMillis(), + TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new HttpException("超时", e); + } + } + + protected HttpResult timeoutResult() { + if (nothrow) { + return new RealHttpResult(this, State.TIMEOUT); + } + throw new HttpException(State.TIMEOUT, "执行超时"); + } + + public Charset charset(Response response) { + ResponseBody b = response.body(); + MediaType type = b != null ? b.contentType() : null; + return type != null ? type.charset(charset) : charset; + } + + protected void execute(Runnable command, boolean onIo) { + httpClient.executor().execute(command, onIo); + } + +} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/HttpUtils.java b/okhttps/src/main/java/com/ejlchina/okhttps/HttpUtils.java similarity index 84% rename from okhttps/src/main/java/cn/zhxu/okhttps/HttpUtils.java rename to okhttps/src/main/java/com/ejlchina/okhttps/HttpUtils.java index 00c2c4f0..d8db6a5a 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/HttpUtils.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/HttpUtils.java @@ -1,5 +1,10 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; +import com.ejlchina.okhttps.internal.AsyncHttpTask; +import com.ejlchina.okhttps.internal.SyncHttpTask; +import com.ejlchina.okhttps.internal.TaskExecutor; + +import com.ejlchina.okhttps.internal.WebSocketTask; import okhttp3.Call; import okhttp3.Request; import okhttp3.WebSocket; @@ -20,6 +25,7 @@ public class HttpUtils { * �置HttpUtils�有的HTTP实例(�调用此方法�默认使用一个没有没有�过任何�置的HTTP懒实例) * @param http HTTP实例 */ + @Deprecated public static void of(HTTP http) { if (http != null) { HttpUtils.http = http; @@ -29,7 +35,7 @@ public static void of(HTTP http) { } - public static synchronized HTTP getHttp() { + static synchronized HTTP getHttp() { if (http != null) { return http; } @@ -44,7 +50,7 @@ public static synchronized HTTP getHttp() { * @param url 请求地� * @return 异步 HttpTask */ - public static AHttpTask async(String url) { + public static AsyncHttpTask async(String url) { return getHttp().async(url); } @@ -53,7 +59,7 @@ public static AHttpTask async(String url) { * @param url 请求地� * @return �步 HttpTask */ - public static SHttpTask sync(String url) { + public static SyncHttpTask sync(String url) { return getHttp().sync(url); } @@ -62,7 +68,7 @@ public static SHttpTask sync(String url) { * @param url 连接地� * @return WebSocket 任务 */ - public static WHttpTask webSocket(String url) { + public static WebSocketTask webSocket(String url) { return getHttp().webSocket(url); } diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/Mapper.java b/okhttps/src/main/java/com/ejlchina/okhttps/Mapper.java new file mode 100644 index 00000000..e99ab8cc --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/Mapper.java @@ -0,0 +1,82 @@ +package com.ejlchina.okhttps; + +import java.util.Set; + +/** + * @since 2.0.0 + * Mapper 接�,类似于 json + * 但为什么���为 json 呢,因它�止是 json,它还�以是 xml�yml�protobuf 等任何一�格�的数� + * @author 15735 + */ +public interface Mapper { + + /** + * @return JSON 的键值对数� + */ + int size(); + + /** + * @return 是�为空 + */ + boolean isEmpty(); + + /** + * @param key 键� + * @return � JsonObj + */ + Mapper getMapper(String key); + + /** + * @param key 键� + * @return � JsonArr + */ + Array getArray(String key); + + /** + * @param key 键� + * @return boolean 值 + */ + boolean getBool(String key); + + /** + * @param key 键� + * @return int 值 + */ + int getInt(String key); + + /** + * @param key 键� + * @return long 值 + */ + long getLong(String key); + + /** + * @param key 键� + * @return float 值 + */ + float getFloat(String key); + + /** + * @param key 键� + * @return double 值 + */ + double getDouble(String key); + + /** + * @param key 键� + * @return String 值 + */ + String getString(String key); + + /** + * @param key 键� + * @return 是�有该键 + */ + boolean has(String key); + + /** + * @return JSON 的键集� + */ + Set keySet(); + +} diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/MsgConvertor.java b/okhttps/src/main/java/com/ejlchina/okhttps/MsgConvertor.java new file mode 100644 index 00000000..8e7a5682 --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/MsgConvertor.java @@ -0,0 +1,116 @@ +package com.ejlchina.okhttps; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.List; + +/** + * 消�转�器接� + */ +public interface MsgConvertor { + + /** + * 消�的媒体类型 + * @return 媒体类型 + */ + String mediaType(); + + /** + * 解� Mapper + * @param in JSON 输入� + * @param charset 编�格� + * @return Mapper + */ + Mapper toMapper(InputStream in, Charset charset); + + /** + * 解� Array + * @param in JSON 输入� + * @param charset 编�格� + * @return Array + */ + Array toArray(InputStream in, Charset charset); + + /** + * 将 Java 对象�列化为字节数组 + * @param object Java 对象 + * @param charset 编�格� + * @return 字节数组 + */ + byte[] serialize(Object object, Charset charset); + + /** + * 解� Java Bean + * @param 目标泛型 + * @param type 目标类型 + * @param in JSON 输入� + * @param charset 编�格� + * @return Java Bean + */ + T toBean(Type type, InputStream in, Charset charset); + + /** + * 解�为 Java List + * @param 目标泛型 + * @param type 目标类型 + * @param in JSON 输入� + * @param charset 编�格� + * @return Java List + */ + List toList(Class type, InputStream in, Charset charset); + + /** + * 表�转�器,�用于自动系列化表��数 + */ + class FormConvertor implements MsgConvertor { + + private MsgConvertor convertor; + + public FormConvertor(MsgConvertor convertor) { + this.convertor = convertor; + } + + @Override + public String mediaType() { + return "application/x-www-form-urlencoded"; + } + + @Override + public Mapper toMapper(InputStream in, Charset charset) { + return convertor.toMapper(in, charset); + } + + @Override + public Array toArray(InputStream in, Charset charset) { + return convertor.toArray(in, charset); + } + + @Override + public byte[] serialize(Object object, Charset charset) { + byte[] data = convertor.serialize(object, charset); + Mapper mapper = convertor.toMapper(new ByteArrayInputStream(data), charset); + StringBuilder sb = new StringBuilder(); + for (String key: mapper.keySet()) { + sb.append(key).append('=').append(mapper.getString(key)).append('&'); + } + if (sb.length() > 1) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString().getBytes(charset); + } + + @Override + public T toBean(Type type, InputStream in, Charset charset) { + return convertor.toBean(type, in, charset); + } + + @Override + public List toList(Class type, InputStream in, Charset charset) { + return convertor.toList(type, in, charset); + } + + } + +} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/OkHttps.java b/okhttps/src/main/java/com/ejlchina/okhttps/OkHttps.java similarity index 71% rename from okhttps/src/main/java/cn/zhxu/okhttps/OkHttps.java rename to okhttps/src/main/java/com/ejlchina/okhttps/OkHttps.java index e592c917..ba95fe8c 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/OkHttps.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/OkHttps.java @@ -1,5 +1,6 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; +import com.ejlchina.okhttps.internal.*; import okhttp3.Call; import okhttp3.Request; import okhttp3.WebSocket; @@ -11,32 +12,24 @@ */ public final class OkHttps { - // 使用 x-www-form-urlencoded 表�格� public static final String FORM = "form"; - // 使用 form-data 表�格�(一般上传文件时使用) - public static final String FORM_DATA = "form-data"; public static final String JSON = "json"; public static final String XML = "xml"; public static final String PROTOBUF = "protobuf"; - public static final String MSGPACK = "msgpack"; - private static HTTP instance; + private static HTTP http; private OkHttps() {} - public static HTTP getHttp() { - if (instance != null) { - return instance; - } - synchronized (OkHttps.class) { - if (instance == null) { - HTTP.Builder builder = HTTP.builder(); - ConvertProvider.inject(builder); - Config.config(builder); - instance = builder.build(); - } - return instance; + static synchronized HTTP getHttp() { + if (http != null) { + return http; } + HTTP.Builder builder = HTTP.builder(); + ConvertProvider.inject(builder); + Config.config(builder); + http = builder.build(); + return http; } /** @@ -44,7 +37,7 @@ public static HTTP getHttp() { * @param url 请求地� * @return 异步 HttpTask */ - public static AHttpTask async(String url) { + public static AsyncHttpTask async(String url) { return getHttp().async(url); } @@ -53,7 +46,7 @@ public static AHttpTask async(String url) { * @param url 请求地� * @return �步 HttpTask */ - public static SHttpTask sync(String url) { + public static SyncHttpTask sync(String url) { return getHttp().sync(url); } @@ -62,7 +55,7 @@ public static SHttpTask sync(String url) { * @param url 连接地� * @return WebSocket 任务 */ - public static WHttpTask webSocket(String url) { + public static WebSocketTask webSocket(String url) { return getHttp().webSocket(url); } diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/OnCallback.java b/okhttps/src/main/java/com/ejlchina/okhttps/OnCallback.java new file mode 100644 index 00000000..bfeeaa3e --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/OnCallback.java @@ -0,0 +1,11 @@ +package com.ejlchina.okhttps; + +/** + * 数�回调接� + * Created by 15735 on 2017/1/3. + */ +public interface OnCallback { + + void on(T data); + +} diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/Platform.java b/okhttps/src/main/java/com/ejlchina/okhttps/Platform.java new file mode 100644 index 00000000..1f8e04a9 --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/Platform.java @@ -0,0 +1,35 @@ +package com.ejlchina.okhttps; + +import java.lang.reflect.Field; + +public class Platform { + + /** + * Android 的 SDK 版本,若�是 Android 平�,则为 0 + */ + public static final int ANDROID_SDK_INT = getAndroidSdkInt(); + + + private static int getAndroidSdkInt() { + try { + Class versionClass = Class.forName("android.os.Build$VERSION"); + Field field = versionClass.getDeclaredField("SDK_INT"); + return field.getInt(field); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { + return 0; + } + } + + public static void logInfo(String message) { + okhttp3.internal.platform.Platform.get().log(okhttp3.internal.platform.Platform.INFO, message, null); + } + + public static void logError(String message) { + logError(message, null); + } + + public static void logError(String message, Throwable t) { + okhttp3.internal.platform.Platform.get().log(okhttp3.internal.platform.Platform.WARN, message, t); + } + +} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/Preprocessor.java b/okhttps/src/main/java/com/ejlchina/okhttps/Preprocessor.java similarity index 65% rename from okhttps/src/main/java/cn/zhxu/okhttps/Preprocessor.java rename to okhttps/src/main/java/com/ejlchina/okhttps/Preprocessor.java index ed11e28f..188667cb 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/Preprocessor.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/Preprocessor.java @@ -1,14 +1,14 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; /** * 预处�器,支�异步 - * 在 HTTP 请求任务正�开始之�执行 + * 在HTTP请求任务正�开始之�执行 * @author Troy.Zhou */ public interface Preprocessor { /** - * 在 HTTP 请求开始之�执行 + * 在HTTP请求开始之�执行 * @param chain 预处�器链 */ void doProcess(PreChain chain); @@ -22,12 +22,12 @@ interface PreChain { HttpTask getTask(); /** - * @return 当�使用的 HTTP 实例 + * @return HTTP */ HTTP getHttp(); /** - * 继续 HTTP 请求任务 + * 继续HTTP请求任务 */ void proceed(); diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/Process.java b/okhttps/src/main/java/com/ejlchina/okhttps/Process.java new file mode 100644 index 00000000..f6eaf3bc --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/Process.java @@ -0,0 +1,30 @@ +package com.ejlchina.okhttps; + +/** + * 进度(上传或下载) + */ +public interface Process { + + int DEFAULT_STEP_BYTES = 8192; + + /** + * @return 完�比例 + */ + double getRate(); + + /** + * @return 总 + */ + long getTotalBytes(); + + /** + * @return 已完�任务� + */ + long getDoneBytes(); + + /** + * @return 任务是�完� + */ + boolean isDone(); + +} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/Scheduler.java b/okhttps/src/main/java/com/ejlchina/okhttps/Scheduler.java similarity index 90% rename from okhttps/src/main/java/cn/zhxu/okhttps/Scheduler.java rename to okhttps/src/main/java/com/ejlchina/okhttps/Scheduler.java index b479835c..2317b6fc 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/Scheduler.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/Scheduler.java @@ -1,4 +1,4 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; import java.util.concurrent.TimeUnit; diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/TaskListener.java b/okhttps/src/main/java/com/ejlchina/okhttps/TaskListener.java similarity index 91% rename from okhttps/src/main/java/cn/zhxu/okhttps/TaskListener.java rename to okhttps/src/main/java/com/ejlchina/okhttps/TaskListener.java index 96e11a84..54f734d1 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/TaskListener.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/TaskListener.java @@ -1,4 +1,4 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; /** * 任务监�接� diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/Toable.java b/okhttps/src/main/java/com/ejlchina/okhttps/Toable.java similarity index 89% rename from okhttps/src/main/java/cn/zhxu/okhttps/Toable.java rename to okhttps/src/main/java/com/ejlchina/okhttps/Toable.java index 4b5f562d..f9636063 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/Toable.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/Toable.java @@ -1,8 +1,5 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; -import cn.zhxu.data.Array; -import cn.zhxu.data.Mapper; -import cn.zhxu.data.TypeRef; import okio.ByteString; import java.io.InputStream; @@ -23,10 +20,8 @@ public interface Toable { byte[] toBytes(); /** - * will be removed in next version * @return ByteString */ - @Deprecated ByteString toByteString(); /** diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/TypeRef.java b/okhttps/src/main/java/com/ejlchina/okhttps/TypeRef.java new file mode 100644 index 00000000..01189a95 --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/TypeRef.java @@ -0,0 +1,19 @@ +package com.ejlchina.okhttps; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public class TypeRef { + + final Type type; + + public TypeRef() { + Type superClass = getClass().getGenericSuperclass(); + this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; + } + + public Type getType() { + return type; + } + +} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/WebSocket.java b/okhttps/src/main/java/com/ejlchina/okhttps/WebSocket.java similarity index 72% rename from okhttps/src/main/java/cn/zhxu/okhttps/WebSocket.java rename to okhttps/src/main/java/com/ejlchina/okhttps/WebSocket.java index 4fdc577e..aecc48e7 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/WebSocket.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/WebSocket.java @@ -1,16 +1,7 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps; public interface WebSocket extends Cancelable { - int STATUS_CANCELED = 0; // WebSocket 连接状�:已�消 - int STATUS_CONNECTING = 2; // WebSocket 连接状�:正在连接 - int STATUS_CONNECTED = 1; // WebSocket 连接状�:已连接 - int STATUS_DISCONNECTED = 3; // WebSocket 连接状�:已断开连接 - int STATUS_NETWORK_ERROR = -2; // WebSocket 连接状�:网络错误 - int STATUS_TIMEOUT = -3; // WebSocket 连接状�:连接超时 - int STATUS_EXCEPTION = -1; // WebSocket 连接状�:�生异常 - - /** * WebSocket 消� */ @@ -122,18 +113,4 @@ interface Listener { */ void msgType(String type); - /** - * WebSocket 当�的连接状� - * @since v2.4.5 - * @see #STATUS_CANCELED - * @see #STATUS_CONNECTING - * @see #STATUS_CONNECTED - * @see #STATUS_DISCONNECTED - * @see #STATUS_NETWORK_ERROR - * @see #STATUS_TIMEOUT - * @see #STATUS_EXCEPTION - * @return 连接状�标识 - */ - int status(); - } diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/AbstractBody.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/AbstractBody.java similarity index 65% rename from okhttps/src/main/java/cn/zhxu/okhttps/internal/AbstractBody.java rename to okhttps/src/main/java/com/ejlchina/okhttps/internal/AbstractBody.java index 0d28e917..efecf9f2 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/AbstractBody.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/AbstractBody.java @@ -1,16 +1,11 @@ -package cn.zhxu.okhttps.internal; +package com.ejlchina.okhttps.internal; -import cn.zhxu.data.Array; -import cn.zhxu.data.Mapper; -import cn.zhxu.data.TypeRef; -import cn.zhxu.okhttps.TaskExecutor; -import cn.zhxu.okhttps.Toable; - -import java.io.InputStream; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.util.List; +import com.ejlchina.okhttps.*; + public abstract class AbstractBody implements Toable { protected TaskExecutor taskExecutor; @@ -26,7 +21,7 @@ public Mapper toMapper() { if (taskExecutor == null) { throw new IllegalStateException("没有 taskExecutor,��� Mapper 转��"); } - return taskExecutor.doMsgConvert(c -> c.toMapper(convertingStream(), charset)); + return taskExecutor.doMsgConvert((MsgConvertor c) -> c.toMapper(toByteStream(), charset)); } @Override @@ -34,7 +29,7 @@ public Array toArray() { if (taskExecutor == null) { throw new IllegalStateException("没有 taskExecutor,��� Array 转��"); } - return taskExecutor.doMsgConvert(c -> c.toArray(convertingStream(), charset)); + return taskExecutor.doMsgConvert((MsgConvertor c) -> c.toArray(toByteStream(), charset)); } @Override @@ -56,7 +51,7 @@ public T doToBean(Type type) { if (taskExecutor == null) { throw new IllegalStateException("没有 taskExecutor,��� Bean 转��"); } - return taskExecutor.doMsgConvert(c -> c.toBean(type, convertingStream(), charset)); + return taskExecutor.doMsgConvert((MsgConvertor c) -> c.toBean(type, toByteStream(), charset)); } @Override @@ -64,14 +59,7 @@ public List toList(Class type) { if (taskExecutor == null) { throw new IllegalStateException("没有 taskExecutor,��� List 转��"); } - return taskExecutor.doMsgConvert(c -> c.toList(type, convertingStream(), charset)); + return taskExecutor.doMsgConvert((MsgConvertor c) -> c.toList(type, toByteStream(), charset)); } - - /** - * @return 待转�的输入� - */ - protected InputStream convertingStream() { - return toByteStream(); - } - + } diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/AHttpTask.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/AsyncHttpTask.java similarity index 61% rename from okhttps/src/main/java/cn/zhxu/okhttps/AHttpTask.java rename to okhttps/src/main/java/com/ejlchina/okhttps/internal/AsyncHttpTask.java index e038cc30..d357c7e3 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/AHttpTask.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/AsyncHttpTask.java @@ -1,21 +1,19 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.data.Array; -import cn.zhxu.data.Mapper; -import cn.zhxu.data.TypeRef; -import cn.zhxu.okhttps.HttpResult.State; -import cn.zhxu.okhttps.internal.AbstractHttpClient; -import cn.zhxu.okhttps.internal.CopyInterceptor; -import cn.zhxu.okhttps.internal.RealHttpResult; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Response; +package com.ejlchina.okhttps.internal; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.function.Consumer; + +import com.ejlchina.okhttps.*; +import com.ejlchina.okhttps.HttpResult.State; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Response; + /** * 异步 Http 请求任务 @@ -23,35 +21,35 @@ * @author Troy.Zhou * */ -public class AHttpTask extends HttpTask { - - - private Consumer onResponse; - private Consumer onException; - private Consumer onComplete; +public class AsyncHttpTask extends HttpTask { + + private OnCallback onResponse; + private OnCallback onException; + private OnCallback onComplete; + private boolean responseOnIO; private boolean exceptionOnIO; private boolean completeOnIO; - - private Consumer onResBody; - private Consumer onResMapper; - private Consumer onResArray; - private Consumer onResString; - private Consumer onResBean; - private Consumer onResList; - + + private OnCallback onResBody; + private OnCallback onResMapper; + private OnCallback onResArray; + private OnCallback onResString; + private OnCallback onResBean; + private OnCallback onResList; + private boolean resBodyOnIO; private boolean resMapperOnIO; private boolean resArrayOnIO; private boolean resStringOnIO; private boolean resBeanOnIO; private boolean resListOnIO; - + private Type beanType; private Class listType; - - public AHttpTask(AbstractHttpClient client, String url) { + + public AsyncHttpTask(HttpClient client, String url) { super(client, url); } @@ -66,7 +64,7 @@ public boolean isAsyncHttp() { * @param onException 请求异常回调 * @return HttpTask 实例 */ - public AHttpTask setOnException(Consumer onException) { + public AsyncHttpTask setOnException(OnCallback onException) { this.onException = onException; exceptionOnIO = nextOnIO; nextOnIO = false; @@ -78,7 +76,7 @@ public AHttpTask setOnException(Consumer onException) { * @param onComplete 请求完�回调 * @return HttpTask 实例 */ - public AHttpTask setOnComplete(Consumer onComplete) { + public AsyncHttpTask setOnComplete(OnCallback onComplete) { this.onComplete = onComplete; completeOnIO = nextOnIO; nextOnIO = false; @@ -90,7 +88,7 @@ public AHttpTask setOnComplete(Consumer onComplete) { * @param onResponse 请求�应回调 * @return HttpTask 实例 */ - public synchronized AHttpTask setOnResponse(Consumer onResponse) { + public AsyncHttpTask setOnResponse(OnCallback onResponse) { this.onResponse = onResponse; responseOnIO = nextOnIO; nextOnIO = false; @@ -102,7 +100,7 @@ public synchronized AHttpTask setOnResponse(Consumer onResponse) { * @param onResBody �应报文体回调 * @return HttpTask 实例 */ - public synchronized AHttpTask setOnResBody(Consumer onResBody) { + public AsyncHttpTask setOnResBody(OnCallback onResBody) { this.onResBody = onResBody; resBodyOnIO = nextOnIO; nextOnIO = false; @@ -116,7 +114,7 @@ public synchronized AHttpTask setOnResBody(Consumer onResBody) * @param onResBean �应 Bean 回调 * @return HttpTask 实例 */ - public synchronized AHttpTask setOnResBean(Class type, Consumer onResBean) { + public AsyncHttpTask setOnResBean(Class type, OnCallback onResBean) { initBeanType(type); this.onResBean = onResBean; resBeanOnIO = nextOnIO; @@ -131,7 +129,7 @@ public synchronized AHttpTask setOnResBean(Class type, Consumer onResB * @param onResBean �应 Bean 回调 * @return HttpTask 实例 */ - public synchronized AHttpTask setOnResBean(TypeRef type, Consumer onResBean) { + public AsyncHttpTask setOnResBean(TypeRef type, OnCallback onResBean) { initBeanType(type.getType()); this.onResBean = onResBean; resBeanOnIO = nextOnIO; @@ -146,7 +144,7 @@ public synchronized AHttpTask setOnResBean(TypeRef type, Consumer onRe * @param onResList 请求�应回调 * @return HttpTask 实例 */ - public synchronized AHttpTask setOnResList(Class type, Consumer> onResList) { + public AsyncHttpTask setOnResList(Class type, OnCallback> onResList) { if (type == null) { throw new IllegalArgumentException(" list type can not be null!"); } @@ -164,7 +162,7 @@ public synchronized AHttpTask setOnResList(Class type, Consumer> * @param onResMapper 请求�应回调 * @return HttpTask 实例 */ - public synchronized AHttpTask setOnResMapper(Consumer onResMapper) { + public AsyncHttpTask setOnResMapper(OnCallback onResMapper) { this.onResMapper = onResMapper; resMapperOnIO = nextOnIO; nextOnIO = false; @@ -176,7 +174,7 @@ public synchronized AHttpTask setOnResMapper(Consumer onResMapper) { * @param onResArray 请求�应回调 * @return HttpTask 实例 */ - public synchronized AHttpTask setOnResArray(Consumer onResArray) { + public AsyncHttpTask setOnResArray(OnCallback onResArray) { this.onResArray = onResArray; resArrayOnIO = nextOnIO; nextOnIO = false; @@ -188,7 +186,7 @@ public synchronized AHttpTask setOnResArray(Consumer onResArray) { * @param onResString 请求�应回调 * @return HttpTask 实例 */ - public synchronized AHttpTask setOnResString(Consumer onResString) { + public AsyncHttpTask setOnResString(OnCallback onResString) { this.onResString = onResString; resStringOnIO = nextOnIO; nextOnIO = false; @@ -303,29 +301,28 @@ void setCall(HttpCall call) { @Override public HttpResult getResult() { - if (timeoutAwait(latch)) { + if (!timeoutAwait(latch)) { cancel(); return timeoutResult(); } if (canceled || call == null) { - return new RealHttpResult(AHttpTask.this, State.CANCELED); + return new RealHttpResult(AsyncHttpTask.this, State.CANCELED); } return call.getResult(); } @Override - public AHttpTask getTask() { - return AHttpTask.this; + public AsyncHttpTask getTask() { + return AsyncHttpTask.this; } } - public class OkHttpCall implements HttpCall { + class OkHttpCall implements HttpCall { - final Call call; + Call call; HttpResult result; CountDownLatch latch = new CountDownLatch(1); - boolean finished = false; OkHttpCall(Call call) { this.call = call; @@ -333,7 +330,7 @@ public class OkHttpCall implements HttpCall { @Override public synchronized boolean cancel() { - if (result == null || !finished) { + if (result == null) { call.cancel(); return true; } @@ -342,18 +339,18 @@ public synchronized boolean cancel() { @Override public boolean isDone() { - return result != null && finished || call.isCanceled(); + return result != null; } @Override public boolean isCanceled() { - return call.isCanceled() || (result != null && result.getState() == State.CANCELED); + return call.isCanceled(); } @Override public HttpResult getResult() { if (result == null) { - if (timeoutAwait(latch)) { + if (!timeoutAwait(latch)) { cancel(); return timeoutResult(); } @@ -362,8 +359,8 @@ public HttpResult getResult() { } @Override - public AHttpTask getTask() { - return AHttpTask.this; + public AsyncHttpTask getTask() { + return AsyncHttpTask.this; } void setResult(HttpResult result) { @@ -371,41 +368,33 @@ void setResult(HttpResult result) { latch.countDown(); } - public void finish() { - this.finished = true; - } - } + } private HttpCall executeCall(Call call) { OkHttpCall httpCall = new OkHttpCall(call); call.enqueue(new Callback() { - @Override - @SuppressWarnings("NullableProblems") public void onFailure(Call call, IOException error) { State state = toState(error); - HttpResult result = new RealHttpResult(AHttpTask.this, state, error); + HttpResult result = new RealHttpResult(AsyncHttpTask.this, state, error); onCallback(httpCall, result, () -> { TaskExecutor executor = httpClient.executor(); - executor.executeOnComplete(AHttpTask.this, onComplete, state, completeOnIO); - if (!httpCall.isCanceled() && !executor.executeOnException(AHttpTask.this, httpCall, onException, error, exceptionOnIO) + executor.executeOnComplete(AsyncHttpTask.this, onComplete, state, completeOnIO); + if (!executor.executeOnException(AsyncHttpTask.this, onException, error, exceptionOnIO) && !nothrow) { - throw new OkHttpsException(state, "异步请求异常:" + getUrl(), error); + throw new HttpException(state, error.getMessage(), error); } }); } - @Override - @SuppressWarnings("NullableProblems") + @Override public void onResponse(Call call, Response response) { TaskExecutor executor = httpClient.executor(); - HttpResult result = new RealHttpResult(AHttpTask.this, response, executor); + HttpResult result = new RealHttpResult(AsyncHttpTask.this, response, executor); onCallback(httpCall, result, () -> { - executor.executeOnComplete(AHttpTask.this, onComplete, State.RESPONSED, completeOnIO); - if (!httpCall.isCanceled()) { - executor.executeOnResponse(AHttpTask.this, httpCall, complexOnResponse(httpCall), result, true); - } + executor.executeOnComplete(AsyncHttpTask.this, onComplete, State.RESPONSED, completeOnIO); + executor.executeOnResponse(AsyncHttpTask.this, complexOnResponse(), result, true); }); } @@ -413,100 +402,101 @@ public void onResponse(Call call, Response response) { return httpCall; } - @SuppressWarnings("all") - private void onCallback(OkHttpCall httpCall, HttpResult result, Runnable runnable) { - synchronized (httpCall) { - removeTagTask(); - if (httpCall.isCanceled() || result.getState() == State.CANCELED) { - httpCall.setResult(new RealHttpResult(AHttpTask.this, State.CANCELED)); - } else { - httpCall.setResult(result); - } - runnable.run(); - } - } - - interface ResponseCallback { - void on(Runnable runnable, boolean onIo); - } - - @SuppressWarnings("unchecked") - private synchronized Consumer complexOnResponse(OkHttpCall call) { + private OnCallback complexOnResponse() { return res -> { - Consumer onResp = onResponse; - Consumer onBody = onResBody; - Consumer onMapper = onResMapper; - Consumer onArray = onResArray; - Consumer onBean = (Consumer) onResBean; - Consumer onList = (Consumer) onResList; - Consumer onString = onResString; - - int count = 0; - if (onResp != null) - count++; - if (onBody != null) - count++; - if (onMapper != null) - count++; - if (onArray != null) - count++; - if (onBean != null) - count++; - if (onList != null) - count++; - if (onString != null) - count++; - int callbackCount = count; - + int count = responseCallbackCount(); HttpResult.Body body = res.getBody(); - if (callbackCount > 1) { - // 如果回调数�多于 1 个,则为报文体自动开�缓存 + if (count > 1) { body.cache(); } - - ResponseCallback callback = (runnable, onIo) -> execute(new Runnable() { - // 记录已�回调的次数 - int count = 0; - @Override - public void run() { - if (!call.isCanceled()) { - runnable.run(); - } - if (++count >= callbackCount) { - call.finish(); - } - } - }, onIo); - - if (onResp != null) { - callback.on(() -> onResp.accept(res), responseOnIO); + if (onResponse != null) { + execute(() -> onResponse.on(res), responseOnIO); } - if (onBody != null) { - callback.on(() -> onBody.accept(body), resBodyOnIO); + if (onResBody != null) { + execute(() -> onResBody.on(body), resBodyOnIO); } - if (onMapper != null) { + if (onResMapper != null) { Mapper mapper = body.toMapper(); - callback.on(() -> onMapper.accept(mapper), resMapperOnIO); + execute(() -> onResMapper.on(mapper), resMapperOnIO); } - if (onArray != null) { + if (onResArray != null) { Array array = body.toArray(); - callback.on(() -> onArray.accept(array), resArrayOnIO); + execute(() -> onResArray.on(array), resArrayOnIO); } - if (onBean != null) { + if (onResBean != null) { Object bean = body.toBean(beanType); - callback.on(() -> onBean.accept(bean), resBeanOnIO); + execute(() -> { + try { + callbackMethod(onResBean.getClass(), bean.getClass()).invoke(onResBean, bean); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new HttpException("回调方法调用失败�", e); + } + }, resBeanOnIO); } - if (onList != null) { + if (onResList != null) { List list = body.toList(listType); - callback.on(() -> onList.accept(list), resListOnIO); + execute(() -> { + try { + callbackMethod(onResList.getClass(), list.getClass()).invoke(onResList, list); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new HttpException("回调方法调用失败�", e); + } + }, resListOnIO); } - if (onString != null) { + if (onResString != null) { String string = body.toString(); - callback.on(() -> onString.accept(string), resStringOnIO); + execute(() -> onResString.on(string), resStringOnIO); } }; } + static final String OnCallbackMethod = OnCallback.class.getDeclaredMethods()[0].getName(); + + private Method callbackMethod(Class clazz, Class paraType) { + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + Class[] paraTypes = method.getParameterTypes(); + if (method.getName().equals(OnCallbackMethod) && paraTypes.length == 1 + && paraTypes[0].isAssignableFrom(paraType)) { + method.setAccessible(true); + return method; + } + } + throw new IllegalStateException("没有�调用的方法"); + } + + private int responseCallbackCount() { + int count = 0; + if (onResponse != null) + count++; + if (onResBody != null) + count++; + if (onResMapper != null) + count++; + if (onResArray != null) + count++; + if (onResBean != null) + count++; + if (onResList != null) + count++; + if (onResString != null) + count++; + return count; + } + + @SuppressWarnings("all") + private void onCallback(OkHttpCall httpCall, HttpResult result, Runnable runnable) { + synchronized (httpCall) { + removeTagTask(); + if (httpCall.isCanceled() || result.getState() == State.CANCELED) { + httpCall.setResult(new RealHttpResult(AsyncHttpTask.this, State.CANCELED)); + return; + } + httpCall.setResult(result); + runnable.run(); + } + } + private void initBeanType(Type type) { if (type == null) { throw new IllegalArgumentException(" bean type can not be null!"); diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/CopyInterceptor.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/CopyInterceptor.java similarity index 93% rename from okhttps/src/main/java/cn/zhxu/okhttps/internal/CopyInterceptor.java rename to okhttps/src/main/java/com/ejlchina/okhttps/internal/CopyInterceptor.java index deb3483e..717d341e 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/CopyInterceptor.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/CopyInterceptor.java @@ -1,4 +1,4 @@ -package cn.zhxu.okhttps.internal; +package com.ejlchina.okhttps.internal; import okhttp3.Interceptor; import okhttp3.Request; @@ -10,7 +10,7 @@ public class CopyInterceptor implements Interceptor { - public static final String TAG = "okhttps-Async-Response-Copy"; + static final String TAG = "okhttps-Async-Response-Copy"; public static boolean notIn(List list) { for (Interceptor item : list) { diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/AbstractHttpClient.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/HttpClient.java similarity index 55% rename from okhttps/src/main/java/cn/zhxu/okhttps/internal/AbstractHttpClient.java rename to okhttps/src/main/java/com/ejlchina/okhttps/internal/HttpClient.java index 01f80471..e42f7d52 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/AbstractHttpClient.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/HttpClient.java @@ -1,17 +1,17 @@ -package cn.zhxu.okhttps.internal; +package com.ejlchina.okhttps.internal; -import cn.zhxu.okhttps.*; -import okhttp3.MediaType; +import com.ejlchina.okhttps.*; +import okhttp3.*; +import okhttp3.WebSocket; import java.nio.charset.Charset; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executor; +import java.util.*; -public abstract class AbstractHttpClient implements HTTP { +public class HttpClient implements HTTP { + + // OkHttpClient + final OkHttpClient okClient; // 根URL final String baseUrl; // 媒体类型 @@ -28,40 +28,38 @@ public abstract class AbstractHttpClient implements HTTP { final Charset charset; // 默认的请求体类型 final String bodyType; - // 下载助手 - final DownloadHelper downloadHelper; - public AbstractHttpClient(HTTP.Builder builder) { + public HttpClient(Builder builder) { + this.okClient = builder.okClient(); this.baseUrl = builder.baseUrl(); this.mediaTypes = builder.getMediaTypes(); - this.executor = new TaskExecutor(builder, ioExecutor(builder)); + this.executor = new TaskExecutor(okClient.dispatcher().executorService(), + builder.mainExecutor(), builder.downloadListener(), + builder.responseListener(), builder.exceptionListener(), + builder.completeListener(), builder.msgConvertors(), + builder.taskScheduler()); this.preprocessors = builder.preprocessors(); this.preprocTimeoutTimes = builder.preprocTimeoutTimes(); this.charset = builder.charset(); this.bodyType = builder.bodyType(); this.tagTasks = new LinkedList<>(); - this.downloadHelper = builder.downloadHelper(); - } - - - public abstract Executor ioExecutor(HTTP.Builder builder); - - - @Override - public AHttpTask async(String url) { - return new AHttpTask(this, urlPath(url, false)); } @Override - public SHttpTask sync(String url) { - return new SHttpTask(this, urlPath(url, false)); + public AsyncHttpTask async(String url) { + return new AsyncHttpTask(this, urlPath(url, false)); } @Override - public WHttpTask webSocket(String url) { - return new WHttpTask(this, urlPath(url, true)); + public SyncHttpTask sync(String url) { + return new SyncHttpTask(this, urlPath(url, false)); } + @Override + public WebSocketTask webSocket(String url) { + return new WebSocketTask(this, urlPath(url, true)); + } + @Override public int cancel(String tag) { if (tag == null) { @@ -69,7 +67,7 @@ public int cancel(String tag) { } int count = 0; synchronized (tagTasks) { - Iterator it = tagTasks.iterator(); + Iterator it = tagTasks.iterator(); while (it.hasNext()) { TagTask tagCall = it.next(); // ��任务的标签包�指定的Tag就会被�消 @@ -88,19 +86,31 @@ public int cancel(String tag) { @Override public void cancelAll() { - doCancelAll(); + okClient.dispatcher().cancelAll(); synchronized (tagTasks) { - tagTasks.clear(); + tagTasks.clear(); } } - public abstract void doCancelAll(); + @Override + public Call request(Request request) { + return okClient.newCall(request); + } - public int preprocTimeoutMillis() { - return preprocTimeoutTimes * totalTimeoutMillis(); + @Override + public WebSocket webSocket(Request request, WebSocketListener listener) { + return okClient.newWebSocket(request, listener); + } + + public OkHttpClient okClient() { + return okClient; } - public abstract int totalTimeoutMillis(); + public int preprocTimeoutMillis() { + return preprocTimeoutTimes * (okClient.connectTimeoutMillis() + + okClient.writeTimeoutMillis() + + okClient.readTimeoutMillis()); + } public int getTagTaskCount() { return tagTasks.size(); @@ -109,14 +119,14 @@ public int getTagTaskCount() { public TagTask addTagTask(String tag, Cancelable canceler, HttpTask task) { TagTask tagTask = new TagTask(tag, canceler, task); synchronized (tagTasks) { - tagTasks.add(tagTask); + tagTasks.add(tagTask); } - return tagTask; + return tagTask; } public void removeTagTask(HttpTask task) { - synchronized (tagTasks) { - Iterator it = tagTasks.iterator(); + synchronized (tagTasks) { + Iterator it = tagTasks.iterator(); while (it.hasNext()) { TagTask tagCall = it.next(); if (tagCall.task == task) { @@ -127,7 +137,7 @@ public void removeTagTask(HttpTask task) { it.remove(); } } - } + } } public class TagTask { @@ -141,17 +151,17 @@ public class TagTask { this.tag = tag; this.canceler = canceler; this.task = task; - this.createAt = System.currentTimeMillis(); + this.createAt = System.nanoTime(); } boolean isExpired() { // 生存时间大于10�的总超时�值 - return System.currentTimeMillis() - createAt > preprocTimeoutMillis(); + return System.nanoTime() - createAt > 1_000_000 * preprocTimeoutMillis(); } - public void setTag(String tag) { - this.tag = tag; - } + public void setTag(String tag) { + this.tag = tag; + } } @@ -160,16 +170,7 @@ public MediaType mediaType(String type) { if (mediaType != null) { return MediaType.parse(mediaType); } - if (type != null) { - if (type.indexOf('/') < 0) { - type = "application/" + type; - } - MediaType mType = MediaType.parse(type); - if (mType != null) { - return mType; - } - } - return MediaType.parse("application/unknown"); + return MediaType.parse("application/octet-stream"); } @Override @@ -177,41 +178,94 @@ public TaskExecutor executor() { return executor; } + public void preprocess(HttpTask httpTask, Runnable request, + boolean skipPreproc, boolean skipSerialPreproc) { + if (preprocessors.length == 0 || skipPreproc) { + request.run(); + return; + } + int index = 0; + if (skipSerialPreproc) { + while (index < preprocessors.length + && preprocessors[index] instanceof SerialPreprocessor) { + index++; + } + } + if (index < preprocessors.length) { + RealPreChain chain = new RealPreChain(preprocessors, + httpTask, request, index + 1, + skipSerialPreproc); + preprocessors[index].doProcess(chain); + } else { + request.run(); + } + } - public void preprocess(HttpTask httpTask, Runnable request, - boolean skipPreproc, boolean skipSerialPreproc) { - if (preprocessors.length == 0 || skipPreproc) { - request.run(); - return; + /** + * 串行预处�器 + * @author Troy.Zhou + */ + public static class SerialPreprocessor implements Preprocessor { + + // 预处�器 + private Preprocessor preprocessor; + // 待处�的任务队列 + private Queue pendings; + // 是�有任务正在执行 + private boolean running = false; + + public SerialPreprocessor(Preprocessor preprocessor) { + this.preprocessor = preprocessor; + this.pendings = new LinkedList<>(); } - int index = 0; - if (skipSerialPreproc) { - while (index < preprocessors.length - && preprocessors[index] instanceof SerialPreprocessor) { - index++; + + @Override + public void doProcess(PreChain chain) { + boolean should = true; + synchronized (this) { + if (running) { + pendings.add(chain); + should = false; + } else { + running = true; + } + } + if (should) { + preprocessor.doProcess(chain); } } - if (index < preprocessors.length) { - RealPreChain chain = new RealPreChain(preprocessors, - httpTask, request, index + 1, - skipSerialPreproc); - preprocessors[index].doProcess(chain); - } else { - request.run(); + + public void afterProcess() { + PreChain chain = null; + synchronized (this) { + if (pendings.size() > 0) { + chain = pendings.poll(); + } else { + running = false; + } + } + if (chain != null) { + preprocessor.doProcess(chain); + } } + } class RealPreChain implements Preprocessor.PreChain { private int index; - final Preprocessor[] preprocessors; - final HttpTask httpTask; - final Runnable request; - final boolean noSerialPreprocess; - public RealPreChain(Preprocessor[] preprocessors, HttpTask httpTask, Runnable request, - int index, boolean noSerialPreprocess) { + private Preprocessor[] preprocessors; + + private HttpTask httpTask; + + private Runnable request; + + private boolean noSerialPreprocess; + + public RealPreChain(Preprocessor[] preprocessors, HttpTask httpTask, Runnable request, + int index, boolean noSerialPreprocess) { this.index = index; // index 大于等于 1 this.preprocessors = preprocessors; this.httpTask = httpTask; @@ -226,22 +280,22 @@ public HttpTask getTask() { @Override public HTTP getHttp() { - return AbstractHttpClient.this; + return HttpClient.this; } @Override public void proceed() { - if (noSerialPreprocess) { - while (index < preprocessors.length - && preprocessors[index] instanceof SerialPreprocessor) { - index++; - } - } else { - Preprocessor last = preprocessors[index - 1]; + if (noSerialPreprocess) { + while (index < preprocessors.length + && preprocessors[index] instanceof SerialPreprocessor) { + index++; + } + } else { + Preprocessor last = preprocessors[index - 1]; if (last instanceof SerialPreprocessor) { ((SerialPreprocessor) last).afterProcess(); } - } + } if (index < preprocessors.length) { preprocessors[index++].doProcess(this); } else { @@ -251,16 +305,20 @@ public void proceed() { } + @Override + public Builder newBuilder() { + return new Builder(this); + } + private String urlPath(String urlPath, boolean websocket) { String fullUrl; if (urlPath == null) { if (baseUrl != null) { fullUrl = baseUrl; } else { - throw new OkHttpsException("在设置 BaseUrl 之�,您必须指定具体路径�能�起请求�"); + throw new HttpException("在设置 BaseUrl 之�,您必须指定具体路径�能�起请求�"); } } else { - urlPath = urlPath.trim(); boolean isFullPath = urlPath.startsWith("https://") || urlPath.startsWith("http://") || urlPath.startsWith("wss://") @@ -270,7 +328,7 @@ private String urlPath(String urlPath, boolean websocket) { } else if (baseUrl != null) { fullUrl = baseUrl + urlPath; } else { - throw new OkHttpsException("在设置 BaseUrl 之�,您必须使用全路径URL�起请求,当�URL为:'" + urlPath + "'"); + throw new HttpException("在设置 BaseUrl 之�,您必须使用全路径URL�起请求,当�URL为:" + urlPath); } } if (websocket && fullUrl.startsWith("http")) { @@ -310,8 +368,5 @@ public String bodyType() { return bodyType; } - public DownloadHelper downloadHelper() { - return downloadHelper; - } } diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/OkHttpsException.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/HttpException.java similarity index 53% rename from okhttps/src/main/java/cn/zhxu/okhttps/OkHttpsException.java rename to okhttps/src/main/java/com/ejlchina/okhttps/internal/HttpException.java index 67993955..60dc6539 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/OkHttpsException.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/HttpException.java @@ -1,9 +1,11 @@ -package cn.zhxu.okhttps; +package com.ejlchina.okhttps.internal; + +import com.ejlchina.okhttps.HttpResult; /** * Created by 周旭(Troy.Zhou) on 2016/8/30. */ -public class OkHttpsException extends RuntimeException { +public class HttpException extends RuntimeException { /** * @@ -12,20 +14,20 @@ public class OkHttpsException extends RuntimeException { private HttpResult.State state; - public OkHttpsException(String detailMessage) { + public HttpException(String detailMessage) { super(detailMessage); } - public OkHttpsException(String detailMessage, Throwable throwable) { + public HttpException(String detailMessage, Throwable throwable) { super(detailMessage, throwable); } - public OkHttpsException(HttpResult.State state, String detailMessage, Throwable throwable) { + public HttpException(HttpResult.State state, String detailMessage, Throwable throwable) { super(detailMessage, throwable); this.state = state; } - public OkHttpsException(HttpResult.State state, String detailMessage) { + public HttpException(HttpResult.State state, String detailMessage) { super(detailMessage); this.state = state; } diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/internal/ProcessInputStream.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/ProcessInputStream.java new file mode 100644 index 00000000..77cf2e91 --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/ProcessInputStream.java @@ -0,0 +1,53 @@ +package com.ejlchina.okhttps.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.Executor; + +import com.ejlchina.okhttps.OnCallback; +import com.ejlchina.okhttps.Process; + +public class ProcessInputStream extends InputStream { + + private InputStream input; + private OnCallback onProcess; + private Executor callbackExecutor; + private long stepBytes; + private long step = 0; + private RealProcess process; + private boolean doneCalled = false; + + public ProcessInputStream(InputStream input, OnCallback onProcess, long totalBytes, long stepBytes, + long doneBytes, Executor callbackExecutor) { + this.input = input; + this.onProcess = onProcess; + this.stepBytes = stepBytes; + this.callbackExecutor = callbackExecutor; + this.process = new RealProcess(totalBytes, doneBytes); + this.step = doneBytes / stepBytes; + } + + + @Override + public int read() throws IOException { + int data = input.read(); + if (data > -1) { + process.increaseDoneBytes(); + } + if (process.notDoneOrReached(step * stepBytes)) { + return data; + } + if (process.isDone()) { + if (doneCalled) { + return data; + } + doneCalled = true; + } + step++; + callbackExecutor.execute(() -> { + onProcess.on(process); + }); + return data; + } + +} diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/internal/ProcessRequestBody.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/ProcessRequestBody.java new file mode 100644 index 00000000..bca5d583 --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/ProcessRequestBody.java @@ -0,0 +1,91 @@ +package com.ejlchina.okhttps.internal; + +import java.io.IOException; +import java.util.concurrent.Executor; + +import com.ejlchina.okhttps.OnCallback; +import com.ejlchina.okhttps.Process; + +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.Buffer; +import okio.BufferedSink; +import okio.ForwardingSink; +import okio.Okio; + +public class ProcessRequestBody extends RequestBody { + + private RequestBody requestBody; + private OnCallback onProcess; + private Executor callbackExecutor; + private long stepBytes; + private long step = 0; + private RealProcess process; + private boolean doneCalled = false; + + public ProcessRequestBody(RequestBody requestBody, OnCallback onProcess, Executor callbackExecutor, + long contentLength, long stepBytes) { + this.requestBody = requestBody; + this.onProcess = onProcess; + this.callbackExecutor = callbackExecutor; + this.stepBytes = stepBytes; + this.process = new RealProcess(contentLength, 0); + } + + @Override + public long contentLength() { + return process.getTotalBytes(); + } + + @Override + public boolean isDuplex() { + return requestBody.isDuplex(); + } + + @Override + public boolean isOneShot() { + return requestBody.isOneShot(); + } + + @Override + public MediaType contentType() { + return requestBody.contentType(); + } + + private BufferedSink bufferedSink; + + @Override + public void writeTo(BufferedSink sink) throws IOException { + if (bufferedSink == null) { + bufferedSink = Okio.buffer(new ForwardingSink(sink) { + + @Override + public void write(Buffer source, long byteCount) throws IOException { + //这个方法会循环调用,byteCount 是�次调用上传的字节数。 + super.write(source, byteCount); + process.addDoneBytes(byteCount); + if (process.notDoneOrReached(step * stepBytes)) { + return; + } + if (process.isDone()) { + if (doneCalled) { + return; + } + doneCalled = true; + } + step++; + callbackExecutor.execute(() -> { + onProcess.on(process); + }); + } + + }); + } + requestBody.writeTo(bufferedSink); + bufferedSink.flush(); + } + + + +} + diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/RealHttpResult.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/RealHttpResult.java similarity index 77% rename from okhttps/src/main/java/cn/zhxu/okhttps/internal/RealHttpResult.java rename to okhttps/src/main/java/com/ejlchina/okhttps/internal/RealHttpResult.java index a0f86671..2b522a17 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/RealHttpResult.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/RealHttpResult.java @@ -1,10 +1,7 @@ -package cn.zhxu.okhttps.internal; +package com.ejlchina.okhttps.internal; -import cn.zhxu.data.ArrayListMap; -import cn.zhxu.data.ListMap; -import cn.zhxu.okhttps.HttpTask; -import cn.zhxu.okhttps.TaskExecutor; -import cn.zhxu.okhttps.HttpResult; +import com.ejlchina.okhttps.HttpResult; +import com.ejlchina.okhttps.HttpTask; import okhttp3.Headers; import okhttp3.Response; @@ -19,7 +16,7 @@ public class RealHttpResult implements HttpResult { private Response response; private IOException error; private TaskExecutor taskExecutor; - private final HttpTask httpTask; + private HttpTask httpTask; private Body body; public RealHttpResult(HttpTask httpTask, State state) { @@ -68,10 +65,6 @@ public int getStatus() { @Override public boolean isSuccessful() { if (response != null) { - if (httpTask != null && httpTask.isWebsocket() - && response.code() == 101) { - return true; - } return response.isSuccessful(); } return false; @@ -84,19 +77,7 @@ public Headers getHeaders() { } return null; } - - @Override - public ListMap allHeaders() { - ListMap map = new ArrayListMap<>(); - if (response != null) { - Headers hs = response.headers(); - for (int i = 0, size = hs.size(); i < size; i++) { - map.put(hs.name(i), hs.value(i)); - } - } - return map; - } - + @Override public List getHeaders(String name) { if (response != null) { @@ -125,18 +106,13 @@ public long getContentLength() { } @Override - public synchronized Body getBody() { + public Body getBody() { if (body == null && response != null) { - body = new ResultBody(this, response, taskExecutor); + body = new ResultBody(httpTask, response, taskExecutor); } return body; } - - @Override - public HttpTask getTask() { - return httpTask; - } - + @Override public IOException getError() { return error; diff --git a/okhttps/src/main/java/com/ejlchina/okhttps/internal/RealProcess.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/RealProcess.java new file mode 100644 index 00000000..895583e3 --- /dev/null +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/RealProcess.java @@ -0,0 +1,50 @@ +package com.ejlchina.okhttps.internal; + +import com.ejlchina.okhttps.Process; + +public class RealProcess implements Process { + + // 总字节数 + private long totalBytes; + // 已�完�字节数 + private long doneBytes; + + + public RealProcess(long totalBytes, long doneBytes) { + this.totalBytes = totalBytes; + this.doneBytes = doneBytes; + } + + @Override + public double getRate() { + return (double) doneBytes / totalBytes; + } + + @Override + public long getTotalBytes() { + return totalBytes; + } + + @Override + public long getDoneBytes() { + return doneBytes; + } + + @Override + public boolean isDone() { + return doneBytes >= totalBytes; + } + + public void addDoneBytes(long delt) { + doneBytes += delt; + } + + public void increaseDoneBytes() { + doneBytes++; + } + + public boolean notDoneOrReached(long bytes) { + return doneBytes < bytes && doneBytes < totalBytes; + } + +} diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/ResultBody.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/ResultBody.java similarity index 57% rename from okhttps/src/main/java/cn/zhxu/okhttps/internal/ResultBody.java rename to okhttps/src/main/java/com/ejlchina/okhttps/internal/ResultBody.java index 65c09095..34b5b372 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/ResultBody.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/ResultBody.java @@ -1,32 +1,36 @@ -package cn.zhxu.okhttps.internal; +package com.ejlchina.okhttps.internal; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URLDecoder; + +import com.ejlchina.okhttps.Download; +import com.ejlchina.okhttps.HttpResult.Body; +import com.ejlchina.okhttps.HttpTask; +import com.ejlchina.okhttps.OnCallback; +import com.ejlchina.okhttps.Process; -import cn.zhxu.okhttps.Process; -import cn.zhxu.okhttps.*; import okhttp3.MediaType; import okhttp3.Response; import okhttp3.ResponseBody; import okio.Buffer; import okio.ByteString; -import java.io.*; -import java.net.HttpURLConnection; -import java.util.function.Consumer; - -public class ResultBody extends AbstractBody implements HttpResult.Body { - - private final HttpResult result; - private final Response response; +public class ResultBody extends AbstractBody implements Body { + + private Response response; private boolean onIO = false; - private Consumer onProcess; + private OnCallback onProcess; private long stepBytes = 0; private double stepRate = -1; private boolean rangeIgnored = false; + private HttpTask httpTask; private boolean cached = false; private byte[] data; - ResultBody(HttpResult result, Response response, TaskExecutor taskExecutor) { - super(taskExecutor, result.getTask().charset(response)); - this.result = result; + ResultBody(HttpTask httpTask, Response response, TaskExecutor taskExecutor) { + super(taskExecutor, httpTask.charset(response)); + this.httpTask = httpTask; this.response = response; } @@ -50,19 +54,17 @@ public long getLength() { } @Override - public HttpResult.Body nextOnIO() { + public Body nextOnIO() { onIO = true; return this; } @Override - public HttpResult.Body setOnProcess(Consumer onProcess) { + public Body setOnProcess(OnCallback onProcess) { if (taskExecutor == null) { - response.close(); throw new IllegalStateException("没有 taskExecutor,��设置下载进度回调�"); } if (cached) { - response.close(); throw new IllegalStateException("开�缓存�,��设置下载进度回调�"); } this.onProcess = onProcess; @@ -70,32 +72,23 @@ public HttpResult.Body setOnProcess(Consumer onProcess) { } @Override - public HttpResult.Body stepBytes(long stepBytes) { + public Body stepBytes(long stepBytes) { this.stepBytes = stepBytes; return this; } @Override - public HttpResult.Body stepRate(double stepRate) { + public Body stepRate(double stepRate) { this.stepRate = stepRate; return this; } @Override - public HttpResult.Body setRangeIgnored() { + public Body setRangeIgnored() { this.rangeIgnored =true; return this; } - - @Override - protected InputStream convertingStream() { - if (taskExecutor.isMulitMsgConvertor()) { - // 多个 MsgConvertor 时,自动开�缓存 - cache(); - } - return toByteStream(); - } - + @Override public InputStream toByteStream() { InputStream input; @@ -158,8 +151,7 @@ public String toString() { return new String(body.bytes(), charset); } } catch (IOException e) { - response.close(); - throw new OkHttpsException("报文体转化字符串出错", e); + throw new HttpException("报文体转化字符串出错", e); } return null; } @@ -176,39 +168,35 @@ public Download toFile(String filePath) { @Override public Download toFile(File file) { - try { - if (taskExecutor == null) { - throw new IllegalStateException("没有 taskExecutor, ��进行下载�作�"); - } - if (!file.exists()) { + if (taskExecutor == null) { + throw new IllegalStateException("没有 taskExecutor, ��进行下载�作�"); + } + if (!file.exists()) { + try { File parent = file.getParentFile(); - if (parent == null) { - throw new IllegalStateException("�正确的下载路径:" + file.getPath()); - } - if (!parent.exists() && !parent.mkdirs()) { - throw new IllegalStateException("�能创建父目录:" + parent.getPath()); - } - if (!file.createNewFile()) { - throw new IllegalStateException("文件刚被其它线程�用:" + parent.getPath()); + if (!parent.exists()) { + parent.mkdirs(); } + file.createNewFile(); + } catch (IOException e) { + response.close(); + throw new HttpException( + "Cannot create file [" + file.getAbsolutePath() + "]", e); } - } catch (Exception e) { - response.close(); - throw new OkHttpsException("文件下载失败", e); } - return taskExecutor.download(result.getTask(), file, toByteStream(), getRangeStart()); + return taskExecutor.download(httpTask, file, toByteStream(), + getRangeStart()); } @Override public Download toFolder(String dirPath) { - DownloadHelper helper = result.getTask().httpClient().downloadHelper; - String fileName = helper.resolveFileName(result); - String filePath = helper.resolveFilePath(dirPath, fileName); + String fileName = resolveFileName(); + String filePath = resolveFilePath(dirPath, fileName); int index = 0; File file = new File(filePath); while (file.exists()) { - String indexFileName = helper.indexFileName(fileName, index++); - filePath = helper.resolveFilePath(dirPath, indexFileName); + String indexFileName = indexFileName(fileName, index++); + filePath = resolveFilePath(dirPath, indexFileName); file = new File(filePath); } return toFile(file); @@ -216,24 +204,19 @@ public Download toFolder(String dirPath) { @Override public Download toFolder(File dir) { - try { - if (dir.exists() && !dir.isDirectory()) { - throw new IllegalStateException("文件[" + dir.getPath() + "]已存在,并且�是一个目录�"); - } - if (!dir.exists() && !dir.mkdirs()) { - throw new IllegalStateException("�能创建目录:" + dir.getPath()); - } - } catch (Exception e) { + if (dir.exists() && !dir.isDirectory()) { response.close(); - throw new OkHttpsException("目录创建失败", e); + throw new HttpException("文件下载失败:文件[" + dir.getAbsolutePath() + "]已存在,并且�是一个目录�"); + } + if (!dir.exists()) { + dir.mkdirs(); } return toFolder(dir.getAbsolutePath()); } @Override - public HttpResult.Body cache() { + public Body cache() { if (onProcess != null) { - response.close(); throw new IllegalStateException("设置了下载进度回调,���开�缓存�"); } cached = true; @@ -241,17 +224,15 @@ public HttpResult.Body cache() { } @Override - public HttpResult.Body close() { + public Body close() { response.close(); data = null; return this; } private byte[] cacheBytes() { - synchronized (response) { - if (data == null) { - data = bodyToBytes(); - } + if (data == null) { + data = bodyToBytes(); } return data; } @@ -261,7 +242,7 @@ private byte[] bodyToBytes() { try (Buffer buffer = new Buffer()) { return buffer.readFrom(toByteStream()).readByteArray(); } catch (IOException e) { - throw new OkHttpsException("报文体转化字节数组出错", e); + throw new HttpException("报文体转化字节数组出错", e); } finally { response.close(); } @@ -271,8 +252,7 @@ private byte[] bodyToBytes() { try { return body.bytes(); } catch (IOException e) { - body.close(); - throw new OkHttpsException("报文体转化字节数组出错", e); + throw new HttpException("报文体转化字节数组出错", e); } } return new byte[0]; @@ -295,5 +275,44 @@ private long getRangeStart() { } return rangeStart; } + + private String resolveFilePath(String dirPath, String fileName) { + if (dirPath.endsWith("\\") || dirPath.endsWith("/")) { + return dirPath + fileName; + } + return dirPath + "\\" + fileName; + } + + private String indexFileName(String fileName, int index) { + int i = fileName.lastIndexOf('.'); + if (i < 0) { + return fileName + "(" + index + ")"; + } + String ext = fileName.substring(i); + if (i > 0) { + String name = fileName.substring(0, i); + return name + "(" + index + ")" + ext; + } + return "(" + index + ")" + ext; + } + + private String resolveFileName() { + String fileName = response.header("Content-Disposition"); + // 通过Content-Disposition获�文件�,这点跟�务器有关,需��活�通 + if (fileName == null || fileName.length() < 1) { + fileName = response.request().url().encodedPath(); + fileName = fileName.substring(fileName.lastIndexOf("/") + 1); + } else { + try { + fileName = URLDecoder.decode(fileName.substring( + fileName.indexOf("filename=") + 9), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new HttpException("解�文件�失败", e); + } + // 有些文件�会被包�在""里�,所以�去掉,�然无法读�文件�缀 + fileName = fileName.replaceAll("\"", ""); + } + return fileName; + } } diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/SHttpTask.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/SyncHttpTask.java similarity index 86% rename from okhttps/src/main/java/cn/zhxu/okhttps/SHttpTask.java rename to okhttps/src/main/java/com/ejlchina/okhttps/internal/SyncHttpTask.java index aa68c5f9..a3ff3897 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/SHttpTask.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/SyncHttpTask.java @@ -1,13 +1,16 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.okhttps.HttpResult.State; -import cn.zhxu.okhttps.internal.AbstractHttpClient; -import cn.zhxu.okhttps.internal.RealHttpResult; -import okhttp3.Call; +package com.ejlchina.okhttps.internal; import java.io.IOException; import java.util.concurrent.CountDownLatch; +import com.ejlchina.okhttps.Cancelable; +import com.ejlchina.okhttps.HTTP; +import com.ejlchina.okhttps.HttpResult; +import com.ejlchina.okhttps.HttpTask; +import com.ejlchina.okhttps.HttpResult.State; + +import okhttp3.Call; + /** * �步 Http 请求任务 @@ -15,10 +18,10 @@ * @author Troy.Zhou * */ -public class SHttpTask extends HttpTask { +public class SyncHttpTask extends HttpTask { - public SHttpTask(AbstractHttpClient httpImpl, String url) { - super(httpImpl, url); + public SyncHttpTask(HttpClient client, String url) { + super(client, url); } @@ -109,7 +112,7 @@ public HttpResult request(String method) { }, skipPreproc, skipSerialPreproc); boolean timeout = false; if (result.getState() == null) { - timeout = timeoutAwait(latch); + timeout = !timeoutAwait(latch); } // 移除标签任务 removeTagTask(); @@ -121,7 +124,7 @@ public HttpResult request(String method) { State state = result.getState(); if (e != null && state != State.CANCELED && !nothrow) { - throw new OkHttpsException(state, "�步请求异常:" + getUrl(), e); + throw new HttpException(state, "执行异常", e); } return result; } diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/TaskExecutor.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/TaskExecutor.java similarity index 54% rename from okhttps/src/main/java/cn/zhxu/okhttps/TaskExecutor.java rename to okhttps/src/main/java/com/ejlchina/okhttps/internal/TaskExecutor.java index 72f15e44..83d16847 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/TaskExecutor.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/TaskExecutor.java @@ -1,41 +1,37 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.okhttps.HttpResult.State; -import okhttp3.MediaType; -import okhttp3.internal.Util; +package com.ejlchina.okhttps.internal; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.function.Consumer; +import java.util.concurrent.*; + +import com.ejlchina.okhttps.*; +import com.ejlchina.okhttps.HttpResult.State; +import okhttp3.internal.Util; public final class TaskExecutor { - private final Executor ioExecutor; - private final Executor mainExecutor; - private final DownListener downloadListener; - private final TaskListener responseListener; - private final TaskListener exceptionListener; - private final TaskListener completeListener; - private final MsgConvertor[] msgConvertors; - private final String[] contentTypes; + private Executor ioExecutor; + private Executor mainExecutor; private Scheduler taskScheduler; + private DownListener downloadListener; + private TaskListener responseListener; + private TaskListener exceptionListener; + private TaskListener completeListener; + private MsgConvertor[] msgConvertors; - public TaskExecutor(HTTP.Builder builder, Executor ioExecutor) { - this.downloadListener = builder.downloadListener(); - this.responseListener = builder.responseListener(); - this.exceptionListener = builder.exceptionListener(); - this.completeListener = builder.completeListener(); - this.msgConvertors = builder.msgConvertors(); - this.taskScheduler = builder.taskScheduler(); - this.contentTypes = builder.contentTypes(); - this.mainExecutor = builder.mainExecutor(); + public TaskExecutor(Executor ioExecutor, Executor mainExecutor, DownListener downloadListener, + TaskListener responseListener, TaskListener exceptionListener, + TaskListener completeListener, MsgConvertor[] msgConvertors, + Scheduler taskScheduler) { this.ioExecutor = ioExecutor; + this.mainExecutor = mainExecutor; + this.downloadListener = downloadListener; + this.responseListener = responseListener; + this.exceptionListener = exceptionListener; + this.completeListener = completeListener; + this.msgConvertors = msgConvertors; + this.taskScheduler = taskScheduler; } public Executor getExecutor(boolean onIo) { @@ -61,53 +57,36 @@ public void execute(Runnable command, boolean onIo) { executor.execute(command); } - public void executeOnResponse(HttpTask task, AHttpTask.OkHttpCall call, Consumer onResponse, HttpResult result, boolean onIo) { - Runnable runnable = () -> { - if (!call.isCanceled()) onResponse.accept(result); - }; + public void executeOnResponse(HttpTask task, OnCallback onResponse, HttpResult result, boolean onIo) { if (responseListener != null) { if (responseListener.listen(task, result) && onResponse != null) { - execute(runnable, onIo); - } else { - call.finish(); + execute(() -> onResponse.on(result), onIo); } } else if (onResponse != null) { - execute(runnable, onIo); - } else { - result.close(); - call.finish(); + execute(() -> onResponse.on(result), onIo); } } - public boolean executeOnException(HttpTask task, AHttpTask.OkHttpCall call, Consumer onException, IOException error, boolean onIo) { - Runnable runnable = () -> { - if (!call.isCanceled()) { - onException.accept(error); - } - call.finish(); - }; + public boolean executeOnException(HttpTask task, OnCallback onException, IOException error, boolean onIo) { if (exceptionListener != null) { if (exceptionListener.listen(task, error) && onException != null) { - execute(runnable, onIo); - } else { - call.finish(); + execute(() -> onException.on(error), onIo); } } else if (onException != null) { - execute(runnable, onIo); + execute(() -> onException.on(error), onIo); } else { - call.finish(); return false; } return true; } - public void executeOnComplete(HttpTask task, Consumer onComplete, State state, boolean onIo) { + public void executeOnComplete(HttpTask task, OnCallback onComplete, State state, boolean onIo) { if (completeListener != null) { if (completeListener.listen(task, state) && onComplete != null) { - execute(() -> onComplete.accept(state), onIo); + execute(() -> onComplete.on(state), onIo); } } else if (onComplete != null) { - execute(() -> onComplete.accept(state), onIo); + execute(() -> onComplete.on(state), onIo); } } @@ -120,21 +99,13 @@ public interface ConvertFunc { public static class Data { - public final T data; - private final String contentType; + public T data; + public String mediaType; - public Data(T data, String contentType) { + public Data(T data, String mediaType) { this.data = data; - this.contentType = contentType; + this.mediaType = mediaType; } - - public MediaType mediaType(Charset charset) { - if (contentType != null) { - return MediaType.parse(contentType.replace("{charset}", charset.name())); - } - return null; - } - } public V doMsgConvert(ConvertFunc callable) { @@ -146,15 +117,15 @@ public Data doMsgConvert(String type, ConvertFunc callable) { for (int i = msgConvertors.length - 1; i >= 0; i--) { MsgConvertor convertor = msgConvertors[i]; String mediaType = convertor.mediaType(); - if (type != null && (mediaType == null || !mediaType.toLowerCase().contains(type))) { + if (type != null && (mediaType == null || !mediaType.contains(type))) { continue; } if (callable == null && mediaType != null) { - return new Data<>(null, mediaType(type, mediaType)); + return new Data<>(null, mediaType); } try { assert callable != null; - return new Data<>(callable.apply(convertor), mediaType(type, mediaType)); + return new Data<>(callable.apply(convertor), mediaType); } catch (Exception e) { if (cause != null) { initRootCause(e, cause); @@ -163,12 +134,13 @@ public Data doMsgConvert(String type, ConvertFunc callable) { } } if (callable == null) { - return new Data<>(null, toContentType(type)); + return new Data<>(null, "application/x-www-form-urlencoded"); } if (cause != null) { - throw new OkHttpsException("转�失败", cause); + throw new HttpException("转�失败", cause); } - throw new OkHttpsException("没有匹�[" + type + "]类型的转�器�"); + + throw new HttpException("没有匹�[" + type + "]类型的转�器�"); } private void initRootCause(Throwable throwable, Throwable cause) { @@ -180,24 +152,6 @@ private void initRootCause(Throwable throwable, Throwable cause) { } } - private String mediaType(String type, String mediaType) { - return type != null && type.contains("/") ? type : mediaType; - } - - private String toContentType(String type) { - if (type != null) { - if (type.contains("/")) { - return type; - } - for (String contentType : contentTypes) { - if (contentType.contains(type)) { - return contentType; - } - } - } - return type; - } - private ScheduledExecutorService scheduledService; public synchronized Scheduler requireScheduler() { @@ -256,12 +210,4 @@ public Scheduler getTaskScheduler() { return taskScheduler; } - public String[] getContentTypes() { - return contentTypes; - } - - public boolean isMulitMsgConvertor() { - return msgConvertors.length > 1; - } - } diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/internal/WebSocketMsg.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/WebSocketMsg.java similarity index 87% rename from okhttps/src/main/java/cn/zhxu/okhttps/internal/WebSocketMsg.java rename to okhttps/src/main/java/com/ejlchina/okhttps/internal/WebSocketMsg.java index 5b7fc09c..2fcebc7b 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/internal/WebSocketMsg.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/WebSocketMsg.java @@ -1,6 +1,7 @@ -package cn.zhxu.okhttps.internal; +package com.ejlchina.okhttps.internal; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; @@ -8,17 +9,16 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import cn.zhxu.okhttps.TaskExecutor; -import cn.zhxu.okhttps.WebSocket; +import com.ejlchina.okhttps.WebSocket.Message; import okio.ByteString; - -public class WebSocketMsg extends AbstractBody implements WebSocket.Message { +public class WebSocketMsg extends AbstractBody implements Message { private String text; private ByteString bytes; + public WebSocketMsg(String text, TaskExecutor taskExecutor, Charset charset) { super(taskExecutor, charset); this.text = text; @@ -79,7 +79,7 @@ public InputStream toByteStream() { return new InputStream() { @Override - public int read() { + public int read() throws IOException { if (buffer.hasRemaining()) { return buffer.get(); } diff --git a/okhttps/src/main/java/cn/zhxu/okhttps/WHttpTask.java b/okhttps/src/main/java/com/ejlchina/okhttps/internal/WebSocketTask.java similarity index 51% rename from okhttps/src/main/java/cn/zhxu/okhttps/WHttpTask.java rename to okhttps/src/main/java/com/ejlchina/okhttps/internal/WebSocketTask.java index 3c24331a..03b1962c 100644 --- a/okhttps/src/main/java/cn/zhxu/okhttps/WHttpTask.java +++ b/okhttps/src/main/java/com/ejlchina/okhttps/internal/WebSocketTask.java @@ -1,16 +1,4 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.okhttps.WebSocket.Close; -import cn.zhxu.okhttps.WebSocket.Listener; -import cn.zhxu.okhttps.WebSocket.Message; -import cn.zhxu.okhttps.internal.AbstractHttpClient; -import cn.zhxu.okhttps.internal.RealHttpResult; -import cn.zhxu.okhttps.internal.WebSocketMsg; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.WebSocketListener; -import okhttp3.internal.ws.RealWebSocket; -import okio.ByteString; +package com.ejlchina.okhttps.internal; import java.io.IOException; import java.net.SocketTimeoutException; @@ -20,9 +8,21 @@ import java.util.concurrent.TimeUnit; import java.util.function.Supplier; +import com.ejlchina.okhttps.*; +import com.ejlchina.okhttps.WebSocket.Close; +import com.ejlchina.okhttps.WebSocket.Listener; +import com.ejlchina.okhttps.WebSocket.Message; -public class WHttpTask extends HttpTask { +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocketListener; +import okhttp3.internal.ws.RealWebSocket; +import okio.ByteString; + + +public class WebSocketTask extends HttpTask { + private Listener onOpen; private Listener onException; private Listener onMessage; @@ -41,18 +41,14 @@ public class WHttpTask extends HttpTask { private long lastPongSecs = 0; // 心跳数��供者 - private Supplier pingSupplier; - - private WebSocketImpl webSocket; + private Supplier pingSupplier; - // Ping 的间隔是��活�� - private boolean flexiblePing = true; + private boolean opened = true; - // 最大关闭时长,�:执行了 OnClosing 回调�,最晚过多少久就会执行 OnClosed 回调 - private int maxClosingSecs = 10; + private WebSocketImpl webSocket; - public WHttpTask(AbstractHttpClient httpClient, String url) { + public WebSocketTask(HttpClient httpClient, String url) { super(httpClient, url); } @@ -68,20 +64,13 @@ public boolean isWebsocket() { * 1�客户端��的任何消�都具有一次心跳作用 * 2��务器��的任何消�都具有一次心跳作用 * 3�若�务器超过 3 * pongSeconds 秒没有回�心跳,�判断心跳超时 - * 4��指定心跳的具体内容(默认为空) - * - * 由于 OkHttp 底层并未暴露 websocket �议里 opcode 的接�,所以该心跳的 opcode 始终是 2,并�是 websocket �议里定义的 9 - * 所以如果�务器�求客户端心跳的 opcode 必须是 9 的�,请使用 OkHttp 的原生心跳: - * https://ok.zhxu.cn/v3/websocket.html#%E5%85%A8%E5%B1%80%E5%BF%83%E8%B7%B3%E9%85%8D%E7%BD%AE - * - * �若需� �使用 {@link #pingSupplier(Supplier)} 方法指定心跳��的具体内容 * * @since v2.3.0 * @param pingSeconds 客户端心跳间隔秒数(0 表示�需�心跳) * @param pongSeconds �务器心跳间隔秒数(0 表示�需�心跳) - * @return WHttpTask + * @return WebSocketTask */ - public WHttpTask heatbeat(int pingSeconds, int pongSeconds) { + public WebSocketTask heatbeat(int pingSeconds, int pongSeconds) { if (pingSeconds < 0 || pongSeconds < 0) { throw new IllegalArgumentException("pingSeconds and pongSeconds must greater equal than 0!"); } @@ -90,39 +79,20 @@ public WHttpTask heatbeat(int pingSeconds, int pongSeconds) { return this; } - /** - * 用于兼容�些强制客户端必须以固定的时间间隔��心跳的�务器 - * @since v2.5.0 - * @param flexiblePing Ping 的间隔是��活��(默认为 true, 为 false 时客户端 Ping 的间隔固定,普通的消���为 Ping) - * @return WHttpTask - */ - public WHttpTask flexiblePing(boolean flexiblePing) { - this.flexiblePing = flexiblePing; - return this; - } - /** * @param pingSupplier 心跳数��供者 - * @return WHttpTask + * @return WebSocketTask */ - public WHttpTask pingSupplier(Supplier pingSupplier) { + public WebSocketTask pingSupplier(Supplier pingSupplier) { this.pingSupplier = pingSupplier; return this; } - public Supplier pingSupplier() { - return pingSupplier; - } - /** * �动 WebSocket 监� * @return WebSocket */ - public synchronized WebSocket listen() { - if (webSocket != null) { - // 如果连接已建立,直接返回 - return webSocket; - } + public WebSocket listen() { WebSocketImpl socket = new WebSocketImpl(); registeTagTask(socket); httpClient.preprocess(this, () -> { @@ -130,7 +100,7 @@ public synchronized WebSocket listen() { if (socket.cancelOrClosed) { removeTagTask(); } else { - Request request = prepareRequest(HTTP.GET); + Request request = prepareRequest("GET"); MessageListener listener = new MessageListener(socket); httpClient.webSocket(request, listener); } @@ -139,27 +109,11 @@ public synchronized WebSocket listen() { webSocket = socket; return socket; } - - /** - * @since v3.1.0 - * @param code 状�� - * @param reason 原因 - * @return true: 被关闭, false: 当�尚未建立连接 - */ - public boolean close(int code, String reason) { - WebSocket ws = webSocket; - if (ws != null) { - ws.close(code, reason); - webSocket = null; - return true; - } - return false; - } - - @SuppressWarnings("NullableProblems") + + class MessageListener extends WebSocketListener { - final WebSocketImpl webSocket; + WebSocketImpl webSocket; Charset charset; @@ -172,17 +126,16 @@ public void onOpen(okhttp3.WebSocket webSocket, Response response) { this.charset = charset(response); this.webSocket.setCharset(charset); this.webSocket.setWebSocket(webSocket); - this.webSocket.setStatus(WebSocket.STATUS_CONNECTED); - TaskListener listener = httpClient.executor().getResponseListener(); - HttpResult result = new RealHttpResult(WHttpTask.this, response, httpClient.executor()); - Listener openListener = onOpen; + TaskListener listener = httpClient.executor.getResponseListener(); + HttpResult result = new RealHttpResult(WebSocketTask.this, response, httpClient.executor); if (listener != null) { - if (listener.listen(WHttpTask.this, result) && openListener != null) { - execute(() -> openListener.on(this.webSocket, result), openOnIO); + if (listener.listen(WebSocketTask.this, result) && onOpen != null) { + execute(() -> onOpen.on(this.webSocket, result), openOnIO); } - } else if (openListener != null) { - execute(() -> openListener.on(this.webSocket, result), openOnIO); + } else if (onOpen != null) { + execute(() -> onOpen.on(this.webSocket, result), openOnIO); } + opened = true; if (pingSeconds > 0) { lastPingSecs = nowSeconds(); schedulePing(); @@ -196,126 +149,102 @@ public void onOpen(okhttp3.WebSocket webSocket, Response response) { // 接收文本数� 仅当 websocket 消�中的 opcode == 1 时 @Override public void onMessage(okhttp3.WebSocket webSocket, String text) { - Listener listener = onMessage; - if (listener != null) { - execute(() -> listener.on(this.webSocket, new WebSocketMsg(text, httpClient.executor(), charset)), messageOnIO); + if (onMessage != null) { + execute(() -> onMessage.on(this.webSocket, new WebSocketMsg(text, httpClient.executor, charset)), messageOnIO); } if (pongSeconds > 0) { lastPongSecs = nowSeconds(); + Platform.logInfo("PONG <<< " + text); } } // 接收二进制数� 仅当 websocket 消�中的 opcode == 2 时 @Override public void onMessage(okhttp3.WebSocket webSocket, ByteString bytes) { - Listener listener = onMessage; - if (listener != null) { - execute(() -> listener.on(this.webSocket, new WebSocketMsg(bytes, httpClient.executor(), charset)), messageOnIO); + if (onMessage != null) { + execute(() -> onMessage.on(this.webSocket, new WebSocketMsg(bytes, httpClient.executor, charset)), messageOnIO); } if (pongSeconds > 0) { lastPongSecs = nowSeconds(); + Platform.logInfo("PONG <<< " + bytes.utf8()); } } @Override - public void onClosing(okhttp3.WebSocket ws, int code, String reason) { - this.webSocket.setStatus(WebSocket.STATUS_DISCONNECTED); - Listener listener = onClosing; - if (listener != null) { - execute(() -> listener.on(this.webSocket, new Close(code, reason)), closingOnIO); + public void onClosing(okhttp3.WebSocket webSocket, int code, String reason) { + opened = false; + if (onClosing != null) { + execute(() -> onClosing.on(this.webSocket, new Close(code, reason)), closingOnIO); } - httpClient.executor().requireScheduler().schedule(() -> { - doOnClose(HttpResult.State.RESPONSED, code, reason); - }, maxClosingSecs, TimeUnit.SECONDS); } @Override - public void onClosed(okhttp3.WebSocket ws, int code, String reason) { + public void onClosed(okhttp3.WebSocket webSocket, int code, String reason) { doOnClose(HttpResult.State.RESPONSED, code, reason); } private void doOnClose(HttpResult.State state, int code, String reason) { - synchronized(WHttpTask.this) { - if (WHttpTask.this.webSocket == null || this.webSocket != WHttpTask.this.webSocket) { - return; // 回调已�执行过 - } - WHttpTask.this.webSocket = null; - Close close = updateStatus(state, code, reason); - TaskListener listener = httpClient.executor().getCompleteListener(); - Listener closeListener = onClosed; - if (listener != null) { - if (listener.listen(WHttpTask.this, state) && closeListener != null) { - execute(() -> closeListener.on(this.webSocket, close), closedOnIO); - } - } else if (closeListener != null) { - execute(() -> closeListener.on(this.webSocket, close), closedOnIO); + opened = false; + TaskListener listener = httpClient.executor.getCompleteListener(); + if (listener != null) { + if (listener.listen(WebSocketTask.this, state) && onClosed != null) { + execute(() -> onClosed.on(this.webSocket, toClose(state, code, reason)), closedOnIO); } + } else if (onClosed != null) { + execute(() -> onClosed.on(this.webSocket, toClose(state, code, reason)), closedOnIO); } } - private Close updateStatus(HttpResult.State state, int code, String reason) { + private Close toClose(HttpResult.State state, int code, String reason) { if (state == HttpResult.State.CANCELED) { - webSocket.setStatus(WebSocket.STATUS_CANCELED); return new Close(Close.CANCELED, "Canceled"); } if (state == HttpResult.State.EXCEPTION) { - webSocket.setStatus(WebSocket.STATUS_EXCEPTION); return new Close(Close.CANCELED, reason); } if (state == HttpResult.State.NETWORK_ERROR) { - webSocket.setStatus(WebSocket.STATUS_NETWORK_ERROR); return new Close(Close.NETWORK_ERROR, reason); } if (state == HttpResult.State.TIMEOUT) { - webSocket.setStatus(WebSocket.STATUS_TIMEOUT); return new Close(Close.TIMEOUT, reason); } - webSocket.setStatus(WebSocket.STATUS_DISCONNECTED); return new Close(code, reason); } @Override - public void onFailure(okhttp3.WebSocket ws, Throwable t, Response response) { + public void onFailure(okhttp3.WebSocket webSocket, Throwable t, Response response) { IOException e = t instanceof IOException ? (IOException) t : new IOException(t.getMessage(), t); doOnClose(toState(e), 0, t.getMessage()); - TaskListener listener = httpClient.executor().getExceptionListener(); - Listener exceptionListener = onException; + TaskListener listener = httpClient.executor.getExceptionListener(); if (listener != null) { - if (listener.listen(WHttpTask.this, e) && exceptionListener != null) { - execute(() -> exceptionListener.on(this.webSocket, t), exceptionOnIO); + if (listener.listen(WebSocketTask.this, e) && onException != null) { + execute(() -> onException.on(this.webSocket, t), exceptionOnIO); } - } else if (exceptionListener != null) { - execute(() -> exceptionListener.on(this.webSocket, t), exceptionOnIO); + } else if (onException != null) { + execute(() -> onException.on(this.webSocket, t), exceptionOnIO); } else if (!nothrow) { - throw new OkHttpsException("WebSockt 连接异常: " + getUrl(), t); + throw new HttpException("WebSockt 异常", t); } } } - /** - * @return 连接是�已建立 - */ - public boolean isConnected() { - WebSocketImpl ws = webSocket; - return ws != null && ws.status == WebSocket.STATUS_CONNECTED; - } - /** * 间隔��心跳 */ private void schedulePing() { - if (!isConnected()) { + if (!opened) { return; } int delay = (int) (pingSeconds + lastPingSecs - nowSeconds()); - httpClient.executor().requireScheduler().schedule(() -> { - if (!isConnected()) { + httpClient.executor.requireScheduler().schedule(() -> { + if (!opened) { return; } - WebSocket ws = webSocket; - if (nowSeconds() - lastPingSecs >= pingSeconds && ws != null) { - ws.send(pingSupplier != null ? pingSupplier.get() : ByteString.EMPTY); + if (nowSeconds() - lastPingSecs >= pingSeconds) { + ByteString ping = pingSupplier != null ? pingSupplier.get() : ByteString.EMPTY; + webSocket.send(ping); + Platform.logInfo("PING >>> " + ping.utf8()); lastPingSecs = nowSeconds(); } schedulePing(); @@ -326,21 +255,18 @@ private void schedulePing() { * 检测�务器的心跳�应 */ private void schedulePong() { - if (!isConnected()) { + if (!opened) { return; } int delay = (int) (pongSeconds + lastPongSecs - nowSeconds()); - httpClient.executor().requireScheduler().schedule(() -> { - if (!isConnected()) { + httpClient.executor.requireScheduler().schedule(() -> { + if (!opened) { return; } long noPongSeconds = nowSeconds() - lastPongSecs; - if (noPongSeconds > 3L * pongSeconds) { - WebSocketImpl ws = webSocket; - if (ws != null) { - Exception e = new SocketTimeoutException("Server didn't pong heart-beat on time. Last received at " + noPongSeconds + " seconds ago."); - ((RealWebSocket) ws.webSocket).failWebSocket(e, null); - } + if (noPongSeconds > 3 * pongSeconds) { + Exception e = new SocketTimeoutException("Server didn't pong heart-beat on time. Last received at " + noPongSeconds + " seconds ago."); + ((RealWebSocket) webSocket.webSocket).failWebSocket(e, null); } else { schedulePong(); } @@ -351,7 +277,7 @@ private void schedulePong() { * @return 当�时间戳(秒) */ private long nowSeconds() { - return System.currentTimeMillis() / 1000; + return System.nanoTime() / 1000_000_000; } class WebSocketImpl implements WebSocket { @@ -366,8 +292,6 @@ class WebSocketImpl implements WebSocket { private String msgType; - private int status = STATUS_CONNECTING; // 当�的连接状� - public WebSocketImpl() { String bodyType = getBodyType(); this.msgType = OkHttps.FORM.equalsIgnoreCase(bodyType) ? OkHttps.JSON : bodyType; @@ -399,12 +323,7 @@ public void msgType(String type) { if (type == null || type.equalsIgnoreCase(OkHttps.FORM)) { throw new IllegalArgumentException("msgType ��为空 或 form"); } - this.msgType = type.toLowerCase(); - } - - @Override - public int status() { - return status; + this.msgType = type; } @Override @@ -439,18 +358,12 @@ void setWebSocket(okhttp3.WebSocket webSocket) { queues.clear(); } } - - public void setStatus(int status) { - this.status = status; - } - + boolean send(okhttp3.WebSocket webSocket, Object msg) { if (msg == null) { return false; } - if (pingSeconds > 0 && flexiblePing) { - lastPingSecs = nowSeconds(); - } + lastPingSecs = nowSeconds(); if (msg instanceof String) { return webSocket.send((String) msg); } @@ -460,7 +373,7 @@ boolean send(okhttp3.WebSocket webSocket, Object msg) { if (msg instanceof byte[]) { return webSocket.send(ByteString.of((byte[]) msg)); } - byte[] bytes = httpClient.executor().doMsgConvert(msgType, c -> c.serialize(msg, charset)).data; + byte[] bytes = httpClient.executor.doMsgConvert(msgType, (MsgConvertor c) -> c.serialize(msg, charset)).data; return webSocket.send(new String(bytes, charset)); } @@ -469,9 +382,9 @@ boolean send(okhttp3.WebSocket webSocket, Object msg) { /** * 连接打开监� * @param onOpen 监�器 - * @return WHttpTask + * @return WebSocketTask */ - public WHttpTask setOnOpen(Listener onOpen) { + public WebSocketTask setOnOpen(Listener onOpen) { this.onOpen = onOpen; openOnIO = nextOnIO; nextOnIO = false; @@ -481,9 +394,9 @@ public WHttpTask setOnOpen(Listener onOpen) { /** * 连接异常监� * @param onException 监�器 - * @return WHttpTask + * @return WebSocketTask */ - public WHttpTask setOnException(Listener onException) { + public WebSocketTask setOnException(Listener onException) { this.onException = onException; exceptionOnIO = nextOnIO; nextOnIO = false; @@ -493,9 +406,9 @@ public WHttpTask setOnException(Listener onException) { /** * 消�监� * @param onMessage 监�器 - * @return WHttpTask + * @return WebSocketTask */ - public WHttpTask setOnMessage(Listener onMessage) { + public WebSocketTask setOnMessage(Listener onMessage) { this.onMessage = onMessage; messageOnIO = nextOnIO; nextOnIO = false; @@ -505,9 +418,9 @@ public WHttpTask setOnMessage(Listener onMessage) { /** * 正在关闭监� * @param onClosing 监�器 - * @return WHttpTask + * @return WebSocketTask */ - public WHttpTask setOnClosing(Listener onClosing) { + public WebSocketTask setOnClosing(Listener onClosing) { this.onClosing = onClosing; closingOnIO = nextOnIO; nextOnIO = false; @@ -517,25 +430,15 @@ public WHttpTask setOnClosing(Listener onClosing) { /** * 已关闭监�(当连接被�消或�生异常时,也会走该回调) * @param onClosed 监�器 - * @return WHttpTask + * @return WebSocketTask */ - public WHttpTask setOnClosed(Listener onClosed) { + public WebSocketTask setOnClosed(Listener onClosed) { this.onClosed = onClosed; closedOnIO = nextOnIO; nextOnIO = false; return this; } - /** - * 设置在 OnClosing 回调执行完毕�,OnClosed 回调执行的最晚延迟时间 - * @param maxClosingSecs 最大 Closing 时长(��:秒,默认:10秒) - * @return WHttpTask - */ - public WHttpTask setMaxClosingSecs(int maxClosingSecs) { - this.maxClosingSecs = maxClosingSecs; - return this; - } - public int pingSeconds() { return pingSeconds; } diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/BaseTest.java b/okhttps/src/test/java/cn/zhxu/okhttps/BaseTest.java deleted file mode 100644 index 72da672c..00000000 --- a/okhttps/src/test/java/cn/zhxu/okhttps/BaseTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.okhttps.HTTP; -import okhttp3.mockwebserver.MockWebServer; - -import java.text.SimpleDateFormat; -import java.util.Date; - -public class BaseTest { - - protected MockWebServer server = new MockWebServer(); - - protected String mockUrl = "http://" + server.getHostName() + ":" + server.getPort(); - - protected HTTP http = HTTP.builder().baseUrl(mockUrl).build(); - - - public static void sleep(int millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - public static void println() { - System.out.println(); - } - - public static void println(Object x) { - System.out.println(x); - } - - public static void println(long t0, String str) { - System.out.println(now() - t0 + "\t" + str); - } - - - public static void log(String str) { - SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); - System.out.println(sdf.format(new Date()) + "\t" + str); - } - - public static long now() { - return System.currentTimeMillis(); - } - -} diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/BodyParaTests.java b/okhttps/src/test/java/cn/zhxu/okhttps/BodyParaTests.java deleted file mode 100644 index 5ec732df..00000000 --- a/okhttps/src/test/java/cn/zhxu/okhttps/BodyParaTests.java +++ /dev/null @@ -1,60 +0,0 @@ -package cn.zhxu.okhttps; - -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.Assert; -import org.junit.Test; - -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; - -public class BodyParaTests extends BaseTest { - - @Test - public void testAddBodyPara() throws InterruptedException, UnsupportedEncodingException { - server.enqueue(new MockResponse().setBody("OK")); - - String resp = http.sync("/") - .addBodyPara("name", "OkHttps") - .addBodyPara("desc", "最好用的网络框架") - .post().getBody().toString(); - - RecordedRequest request = server.takeRequest(); - - Assert.assertEquals("OK", resp); - Assert.assertTrue(request.getHeader("Content-Type").startsWith("application/x-www-form-urlencoded")); - Assert.assertEquals("name=OkHttps&desc=最好用的网络框架", URLDecoder.decode(request.getBody().readUtf8(), "UTF-8")); - } - - @Test - public void testSetBodyParaFormString() throws InterruptedException { - server.enqueue(new MockResponse().setBody("OK")); - - String resp = http.sync("/") - .setBodyPara("name=OkHttps&desc=最好用的网络框架") - .post().getBody().toString(); - - RecordedRequest request = server.takeRequest(); - - Assert.assertEquals("OK", resp); - Assert.assertTrue(request.getHeader("Content-Type").startsWith("application/x-www-form-urlencoded")); - Assert.assertEquals("name=OkHttps&desc=最好用的网络框架", request.getBody().readUtf8()); - } - - @Test - public void testSetBodyParaJsonString() throws InterruptedException { - server.enqueue(new MockResponse().setBody("OK")); - String resp = http.sync(mockUrl) - .bodyType("json") - .setBodyPara("{\"name\":\"OkHttps\"}") - .post() - .getBody() - .toString(); - - RecordedRequest request = server.takeRequest(); - Assert.assertEquals("OK", resp); - Assert.assertTrue(request.getHeader("Content-Type").startsWith("application/json")); - Assert.assertEquals("{\"name\":\"OkHttps\"}", request.getBody().readUtf8()); - } - -} diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/CacheTests.java b/okhttps/src/test/java/cn/zhxu/okhttps/CacheTests.java deleted file mode 100644 index 307119ca..00000000 --- a/okhttps/src/test/java/cn/zhxu/okhttps/CacheTests.java +++ /dev/null @@ -1,23 +0,0 @@ -package cn.zhxu.okhttps; - -import okhttp3.mockwebserver.MockResponse; -import org.junit.Assert; -import org.junit.Test; - -public class CacheTests extends BaseTest { - - /** - * �用 cache 示例 - */ - @Test - public void testCache() { - String content = "test cache method"; - server.enqueue(new MockResponse().setBody(content)); - HttpResult.Body body = http.sync("/users").get().getBody() - .cache(); // �用 cache - // 使用 cache �,�以多次使用 toXXX() 方法 - Assert.assertEquals(content, body.toString()); - Assert.assertEquals(content, body.toString()); - } - -} diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/CancelTests.java b/okhttps/src/test/java/cn/zhxu/okhttps/CancelTests.java deleted file mode 100644 index fe6b602a..00000000 --- a/okhttps/src/test/java/cn/zhxu/okhttps/CancelTests.java +++ /dev/null @@ -1,97 +0,0 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.okhttps.HttpResult.State; -import cn.zhxu.okhttps.internal.AbstractHttpClient; -import org.junit.Test; - -import java.io.IOException; - -/** - * 测试�消 - */ -public class CancelTests extends BaseTest { - - - @Test - public void testCancelByTag() { - new Thread(() -> { - sleep(50); -// http.cancelAll(); -// int count = http.cancel("A"); -// println("count = " + count); - }).start(); - - http.async("/ejlchina/comm/provinces") - .tag("A") - .setOnResponse((HttpResult result) -> { - println("异步:result = " + result); - }) - .setOnComplete((State state) -> { - println("异步:state = " + state); - }) - .get(); - - try { - HttpResult result = http.sync("/ejlchina/comm/provinces") - .tag("A") - .get(); - - println("�步:result = " + result); - } catch (Exception e) { - e.printStackTrace(); - } - sleep(5000); - } - - @Test - public void testCancel() { - http.async("/user/show/1") - .setOnResponse(BaseTest::println) - .setOnException((IOException e) -> { - println("异常�获:" + e.getMessage()); - }) - .setOnComplete(BaseTest::println) - .tag("A") - .get(); - - println(((AbstractHttpClient) http).getTagTaskCount()); - - http.async("/user/show/2") - .setOnResponse(BaseTest::println) - .tag("A.B") - .get(); - - println(((AbstractHttpClient) http).getTagTaskCount()); - - http.async("/user/show/3") - .setOnResponse(BaseTest::println) - .tag("B.C") - .get(); - - println(((AbstractHttpClient) http).getTagTaskCount()); - - http.async("/user/show/4") - .setOnResponse(BaseTest::println) - .tag("C") - .get(); - - println(((AbstractHttpClient) http).getTagTaskCount()); - - println("标签�消:" + http.cancel("B")); - - println(((AbstractHttpClient) http).getTagTaskCount()); - - sleep(5000); - - println(((AbstractHttpClient) http).getTagTaskCount()); - - sleep(5000); - - println(((AbstractHttpClient) http).getTagTaskCount()); - - sleep(5000); - - println(((AbstractHttpClient) http).getTagTaskCount()); - } - -} diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/DownloadTests.java b/okhttps/src/test/java/cn/zhxu/okhttps/DownloadTests.java deleted file mode 100644 index e5cf2ef4..00000000 --- a/okhttps/src/test/java/cn/zhxu/okhttps/DownloadTests.java +++ /dev/null @@ -1,252 +0,0 @@ -package cn.zhxu.okhttps; - -import okhttp3.OkHttpClient; -import org.junit.Test; - -import java.io.*; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.concurrent.CountDownLatch; - -public class DownloadTests extends BaseTest { - - - /** - * 分�下载测试 - * @param args - */ - public static void main(String[] args) { - long totalSize = HttpUtils.sync("/download/test.zip").get().getBody() - .close() // 因为这次请求�是为了获得文件大�,�消费报文体,所以直接关闭 - .getLength(); - download(totalSize, 0); // 从第 0 �开始下载 - sleep(50000); // 等待下载完� - } - - static void download(long totalSize, int index) { - long size = 3 * 1024 * 1024; // ��下载 3M - long start = index * size; - long end = Math.min(start + size, totalSize); - HttpUtils.sync("/download/test.zip") - .setRange(start, end) // 设置本次下载的范围 - .get().getBody() - .toFile("D:/download/test.zip") // 下载到�一个文件里 - .setAppended() // 开�文件追加模� - .setOnSuccess((File file) -> { - if (end < totalSize) { // 若未下载完,则继续下载下一� - download(totalSize, index + 1); - } else { - println("下载完�"); - } - }) - .start(); - } - - @Test - public void testDownload1() { - HTTP http = HTTP.builder().build(); - http.async("https://download.cocos.com/CocosDashboard/v1.0.1/CocosDashboard-v1.0.1-win32-031816.exe") - .setOnResponse((HttpResult result) -> { - System.out.println(result.toString()); - System.out.println("ContentLength = " + result.getContentLength()); - System.out.println(result.getBody().getLength()); - System.out.println("type = " + result.getBody().getType()); - System.out.println("body = " + result.getBody().toString()); - }) - .head(); - sleep(5000); - } - - /** - * 多线程并行下载到�一个文件 - * 这�下载方法�一定高效,因为多线程��一个文件内写入数�存在竞争,下载时间�一定会�短 - * 例如 文件总大�:82.3 MB (86,336,432 字节): - * 1 个线程并行下载: 7727 毫秒 - * 2 个线程并行下载: 7842 毫秒 - * 3 个线程并行下载: 8075 毫秒 - * 5 个线程并行下载: 8527 毫秒 - * 9 个线程并行下载: 8024 毫秒 - */ - @Test - public void parallel() throws InterruptedException { - long t0 = System.currentTimeMillis(); - - String url = "https://download.cocos.com/CocosDashboard/v1.0.1/CocosDashboard-v1.0.1-win32-031816.exe"; - - long totalLength = OkHttps.sync(url).head().getContentLength(); - long size = 30 * 1024 * 1024; // ��最多下载 30 M - - int count = new BigDecimal(totalLength).divide(new BigDecimal(size), RoundingMode.UP).intValue(); - - println("共需下载 " + count + "�"); - - CountDownLatch latch = new CountDownLatch(count); - - for (int i = 0; i < count; i++) { - int index = i; - long start = index * size; - new Thread(() -> { - OkHttps.sync(url) - .setRange(start, start + size) - .get() - .getBody() - .stepRate(0.1) - .setOnProcess(p -> println("下载进度 [" + index + "]: " + p)) - .toFile("D:/CocosDashboard.exe") - .setOnSuccess((f) -> latch.countDown()) - .start(); - println("已开始下载第 " + index + "�"); - }).start(); - } - latch.await(); - - long t1 = System.currentTimeMillis(); - - println("共耗时:" + (t1 - t0) + " 毫秒"); - } - - - @Test - public void testRandomAccessFile() throws IOException { - String inputFile = "G:/1.jpg"; - String outputFile = "G:/2.jpg"; - - FileInputStream input = new FileInputStream(inputFile); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - byte[] buff = new byte[1024]; - int len; - while ((len = input.read(buff)) != -1) { - output.write(buff, 0, len); - } - input.close(); - byte[] data = output.toByteArray(); - output.close(); - System.out.println("data length = " + data.length); - - File outFile = new File(outputFile); - if (!outFile.exists()) { - outFile.createNewFile(); - } - int size = data.length / 3; - - for (int i = 2; i >= 0; i--) { - final int index = i; - new Thread(() -> { - try { - int start = index * size; - RandomAccessFile raf = new RandomAccessFile(outFile, "rw"); - raf.seek(start); - if (index < 2) { - raf.write(data, start, size); - } else { - raf.write(data, start, data.length - start); - } - raf.close(); - } catch (IOException e) { - e.printStackTrace(); - } - System.out.println("第 " + index + " �写入完��"); - }).start(); - } - - sleep(5000); - - } - - - - @Test - public void testDownload() { - HTTP http = HTTP.builder() - .config((OkHttpClient.Builder builder) -> { -// builder.readTimeout(300, TimeUnit.MILLISECONDS); - }) - .build(); - - String url = "https://gitee.com/troyzhxu/okhttps/blob/master/README.md"; -// String url = "http://47.100.7.202/ejl-test.zip"; - - long t0 = now(); - -// Ctrl ctrl = - HttpResult result = http.sync(url) -// .setRange(24771214) - .bind(this) - .get(); - - println(result.getHeaders()); - println(); - - result - .getBody() - .setOnProcess((Process process) -> { - println(t0, process.getDoneBytes() + "/" + process.getTotalBytes() + "\t" + process.getRate()); - }) - .stepRate(0.1) - .toFolder("D:/WorkSpace/download/") -// .toFile("D:/WorkSpace/test/README.md") -// .setAppended() // �用 断点续传 - .nextOnIO() - .setOnSuccess((File file) -> { - println(t0, "下载�功:" + file.getAbsolutePath()); - println(); - }) - .setOnFailure((Download.Failure failure) -> { - println(t0, "下载失败:" + failure.getDoneBytes() + ", path = " + failure.getFile().getAbsolutePath()); - println(); - }) - .start(); - - sleep(10000); - -// ctrl.status(); -// ctrl.pause(); -// println("暂�"); -// sleep(5000); -// -// ctrl.resume(); -// println("继续"); -// sleep(5000); -// -// ctrl.cancel(); -// println("�消"); - - -// - -// -// ctrl.pause(); -// println("暂�"); -// sleep(5000); -// -// ctrl.resume(); -// println("继续"); - sleep(5000); - - } - - - @Test - public void syncHttpExample() throws InterruptedException { - CountDownLatch latch = new CountDownLatch(1); - // �步请求 - Download download = http.sync("https://gimg2.baidu.com/image_search/src=http%3A%2F%2F01.minipic.eastday.com%2F20170224%2F20170224133956_9b9688ef44943a76c1abcfbc19d3182d_8.jpeg&refer=http%3A%2F%2F01.minipic.eastday.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1633097548&t=9c0530c59afe492fd238e70020ca6786") - .get() - .getBody() - .stepRate(0.2) - .setOnProcess(p -> { - println("下载:" + p.getDoneBytes() + "/" + p.getTotalBytes() + "\t" + p.getRate()); - }) - .toFile("G:\\Download\\2.jpg"); - - long t0 = System.nanoTime(); - download.setOnSuccess(f -> { - long t = System.nanoTime() - t0; - println("下载完�,耗时:" + (t / 1000) + "us"); - latch.countDown(); - }) - .start(); - latch.await(); - } - -} diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/ListMapTests.java b/okhttps/src/test/java/cn/zhxu/okhttps/ListMapTests.java deleted file mode 100644 index f880ded6..00000000 --- a/okhttps/src/test/java/cn/zhxu/okhttps/ListMapTests.java +++ /dev/null @@ -1,55 +0,0 @@ -package cn.zhxu.okhttps; - -import cn.zhxu.data.ArrayListMap; -import cn.zhxu.data.LinkedListMap; -import cn.zhxu.data.ListMap; -import org.junit.Test; - -public class ListMapTests { - - @Test - public void map() { - long t0 = System.currentTimeMillis(); - for (int i = 0; i < 10000; i++) { - ListMap map = new ArrayListMap<>(); - map.put("name", "jack"); - map.put("school", "High School"); - map.put("age", 20); - map.put("sex", "Male"); - map.put("height", 170); - map.put("type", 2); - map.put("timestamp", "1234567890"); - map.put("name", "jack"); - map.put("school", "High School"); - map.put("age", 20); - map.put("sex", "Male"); - map.put("height", 170); - map.put("type", 2); - map.put("timestamp", "1234567890"); - map.forEach((key, value) -> { }); - } - long t1 = System.currentTimeMillis(); - for (int i = 0; i < 10000; i++) { - ListMap map = new LinkedListMap<>(); - map.put("name", "jack"); - map.put("school", "High School"); - map.put("age", 20); - map.put("sex", "Male"); - map.put("height", 170); - map.put("type", 2); - map.put("timestamp", "1234567890"); - map.put("name", "jack"); - map.put("school", "High School"); - map.put("age", 20); - map.put("sex", "Male"); - map.put("height", 170); - map.put("type", 2); - map.put("timestamp", "1234567890"); - map.forEach((key, value) -> { }); - } - long t2 = System.currentTimeMillis(); - System.out.println(t1 - t0); - System.out.println(t2 - t1); - } - -} diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/SimpleTests.java b/okhttps/src/test/java/cn/zhxu/okhttps/SimpleTests.java deleted file mode 100644 index 956e086d..00000000 --- a/okhttps/src/test/java/cn/zhxu/okhttps/SimpleTests.java +++ /dev/null @@ -1,86 +0,0 @@ -package cn.zhxu.okhttps; - -import okhttp3.mockwebserver.MockResponse; -import org.junit.Assert; -import org.junit.Test; - -public class SimpleTests extends BaseTest { - - @Test - public void testSyncPost() throws InterruptedException { - http.async("/").bodyType(OkHttps.JSON).post(); - Assert.assertEquals(server.takeRequest().getHeader("Content-Type"), "application/json; charset=UTF-8"); - http.async("/").bodyType(OkHttps.FORM).post(); - Assert.assertEquals(server.takeRequest().getHeader("Content-Type"), "application/x-www-form-urlencoded; charset=UTF-8"); - } - - @Test - public void test() { - HttpResult res = HTTP.builder() - .build() - .sync("http://www.baidu.com") - .bodyType("application/x-www-form-urlencoded") - .addBodyPara("name", "Jack") - .addBodyPara("age", 11) - .post() - .close(); - System.out.println(res); - } - - - /** - * �步请求示例 - * �步请求直接得到结果,无需设置回调 - */ - @Test - public void testSyncToString() { - server.enqueue(new MockResponse().setBody("Hello World!")); - String hello = http.sync("/users") // http://localhost:8080/users - .get() // GET请求 - .getBody() // 获��应报文体 - .toString(); // 得到目标数� - Assert.assertEquals(hello, "Hello World!"); - } - - @Test - public void testUserAgent() throws InterruptedException { - http.async("/user").addHeader("User-Agent", "123456").get(); - String userAgent = server.takeRequest().getHeader("User-Agent"); - Assert.assertEquals("123456", userAgent); - } - -// @Test -// public void testSyncToList() { -// User u1 = new User(1, "Jack"); -// User u2 = new User(2, "Tom"); -// List list = Arrays.asList(u1, u2); -// server.enqueue(new MockResponse().setBody(JSON.toJSONString(list))); -// -// List users = http.sync("/users") // http://localhost:8080/users -// .get() // GET请求 -// .getBody() // 获��应报文体 -// .toList(User.class); // 得到目标数� -// -// assertEquals(u1, users.get(0)); -// assertEquals(u2, users.get(1)); -// } -// -// /** -// * 异步请求示例 -// * 异步请求在OnResponse里接收请求结果 -// */ -// @Test -// public void testAsync() { -// http.async("/users/1") // http://api.demo.com/users/1 -// .setOnResponse((HttpResult result) -> { -// // 得到目标数� -// User user = result.getBody().toBean(User.class); -// System.out.println("user = " + user); -// }) -// .get(); -// sleep(5000); -// } - -} - - diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/UploadTests.java b/okhttps/src/test/java/cn/zhxu/okhttps/UploadTests.java deleted file mode 100644 index 83506cd8..00000000 --- a/okhttps/src/test/java/cn/zhxu/okhttps/UploadTests.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.zhxu.okhttps; - -import okhttp3.mockwebserver.MockResponse; -import org.junit.Assert; -import org.junit.Test; - -import java.util.Random; -import java.util.concurrent.CountDownLatch; - -public class UploadTests extends BaseTest { - - private byte[] randomFileContent(int length) { - Random random = new Random(); - byte[] content = new byte[length]; - for (int i = 0; i < length; i++) { - content[i] = (byte) (random.nextInt() % 256); - } - return content; - } - - @Test - public void testUpload() throws InterruptedException { - CountDownLatch latch = new CountDownLatch(1); - // 512 KB - byte[] content = randomFileContent(5 * 1024); - server.enqueue(new MockResponse().setResponseCode(200).setBody("OK")); - long t0 = System.currentTimeMillis(); - http.async("/upload") - .addBodyPara("path", "123456") - .addFilePara("file", "txt", "222", content) - .stepRate(0.1) - .setOnProcess((Process process) -> println(t0, "上传:" + process.getDoneBytes() + "/" + process.getTotalBytes() + "\t" + process.getRate())) - .setOnResponse(res -> { - String result = res.getBody().toString(); - println(result); - Assert.assertEquals(result, "OK"); - }) - .setOnComplete(state -> latch.countDown()) - .post(); - byte[] uploadData = server.takeRequest().getBody().buffer().readByteArray(); - println("数�长度:" + uploadData.length); - latch.await(); - } - -} diff --git a/okhttps/src/test/java/com/ejlchina/test/BaseTest.java b/okhttps/src/test/java/com/ejlchina/test/BaseTest.java new file mode 100644 index 00000000..983f6bdf --- /dev/null +++ b/okhttps/src/test/java/com/ejlchina/test/BaseTest.java @@ -0,0 +1,101 @@ +package com.ejlchina.test; + +import okhttp3.mockwebserver.MockWebServer; + +public class BaseTest { + + protected MockWebServer server = new MockWebServer(); + + protected String mockUrl = "http://" + server.getHostName() + ":" + server.getPort(); + + + public static void sleep(int millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public static void println() { + System.out.println(); + } + + public static void println(Object x) { + System.out.println(x); + } + + public static void println(long t0, String str) { + System.out.println(now() - t0 + "\t" + str); + } + + public static long now() { + return System.currentTimeMillis(); + } + + public static class User { + + private int id; + private String name; + + + public User() { + } + + public User(int id, String name) { + this.id = id; + this.name = name; + } + + public void setId(int id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "User [id=" + id + ", name=" + name + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + id; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + User other = (User) obj; + if (id != other.id) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + } + +} diff --git a/okhttps/src/test/java/com/ejlchina/test/CacheTests.java b/okhttps/src/test/java/com/ejlchina/test/CacheTests.java new file mode 100644 index 00000000..112b55ae --- /dev/null +++ b/okhttps/src/test/java/com/ejlchina/test/CacheTests.java @@ -0,0 +1,74 @@ +package com.ejlchina.test; + +import com.ejlchina.okhttps.HTTP; +import com.ejlchina.okhttps.HttpResult; +import com.ejlchina.okhttps.OnCallback; +import com.ejlchina.okhttps.HttpResult.State; +import com.ejlchina.okhttps.HttpTask; +import com.ejlchina.okhttps.Mapper; + +import org.junit.Test; + +public class CacheTests extends BaseTest { + + @Test + public void testCache() { + HTTP http = HTTP.builder().build(); + + + +// +// +// HttpResult.Body body = http.sync("http://xxx.cdyun.vip/comm/provinces") +// +// .get() +// .getBody() +// .cache(); +// +// println("result = " + body.toString()); +// println("result = " + body.toArray()); +// +// body.close(); +// +// println("result = " + body.toString()); +// println("result = " + body.toArray()); +// +// +// +// +// http.async("") +// .addBodyPara("", "") +// +// +// .setOnResponse((HttpResult res) -> { +// +// Mapper mapper = res.getBody().toMapper(); +// +// }) +// .setOnComplete(state -> { +// +// testCache(); +// }) +// .setOnProcess(process -> { +// +// double rate = process.getRate(); +// +// }) +// .post() +// .cancel() +// ; + + + } + + class OnComplete implements OnCallback { + + @Override + public void on(State date) { + + testCache(); + } + + } + +} diff --git a/okhttps/src/test/java/com/ejlchina/test/CancelTests.java b/okhttps/src/test/java/com/ejlchina/test/CancelTests.java new file mode 100644 index 00000000..c9994a8e --- /dev/null +++ b/okhttps/src/test/java/com/ejlchina/test/CancelTests.java @@ -0,0 +1,133 @@ +package com.ejlchina.test; + +import com.ejlchina.okhttps.HTTP; +import com.ejlchina.okhttps.HttpResult; +import com.ejlchina.okhttps.HttpResult.State; +import com.ejlchina.okhttps.internal.HttpClient; +import org.junit.Test; + +import java.io.IOException; + +/** + * 测试�消 + */ +public class CancelTests extends BaseTest { + + + @Test + public void testCancelByTag() { + HTTP http = HTTP.builder() + .baseUrl("http://tst-api-mini.cdyun.vip") + .build(); + + new Thread(() -> { + sleep(50); +// http.cancelAll(); +// int count = http.cancel("A"); +// println("count = " + count); + }).start(); + + http.async("/ejlchina/comm/provinces") + .tag("A") + .setOnResponse((HttpResult result) -> { + println("异步:result = " + result); + }) + .setOnComplete((State state) -> { + println("异步:state = " + state); + }) + .get(); + + try { + HttpResult result = http.sync("/ejlchina/comm/provinces") + .tag("A") + .get(); + + println("�步:result = " + result); + } catch (Exception e) { + e.printStackTrace(); + } + sleep(5000); + } + + @Test + public void testCancel() { + + HTTP http = HTTP.builder() + .baseUrl("http://localhost:8080") + .build(); + + http.async("/user/show/1") + .setOnResponse((HttpResult result) -> { + println(result); + }) + .setOnException((IOException e) -> { + println("异常�获:" + e.getMessage()); + }) + .setOnComplete((HttpResult.State state) -> { + println(state); + }) + .tag("A") + .get(); + + println(((HttpClient) http).getTagTaskCount()); + + http.async("/user/show/2") + .setOnResponse((HttpResult result) -> { + println(result); + }) + .tag("A.B") + .get(); + + println(((HttpClient) http).getTagTaskCount()); + + http.async("/user/show/3") + .setOnResponse((HttpResult result) -> { + println(result); + }) + .tag("B.C") + .get(); + + println(((HttpClient) http).getTagTaskCount()); + + http.async("/user/show/4") + .setOnResponse((HttpResult result) -> { + println(result); + }) + .tag("C") + .get(); + + println(((HttpClient) http).getTagTaskCount()); + + + println("标签�消:" + http.cancel("B")); + + println(((HttpClient) http).getTagTaskCount()); + + sleep(5000); + + println(((HttpClient) http).getTagTaskCount()); + + sleep(5000); + + println(((HttpClient) http).getTagTaskCount()); + + sleep(5000); + + println(((HttpClient) http).getTagTaskCount()); + +// println("isDone = " + call.isDone()); +// println("isCanceled = " + call.isCanceled()); +// +// println("�消结果 = " + call.cancel()); +// +// println("isDone = " + call.isDone()); +// println("isCanceled = " + call.isCanceled()); +// +// sleep(100); +// println("++++++++"); +// +// println("isDone = " + call.isDone()); +// println("isCanceled = " + call.isCanceled()); + } + +} diff --git a/okhttps/src/test/java/com/ejlchina/test/DownloadTests.java b/okhttps/src/test/java/com/ejlchina/test/DownloadTests.java new file mode 100644 index 00000000..0f45d4be --- /dev/null +++ b/okhttps/src/test/java/com/ejlchina/test/DownloadTests.java @@ -0,0 +1,158 @@ +package com.ejlchina.test; + +import com.ejlchina.okhttps.*; +import com.ejlchina.okhttps.Process; +import okhttp3.OkHttpClient; +import org.junit.Test; + +import java.io.File; +import java.util.concurrent.TimeUnit; + +public class DownloadTests extends BaseTest { + + + /** + * 分�下载测试 + * @param args + */ + public static void main(String[] args) { + long totalSize = HttpUtils.sync("/download/test.zip").get().getBody() + .close() // 因为这次请求�是为了获得文件大�,�消费报文体,所以直接关闭 + .getLength(); + download(totalSize, 0); // 从第 0 �开始下载 + sleep(50000); // 等待下载完� + } + + static void download(long totalSize, int index) { + long size = 3 * 1024 * 1024; // ��下载 3M + long start = index * size; + long end = Math.min(start + size, totalSize); + HttpUtils.sync("/download/test.zip") + .setRange(start, end) // 设置本次下载的范围 + .get().getBody() + .toFile("D:/download/test.zip") // 下载到�一个文件里 + .setAppended() // 开�文件追加模� + .setOnSuccess((File file) -> { + if (end < totalSize) { // 若未下载完,则继续下载下一� + download(totalSize, index + 1); + } else { + println("下载完�"); + } + }) + .start(); + } + + @Test + public void testDownload1() { + HTTP http = HTTP.builder().build(); + http.async("https://download.cocos.com/CocosDashboard/v1.0.1/CocosDashboard-v1.0.1-win32-031816.exe") + .setOnResponse((HttpResult result) -> { + System.out.println(result.toString()); + System.out.println("ContentLength = " + result.getContentLength()); + System.out.println(result.getBody().getLength()); + System.out.println("type = " + result.getBody().getType()); + System.out.println("body = " + result.getBody().toString()); + }) + .head(); + sleep(5000); + } + + @Test + public void testDownload() { + HTTP http = HTTP.builder() + .config((OkHttpClient.Builder builder) -> { +// builder.readTimeout(300, TimeUnit.MILLISECONDS); + }) + .build(); + + String url = "https://gitee.com/ejlchina-zhxu/okhttps/blob/master/README.md"; +// String url = "http://47.100.7.202/ejl-test.zip"; + + long t0 = now(); + +// Ctrl ctrl = + HttpResult result = http.sync(url) +// .setRange(24771214) + .bind(this) + .get(); + + println(result.getHeaders()); + println(); + + result + .getBody() + .setOnProcess((Process process) -> { + println(t0, process.getDoneBytes() + "/" + process.getTotalBytes() + "\t" + process.getRate()); + }) + .stepRate(0.1) + .toFolder("D:/WorkSpace/download/") +// .toFile("D:\\WorkSpace\\download\\CocosDashboard-v1.0.1-win32-031816(9).exe") +// .setAppended() // �用 断点续传 + .nextOnIO() + .setOnSuccess((File file) -> { + println(t0, "下载�功:" + file.getAbsolutePath()); + println(); + }) + .setOnFailure((Download.Failure failure) -> { + println(t0, "下载失败:" + failure.getDoneBytes() + ", path = " + failure.getFile().getAbsolutePath()); + println(); + }) + .start(); + + sleep(10000); + +// ctrl.status(); +// ctrl.pause(); +// println("暂�"); +// sleep(5000); +// +// ctrl.resume(); +// println("继续"); +// sleep(5000); +// +// ctrl.cancel(); +// println("�消"); + + +// + +// +// ctrl.pause(); +// println("暂�"); +// sleep(5000); +// +// ctrl.resume(); +// println("继续"); + sleep(5000); + + } + + + @Test + public void syncHttpExample() { + + HTTP http = HTTP.builder().build(); + // �步请求 + HttpResult result = http.sync("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1584365400942&di=d9e7890f13b7bc4b76080fdd490ed5d5&imgtype=0&src=http%3A%2F%2Ft8.baidu.com%2Fit%2Fu%3D1484500186%2C1503043093%26fm%3D79%26app%3D86%26f%3DJPEG%3Fw%3D1280%26h%3D853") + .get(); + + result.getBody().toFile("E:/3.jpg"); +// // 得到状�� +// int status = result.getStatus(); +// +// // 得到返回头 +// Headers headers = result.getHeaders(); +// +// User user = result.getBody().toBean(User.class); +// // 得到目标数� +// +// +// println("status = " + status); +// println("headers = " + headers); +// println("user = " + user); + + println("status = " + result.getStatus()); + + } + +} diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/ExceptionTests.java b/okhttps/src/test/java/com/ejlchina/test/ExceptionTests.java similarity index 93% rename from okhttps/src/test/java/cn/zhxu/okhttps/ExceptionTests.java rename to okhttps/src/test/java/com/ejlchina/test/ExceptionTests.java index ae1245d4..7fd25505 100644 --- a/okhttps/src/test/java/cn/zhxu/okhttps/ExceptionTests.java +++ b/okhttps/src/test/java/com/ejlchina/test/ExceptionTests.java @@ -1,5 +1,8 @@ -package cn.zhxu.okhttps; +package com.ejlchina.test; +import com.ejlchina.okhttps.HTTP; +import com.ejlchina.okhttps.HttpResult; +import com.ejlchina.okhttps.internal.HttpException; import okhttp3.OkHttpClient; import org.junit.Test; @@ -28,7 +31,7 @@ public void testSync0() { try { HttpResult result = http.sync("/users/1").get(); System.out.println("请求结果:" + result); - } catch (OkHttpsException e) { + } catch (HttpException e) { Throwable cause = e.getCause(); // 得到异常原因 if (cause instanceof ConnectException) { // 当没网络时,会抛出连接异常 diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/ExecutorTests.java b/okhttps/src/test/java/com/ejlchina/test/ExecutorTests.java similarity index 87% rename from okhttps/src/test/java/cn/zhxu/okhttps/ExecutorTests.java rename to okhttps/src/test/java/com/ejlchina/test/ExecutorTests.java index a598128c..ccbed45f 100644 --- a/okhttps/src/test/java/cn/zhxu/okhttps/ExecutorTests.java +++ b/okhttps/src/test/java/com/ejlchina/test/ExecutorTests.java @@ -1,5 +1,8 @@ -package cn.zhxu.okhttps; +package com.ejlchina.test; +import com.ejlchina.okhttps.HTTP; +import com.ejlchina.okhttps.HttpResult; +import com.ejlchina.okhttps.Process; import org.junit.Test; public class ExecutorTests extends BaseTest { diff --git a/okhttps/src/test/java/com/ejlchina/test/FormParaTests.java b/okhttps/src/test/java/com/ejlchina/test/FormParaTests.java new file mode 100644 index 00000000..c1538d7d --- /dev/null +++ b/okhttps/src/test/java/com/ejlchina/test/FormParaTests.java @@ -0,0 +1,95 @@ +package com.ejlchina.test; + +import java.math.BigDecimal; + +import org.junit.Assert; +import org.junit.Test; + +import com.ejlchina.okhttps.HTTP; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; + +public class FormParaTests extends BaseTest { + + @Test + public void testSetBodyParaFormString() throws InterruptedException { + HTTP http = HTTP.builder().build(); + + server.enqueue(new MockResponse().setBody("OK")); + + String resp = http.sync(mockUrl) + .setBodyPara("name=OkHttps&desc=最好用的网络框架") + .post().getBody().toString(); + + RecordedRequest request = server.takeRequest(); + + Assert.assertEquals("OK", resp); + Assert.assertTrue(request.getHeader("Content-Type").startsWith("application/x-www-form-urlencoded")); + Assert.assertEquals("name=OkHttps&desc=最好用的网络框架", request.getBody().readUtf8()); + } + + @Test + public void testSetBodyParaJsonString() throws InterruptedException { + HTTP http = HTTP.builder().bodyType("json").build(); + + server.enqueue(new MockResponse().setBody("OK")); + + String resp = http.sync(mockUrl) + .setBodyPara("{\"name\":\"OkHttps\"}") + .post().getBody().toString(); + + RecordedRequest request = server.takeRequest(); + + System.out.println(request.getHeader("Content-Type")); + + System.out.println(request.getBody().readUtf8()); + +// Assert.assertEquals("OK", resp); +// Assert.assertTrue(request.getHeader("Content-Type").startsWith("application/x-www-form-urlencoded")); +// Assert.assertEquals("name=OkHttps&desc=最好用的网络框架", request.getBody().readUtf8()); + } + + + + + class A { + + T a; + + A(T a) { + this.a = a; + } + + void dosomething() { + if (a instanceof String) { + + } + if (a instanceof BigDecimal) { + ((BigDecimal) a).byteValue(); + } + + } + + + + } + + + + + + + + + + + + + + + + + + +} diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/ListenerTests.java b/okhttps/src/test/java/com/ejlchina/test/ListenerTests.java similarity index 93% rename from okhttps/src/test/java/cn/zhxu/okhttps/ListenerTests.java rename to okhttps/src/test/java/com/ejlchina/test/ListenerTests.java index 2d38a050..a0450840 100644 --- a/okhttps/src/test/java/cn/zhxu/okhttps/ListenerTests.java +++ b/okhttps/src/test/java/com/ejlchina/test/ListenerTests.java @@ -1,5 +1,10 @@ -package cn.zhxu.okhttps; +package com.ejlchina.test; +import com.ejlchina.okhttps.Download; +import com.ejlchina.okhttps.HTTP; +import com.ejlchina.okhttps.HttpResult; +import com.ejlchina.okhttps.HttpTask; +import com.ejlchina.okhttps.Process; import org.junit.Test; import java.io.File; diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/PreprocessorTests.java b/okhttps/src/test/java/com/ejlchina/test/PreprocessorTests.java similarity index 93% rename from okhttps/src/test/java/cn/zhxu/okhttps/PreprocessorTests.java rename to okhttps/src/test/java/com/ejlchina/test/PreprocessorTests.java index bd490cf1..f20fe559 100644 --- a/okhttps/src/test/java/cn/zhxu/okhttps/PreprocessorTests.java +++ b/okhttps/src/test/java/com/ejlchina/test/PreprocessorTests.java @@ -1,5 +1,7 @@ -package cn.zhxu.okhttps; +package com.ejlchina.test; +import com.ejlchina.okhttps.HTTP; +import com.ejlchina.okhttps.Preprocessor; import org.junit.Test; public class PreprocessorTests extends BaseTest { diff --git a/okhttps/src/test/java/com/ejlchina/test/SimpleTests.java b/okhttps/src/test/java/com/ejlchina/test/SimpleTests.java new file mode 100644 index 00000000..019aaf2f --- /dev/null +++ b/okhttps/src/test/java/com/ejlchina/test/SimpleTests.java @@ -0,0 +1,105 @@ +package com.ejlchina.test; + +import com.ejlchina.okhttps.HTTP; + +import okhttp3.mockwebserver.MockWebServer; + +public class SimpleTests extends BaseTest { + + + MockWebServer server = new MockWebServer(); + + HTTP http = HTTP.builder() + .baseUrl("http://" + server.getHostName() + ":" + server.getPort()) + .build(); + + /** + * �步请求示例 + * �步请求直接得到结果,无需设置回调 + */ +// @Test +// public void testSyncToBean() { +// server.enqueue(new MockResponse().setBody("Hello World!")); +// +// JsonObj hello = http.sync("/users") // http://localhost:8080/users +// .get() // GET请求 +// .getBody() // 获��应报文体 +// .toJsonObj(); // 得到目标数� +// +// System.out.println("hello = " + hello); +// } + + /** + * �步请求示例 + * �步请求直接得到结果,无需设置回调 + */ +// @Test +// public void testSyncToList() { +// User u1 = new User(1, "Jack"); +// User u2 = new User(2, "Tom"); +// List list = Arrays.asList(u1, u2); +// server.enqueue(new MockResponse().setBody(JSON.toJSONString(list))); +// +// List users = http.sync("/users") // http://localhost:8080/users +// .get() // GET请求 +// .getBody() // 获��应报文体 +// .toList(User.class); // 得到目标数� +// +// assertEquals(u1, users.get(0)); +// assertEquals(u2, users.get(1)); +// } +// +// /** +// * 异步请求示例 +// * 异步请求在OnResponse里接收请求结果 +// */ +// @Test +// public void testAsync() { +// http.async("/users/1") // http://api.demo.com/users/1 +// .setOnResponse((HttpResult result) -> { +// // 得到目标数� +// User user = result.getBody().toBean(User.class); +// System.out.println("user = " + user); +// }) +// .get(); +// sleep(5000); +// } +// +// +// /** +// * �用 cache 示例 +// */ +// @Test +// public void testCache() { +// Body body = http.sync("/users").get().getBody() +// .cache(); // �用 cache +// // 使用 cache �,�以多次使用 toXXX() 方法 +// System.out.println(body.toString()); +// System.out.println(body.toJsonArray()); +// System.out.println(body.toList(User.class)); +// } +// +// +// /** +// * 获�文件大�示例 +// */ +// @Test +// public void testContentLength() { +// long t = System.currentTimeMillis(); +// +// long size = http.sync("https://download.cocos.com/CocosDashboard/v1.0.1/CocosDashboard-v1.0.1-win32-031816.exe") +// .get().getBody() +// .close() // �是想获得文件大�,�消费报文体,所以直接关闭 +// .getContentLength(); // 获得待下载文件的大� +// +// t = System.currentTimeMillis() - t; +// +// // 由于未消费报文体,所以本次请求�会消耗下载报文体的时间和网络��) +// System.out.println("耗时:" + t + " 毫秒"); +// System.out.println("size = " + (size / 1024) + " KB"); +// } + + +} + + diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/TagTests.java b/okhttps/src/test/java/com/ejlchina/test/TagTests.java similarity index 84% rename from okhttps/src/test/java/cn/zhxu/okhttps/TagTests.java rename to okhttps/src/test/java/com/ejlchina/test/TagTests.java index 2ab0f0e8..485f02df 100644 --- a/okhttps/src/test/java/cn/zhxu/okhttps/TagTests.java +++ b/okhttps/src/test/java/com/ejlchina/test/TagTests.java @@ -1,6 +1,8 @@ -package cn.zhxu.okhttps; +package com.ejlchina.test; -import cn.zhxu.okhttps.internal.AbstractHttpClient; +import com.ejlchina.okhttps.HTTP; +import com.ejlchina.okhttps.HttpResult; +import com.ejlchina.okhttps.internal.HttpClient; import okhttp3.OkHttpClient; import org.junit.Test; @@ -10,7 +12,7 @@ public class TagTests extends BaseTest { @Test public void testTag() { - AbstractHttpClient http = (AbstractHttpClient) HTTP.builder() + HttpClient http = (HttpClient) HTTP.builder() .config((OkHttpClient.Builder builder) -> { builder.connectTimeout(1, TimeUnit.SECONDS); builder.writeTimeout(1, TimeUnit.SECONDS); diff --git a/okhttps/src/test/java/com/ejlchina/test/UploadTests.java b/okhttps/src/test/java/com/ejlchina/test/UploadTests.java new file mode 100644 index 00000000..06a8c744 --- /dev/null +++ b/okhttps/src/test/java/com/ejlchina/test/UploadTests.java @@ -0,0 +1,36 @@ +package com.ejlchina.test; + +import com.ejlchina.okhttps.HTTP; +import com.ejlchina.okhttps.Process; +import org.junit.Test; + +public class UploadTests extends BaseTest { + + @Test + public void testUpload() { + String data = "0123456789abcdefghijklmnopqrstuvwsyz0123456789abcdefghijklmnopqrstuvwsyz0123456789abcdefghijklmnopqrstuvwsyz0123456789abcdefghijklmnopqrstuvwsyz0123456789abcdefghijklmnopqrstuvwsyz"; + + HTTP http = HTTP.builder().build(); + + long t0 = System.currentTimeMillis(); + + String res = http.sync("http://localhost:8080/test/index") + .addBodyPara("data", data) + .addFilePara("file", "D:\\WorkSpace\\download\\CocosDashboard-v1.0.1-win32-031816.exe") + .stepRate(0.01) + .setOnProcess((Process process) -> { + println(t0, "上传:" + process.getDoneBytes() + "/" + process.getTotalBytes() + "\t" + process.getRate()); + }) + .post() + .getBody() + .stepBytes(5) + .setOnProcess((Process process) -> { + println(t0, "下载:" + process.getDoneBytes() + "/" + process.getTotalBytes() + "\t" + process.getRate()); + }) + .toString(); + + println("�应:" + res); + } + + +} diff --git a/okhttps/src/test/java/cn/zhxu/okhttps/WebSocketTests.java b/okhttps/src/test/java/com/ejlchina/test/WebSocketTests.java similarity index 89% rename from okhttps/src/test/java/cn/zhxu/okhttps/WebSocketTests.java rename to okhttps/src/test/java/com/ejlchina/test/WebSocketTests.java index 47a52963..3f1b0c44 100644 --- a/okhttps/src/test/java/cn/zhxu/okhttps/WebSocketTests.java +++ b/okhttps/src/test/java/com/ejlchina/test/WebSocketTests.java @@ -1,9 +1,9 @@ -package cn.zhxu.okhttps; +package com.ejlchina.test; -import cn.zhxu.okhttps.HTTP; -import cn.zhxu.okhttps.HttpResult; -import cn.zhxu.okhttps.WebSocket; -import cn.zhxu.okhttps.WebSocket.Message; +import com.ejlchina.okhttps.HTTP; +import com.ejlchina.okhttps.HttpResult; +import com.ejlchina.okhttps.WebSocket; +import com.ejlchina.okhttps.WebSocket.Message; import org.junit.Test; public class WebSocketTests { diff --git a/pom.xml b/pom.xml index 27ee625c..66a94cd9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,33 +1,29 @@ 4.0.0 - cn.zhxu + com.ejlchina okhttps-parent - 4.1.0 + 2.3.0 OkHttps pom okhttps - okhttps-stomp + okhttps-fastjson okhttps-gson okhttps-jackson - okhttps-jackson-xml - okhttps-jackson-yaml - okhttps-fastjson - okhttps-fastjson2 - okhttps-fastjson2-jsonb - okhttps-snack3 okhttps-test - 1.8 - 1.6.2 + 2.3.0 + 1.8 3.14.9 - 1.17.6 - 4.13.2 + 2.8.6 + 1.2.67 + 2.11.0 + 4.12 UTF-8 https://oss.sonatype.org/content/repositories/snapshots/ @@ -49,10 +45,10 @@ - scm:git://gitee.com/troyzhxu/okhttps.git - scm:http://gitee.com/troyzhxu/okhttps - https://gitee.com/troyzhxu/okhttps - v${project.version} + scm:git://gitee.com/ejlchina-zhxu/okhttps.git + scm:http://gitee.com/ejlchina-zhxu/okhttps + https://gitee.com/ejlchina-zhxu/okhttps + v2.3.0 @@ -63,15 +59,31 @@ test - + + + + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + ${sonatypeOssDistMgmtSnapshotsUrl} - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + sonatype-nexus-staging + Nexus Release Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ @@ -81,7 +93,7 @@ org.apache.maven.plugins maven-release-plugin - 2.5.3 + 2.1 forked-path false @@ -92,34 +104,9 @@ org.apache.maven.plugins maven-compiler-plugin 3.8.1 - - - default-compile - - - 1.8 - - - - - - base-compile - - compile - - - - module-info.java - - - - - - [1.8,17) - - ${java.version} - ${java.version} + ${java-version} + ${java-version} ${project.build.sourceEncoding} true @@ -127,7 +114,7 @@ org.apache.maven.plugins maven-source-plugin - 3.2.1 + 2.1.2 attach-sources @@ -140,10 +127,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.3.1 - - ${project.build.sourceEncoding} - + 2.7 attach-javadocs @@ -156,7 +140,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.0.1 + 1.1 sign-artifacts @@ -170,13 +154,13 @@ - https://gitee.com/troyzhxu/okhttps + https://gitee.com/ejlchina-zhxu/okhttps 比 Retrofit 更好用的网络框架� OkHttp3 轻��装 ,开箱�用�Java领域��端处� Http问题的新选择。 - zhouxu - https://zhxu.cn/ + 江�一键�信�科技有�公� + http://www.ejlchina.com/ - https://gitee.com/troyzhxu/okhttps/issues + https://gitee.com/ejlchina-zhxu/okhttps/issues \ No newline at end of file