| @@ -0,0 +1,201 @@ | |||
| Apache License | |||
| Version 2.0, January 2004 | |||
| http://www.apache.org/licenses/ | |||
| TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |||
| 1. Definitions. | |||
| "License" shall mean the terms and conditions for use, reproduction, | |||
| and distribution as defined by Sections 1 through 9 of this document. | |||
| "Licensor" shall mean the copyright owner or entity authorized by | |||
| the copyright owner that is granting the License. | |||
| "Legal Entity" shall mean the union of the acting entity and all | |||
| other entities that control, are controlled by, or are under common | |||
| control with that entity. For the purposes of this definition, | |||
| "control" means (i) the power, direct or indirect, to cause the | |||
| direction or management of such entity, whether by contract or | |||
| otherwise, or (ii) ownership of fifty percent (50%) or more of the | |||
| outstanding shares, or (iii) beneficial ownership of such entity. | |||
| "You" (or "Your") shall mean an individual or Legal Entity | |||
| exercising permissions granted by this License. | |||
| "Source" form shall mean the preferred form for making modifications, | |||
| including but not limited to software source code, documentation | |||
| source, and configuration files. | |||
| "Object" form shall mean any form resulting from mechanical | |||
| transformation or translation of a Source form, including but | |||
| not limited to compiled object code, generated documentation, | |||
| and conversions to other media types. | |||
| "Work" shall mean the work of authorship, whether in Source or | |||
| Object form, made available under the License, as indicated by a | |||
| copyright notice that is included in or attached to the work | |||
| (an example is provided in the Appendix below). | |||
| "Derivative Works" shall mean any work, whether in Source or Object | |||
| form, that is based on (or derived from) the Work and for which the | |||
| editorial revisions, annotations, elaborations, or other modifications | |||
| represent, as a whole, an original work of authorship. For the purposes | |||
| of this License, Derivative Works shall not include works that remain | |||
| separable from, or merely link (or bind by name) to the interfaces of, | |||
| the Work and Derivative Works thereof. | |||
| "Contribution" shall mean any work of authorship, including | |||
| the original version of the Work and any modifications or additions | |||
| to that Work or Derivative Works thereof, that is intentionally | |||
| submitted to Licensor for inclusion in the Work by the copyright owner | |||
| or by an individual or Legal Entity authorized to submit on behalf of | |||
| the copyright owner. For the purposes of this definition, "submitted" | |||
| means any form of electronic, verbal, or written communication sent | |||
| to the Licensor or its representatives, including but not limited to | |||
| communication on electronic mailing lists, source code control systems, | |||
| and issue tracking systems that are managed by, or on behalf of, the | |||
| Licensor for the purpose of discussing and improving the Work, but | |||
| excluding communication that is conspicuously marked or otherwise | |||
| designated in writing by the copyright owner as "Not a Contribution." | |||
| "Contributor" shall mean Licensor and any individual or Legal Entity | |||
| on behalf of whom a Contribution has been received by Licensor and | |||
| subsequently incorporated within the Work. | |||
| 2. Grant of Copyright License. Subject to the terms and conditions of | |||
| this License, each Contributor hereby grants to You a perpetual, | |||
| worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
| copyright license to reproduce, prepare Derivative Works of, | |||
| publicly display, publicly perform, sublicense, and distribute the | |||
| Work and such Derivative Works in Source or Object form. | |||
| 3. Grant of Patent License. Subject to the terms and conditions of | |||
| this License, each Contributor hereby grants to You a perpetual, | |||
| worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |||
| (except as stated in this section) patent license to make, have made, | |||
| use, offer to sell, sell, import, and otherwise transfer the Work, | |||
| where such license applies only to those patent claims licensable | |||
| by such Contributor that are necessarily infringed by their | |||
| Contribution(s) alone or by combination of their Contribution(s) | |||
| with the Work to which such Contribution(s) was submitted. If You | |||
| institute patent litigation against any entity (including a | |||
| cross-claim or counterclaim in a lawsuit) alleging that the Work | |||
| or a Contribution incorporated within the Work constitutes direct | |||
| or contributory patent infringement, then any patent licenses | |||
| granted to You under this License for that Work shall terminate | |||
| as of the date such litigation is filed. | |||
| 4. Redistribution. You may reproduce and distribute copies of the | |||
| Work or Derivative Works thereof in any medium, with or without | |||
| modifications, and in Source or Object form, provided that You | |||
| meet the following conditions: | |||
| (a) You must give any other recipients of the Work or | |||
| Derivative Works a copy of this License; and | |||
| (b) You must cause any modified files to carry prominent notices | |||
| stating that You changed the files; and | |||
| (c) You must retain, in the Source form of any Derivative Works | |||
| that You distribute, all copyright, patent, trademark, and | |||
| attribution notices from the Source form of the Work, | |||
| excluding those notices that do not pertain to any part of | |||
| the Derivative Works; and | |||
| (d) If the Work includes a "NOTICE" text file as part of its | |||
| distribution, then any Derivative Works that You distribute must | |||
| include a readable copy of the attribution notices contained | |||
| within such NOTICE file, excluding those notices that do not | |||
| pertain to any part of the Derivative Works, in at least one | |||
| of the following places: within a NOTICE text file distributed | |||
| as part of the Derivative Works; within the Source form or | |||
| documentation, if provided along with the Derivative Works; or, | |||
| within a display generated by the Derivative Works, if and | |||
| wherever such third-party notices normally appear. The contents | |||
| of the NOTICE file are for informational purposes only and | |||
| do not modify the License. You may add Your own attribution | |||
| notices within Derivative Works that You distribute, alongside | |||
| or as an addendum to the NOTICE text from the Work, provided | |||
| that such additional attribution notices cannot be construed | |||
| as modifying the License. | |||
| You may add Your own copyright statement to Your modifications and | |||
| may provide additional or different license terms and conditions | |||
| for use, reproduction, or distribution of Your modifications, or | |||
| for any such Derivative Works as a whole, provided Your use, | |||
| reproduction, and distribution of the Work otherwise complies with | |||
| the conditions stated in this License. | |||
| 5. Submission of Contributions. Unless You explicitly state otherwise, | |||
| any Contribution intentionally submitted for inclusion in the Work | |||
| by You to the Licensor shall be under the terms and conditions of | |||
| this License, without any additional terms or conditions. | |||
| Notwithstanding the above, nothing herein shall supersede or modify | |||
| the terms of any separate license agreement you may have executed | |||
| with Licensor regarding such Contributions. | |||
| 6. Trademarks. This License does not grant permission to use the trade | |||
| names, trademarks, service marks, or product names of the Licensor, | |||
| except as required for reasonable and customary use in describing the | |||
| origin of the Work and reproducing the content of the NOTICE file. | |||
| 7. Disclaimer of Warranty. Unless required by applicable law or | |||
| agreed to in writing, Licensor provides the Work (and each | |||
| Contributor provides its Contributions) on an "AS IS" BASIS, | |||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||
| implied, including, without limitation, any warranties or conditions | |||
| of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |||
| PARTICULAR PURPOSE. You are solely responsible for determining the | |||
| appropriateness of using or redistributing the Work and assume any | |||
| risks associated with Your exercise of permissions under this License. | |||
| 8. Limitation of Liability. In no event and under no legal theory, | |||
| whether in tort (including negligence), contract, or otherwise, | |||
| unless required by applicable law (such as deliberate and grossly | |||
| negligent acts) or agreed to in writing, shall any Contributor be | |||
| liable to You for damages, including any direct, indirect, special, | |||
| incidental, or consequential damages of any character arising as a | |||
| result of this License or out of the use or inability to use the | |||
| Work (including but not limited to damages for loss of goodwill, | |||
| work stoppage, computer failure or malfunction, or any and all | |||
| other commercial damages or losses), even if such Contributor | |||
| has been advised of the possibility of such damages. | |||
| 9. Accepting Warranty or Additional Liability. While redistributing | |||
| the Work or Derivative Works thereof, You may choose to offer, | |||
| and charge a fee for, acceptance of support, warranty, indemnity, | |||
| or other liability obligations and/or rights consistent with this | |||
| License. However, in accepting such obligations, You may act only | |||
| on Your own behalf and on Your sole responsibility, not on behalf | |||
| of any other Contributor, and only if You agree to indemnify, | |||
| defend, and hold each Contributor harmless for any liability | |||
| incurred by, or claims asserted against, such Contributor by reason | |||
| of your accepting any such warranty or additional liability. | |||
| END OF TERMS AND CONDITIONS | |||
| APPENDIX: How to apply the Apache License to your work. | |||
| To apply the Apache License to your work, attach the following | |||
| boilerplate notice, with the fields enclosed by brackets "{}" | |||
| replaced with your own identifying information. (Don't include | |||
| the brackets!) The text should be enclosed in the appropriate | |||
| comment syntax for the file format. We also recommend that a | |||
| file or class name and description of purpose be included on the | |||
| same "printed page" as the copyright notice for easier | |||
| identification within third-party archives. | |||
| Copyright {yyyy} {name of copyright owner} | |||
| Licensed under the Apache License, Version 2.0 (the "License"); | |||
| you may not use this file except in compliance with the License. | |||
| You may obtain a copy of the License at | |||
| http://www.apache.org/licenses/LICENSE-2.0 | |||
| Unless required by applicable law or agreed to in writing, software | |||
| distributed under the License is distributed on an "AS IS" BASIS, | |||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
| See the License for the specific language governing permissions and | |||
| limitations under the License. | |||
| @@ -19,7 +19,7 @@ weixin-java-tools | |||
| <dependency> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-mp</artifactId> | |||
| <version>1.0.6</version> | |||
| <version>1.0.7</version> | |||
| </dependency> | |||
| ``` | |||
| @@ -29,10 +29,24 @@ weixin-java-tools | |||
| <dependency> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-cp</artifactId> | |||
| <version>1.0.6</version> | |||
| <version>1.0.7</version> | |||
| </dependency> | |||
| ``` | |||
| ## SNAPSHOT版 | |||
| 本项目的BUG修复和新特性一般会先发布在*-SNAPSHOT版里供大家预览,如果要使用*-SNAPSHOT版,则需要在你的pom.xml中添加这段: | |||
| ```xml | |||
| <repositories> | |||
| <repository> | |||
| <snapshots /> | |||
| <id>sonatype snapshots</id> | |||
| <url>https://oss.sonatype.org/content/repositories/snapshots/</url> | |||
| </repository> | |||
| </repositories> | |||
| ``` | |||
| ## 升级指南 | |||
| 原``1.0.0~1.0.2``版本用户无法平滑升级到``1.0.3``。需要做的是: | |||
| @@ -1,229 +1,232 @@ | |||
| <?xml version="1.0"?> | |||
| <project | |||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||
| <modelVersion>4.0.0</modelVersion> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-parent</artifactId> | |||
| <version>1.0.6</version> | |||
| <packaging>pom</packaging> | |||
| <name>WeiXin Java Tools - Parent</name> | |||
| <description>微信公众号、企业号上级POM</description> | |||
| <url>https://github.com/chanjarster/weixin-java-tools</url> | |||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||
| <modelVersion>4.0.0</modelVersion> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-parent</artifactId> | |||
| <version>1.1.0-SNAPSHOT</version> | |||
| <packaging>pom</packaging> | |||
| <name>WeiXin Java Tools - Parent</name> | |||
| <description>微信公众号、企业号上级POM</description> | |||
| <url>https://github.com/chanjarster/weixin-java-tools</url> | |||
| <licenses> | |||
| <license> | |||
| <name>The Apache License, Version 2.0</name> | |||
| <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> | |||
| </license> | |||
| </licenses> | |||
| <licenses> | |||
| <license> | |||
| <name>The Apache License, Version 2.0</name> | |||
| <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> | |||
| </license> | |||
| </licenses> | |||
| <developers> | |||
| <developer> | |||
| <name>Daniel Qian</name> | |||
| <email>chanjarster@gmail.com</email> | |||
| </developer> | |||
| </developers> | |||
| <developers> | |||
| <developer> | |||
| <name>Daniel Qian</name> | |||
| <email>chanjarster@gmail.com</email> | |||
| </developer> | |||
| </developers> | |||
| <scm> | |||
| <connection>scm:git:https://github.com/chanjarster/weixin-java-tools.git</connection> | |||
| <developerConnection>scm:git:git@github.com:chanjarster/weixin-java-tools.git</developerConnection> | |||
| <url>https://github.com/chanjarster/weixin-java-tools</url> | |||
| </scm> | |||
| <scm> | |||
| <connection>scm:git:https://github.com/chanjarster/weixin-java-tools.git</connection> | |||
| <developerConnection>scm:git:git@github.com:chanjarster/weixin-java-tools.git</developerConnection> | |||
| <url>https://github.com/chanjarster/weixin-java-tools</url> | |||
| </scm> | |||
| <modules> | |||
| <module>weixin-java-common</module> | |||
| <module>weixin-java-cp</module> | |||
| <module>weixin-java-mp</module> | |||
| </modules> | |||
| <modules> | |||
| <module>weixin-java-common</module> | |||
| <module>weixin-java-cp</module> | |||
| <module>weixin-java-mp</module> | |||
| </modules> | |||
| <properties> | |||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||
| <downloadJavadocs>true</downloadJavadocs> | |||
| <downloadSources>true</downloadSources> | |||
| <httpclient.version>4.3.5</httpclient.version> | |||
| <slf4j.version>1.7.10</slf4j.version> | |||
| <logback.version>1.1.2</logback.version> | |||
| </properties> | |||
| <properties> | |||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||
| <downloadJavadocs>true</downloadJavadocs> | |||
| <downloadSources>true</downloadSources> | |||
| <httpclient.version>4.3.5</httpclient.version> | |||
| </properties> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>org.slf4j</groupId> | |||
| <artifactId>slf4j-api</artifactId> | |||
| <version>${slf4j.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>ch.qos.logback</groupId> | |||
| <artifactId>logback-classic</artifactId> | |||
| <version>${logback.version}</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.apache.httpcomponents</groupId> | |||
| <artifactId>fluent-hc</artifactId> | |||
| <version>${httpclient.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.apache.httpcomponents</groupId> | |||
| <artifactId>httpmime</artifactId> | |||
| <version>${httpclient.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.code.gson</groupId> | |||
| <artifactId>gson</artifactId> | |||
| <version>2.2.2</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>commons-codec</groupId> | |||
| <artifactId>commons-codec</artifactId> | |||
| <version>1.9</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>commons-io</groupId> | |||
| <artifactId>commons-io</artifactId> | |||
| <version>2.4</version> | |||
| </dependency> | |||
| </dependencies> | |||
| <dependencyManagement> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>org.apache.httpcomponents</groupId> | |||
| <artifactId>fluent-hc</artifactId> | |||
| <version>${httpclient.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.apache.httpcomponents</groupId> | |||
| <artifactId>httpmime</artifactId> | |||
| <version>${httpclient.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>javax.xml.bind</groupId> | |||
| <artifactId>jaxb-api</artifactId> | |||
| <version>2.2.7</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.sun.xml.bind</groupId> | |||
| <artifactId>jaxb-impl</artifactId> | |||
| <version>2.2.7</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.code.gson</groupId> | |||
| <artifactId>gson</artifactId> | |||
| <version>2.2.2</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>commons-codec</groupId> | |||
| <artifactId>commons-codec</artifactId> | |||
| <version>1.9</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>commons-io</groupId> | |||
| <artifactId>commons-io</artifactId> | |||
| <version>2.4</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>junit</groupId> | |||
| <artifactId>junit</artifactId> | |||
| <version>4.11</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.inject</groupId> | |||
| <artifactId>guice</artifactId> | |||
| <version>3.0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.testng</groupId> | |||
| <artifactId>testng</artifactId> | |||
| <version>6.8.7</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.mockito</groupId> | |||
| <artifactId>mockito-all</artifactId> | |||
| <version>1.9.5</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-server</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-servlet</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| </dependencies> | |||
| </dependencyManagement> | |||
| <dependencyManagement> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>junit</groupId> | |||
| <artifactId>junit</artifactId> | |||
| <version>4.11</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.inject</groupId> | |||
| <artifactId>guice</artifactId> | |||
| <version>3.0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.testng</groupId> | |||
| <artifactId>testng</artifactId> | |||
| <version>6.8.7</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.mockito</groupId> | |||
| <artifactId>mockito-all</artifactId> | |||
| <version>1.9.5</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-server</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-servlet</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| </dependencies> | |||
| </dependencyManagement> | |||
| <distributionManagement> | |||
| <snapshotRepository> | |||
| <id>ossrh</id> | |||
| <url>https://oss.sonatype.org/content/repositories/snapshots</url> | |||
| </snapshotRepository> | |||
| <repository> | |||
| <id>ossrh</id> | |||
| <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> | |||
| </repository> | |||
| </distributionManagement> | |||
| <profiles> | |||
| <profile> | |||
| <id>release</id> | |||
| <build> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-source-plugin</artifactId> | |||
| <version>2.2.1</version> | |||
| <executions> | |||
| <execution> | |||
| <id>attach-sources</id> | |||
| <goals> | |||
| <goal>jar-no-fork</goal> | |||
| </goals> | |||
| </execution> | |||
| </executions> | |||
| </plugin> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-javadoc-plugin</artifactId> | |||
| <version>2.9.1</version> | |||
| <executions> | |||
| <execution> | |||
| <id>attach-javadocs</id> | |||
| <goals> | |||
| <goal>jar</goal> | |||
| </goals> | |||
| </execution> | |||
| </executions> | |||
| <configuration> | |||
| <charset>UTF-8</charset> | |||
| <locale>zh_CN</locale> | |||
| </configuration> | |||
| </plugin> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-gpg-plugin</artifactId> | |||
| <version>1.5</version> | |||
| <executions> | |||
| <execution> | |||
| <id>sign-artifacts</id> | |||
| <phase>verify</phase> | |||
| <goals> | |||
| <goal>sign</goal> | |||
| </goals> | |||
| </execution> | |||
| </executions> | |||
| </plugin> | |||
| </plugins> | |||
| </build> | |||
| </profile> | |||
| </profiles> | |||
| <build> | |||
| <pluginManagement> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-surefire-plugin</artifactId> | |||
| <version>2.17</version> | |||
| </plugin> | |||
| </plugins> | |||
| </pluginManagement> | |||
| <distributionManagement> | |||
| <snapshotRepository> | |||
| <id>ossrh</id> | |||
| <url>https://oss.sonatype.org/content/repositories/snapshots</url> | |||
| </snapshotRepository> | |||
| <repository> | |||
| <id>ossrh</id> | |||
| <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> | |||
| </repository> | |||
| </distributionManagement> | |||
| <profiles> | |||
| <profile> | |||
| <id>release</id> | |||
| <build> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.sonatype.plugins</groupId> | |||
| <artifactId>nexus-staging-maven-plugin</artifactId> | |||
| <version>1.6.3</version> | |||
| <extensions>true</extensions> | |||
| <configuration> | |||
| <serverId>ossrh</serverId> | |||
| <nexusUrl>https://oss.sonatype.org/</nexusUrl> | |||
| <autoReleaseAfterClose>false</autoReleaseAfterClose> | |||
| </configuration> | |||
| </plugin> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-release-plugin</artifactId> | |||
| <version>2.5</version> | |||
| <configuration> | |||
| <autoVersionSubmodules>true</autoVersionSubmodules> | |||
| <useReleaseProfile>false</useReleaseProfile> | |||
| <releaseProfiles>release</releaseProfiles> | |||
| <goals>deploy</goals> | |||
| </configuration> | |||
| </plugin> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-source-plugin</artifactId> | |||
| <version>2.2.1</version> | |||
| <executions> | |||
| <execution> | |||
| <id>attach-sources</id> | |||
| <goals> | |||
| <goal>jar-no-fork</goal> | |||
| </goals> | |||
| </execution> | |||
| </executions> | |||
| </plugin> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-javadoc-plugin</artifactId> | |||
| <version>2.9.1</version> | |||
| <executions> | |||
| <execution> | |||
| <id>attach-javadocs</id> | |||
| <goals> | |||
| <goal>jar</goal> | |||
| </goals> | |||
| </execution> | |||
| </executions> | |||
| <configuration> | |||
| <charset>UTF-8</charset> | |||
| <locale>zh_CN</locale> | |||
| </configuration> | |||
| </plugin> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-gpg-plugin</artifactId> | |||
| <version>1.5</version> | |||
| <executions> | |||
| <execution> | |||
| <id>sign-artifacts</id> | |||
| <phase>verify</phase> | |||
| <goals> | |||
| <goal>sign</goal> | |||
| </goals> | |||
| </execution> | |||
| </executions> | |||
| </plugin> | |||
| </plugins> | |||
| </build> | |||
| </build> | |||
| </profile> | |||
| </profiles> | |||
| <build> | |||
| <pluginManagement> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-surefire-plugin</artifactId> | |||
| <version>2.17</version> | |||
| </plugin> | |||
| </plugins> | |||
| </pluginManagement> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.sonatype.plugins</groupId> | |||
| <artifactId>nexus-staging-maven-plugin</artifactId> | |||
| <version>1.6.3</version> | |||
| <extensions>true</extensions> | |||
| <configuration> | |||
| <serverId>ossrh</serverId> | |||
| <nexusUrl>https://oss.sonatype.org/</nexusUrl> | |||
| <autoReleaseAfterClose>false</autoReleaseAfterClose> | |||
| </configuration> | |||
| </plugin> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-release-plugin</artifactId> | |||
| <version>2.5</version> | |||
| <configuration> | |||
| <autoVersionSubmodules>true</autoVersionSubmodules> | |||
| <useReleaseProfile>false</useReleaseProfile> | |||
| <releaseProfiles>release</releaseProfiles> | |||
| <goals>deploy</goals> | |||
| </configuration> | |||
| </plugin> | |||
| </plugins> | |||
| </build> | |||
| </project> | |||
| @@ -1,229 +0,0 @@ | |||
| <?xml version="1.0"?> | |||
| <project | |||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||
| <modelVersion>4.0.0</modelVersion> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-parent</artifactId> | |||
| <version>1.0.5-SNAPSHOT</version> | |||
| <packaging>pom</packaging> | |||
| <name>WeiXin Java Tools - Parent</name> | |||
| <description>微信公众号、企业号上级POM</description> | |||
| <url>https://github.com/chanjarster/weixin-java-tools</url> | |||
| <licenses> | |||
| <license> | |||
| <name>The Apache License, Version 2.0</name> | |||
| <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> | |||
| </license> | |||
| </licenses> | |||
| <developers> | |||
| <developer> | |||
| <name>Daniel Qian</name> | |||
| <email>chanjarster@gmail.com</email> | |||
| </developer> | |||
| </developers> | |||
| <scm> | |||
| <connection>scm:git:https://github.com/chanjarster/weixin-java-tools.git</connection> | |||
| <developerConnection>scm:git:git@github.com:chanjarster/weixin-java-tools.git</developerConnection> | |||
| <url>https://github.com/chanjarster/weixin-java-tools</url> | |||
| </scm> | |||
| <modules> | |||
| <module>weixin-java-common</module> | |||
| <module>weixin-java-cp</module> | |||
| <module>weixin-java-mp</module> | |||
| </modules> | |||
| <properties> | |||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||
| <downloadJavadocs>true</downloadJavadocs> | |||
| <downloadSources>true</downloadSources> | |||
| <httpclient.version>4.3.5</httpclient.version> | |||
| </properties> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>org.apache.httpcomponents</groupId> | |||
| <artifactId>fluent-hc</artifactId> | |||
| <version>${httpclient.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.apache.httpcomponents</groupId> | |||
| <artifactId>httpmime</artifactId> | |||
| <version>${httpclient.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>javax.xml.bind</groupId> | |||
| <artifactId>jaxb-api</artifactId> | |||
| <version>2.2.7</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.sun.xml.bind</groupId> | |||
| <artifactId>jaxb-impl</artifactId> | |||
| <version>2.2.7</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.code.gson</groupId> | |||
| <artifactId>gson</artifactId> | |||
| <version>2.2.2</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>commons-codec</groupId> | |||
| <artifactId>commons-codec</artifactId> | |||
| <version>1.9</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>commons-io</groupId> | |||
| <artifactId>commons-io</artifactId> | |||
| <version>2.4</version> | |||
| </dependency> | |||
| </dependencies> | |||
| <dependencyManagement> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>junit</groupId> | |||
| <artifactId>junit</artifactId> | |||
| <version>4.11</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.inject</groupId> | |||
| <artifactId>guice</artifactId> | |||
| <version>3.0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.testng</groupId> | |||
| <artifactId>testng</artifactId> | |||
| <version>6.8.7</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.mockito</groupId> | |||
| <artifactId>mockito-all</artifactId> | |||
| <version>1.9.5</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-server</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-servlet</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| </dependencies> | |||
| </dependencyManagement> | |||
| <distributionManagement> | |||
| <snapshotRepository> | |||
| <id>ossrh</id> | |||
| <url>https://oss.sonatype.org/content/repositories/snapshots</url> | |||
| </snapshotRepository> | |||
| <repository> | |||
| <id>ossrh</id> | |||
| <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url> | |||
| </repository> | |||
| </distributionManagement> | |||
| <profiles> | |||
| <profile> | |||
| <id>release</id> | |||
| <build> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-source-plugin</artifactId> | |||
| <version>2.2.1</version> | |||
| <executions> | |||
| <execution> | |||
| <id>attach-sources</id> | |||
| <goals> | |||
| <goal>jar-no-fork</goal> | |||
| </goals> | |||
| </execution> | |||
| </executions> | |||
| </plugin> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-javadoc-plugin</artifactId> | |||
| <version>2.9.1</version> | |||
| <executions> | |||
| <execution> | |||
| <id>attach-javadocs</id> | |||
| <goals> | |||
| <goal>jar</goal> | |||
| </goals> | |||
| </execution> | |||
| </executions> | |||
| <configuration> | |||
| <charset>UTF-8</charset> | |||
| <locale>zh_CN</locale> | |||
| </configuration> | |||
| </plugin> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-gpg-plugin</artifactId> | |||
| <version>1.5</version> | |||
| <executions> | |||
| <execution> | |||
| <id>sign-artifacts</id> | |||
| <phase>verify</phase> | |||
| <goals> | |||
| <goal>sign</goal> | |||
| </goals> | |||
| </execution> | |||
| </executions> | |||
| </plugin> | |||
| </plugins> | |||
| </build> | |||
| </profile> | |||
| </profiles> | |||
| <build> | |||
| <pluginManagement> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-surefire-plugin</artifactId> | |||
| <version>2.17</version> | |||
| </plugin> | |||
| </plugins> | |||
| </pluginManagement> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.sonatype.plugins</groupId> | |||
| <artifactId>nexus-staging-maven-plugin</artifactId> | |||
| <version>1.6.3</version> | |||
| <extensions>true</extensions> | |||
| <configuration> | |||
| <serverId>ossrh</serverId> | |||
| <nexusUrl>https://oss.sonatype.org/</nexusUrl> | |||
| <autoReleaseAfterClose>false</autoReleaseAfterClose> | |||
| </configuration> | |||
| </plugin> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-release-plugin</artifactId> | |||
| <version>2.5</version> | |||
| <configuration> | |||
| <autoVersionSubmodules>true</autoVersionSubmodules> | |||
| <useReleaseProfile>false</useReleaseProfile> | |||
| <releaseProfiles>release</releaseProfiles> | |||
| <goals>deploy</goals> | |||
| </configuration> | |||
| </plugin> | |||
| </plugins> | |||
| </build> | |||
| </project> | |||
| @@ -1,68 +1,73 @@ | |||
| <?xml version="1.0"?> | |||
| <project | |||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||
| <modelVersion>4.0.0</modelVersion> | |||
| <parent> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-parent</artifactId> | |||
| <version>1.0.6</version> | |||
| </parent> | |||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||
| <modelVersion>4.0.0</modelVersion> | |||
| <parent> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-parent</artifactId> | |||
| <version>1.1.0-SNAPSHOT</version> | |||
| </parent> | |||
| <artifactId>weixin-java-common</artifactId> | |||
| <name>WeiXin Java Tools - Common</name> | |||
| <description>微信公众号、企业号Java SDK Common</description> | |||
| <artifactId>weixin-java-common</artifactId> | |||
| <name>WeiXin Java Tools - Common</name> | |||
| <description>微信公众号、企业号Java SDK Common</description> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>junit</groupId> | |||
| <artifactId>junit</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.testng</groupId> | |||
| <artifactId>testng</artifactId> | |||
| <version>6.8.7</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.mockito</groupId> | |||
| <artifactId>mockito-all</artifactId> | |||
| <version>1.9.5</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.inject</groupId> | |||
| <artifactId>guice</artifactId> | |||
| <version>3.0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-server</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-servlet</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| </dependencies> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>com.thoughtworks.xstream</groupId> | |||
| <artifactId>xstream</artifactId> | |||
| <version>1.4.7</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>junit</groupId> | |||
| <artifactId>junit</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.testng</groupId> | |||
| <artifactId>testng</artifactId> | |||
| <version>6.8.7</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.mockito</groupId> | |||
| <artifactId>mockito-all</artifactId> | |||
| <version>1.9.5</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.inject</groupId> | |||
| <artifactId>guice</artifactId> | |||
| <version>3.0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-server</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-servlet</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| </dependencies> | |||
| <build> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-surefire-plugin</artifactId> | |||
| <configuration> | |||
| <suiteXmlFiles> | |||
| <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile> | |||
| </suiteXmlFiles> | |||
| </configuration> | |||
| </plugin> | |||
| </plugins> | |||
| </build> | |||
| <build> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-surefire-plugin</artifactId> | |||
| <configuration> | |||
| <suiteXmlFiles> | |||
| <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile> | |||
| </suiteXmlFiles> | |||
| </configuration> | |||
| </plugin> | |||
| </plugins> | |||
| </build> | |||
| </project> | |||
| @@ -1,68 +0,0 @@ | |||
| <?xml version="1.0"?> | |||
| <project | |||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||
| <modelVersion>4.0.0</modelVersion> | |||
| <parent> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-parent</artifactId> | |||
| <version>1.0.5-SNAPSHOT</version> | |||
| </parent> | |||
| <artifactId>weixin-java-common</artifactId> | |||
| <name>WeiXin Java Tools - Common</name> | |||
| <description>微信公众号、企业号Java SDK Common</description> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>junit</groupId> | |||
| <artifactId>junit</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.testng</groupId> | |||
| <artifactId>testng</artifactId> | |||
| <version>6.8.7</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.mockito</groupId> | |||
| <artifactId>mockito-all</artifactId> | |||
| <version>1.9.5</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.inject</groupId> | |||
| <artifactId>guice</artifactId> | |||
| <version>3.0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-server</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-servlet</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| </dependencies> | |||
| <build> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-surefire-plugin</artifactId> | |||
| <configuration> | |||
| <suiteXmlFiles> | |||
| <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile> | |||
| </suiteXmlFiles> | |||
| </configuration> | |||
| </plugin> | |||
| </plugins> | |||
| </build> | |||
| </project> | |||
| @@ -0,0 +1,31 @@ | |||
| /* | |||
| * Licensed to the Apache Software Foundation (ASF) under one or more | |||
| * contributor license agreements. See the NOTICE file distributed with | |||
| * this work for additional information regarding copyright ownership. | |||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | |||
| * (the "License"); you may not use this file except in compliance with | |||
| * the License. You may obtain a copy of the License at | |||
| * | |||
| * http://www.apache.org/licenses/LICENSE-2.0 | |||
| * | |||
| * Unless required by applicable law or agreed to in writing, software | |||
| * distributed under the License is distributed on an "AS IS" BASIS, | |||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
| * See the License for the specific language governing permissions and | |||
| * limitations under the License. | |||
| */ | |||
| package me.chanjar.weixin.common.session; | |||
| /** | |||
| * Manifest constants for the <code>org.apache.catalina.session</code> | |||
| * package. | |||
| * | |||
| * @author Craig R. McClanahan | |||
| */ | |||
| public class Constants { | |||
| public static final String Package = "me.chanjar.weixin.common.session"; | |||
| } | |||
| @@ -0,0 +1,68 @@ | |||
| package me.chanjar.weixin.common.session; | |||
| public interface InternalSession { | |||
| /** | |||
| * Return the <code>HttpSession</code> for which this object | |||
| * is the facade. | |||
| */ | |||
| WxSession getSession(); | |||
| /** | |||
| * Set the <code>isValid</code> flag for this session. | |||
| * | |||
| * @param isValid The new value for the <code>isValid</code> flag | |||
| */ | |||
| public void setValid(boolean isValid); | |||
| /** | |||
| * Return the <code>isValid</code> flag for this session. | |||
| */ | |||
| boolean isValid(); | |||
| /** | |||
| * Return the session identifier for this session. | |||
| */ | |||
| String getIdInternal(); | |||
| /** | |||
| * Perform the internal processing required to invalidate this session, | |||
| * without triggering an exception if the session has already expired. | |||
| */ | |||
| void expire(); | |||
| /** | |||
| * Update the accessed time information for this session. This method | |||
| * should be called by the context when a request comes in for a particular | |||
| * session, even if the application does not reference it. | |||
| */ | |||
| void access(); | |||
| /** | |||
| * End the access. | |||
| */ | |||
| void endAccess(); | |||
| /** | |||
| * Set the creation time for this session. This method is called by the | |||
| * Manager when an existing Session instance is reused. | |||
| * | |||
| * @param time The new creation time | |||
| */ | |||
| void setCreationTime(long time); | |||
| /** | |||
| * Set the default maximum inactive interval (in seconds) | |||
| * for Sessions created by this Manager. | |||
| * | |||
| * @param interval The new default value | |||
| */ | |||
| void setMaxInactiveInterval(int interval); | |||
| /** | |||
| * Set the session identifier for this session. | |||
| * | |||
| * @param id The new session identifier | |||
| */ | |||
| void setId(String id); | |||
| } | |||
| @@ -0,0 +1,113 @@ | |||
| package me.chanjar.weixin.common.session; | |||
| public interface InternalSessionManager { | |||
| /** | |||
| * Return the active Session, associated with this Manager, with the | |||
| * specified session id (if any); otherwise return <code>null</code>. | |||
| * | |||
| * @param id The session id for the session to be returned | |||
| * | |||
| * @exception IllegalStateException if a new session cannot be | |||
| * instantiated for any reason | |||
| * @exception java.io.IOException if an input/output error occurs while | |||
| * processing this request | |||
| */ | |||
| InternalSession findSession(String id); | |||
| /** | |||
| * Construct and return a new session object, based on the default | |||
| * settings specified by this Manager's properties. The session | |||
| * id specified will be used as the session id. | |||
| * If a new session cannot be created for any reason, return | |||
| * <code>null</code>. | |||
| * | |||
| * @param sessionId The session id which should be used to create the | |||
| * new session; if <code>null</code>, a new session id will be | |||
| * generated | |||
| * @exception IllegalStateException if a new session cannot be | |||
| * instantiated for any reason | |||
| */ | |||
| public InternalSession createSession(String sessionId); | |||
| /** | |||
| * Remove this Session from the active Sessions for this Manager. | |||
| * | |||
| * @param session Session to be removed | |||
| */ | |||
| public void remove(InternalSession session); | |||
| /** | |||
| * Remove this Session from the active Sessions for this Manager. | |||
| * | |||
| * @param session Session to be removed | |||
| * @param update Should the expiration statistics be updated | |||
| */ | |||
| public void remove(InternalSession session, boolean update); | |||
| /** | |||
| * Add this Session to the set of active Sessions for this Manager. | |||
| * | |||
| * @param session Session to be added | |||
| */ | |||
| void add(InternalSession session); | |||
| /** | |||
| * Returns the number of active sessions | |||
| * | |||
| * @return number of sessions active | |||
| */ | |||
| int getActiveSessions(); | |||
| /** | |||
| * Get a session from the recycled ones or create a new empty one. | |||
| * The PersistentManager manager does not need to create session data | |||
| * because it reads it from the Store. | |||
| */ | |||
| InternalSession createEmptySession(); | |||
| InternalSession[] findSessions(); | |||
| /** | |||
| * Implements the Manager interface, direct call to processExpires | |||
| */ | |||
| public void backgroundProcess(); | |||
| /** | |||
| * Set the default maximum inactive interval (in seconds) | |||
| * for Sessions created by this Manager. | |||
| * | |||
| * @param interval The new default value | |||
| */ | |||
| void setMaxInactiveInterval(int interval); | |||
| /** | |||
| * <pre> | |||
| * Set the manager checks frequency. | |||
| * 设置每尝试多少次清理过期session,才真的会执行一次清理动作 | |||
| * 要和{@link #setBackgroundProcessorDelay(int)}联合起来看 | |||
| * 如果把这个数字设置为6(默认),那么就是说manager要等待 6 * backgroundProcessorDay的时间才会清理过期session | |||
| * </pre> | |||
| * @param processExpiresFrequency the new manager checks frequency | |||
| */ | |||
| void setProcessExpiresFrequency(int processExpiresFrequency); | |||
| /** | |||
| * <pre> | |||
| * Set the manager background processor delay | |||
| * 设置manager sleep几秒,尝试执行一次background操作(清理过期session) | |||
| * </pre> | |||
| * @param backgroundProcessorDelay | |||
| */ | |||
| void setBackgroundProcessorDelay(int backgroundProcessorDelay); | |||
| /** | |||
| * Set the maximum number of active Sessions allowed, or -1 for | |||
| * no limit. | |||
| * 设置最大活跃session数,默认无限 | |||
| * @param max The new maximum number of sessions | |||
| */ | |||
| void setMaxActiveSessions(int max); | |||
| } | |||
| @@ -0,0 +1,80 @@ | |||
| # Licensed to the Apache Software Foundation (ASF) under one or more | |||
| # contributor license agreements. See the NOTICE file distributed with | |||
| # this work for additional information regarding copyright ownership. | |||
| # The ASF licenses this file to You under the Apache License, Version 2.0 | |||
| # (the "License"); you may not use this file except in compliance with | |||
| # the License. You may obtain a copy of the License at | |||
| # | |||
| # http://www.apache.org/licenses/LICENSE-2.0 | |||
| # | |||
| # Unless required by applicable law or agreed to in writing, software | |||
| # distributed under the License is distributed on an "AS IS" BASIS, | |||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
| # See the License for the specific language governing permissions and | |||
| # limitations under the License. | |||
| applicationSession.session.ise=invalid session state | |||
| applicationSession.value.iae=null value | |||
| fileStore.saving=Saving Session {0} to file {1} | |||
| fileStore.loading=Loading Session {0} from file {1} | |||
| fileStore.removing=Removing Session {0} at file {1} | |||
| fileStore.deleteFailed=Unable to delete file [{0}] which is preventing the creation of the session storage location | |||
| fileStore.createFailed=Unable to create directory [{0}] for the storage of session data | |||
| JDBCStore.close=Exception closing database connection {0} | |||
| JDBCStore.saving=Saving Session {0} to database {1} | |||
| JDBCStore.loading=Loading Session {0} from database {1} | |||
| JDBCStore.removing=Removing Session {0} at database {1} | |||
| JDBCStore.SQLException=SQL Error {0} | |||
| serverSession.value.iae=null value | |||
| sessionManagerImpl.createRandom=Created random number generator for session ID generation in {0}ms. | |||
| sessionManagerImpl.createSession.tmase=createSession: Too many active sessions | |||
| sessionManagerImpl.sessionTimeout=Invalid session timeout setting {0} | |||
| sessionManagerImpl.getSession.ise=getSession: Session id cannot be null | |||
| sessionManagerImpl.expireException=processsExpire: Exception during session expiration | |||
| sessionManagerImpl.loading=Loading persisted sessions from {0} | |||
| sessionManagerImpl.loading.cnfe=ClassNotFoundException while loading persisted sessions: {0} | |||
| sessionManagerImpl.loading.ioe=IOException while loading persisted sessions: {0} | |||
| sessionManagerImpl.unloading=Saving persisted sessions to {0} | |||
| sessionManagerImpl.unloading.debug=Unloading persisted sessions | |||
| sessionManagerImpl.unloading.ioe=IOException while saving persisted sessions: {0} | |||
| sessionManagerImpl.unloading.nosessions=No persisted sessions to unload | |||
| sessionManagerImpl.managerLoad=Exception loading sessions from persistent storage | |||
| sessionManagerImpl.managerUnload=Exception unloading sessions to persistent storage | |||
| sessionManagerImpl.createSession.ise=createSession: Session id cannot be null | |||
| sessionImpl.attributeEvent=Session attribute event listener threw exception | |||
| sessionImpl.bindingEvent=Session binding event listener threw exception | |||
| sessionImpl.invalidate.ise=invalidate: Session already invalidated | |||
| sessionImpl.isNew.ise=isNew: Session already invalidated | |||
| sessionImpl.getAttribute.ise=getAttribute: Session already invalidated | |||
| sessionImpl.getAttributeNames.ise=getAttributeNames: Session already invalidated | |||
| sessionImpl.getCreationTime.ise=getCreationTime: Session already invalidated | |||
| sessionImpl.getThisAccessedTime.ise=getThisAccessedTime: Session already invalidated | |||
| sessionImpl.getLastAccessedTime.ise=getLastAccessedTime: Session already invalidated | |||
| sessionImpl.getId.ise=getId: Session already invalidated | |||
| sessionImpl.getMaxInactiveInterval.ise=getMaxInactiveInterval: Session already invalidated | |||
| sessionImpl.getValueNames.ise=getValueNames: Session already invalidated | |||
| sessionImpl.logoutfail=Exception logging out user when expiring session | |||
| sessionImpl.notSerializable=Cannot serialize session attribute {0} for session {1} | |||
| sessionImpl.removeAttribute.ise=removeAttribute: Session already invalidated | |||
| sessionImpl.sessionEvent=Session event listener threw exception | |||
| sessionImpl.setAttribute.iae=setAttribute: Non-serializable attribute {0} | |||
| sessionImpl.setAttribute.ise=setAttribute: Session [{0}] has already been invalidated | |||
| sessionImpl.setAttribute.namenull=setAttribute: name parameter cannot be null | |||
| sessionImpl.sessionCreated=Created Session id = {0} | |||
| persistentManager.loading=Loading {0} persisted sessions | |||
| persistentManager.unloading=Saving {0} persisted sessions | |||
| persistentManager.expiring=Expiring {0} sessions before saving them | |||
| persistentManager.deserializeError=Error deserializing Session {0}: {1} | |||
| persistentManager.serializeError=Error serializing Session {0}: {1} | |||
| persistentManager.swapMaxIdle=Swapping session {0} to Store, idle for {1} seconds | |||
| persistentManager.backupMaxIdle=Backing up session {0} to Store, idle for {1} seconds | |||
| persistentManager.backupException=Exception occurred when backing up Session {0}: {1} | |||
| persistentManager.tooManyActive=Too many active sessions, {0}, looking for idle sessions to swap out | |||
| persistentManager.swapTooManyActive=Swapping out session {0}, idle for {1} seconds too many sessions active | |||
| persistentManager.processSwaps=Checking for sessions to swap out, {0} active sessions in memory | |||
| persistentManager.activeSession=Session {0} has been idle for {1} seconds | |||
| persistentManager.swapIn=Swapping session {0} in from Store | |||
| persistentManager.swapInException=Exception in the Store during swapIn: {0} | |||
| persistentManager.swapInInvalid=Swapped session {0} is invalid | |||
| persistentManager.storeKeysException=Unable to determine the list of session IDs for sessions in the session store, assuming that the store is empty | |||
| persistentManager.storeSizeException=Unable to determine the number of sessions in the session store, assuming that the store is empty | |||
| @@ -0,0 +1,346 @@ | |||
| package me.chanjar.weixin.common.session; | |||
| import me.chanjar.weixin.common.util.res.StringManager; | |||
| import java.util.*; | |||
| import java.util.concurrent.ConcurrentHashMap; | |||
| import java.util.concurrent.atomic.AtomicInteger; | |||
| public class StandardSession implements WxSession, InternalSession { | |||
| /** | |||
| * The string manager for this package. | |||
| */ | |||
| protected static final StringManager sm = | |||
| StringManager.getManager(Constants.Package); | |||
| // ------------------------------ WxSession | |||
| protected Map<String, Object> attributes = new ConcurrentHashMap<String, Object>(); | |||
| @Override | |||
| public Object getAttribute(String name) { | |||
| if (!isValidInternal()) | |||
| throw new IllegalStateException | |||
| (sm.getString("sessionImpl.getAttribute.ise")); | |||
| if (name == null) return null; | |||
| return (attributes.get(name)); | |||
| } | |||
| @Override | |||
| public Enumeration<String> getAttributeNames() { | |||
| if (!isValidInternal()) | |||
| throw new IllegalStateException | |||
| (sm.getString("sessionImpl.getAttributeNames.ise")); | |||
| Set<String> names = new HashSet<String>(); | |||
| names.addAll(attributes.keySet()); | |||
| return Collections.enumeration(names); | |||
| } | |||
| @Override | |||
| public void setAttribute(String name, Object value) { | |||
| // Name cannot be null | |||
| if (name == null) | |||
| throw new IllegalArgumentException | |||
| (sm.getString("sessionImpl.setAttribute.namenull")); | |||
| // Null value is the same as removeAttribute() | |||
| if (value == null) { | |||
| removeAttribute(name); | |||
| return; | |||
| } | |||
| // Validate our current state | |||
| if (!isValidInternal()) | |||
| throw new IllegalStateException(sm.getString( | |||
| "sessionImpl.setAttribute.ise", getIdInternal())); | |||
| attributes.put(name, value); | |||
| } | |||
| @Override | |||
| public void removeAttribute(String name) { | |||
| removeAttributeInternal(name); | |||
| } | |||
| @Override | |||
| public void invalidate() { | |||
| if (!isValidInternal()) | |||
| throw new IllegalStateException | |||
| (sm.getString("sessionImpl.invalidate.ise")); | |||
| // Cause this session to expire | |||
| expire(); | |||
| } | |||
| // ------------------------------ InternalSession | |||
| /** | |||
| * The session identifier of this Session. | |||
| */ | |||
| protected String id = null; | |||
| /** | |||
| * Flag indicating whether this session is valid or not. | |||
| */ | |||
| protected volatile boolean isValid = false; | |||
| /** | |||
| * We are currently processing a session expiration, so bypass | |||
| * certain IllegalStateException tests. NOTE: This value is not | |||
| * included in the serialized version of this object. | |||
| */ | |||
| protected transient volatile boolean expiring = false; | |||
| /** | |||
| * The Manager with which this Session is associated. | |||
| */ | |||
| protected transient InternalSessionManager manager = null; | |||
| /** | |||
| * Type array. | |||
| */ | |||
| protected static final String EMPTY_ARRAY[] = new String[0]; | |||
| /** | |||
| * The time this session was created, in milliseconds since midnight, | |||
| * January 1, 1970 GMT. | |||
| */ | |||
| protected long creationTime = 0L; | |||
| /** | |||
| * The current accessed time for this session. | |||
| */ | |||
| protected volatile long thisAccessedTime = creationTime; | |||
| /** | |||
| * The default maximum inactive interval for Sessions created by | |||
| * this Manager. | |||
| */ | |||
| protected int maxInactiveInterval = 30 * 60; | |||
| /** | |||
| * The facade associated with this session. NOTE: This value is not | |||
| * included in the serialized version of this object. | |||
| */ | |||
| protected transient StandardSessionFacade facade = null; | |||
| /** | |||
| * The access count for this session. | |||
| */ | |||
| protected transient AtomicInteger accessCount = null; | |||
| public StandardSession(InternalSessionManager manager) { | |||
| this.manager = manager; | |||
| this.accessCount = new AtomicInteger(); | |||
| } | |||
| @Override | |||
| public WxSession getSession() { | |||
| if (facade == null){ | |||
| facade = new StandardSessionFacade(this); | |||
| } | |||
| return (facade); | |||
| } | |||
| /** | |||
| * Return the <code>isValid</code> flag for this session without any expiration | |||
| * check. | |||
| */ | |||
| protected boolean isValidInternal() { | |||
| return this.isValid; | |||
| } | |||
| /** | |||
| * Set the <code>isValid</code> flag for this session. | |||
| * | |||
| * @param isValid The new value for the <code>isValid</code> flag | |||
| */ | |||
| @Override | |||
| public void setValid(boolean isValid) { | |||
| this.isValid = isValid; | |||
| } | |||
| @Override | |||
| public boolean isValid() { | |||
| if (!this.isValid) { | |||
| return false; | |||
| } | |||
| if (this.expiring) { | |||
| return true; | |||
| } | |||
| if (accessCount.get() > 0) { | |||
| return true; | |||
| } | |||
| if (maxInactiveInterval > 0) { | |||
| long timeNow = System.currentTimeMillis(); | |||
| int timeIdle; | |||
| timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L); | |||
| if (timeIdle >= maxInactiveInterval) { | |||
| expire(); | |||
| } | |||
| } | |||
| return this.isValid; | |||
| } | |||
| @Override | |||
| public String getIdInternal() { | |||
| return (this.id); | |||
| } | |||
| protected void removeAttributeInternal(String name) { | |||
| // Avoid NPE | |||
| if (name == null) return; | |||
| // Remove this attribute from our collection | |||
| attributes.remove(name); | |||
| } | |||
| @Override | |||
| public void expire() { | |||
| // Check to see if session has already been invalidated. | |||
| // Do not check expiring at this point as expire should not return until | |||
| // isValid is false | |||
| if (!isValid) | |||
| return; | |||
| synchronized (this) { | |||
| // Check again, now we are inside the sync so this code only runs once | |||
| // Double check locking - isValid needs to be volatile | |||
| // The check of expiring is to ensure that an infinite loop is not | |||
| // entered as per bug 56339 | |||
| if (expiring || !isValid) | |||
| return; | |||
| if (manager == null) | |||
| return; | |||
| // Mark this session as "being expired" | |||
| expiring = true; | |||
| accessCount.set(0); | |||
| // Remove this session from our manager's active sessions | |||
| manager.remove(this, true); | |||
| // We have completed expire of this session | |||
| setValid(false); | |||
| expiring = false; | |||
| // Unbind any objects associated with this session | |||
| String keys[] = keys(); | |||
| for (int i = 0; i < keys.length; i++) { | |||
| removeAttributeInternal(keys[i]); | |||
| } | |||
| } | |||
| } | |||
| @Override | |||
| public void access() { | |||
| this.thisAccessedTime = System.currentTimeMillis(); | |||
| accessCount.incrementAndGet(); | |||
| } | |||
| @Override | |||
| public void endAccess() { | |||
| this.thisAccessedTime = System.currentTimeMillis(); | |||
| accessCount.decrementAndGet(); | |||
| } | |||
| @Override | |||
| public void setCreationTime(long time) { | |||
| this.creationTime = time; | |||
| this.thisAccessedTime = time; | |||
| } | |||
| @Override | |||
| public void setMaxInactiveInterval(int interval) { | |||
| this.maxInactiveInterval = interval; | |||
| } | |||
| @Override | |||
| public void setId(String id) { | |||
| if ((this.id != null) && (manager != null)) | |||
| manager.remove(this); | |||
| this.id = id; | |||
| if (manager != null) | |||
| manager.add(this); | |||
| } | |||
| /** | |||
| * Return the names of all currently defined session attributes | |||
| * as an array of Strings. If there are no defined attributes, a | |||
| * zero-length array is returned. | |||
| */ | |||
| protected String[] keys() { | |||
| return attributes.keySet().toArray(EMPTY_ARRAY); | |||
| } | |||
| @Override | |||
| public boolean equals(Object o) { | |||
| if (this == o) return true; | |||
| if (!(o instanceof StandardSession)) return false; | |||
| StandardSession session = (StandardSession) o; | |||
| if (creationTime != session.creationTime) return false; | |||
| if (expiring != session.expiring) return false; | |||
| if (isValid != session.isValid) return false; | |||
| if (maxInactiveInterval != session.maxInactiveInterval) return false; | |||
| if (thisAccessedTime != session.thisAccessedTime) return false; | |||
| if (!accessCount.equals(session.accessCount)) return false; | |||
| if (!attributes.equals(session.attributes)) return false; | |||
| if (!facade.equals(session.facade)) return false; | |||
| if (!id.equals(session.id)) return false; | |||
| if (!manager.equals(session.manager)) return false; | |||
| return true; | |||
| } | |||
| @Override | |||
| public int hashCode() { | |||
| int result = attributes.hashCode(); | |||
| result = 31 * result + id.hashCode(); | |||
| result = 31 * result + (isValid ? 1 : 0); | |||
| result = 31 * result + (expiring ? 1 : 0); | |||
| result = 31 * result + manager.hashCode(); | |||
| result = 31 * result + (int) (creationTime ^ (creationTime >>> 32)); | |||
| result = 31 * result + (int) (thisAccessedTime ^ (thisAccessedTime >>> 32)); | |||
| result = 31 * result + maxInactiveInterval; | |||
| result = 31 * result + facade.hashCode(); | |||
| result = 31 * result + accessCount.hashCode(); | |||
| return result; | |||
| } | |||
| } | |||
| @@ -0,0 +1,45 @@ | |||
| package me.chanjar.weixin.common.session; | |||
| import java.util.Enumeration; | |||
| public class StandardSessionFacade implements WxSession { | |||
| /** | |||
| * Wrapped session object. | |||
| */ | |||
| private WxSession session = null; | |||
| public StandardSessionFacade(StandardSession session) { | |||
| this.session = session; | |||
| } | |||
| public InternalSession getInternalSession() { | |||
| return (InternalSession) session; | |||
| } | |||
| @Override | |||
| public Object getAttribute(String name) { | |||
| return session.getAttribute(name); | |||
| } | |||
| @Override | |||
| public Enumeration<String> getAttributeNames() { | |||
| return session.getAttributeNames(); | |||
| } | |||
| @Override | |||
| public void setAttribute(String name, Object value) { | |||
| session.setAttribute(name, value); | |||
| } | |||
| @Override | |||
| public void removeAttribute(String name) { | |||
| session.removeAttribute(name); | |||
| } | |||
| @Override | |||
| public void invalidate() { | |||
| session.invalidate(); | |||
| } | |||
| } | |||
| @@ -0,0 +1,321 @@ | |||
| package me.chanjar.weixin.common.session; | |||
| import me.chanjar.weixin.common.util.res.StringManager; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import java.util.Map; | |||
| import java.util.concurrent.ConcurrentHashMap; | |||
| import java.util.concurrent.atomic.AtomicBoolean; | |||
| /** | |||
| * 基于内存的session manager | |||
| */ | |||
| public class StandardSessionManager implements WxSessionManager, InternalSessionManager { | |||
| protected final Logger log = LoggerFactory.getLogger(StandardSessionManager.class); | |||
| protected static final StringManager sm = | |||
| StringManager.getManager(Constants.Package); | |||
| /** | |||
| * The set of currently active Sessions for this Manager, keyed by | |||
| * session identifier. | |||
| */ | |||
| protected Map<String, InternalSession> sessions = new ConcurrentHashMap<String, InternalSession>(); | |||
| @Override | |||
| public WxSession getSession(String sessionId) { | |||
| return getSession(sessionId, true); | |||
| } | |||
| @Override | |||
| public WxSession getSession(String sessionId, boolean create) { | |||
| if (sessionId == null) { | |||
| throw new IllegalStateException | |||
| (sm.getString("sessionManagerImpl.getSession.ise")); | |||
| } | |||
| InternalSession session = findSession(sessionId); | |||
| if ((session != null) && !session.isValid()) { | |||
| session = null; | |||
| } | |||
| if (session != null) { | |||
| session.access(); | |||
| return session.getSession(); | |||
| } | |||
| // Create a new session if requested and the response is not committed | |||
| if (!create) { | |||
| return (null); | |||
| } | |||
| session = createSession(sessionId); | |||
| if (session == null) { | |||
| return null; | |||
| } | |||
| session.access(); | |||
| return session.getSession(); | |||
| } | |||
| // -------------------------------------- InternalSessionManager | |||
| /** | |||
| * The descriptive name of this Manager implementation (for logging). | |||
| */ | |||
| private static final String name = "SessionManagerImpl"; | |||
| /** | |||
| * The maximum number of active Sessions allowed, or -1 for no limit. | |||
| */ | |||
| protected int maxActiveSessions = -1; | |||
| /** | |||
| * Number of session creations that failed due to maxActiveSessions. | |||
| */ | |||
| protected int rejectedSessions = 0; | |||
| /** | |||
| * The default maximum inactive interval for Sessions created by | |||
| * this Manager. | |||
| */ | |||
| protected int maxInactiveInterval = 30 * 60; | |||
| // Number of sessions created by this manager | |||
| protected long sessionCounter=0; | |||
| protected volatile int maxActive=0; | |||
| private final Object maxActiveUpdateLock = new Object(); | |||
| /** | |||
| * Processing time during session expiration. | |||
| */ | |||
| protected long processingTime = 0; | |||
| /** | |||
| * Iteration count for background processing. | |||
| */ | |||
| private int count = 0; | |||
| /** | |||
| * Frequency of the session expiration, and related manager operations. | |||
| * Manager operations will be done once for the specified amount of | |||
| * backgrondProcess calls (ie, the lower the amount, the most often the | |||
| * checks will occur). | |||
| */ | |||
| protected int processExpiresFrequency = 6; | |||
| /** | |||
| * background processor delay in seconds | |||
| */ | |||
| protected int backgroundProcessorDelay = 10; | |||
| /** | |||
| * 后台清理线程是否已经开启 | |||
| */ | |||
| private final AtomicBoolean backgroundProcessStarted = new AtomicBoolean(false); | |||
| @Override | |||
| public void remove(InternalSession session) { | |||
| remove(session, false); | |||
| } | |||
| @Override | |||
| public void remove(InternalSession session, boolean update) { | |||
| if (session.getIdInternal() != null) { | |||
| sessions.remove(session.getIdInternal()); | |||
| } | |||
| } | |||
| @Override | |||
| public InternalSession findSession(String id) { | |||
| if (id == null) | |||
| return (null); | |||
| return sessions.get(id); | |||
| } | |||
| @Override | |||
| public InternalSession createSession(String sessionId) { | |||
| if (sessionId == null) { | |||
| throw new IllegalStateException | |||
| (sm.getString("sessionManagerImpl.createSession.ise")); | |||
| } | |||
| if ((maxActiveSessions >= 0) && | |||
| (getActiveSessions() >= maxActiveSessions)) { | |||
| rejectedSessions++; | |||
| throw new TooManyActiveSessionsException( | |||
| sm.getString("sessionManagerImpl.createSession.tmase"), | |||
| maxActiveSessions); | |||
| } | |||
| // Recycle or create a Session instance | |||
| InternalSession session = createEmptySession(); | |||
| // Initialize the properties of the new session and return it | |||
| session.setValid(true); | |||
| session.setCreationTime(System.currentTimeMillis()); | |||
| session.setMaxInactiveInterval(this.maxInactiveInterval); | |||
| String id = sessionId; | |||
| session.setId(id); | |||
| sessionCounter++; | |||
| return (session); | |||
| } | |||
| @Override | |||
| public int getActiveSessions() { | |||
| return sessions.size(); | |||
| } | |||
| @Override | |||
| public InternalSession createEmptySession() { | |||
| return (getNewSession()); | |||
| } | |||
| /** | |||
| * Get new session class to be used in the doLoad() method. | |||
| */ | |||
| protected InternalSession getNewSession() { | |||
| return new StandardSession(this); | |||
| } | |||
| @Override | |||
| public void add(InternalSession session) { | |||
| // 当第一次有session创建的时候,开启session清理线程 | |||
| if (!backgroundProcessStarted.getAndSet(true)) { | |||
| Thread t = new Thread(new Runnable() { | |||
| @Override | |||
| public void run() { | |||
| while (true) { | |||
| try { | |||
| // 每秒清理一次 | |||
| Thread.sleep(backgroundProcessorDelay * 1000l); | |||
| backgroundProcess(); | |||
| } catch (InterruptedException e) { | |||
| log.error("SessionManagerImpl.backgroundProcess error", e); | |||
| } | |||
| } | |||
| } | |||
| }); | |||
| t.setDaemon(true); | |||
| t.start(); | |||
| } | |||
| sessions.put(session.getIdInternal(), session); | |||
| int size = getActiveSessions(); | |||
| if( size > maxActive ) { | |||
| synchronized(maxActiveUpdateLock) { | |||
| if( size > maxActive ) { | |||
| maxActive = size; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * Return the set of active Sessions associated with this Manager. | |||
| * If this Manager has no active Sessions, a zero-length array is returned. | |||
| */ | |||
| @Override | |||
| public InternalSession[] findSessions() { | |||
| return sessions.values().toArray(new InternalSession[0]); | |||
| } | |||
| @Override | |||
| public void backgroundProcess() { | |||
| count = (count + 1) % processExpiresFrequency; | |||
| if (count == 0) | |||
| processExpires(); | |||
| } | |||
| /** | |||
| * Invalidate all sessions that have expired. | |||
| */ | |||
| public void processExpires() { | |||
| long timeNow = System.currentTimeMillis(); | |||
| InternalSession sessions[] = findSessions(); | |||
| int expireHere = 0 ; | |||
| if(log.isDebugEnabled()) | |||
| log.debug("Start expire sessions {} at {} sessioncount {}", getName(), timeNow, sessions.length); | |||
| for (int i = 0; i < sessions.length; i++) { | |||
| if (sessions[i]!=null && !sessions[i].isValid()) { | |||
| expireHere++; | |||
| } | |||
| } | |||
| long timeEnd = System.currentTimeMillis(); | |||
| if(log.isDebugEnabled()) | |||
| log.debug("End expire sessions {} processingTime {} expired sessions: {}", getName(), timeEnd - timeNow, expireHere); | |||
| processingTime += ( timeEnd - timeNow ); | |||
| } | |||
| @Override | |||
| public void setMaxInactiveInterval(int interval) { | |||
| this.maxInactiveInterval = interval; | |||
| } | |||
| /** | |||
| * Set the manager checks frequency. | |||
| * | |||
| * @param processExpiresFrequency the new manager checks frequency | |||
| */ | |||
| @Override | |||
| public void setProcessExpiresFrequency(int processExpiresFrequency) { | |||
| if (processExpiresFrequency <= 0) { | |||
| return; | |||
| } | |||
| this.processExpiresFrequency = processExpiresFrequency; | |||
| } | |||
| @Override | |||
| public void setBackgroundProcessorDelay(int backgroundProcessorDelay) { | |||
| this.backgroundProcessorDelay = backgroundProcessorDelay; | |||
| } | |||
| /** | |||
| * Return the descriptive short name of this Manager implementation. | |||
| */ | |||
| public String getName() { | |||
| return (name); | |||
| } | |||
| /** | |||
| * Set the maximum number of active Sessions allowed, or -1 for | |||
| * no limit. | |||
| * | |||
| * @param max The new maximum number of sessions | |||
| */ | |||
| @Override | |||
| public void setMaxActiveSessions(int max) { | |||
| this.maxActiveSessions = max; | |||
| } | |||
| } | |||
| @@ -0,0 +1,57 @@ | |||
| /* | |||
| * Licensed to the Apache Software Foundation (ASF) under one or more | |||
| * contributor license agreements. See the NOTICE file distributed with | |||
| * this work for additional information regarding copyright ownership. | |||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | |||
| * (the "License"); you may not use this file except in compliance with | |||
| * the License. You may obtain a copy of the License at | |||
| * | |||
| * http://www.apache.org/licenses/LICENSE-2.0 | |||
| * | |||
| * Unless required by applicable law or agreed to in writing, software | |||
| * distributed under the License is distributed on an "AS IS" BASIS, | |||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
| * See the License for the specific language governing permissions and | |||
| * limitations under the License. | |||
| */ | |||
| package me.chanjar.weixin.common.session; | |||
| /** | |||
| * An exception that indicates the maximum number of active sessions has been | |||
| * reached and the server is refusing to create any new sessions. | |||
| */ | |||
| public class TooManyActiveSessionsException | |||
| extends IllegalStateException | |||
| { | |||
| private static final long serialVersionUID = 1L; | |||
| /** | |||
| * The maximum number of active sessions the server will tolerate. | |||
| */ | |||
| private final int maxActiveSessions; | |||
| /** | |||
| * Creates a new TooManyActiveSessionsException. | |||
| * | |||
| * @param message A description for the exception. | |||
| * @param maxActive The maximum number of active sessions allowed by the | |||
| * session manager. | |||
| */ | |||
| public TooManyActiveSessionsException(String message, | |||
| int maxActive) | |||
| { | |||
| super(message); | |||
| maxActiveSessions = maxActive; | |||
| } | |||
| /** | |||
| * Gets the maximum number of sessions allowed by the session manager. | |||
| * | |||
| * @return The maximum number of sessions allowed by the session manager. | |||
| */ | |||
| public int getMaxActiveSessions() | |||
| { | |||
| return maxActiveSessions; | |||
| } | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| package me.chanjar.weixin.common.session; | |||
| import java.util.Enumeration; | |||
| public interface WxSession { | |||
| public Object getAttribute(String name); | |||
| public Enumeration<String> getAttributeNames(); | |||
| public void setAttribute(String name, Object value); | |||
| public void removeAttribute(String name); | |||
| public void invalidate(); | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| package me.chanjar.weixin.common.session; | |||
| public interface WxSessionManager { | |||
| /** | |||
| * 获取某个sessionId对应的session,如果sessionId没有对应的session,则新建一个并返回。 | |||
| * @param sessionId | |||
| * @return | |||
| */ | |||
| public WxSession getSession(String sessionId); | |||
| /** | |||
| * 获取某个sessionId对应的session,如果sessionId没有对应的session,若create为true则新建一个,否则返回null。 | |||
| * @param sessionId | |||
| * @param create | |||
| * @return | |||
| */ | |||
| public WxSession getSession(String sessionId, boolean create); | |||
| } | |||
| @@ -0,0 +1,27 @@ | |||
| package me.chanjar.weixin.common.util; | |||
| /** | |||
| * <pre> | |||
| * 消息重复检查器 | |||
| * 微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次 | |||
| * </pre> | |||
| */ | |||
| public interface WxMessageDuplicateChecker { | |||
| /** | |||
| * <h2>公众号的排重方式</h2> | |||
| * | |||
| * <p>普通消息:关于重试的消息排重,推荐使用msgid排重。<a href="http://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html">文档参考</a>。</p> | |||
| * <p>事件消息:关于重试的消息排重,推荐使用FromUserName + CreateTime 排重。<a href="http://mp.weixin.qq.com/wiki/2/5baf56ce4947d35003b86a9805634b1e.html">文档参考</a></p> | |||
| * | |||
| * <h2>企业号的排重方式</h2> | |||
| * | |||
| * 官方文档完全没有写,参照公众号的方式排重。 | |||
| * | |||
| * <p>或者可以采取更简单的方式,如果有MsgId就用MsgId排重,如果没有就用FromUserName+CreateTime排重</p> | |||
| * @param messageId messageId需要根据上面讲的方式构造 | |||
| * @return 如果是重复消息,返回true,否则返回false | |||
| */ | |||
| public boolean isDuplicate(String messageId); | |||
| } | |||
| @@ -0,0 +1,98 @@ | |||
| package me.chanjar.weixin.common.util; | |||
| import java.util.Map; | |||
| import java.util.concurrent.ConcurrentHashMap; | |||
| import java.util.concurrent.atomic.AtomicBoolean; | |||
| /** | |||
| * <pre> | |||
| * 默认消息重复检查器 | |||
| * 将每个消息id保存在内存里,每隔5秒清理已经过期的消息id,每个消息id的过期时间是15秒 | |||
| * </pre> | |||
| */ | |||
| public class WxMessageInMemoryDuplicateChecker implements WxMessageDuplicateChecker { | |||
| /** | |||
| * 一个消息ID在内存的过期时间:15秒 | |||
| */ | |||
| private final Long timeToLive; | |||
| /** | |||
| * 每隔多少周期检查消息ID是否过期:5秒 | |||
| */ | |||
| private final Long clearPeriod; | |||
| /** | |||
| * 消息id->消息时间戳的map | |||
| */ | |||
| private final ConcurrentHashMap<String, Long> msgId2Timestamp = new ConcurrentHashMap<String, Long>(); | |||
| /** | |||
| * 后台清理线程是否已经开启 | |||
| */ | |||
| private final AtomicBoolean backgroundProcessStarted = new AtomicBoolean(false); | |||
| /** | |||
| * WxMsgIdInMemoryDuplicateChecker构造函数 | |||
| * <pre> | |||
| * 一个消息ID在内存的过期时间:15秒 | |||
| * 每隔多少周期检查消息ID是否过期:5秒 | |||
| * </pre> | |||
| */ | |||
| public WxMessageInMemoryDuplicateChecker() { | |||
| this.timeToLive = 15 * 1000l; | |||
| this.clearPeriod = 5 * 1000l; | |||
| } | |||
| /** | |||
| * WxMsgIdInMemoryDuplicateChecker构造函数 | |||
| * @param timeToLive 一个消息ID在内存的过期时间:毫秒 | |||
| * @param clearPeriod 每隔多少周期检查消息ID是否过期:毫秒 | |||
| */ | |||
| public WxMessageInMemoryDuplicateChecker(Long timeToLive, Long clearPeriod) { | |||
| this.timeToLive = timeToLive; | |||
| this.clearPeriod = clearPeriod; | |||
| } | |||
| protected void checkBackgroundProcessStarted() { | |||
| if (backgroundProcessStarted.getAndSet(true)) { | |||
| return; | |||
| } | |||
| Thread t = new Thread(new Runnable() { | |||
| @Override | |||
| public void run() { | |||
| try { | |||
| while (true) { | |||
| Thread.sleep(clearPeriod); | |||
| Long now = System.currentTimeMillis(); | |||
| for (Map.Entry<String, Long> entry : msgId2Timestamp.entrySet()) { | |||
| if (now - entry.getValue() > timeToLive) { | |||
| msgId2Timestamp.entrySet().remove(entry); | |||
| } | |||
| } | |||
| } | |||
| } catch (InterruptedException e) { | |||
| e.printStackTrace(); | |||
| } | |||
| } | |||
| }); | |||
| t.setDaemon(true); | |||
| t.start(); | |||
| } | |||
| @Override | |||
| public boolean isDuplicate(String messageId) { | |||
| if (messageId == null) { | |||
| return false; | |||
| } | |||
| checkBackgroundProcessStarted(); | |||
| Long timestamp = msgId2Timestamp.putIfAbsent(messageId, System.currentTimeMillis()); | |||
| if (timestamp == null) { | |||
| // 第一次接收到这个消息 | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| } | |||
| @@ -10,27 +10,49 @@ import java.util.Arrays; | |||
| public class SHA1 { | |||
| /** | |||
| * 生成SHA1签名 | |||
| * 串接arr参数,生成sha1 digest | |||
| * | |||
| * @param arr | |||
| * @return | |||
| */ | |||
| public static String gen(String... arr) throws NoSuchAlgorithmException { | |||
| Arrays.sort(arr); | |||
| StringBuilder sb = new StringBuilder(); | |||
| for(String a : arr) { | |||
| for (String a : arr) { | |||
| sb.append(a); | |||
| } | |||
| return genStr(sb.toString()); | |||
| } | |||
| /** | |||
| * 用&串接arr参数,生成sha1 digest | |||
| * | |||
| * @param arr | |||
| * @return | |||
| */ | |||
| public static String genWithAmple(String... arr) throws NoSuchAlgorithmException { | |||
| Arrays.sort(arr); | |||
| StringBuilder sb = new StringBuilder(); | |||
| for (int i = 0; i < arr.length; i++) { | |||
| String a = arr[i]; | |||
| sb.append(a); | |||
| if (i != arr.length - 1) { | |||
| sb.append('&'); | |||
| } | |||
| } | |||
| return genStr(sb.toString()); | |||
| } | |||
| public static String genStr(String str) throws NoSuchAlgorithmException { | |||
| MessageDigest sha1 = MessageDigest.getInstance("SHA1"); | |||
| sha1.update(sb.toString().getBytes()); | |||
| sha1.update(str.getBytes()); | |||
| byte[] output = sha1.digest(); | |||
| return bytesToHex(output); | |||
| } | |||
| protected static String bytesToHex(byte[] b) { | |||
| char hexDigit[] = {'0', '1', '2', '3', '4', '5', '6', '7', | |||
| '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; | |||
| char hexDigit[] = { '0', '1', '2', '3', '4', '5', '6', '7', | |||
| '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; | |||
| StringBuffer buf = new StringBuffer(); | |||
| for (int j = 0; j < b.length; j++) { | |||
| buf.append(hexDigit[(b[j] >> 4) & 0x0f]); | |||
| @@ -0,0 +1,251 @@ | |||
| /* | |||
| * Licensed to the Apache Software Foundation (ASF) under one or more | |||
| * contributor license agreements. See the NOTICE file distributed with | |||
| * this work for additional information regarding copyright ownership. | |||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | |||
| * (the "License"); you may not use this file except in compliance with | |||
| * the License. You may obtain a copy of the License at | |||
| * | |||
| * http://www.apache.org/licenses/LICENSE-2.0 | |||
| * | |||
| * Unless required by applicable law or agreed to in writing, software | |||
| * distributed under the License is distributed on an "AS IS" BASIS, | |||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
| * See the License for the specific language governing permissions and | |||
| * limitations under the License. | |||
| */ | |||
| package me.chanjar.weixin.common.util.res; | |||
| import java.text.MessageFormat; | |||
| import java.util.Enumeration; | |||
| import java.util.Hashtable; | |||
| import java.util.LinkedHashMap; | |||
| import java.util.Locale; | |||
| import java.util.Map; | |||
| import java.util.MissingResourceException; | |||
| import java.util.ResourceBundle; | |||
| /** | |||
| * An internationalization / localization helper class which reduces | |||
| * the bother of handling ResourceBundles and takes care of the | |||
| * common cases of message formating which otherwise require the | |||
| * creation of Object arrays and such. | |||
| * | |||
| * <p>The StringManager operates on a package basis. One StringManager | |||
| * per package can be created and accessed via the getManager method | |||
| * call. | |||
| * | |||
| * <p>The StringManager will look for a ResourceBundle named by | |||
| * the package name given plus the suffix of "LocalStrings". In | |||
| * practice, this means that the localized information will be contained | |||
| * in a LocalStrings.properties file located in the package | |||
| * directory of the classpath. | |||
| * | |||
| * <p>Please see the documentation for java.util.ResourceBundle for | |||
| * more information. | |||
| * | |||
| * @author James Duncan Davidson [duncan@eng.sun.com] | |||
| * @author James Todd [gonzo@eng.sun.com] | |||
| * @author Mel Martinez [mmartinez@g1440.com] | |||
| * @see java.util.ResourceBundle | |||
| */ | |||
| public class StringManager { | |||
| private static int LOCALE_CACHE_SIZE = 10; | |||
| /** | |||
| * The ResourceBundle for this StringManager. | |||
| */ | |||
| private final ResourceBundle bundle; | |||
| private final Locale locale; | |||
| /** | |||
| * Creates a new StringManager for a given package. This is a | |||
| * private method and all access to it is arbitrated by the | |||
| * static getManager method call so that only one StringManager | |||
| * per package will be created. | |||
| * | |||
| * @param packageName Name of package to create StringManager for. | |||
| */ | |||
| private StringManager(String packageName, Locale locale) { | |||
| String bundleName = packageName + ".LocalStrings"; | |||
| ResourceBundle bnd = null; | |||
| try { | |||
| bnd = ResourceBundle.getBundle(bundleName, locale); | |||
| } catch( MissingResourceException ex ) { | |||
| // Try from the current loader (that's the case for trusted apps) | |||
| // Should only be required if using a TC5 style classloader structure | |||
| // where common != shared != server | |||
| ClassLoader cl = Thread.currentThread().getContextClassLoader(); | |||
| if( cl != null ) { | |||
| try { | |||
| bnd = ResourceBundle.getBundle(bundleName, locale, cl); | |||
| } catch(MissingResourceException ex2) { | |||
| // Ignore | |||
| } | |||
| } | |||
| } | |||
| bundle = bnd; | |||
| // Get the actual locale, which may be different from the requested one | |||
| if (bundle != null) { | |||
| Locale bundleLocale = bundle.getLocale(); | |||
| if (bundleLocale.equals(Locale.ROOT)) { | |||
| this.locale = Locale.ENGLISH; | |||
| } else { | |||
| this.locale = bundleLocale; | |||
| } | |||
| } else { | |||
| this.locale = null; | |||
| } | |||
| } | |||
| /** | |||
| Get a string from the underlying resource bundle or return | |||
| null if the String is not found. | |||
| @param key to desired resource String | |||
| @return resource String matching <i>key</i> from underlying | |||
| bundle or null if not found. | |||
| @throws IllegalArgumentException if <i>key</i> is null. | |||
| */ | |||
| public String getString(String key) { | |||
| if(key == null){ | |||
| String msg = "key may not have a null value"; | |||
| throw new IllegalArgumentException(msg); | |||
| } | |||
| String str = null; | |||
| try { | |||
| // Avoid NPE if bundle is null and treat it like an MRE | |||
| if (bundle != null) { | |||
| str = bundle.getString(key); | |||
| } | |||
| } catch(MissingResourceException mre) { | |||
| //bad: shouldn't mask an exception the following way: | |||
| // str = "[cannot find message associated with key '" + key + | |||
| // "' due to " + mre + "]"; | |||
| // because it hides the fact that the String was missing | |||
| // from the calling code. | |||
| //good: could just throw the exception (or wrap it in another) | |||
| // but that would probably cause much havoc on existing | |||
| // code. | |||
| //better: consistent with container pattern to | |||
| // simply return null. Calling code can then do | |||
| // a null check. | |||
| str = null; | |||
| } | |||
| return str; | |||
| } | |||
| /** | |||
| * Get a string from the underlying resource bundle and format | |||
| * it with the given set of arguments. | |||
| * | |||
| * @param key | |||
| * @param args | |||
| */ | |||
| public String getString(final String key, final Object... args) { | |||
| String value = getString(key); | |||
| if (value == null) { | |||
| value = key; | |||
| } | |||
| MessageFormat mf = new MessageFormat(value); | |||
| mf.setLocale(locale); | |||
| return mf.format(args, new StringBuffer(), null).toString(); | |||
| } | |||
| /** | |||
| * Identify the Locale this StringManager is associated with | |||
| */ | |||
| public Locale getLocale() { | |||
| return locale; | |||
| } | |||
| // -------------------------------------------------------------- | |||
| // STATIC SUPPORT METHODS | |||
| // -------------------------------------------------------------- | |||
| private static final Map<String, Map<Locale,StringManager>> managers = | |||
| new Hashtable<String, Map<Locale,StringManager>>(); | |||
| /** | |||
| * Get the StringManager for a particular package. If a manager for | |||
| * a package already exists, it will be reused, else a new | |||
| * StringManager will be created and returned. | |||
| * | |||
| * @param packageName The package name | |||
| */ | |||
| public static final synchronized StringManager getManager( | |||
| String packageName) { | |||
| return getManager(packageName, Locale.getDefault()); | |||
| } | |||
| /** | |||
| * Get the StringManager for a particular package and Locale. If a manager | |||
| * for a package/Locale combination already exists, it will be reused, else | |||
| * a new StringManager will be created and returned. | |||
| * | |||
| * @param packageName The package name | |||
| * @param locale The Locale | |||
| */ | |||
| public static final synchronized StringManager getManager( | |||
| String packageName, Locale locale) { | |||
| Map<Locale,StringManager> map = managers.get(packageName); | |||
| if (map == null) { | |||
| /* | |||
| * Don't want the HashMap to be expanded beyond LOCALE_CACHE_SIZE. | |||
| * Expansion occurs when size() exceeds capacity. Therefore keep | |||
| * size at or below capacity. | |||
| * removeEldestEntry() executes after insertion therefore the test | |||
| * for removal needs to use one less than the maximum desired size | |||
| * | |||
| */ | |||
| map = new LinkedHashMap<Locale,StringManager>(LOCALE_CACHE_SIZE, 1, true) { | |||
| private static final long serialVersionUID = 1L; | |||
| @Override | |||
| protected boolean removeEldestEntry( | |||
| Map.Entry<Locale,StringManager> eldest) { | |||
| if (size() > (LOCALE_CACHE_SIZE - 1)) { | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| }; | |||
| managers.put(packageName, map); | |||
| } | |||
| StringManager mgr = map.get(locale); | |||
| if (mgr == null) { | |||
| mgr = new StringManager(packageName, locale); | |||
| map.put(locale, mgr); | |||
| } | |||
| return mgr; | |||
| } | |||
| /** | |||
| * Retrieve the StringManager for a list of Locales. The first StringManager | |||
| * found will be returned. | |||
| * | |||
| * @param requestedLocales the list of Locales | |||
| * | |||
| * @return the found StringManager or the default StringManager | |||
| */ | |||
| public static StringManager getManager(String packageName, | |||
| Enumeration<Locale> requestedLocales) { | |||
| while (requestedLocales.hasMoreElements()) { | |||
| Locale locale = requestedLocales.nextElement(); | |||
| StringManager result = getManager(packageName, locale); | |||
| if (result.getLocale().equals(locale)) { | |||
| return result; | |||
| } | |||
| } | |||
| // Return the default | |||
| return getManager(packageName); | |||
| } | |||
| } | |||
| @@ -1,24 +0,0 @@ | |||
| package me.chanjar.weixin.common.util.xml; | |||
| import javax.xml.bind.annotation.adapters.XmlAdapter; | |||
| /** | |||
| * | |||
| * http://stackoverflow.com/questions/14193944/jaxb-marshalling-unmarshalling-with-cdata | |||
| * | |||
| * @author Daniel Qian | |||
| * | |||
| */ | |||
| public class AdapterCDATA extends XmlAdapter<String, String> { | |||
| @Override | |||
| public String marshal(String arg0) throws Exception { | |||
| return "<![CDATA[" + arg0 + "]]>"; | |||
| } | |||
| @Override | |||
| public String unmarshal(String arg0) throws Exception { | |||
| return arg0; | |||
| } | |||
| } | |||
| @@ -1,21 +0,0 @@ | |||
| package me.chanjar.weixin.common.util.xml; | |||
| import javax.xml.bind.annotation.adapters.XmlAdapter; | |||
| /** | |||
| * @author Daniel Qian | |||
| */ | |||
| public class MediaIdMarshaller extends XmlAdapter<String, String> { | |||
| @Override | |||
| public String marshal(String arg0) throws Exception { | |||
| return "<MediaId><![CDATA[" + arg0 + "]]></MediaId>"; | |||
| } | |||
| @Override | |||
| public String unmarshal(String arg0) throws Exception { | |||
| // do nothing | |||
| return arg0; | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| package me.chanjar.weixin.common.util.xml; | |||
| import com.thoughtworks.xstream.converters.basic.StringConverter; | |||
| public class XStreamCDataConverter extends StringConverter { | |||
| @Override | |||
| public String toString(Object obj) { | |||
| return "<![CDATA[" + super.toString(obj) + "]]>"; | |||
| } | |||
| } | |||
| @@ -0,0 +1,50 @@ | |||
| package me.chanjar.weixin.common.util.xml; | |||
| import com.thoughtworks.xstream.XStream; | |||
| import com.thoughtworks.xstream.converters.basic.DoubleConverter; | |||
| import com.thoughtworks.xstream.converters.basic.FloatConverter; | |||
| import com.thoughtworks.xstream.converters.basic.IntConverter; | |||
| import com.thoughtworks.xstream.core.util.QuickWriter; | |||
| import com.thoughtworks.xstream.io.HierarchicalStreamWriter; | |||
| import com.thoughtworks.xstream.io.xml.PrettyPrintWriter; | |||
| import com.thoughtworks.xstream.io.xml.XppDriver; | |||
| import com.thoughtworks.xstream.security.NoTypePermission; | |||
| import com.thoughtworks.xstream.security.NullPermission; | |||
| import com.thoughtworks.xstream.security.PrimitiveTypePermission; | |||
| import java.io.Writer; | |||
| public class XStreamInitializer { | |||
| public static XStream getInstance() { | |||
| XStream xstream = new XStream(new XppDriver() { | |||
| @Override | |||
| public HierarchicalStreamWriter createWriter(Writer out) { | |||
| return new PrettyPrintWriter(out, getNameCoder()) { | |||
| protected String PREFIX_CDATA = "<![CDATA["; | |||
| protected String SUFFIX_CDATA = "]]>"; | |||
| protected String PREFIX_MEDIA_ID = "<MediaId>"; | |||
| protected String SUFFIX_MEDIA_ID = "</MediaId>"; | |||
| @Override | |||
| protected void writeText(QuickWriter writer, String text) { | |||
| if (text.startsWith(PREFIX_CDATA) && text.endsWith(SUFFIX_CDATA)) { | |||
| writer.write(text); | |||
| } else if (text.startsWith(PREFIX_MEDIA_ID) && text.endsWith(SUFFIX_MEDIA_ID)) { | |||
| writer.write(text); | |||
| } else { | |||
| super.writeText(writer, text); | |||
| } | |||
| } | |||
| }; | |||
| } | |||
| }); | |||
| xstream.ignoreUnknownElements(); | |||
| xstream.setMode(XStream.NO_REFERENCES); | |||
| xstream.addPermission(NullPermission.NULL); | |||
| xstream.addPermission(PrimitiveTypePermission.PRIMITIVES); | |||
| return xstream; | |||
| } | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| package me.chanjar.weixin.common.util.xml; | |||
| public class XStreamMediaIdConverter extends XStreamCDataConverter { | |||
| @Override | |||
| public String toString(Object obj) { | |||
| return "<MediaId>" + super.toString(obj) + "</MediaId>"; | |||
| } | |||
| } | |||
| @@ -0,0 +1,132 @@ | |||
| package me.chanjar.weixin.common.session; | |||
| import org.testng.Assert; | |||
| import org.testng.annotations.DataProvider; | |||
| import org.testng.annotations.Test; | |||
| @Test | |||
| public class SessionTest { | |||
| @DataProvider | |||
| public Object[][] getSessionManager() { | |||
| return new Object[][] { | |||
| new Object[] { new StandardSessionManager() } | |||
| }; | |||
| } | |||
| @Test(dataProvider = "getSessionManager", expectedExceptions = IllegalStateException.class) | |||
| public void testInvalidate(WxSessionManager sessionManager) { | |||
| WxSession session = sessionManager.getSession("abc"); | |||
| session.invalidate(); | |||
| session.getAttributeNames(); | |||
| } | |||
| @Test(dataProvider = "getSessionManager") | |||
| public void testInvalidate2(InternalSessionManager sessionManager) { | |||
| Assert.assertEquals(sessionManager.getActiveSessions(), 0); | |||
| WxSession session = ((WxSessionManager) sessionManager).getSession("abc"); | |||
| Assert.assertEquals(sessionManager.getActiveSessions(), 1); | |||
| session.invalidate(); | |||
| Assert.assertEquals(sessionManager.getActiveSessions(), 0); | |||
| } | |||
| @Test(dataProvider = "getSessionManager") | |||
| public void testGetSession(WxSessionManager sessionManager) { | |||
| WxSession session1 = sessionManager.getSession("abc"); | |||
| WxSession session2 = sessionManager.getSession("abc"); | |||
| Assert.assertEquals(session1, session2); | |||
| Assert.assertTrue(session1 == session2); | |||
| WxSession abc1 = sessionManager.getSession("abc1"); | |||
| Assert.assertNotEquals(session1, abc1); | |||
| WxSession abc1b = sessionManager.getSession("abc1", false); | |||
| Assert.assertEquals(abc1, abc1b); | |||
| WxSession def = sessionManager.getSession("def", false); | |||
| Assert.assertNull(def); | |||
| } | |||
| @Test(dataProvider = "getSessionManager") | |||
| public void testInvalidateAngGet(WxSessionManager sessionManager) { | |||
| WxSession session1 = sessionManager.getSession("abc"); | |||
| session1.invalidate(); | |||
| WxSession session2 = sessionManager.getSession("abc"); | |||
| Assert.assertNotEquals(session1, session2); | |||
| InternalSessionManager ism = (InternalSessionManager) sessionManager; | |||
| Assert.assertEquals(ism.getActiveSessions(), 1); | |||
| } | |||
| @Test(dataProvider = "getSessionManager") | |||
| public void testBackgroundProcess(WxSessionManager sessionManager) throws InterruptedException { | |||
| InternalSessionManager ism = (InternalSessionManager) sessionManager; | |||
| ism.setMaxInactiveInterval(1); | |||
| ism.setProcessExpiresFrequency(1); | |||
| ism.setBackgroundProcessorDelay(1); | |||
| Assert.assertEquals(ism.getActiveSessions(), 0); | |||
| InternalSession abc = ism.createSession("abc"); | |||
| abc.endAccess(); | |||
| Thread.sleep(2000l); | |||
| Assert.assertEquals(ism.getActiveSessions(), 0); | |||
| } | |||
| @Test(dataProvider = "getSessionManager") | |||
| public void testBackgroundProcess2(WxSessionManager sessionManager) throws InterruptedException { | |||
| InternalSessionManager ism = (InternalSessionManager) sessionManager; | |||
| ism.setMaxInactiveInterval(100); | |||
| ism.setProcessExpiresFrequency(1); | |||
| ism.setBackgroundProcessorDelay(1); | |||
| Assert.assertEquals(ism.getActiveSessions(), 0); | |||
| InternalSession abc = ism.createSession("abc"); | |||
| abc.setMaxInactiveInterval(1); | |||
| abc.endAccess(); | |||
| Thread.sleep(2000l); | |||
| Assert.assertEquals(ism.getActiveSessions(), 0); | |||
| } | |||
| @Test(dataProvider = "getSessionManager") | |||
| public void testMaxActive(WxSessionManager sessionManager) throws InterruptedException { | |||
| InternalSessionManager ism = (InternalSessionManager) sessionManager; | |||
| ism.setMaxActiveSessions(2); | |||
| ism.createSession("abc"); | |||
| ism.createSession("abc"); | |||
| ism.createSession("def"); | |||
| } | |||
| @Test(dataProvider = "getSessionManager", expectedExceptions = TooManyActiveSessionsException.class) | |||
| public void testMaxActive2(WxSessionManager sessionManager) throws InterruptedException { | |||
| InternalSessionManager ism = (InternalSessionManager) sessionManager; | |||
| ism.setMaxActiveSessions(2); | |||
| ism.createSession("abc"); | |||
| ism.createSession("abc"); | |||
| ism.createSession("def"); | |||
| ism.createSession("xyz"); | |||
| } | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| package me.chanjar.weixin.common.util; | |||
| import org.testng.Assert; | |||
| import org.testng.annotations.Test; | |||
| @Test | |||
| public class WxMessageInMemoryDuplicateCheckerTest { | |||
| public void test() throws InterruptedException { | |||
| Long[] msgIds = new Long[] { 1l, 2l, 3l, 4l, 5l, 6l, 7l, 8l }; | |||
| WxMessageInMemoryDuplicateChecker checker = new WxMessageInMemoryDuplicateChecker(2000l, 1000l); | |||
| // 第一次检查 | |||
| for (Long msgId : msgIds) { | |||
| boolean result = checker.isDuplicate(String.valueOf(msgId)); | |||
| Assert.assertFalse(result); | |||
| } | |||
| // 过1秒再检查 | |||
| Thread.sleep(1000l); | |||
| for (Long msgId : msgIds) { | |||
| boolean result = checker.isDuplicate(String.valueOf(msgId)); | |||
| Assert.assertTrue(result); | |||
| } | |||
| // 过1.5秒再检查 | |||
| Thread.sleep(1500l); | |||
| for (Long msgId : msgIds) { | |||
| boolean result = checker.isDuplicate(String.valueOf(msgId)); | |||
| Assert.assertFalse(result); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| <configuration> | |||
| <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | |||
| <!-- encoders are assigned the type | |||
| ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> | |||
| <encoder> | |||
| <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | |||
| </encoder> | |||
| </appender> | |||
| <root level="info"> | |||
| <appender-ref ref="STDOUT" /> | |||
| </root> | |||
| <logger name="me.chanjar.weixin.common" level="debug" /> | |||
| </configuration> | |||
| @@ -1,12 +1,14 @@ | |||
| <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > | |||
| <suite name="Weixin-java-tool-suite" verbose="1"> | |||
| <test name="Bean_Test"> | |||
| <classes> | |||
| <class name="me.chanjar.weixin.common.bean.WxAccessTokenTest" /> | |||
| <class name="me.chanjar.weixin.common.bean.WxErrorTest" /> | |||
| <class name="me.chanjar.weixin.common.bean.WxMenuTest" /> | |||
| <class name="me.chanjar.weixin.common.util.crypto.WxCryptUtilTest" /> | |||
| </classes> | |||
| </test> | |||
| <test name="Bean_Test"> | |||
| <classes> | |||
| <class name="me.chanjar.weixin.common.bean.WxAccessTokenTest"/> | |||
| <class name="me.chanjar.weixin.common.bean.WxErrorTest"/> | |||
| <class name="me.chanjar.weixin.common.bean.WxMenuTest"/> | |||
| <class name="me.chanjar.weixin.common.util.crypto.WxCryptUtilTest"/> | |||
| <class name="me.chanjar.weixin.common.util.WxMessageInMemoryDuplicateCheckerTest"/> | |||
| <class name="me.chanjar.weixin.common.session.SessionTest" /> | |||
| </classes> | |||
| </test> | |||
| </suite> | |||
| @@ -6,7 +6,7 @@ | |||
| <parent> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-parent</artifactId> | |||
| <version>1.0.6</version> | |||
| <version>1.1.0-SNAPSHOT</version> | |||
| </parent> | |||
| <artifactId>weixin-java-cp</artifactId> | |||
| @@ -1,73 +0,0 @@ | |||
| <?xml version="1.0"?> | |||
| <project | |||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||
| <modelVersion>4.0.0</modelVersion> | |||
| <parent> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-parent</artifactId> | |||
| <version>1.0.5-SNAPSHOT</version> | |||
| </parent> | |||
| <artifactId>weixin-java-cp</artifactId> | |||
| <name>WeiXin Java Tools - CP</name> | |||
| <description>微信企业号Java SDK</description> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-common</artifactId> | |||
| <version>${project.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>junit</groupId> | |||
| <artifactId>junit</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.testng</groupId> | |||
| <artifactId>testng</artifactId> | |||
| <version>6.8.7</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.mockito</groupId> | |||
| <artifactId>mockito-all</artifactId> | |||
| <version>1.9.5</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.inject</groupId> | |||
| <artifactId>guice</artifactId> | |||
| <version>3.0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-server</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-servlet</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| </dependencies> | |||
| <build> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-surefire-plugin</artifactId> | |||
| <configuration> | |||
| <suiteXmlFiles> | |||
| <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile> | |||
| </suiteXmlFiles> | |||
| </configuration> | |||
| </plugin> | |||
| </plugins> | |||
| </build> | |||
| </project> | |||
| @@ -9,12 +9,20 @@ import me.chanjar.weixin.common.bean.WxAccessToken; | |||
| */ | |||
| public interface WxCpConfigStorage { | |||
| public String getAccessToken(); | |||
| public boolean isAccessTokenExpired(); | |||
| /** | |||
| * 强制将access token过期掉 | |||
| */ | |||
| public void expireAccessToken(); | |||
| public void updateAccessToken(WxAccessToken accessToken); | |||
| public void updateAccessToken(String accessToken, int expiresIn); | |||
| public String getAccessToken(); | |||
| public String getCorpId(); | |||
| public String getCorpSecret(); | |||
| @@ -25,7 +33,7 @@ public interface WxCpConfigStorage { | |||
| public String getAesKey(); | |||
| public int getExpiresIn(); | |||
| public long getExpiresTime(); | |||
| public String getOauth2redirectUri(); | |||
| @@ -16,7 +16,7 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| protected String accessToken; | |||
| protected String aesKey; | |||
| protected String agentId; | |||
| protected int expiresIn; | |||
| protected long expiresTime; | |||
| protected String oauth2redirectUri; | |||
| @@ -25,17 +25,25 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| protected String http_proxy_username; | |||
| protected String http_proxy_password; | |||
| public void updateAccessToken(WxAccessToken accessToken) { | |||
| public String getAccessToken() { | |||
| return this.accessToken; | |||
| } | |||
| public boolean isAccessTokenExpired() { | |||
| return System.currentTimeMillis() > this.expiresTime; | |||
| } | |||
| public void expireAccessToken() { | |||
| this.expiresTime = 0; | |||
| } | |||
| public synchronized void updateAccessToken(WxAccessToken accessToken) { | |||
| updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } | |||
| public void updateAccessToken(String accessToken, int expiresIn) { | |||
| public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) { | |||
| this.accessToken = accessToken; | |||
| this.expiresIn = expiresIn; | |||
| } | |||
| public String getAccessToken() { | |||
| return this.accessToken; | |||
| this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l; | |||
| } | |||
| public String getCorpId() { | |||
| @@ -50,8 +58,8 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| return this.token; | |||
| } | |||
| public int getExpiresIn() { | |||
| return this.expiresIn; | |||
| public long getExpiresTime() { | |||
| return this.expiresTime; | |||
| } | |||
| public void setCorpId(String corpId) { | |||
| @@ -78,8 +86,8 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| this.accessToken = accessToken; | |||
| } | |||
| public void setExpiresIn(int expiresIn) { | |||
| this.expiresIn = expiresIn; | |||
| public void setExpiresTime(long expiresTime) { | |||
| this.expiresTime = expiresTime; | |||
| } | |||
| public String getAgentId() { | |||
| @@ -140,7 +148,7 @@ public class WxCpInMemoryConfigStorage implements WxCpConfigStorage { | |||
| ", accessToken='" + accessToken + '\'' + | |||
| ", aesKey='" + aesKey + '\'' + | |||
| ", agentId='" + agentId + '\'' + | |||
| ", expiresIn=" + expiresIn + | |||
| ", expiresTime=" + expiresTime + | |||
| ", http_proxy_host='" + http_proxy_host + '\'' + | |||
| ", http_proxy_port=" + http_proxy_port + | |||
| ", http_proxy_username='" + http_proxy_username + '\'' + | |||
| @@ -1,24 +1,29 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import java.util.Map; | |||
| import me.chanjar.weixin.common.session.WxSession; | |||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | |||
| import java.util.Map; | |||
| /** | |||
| * 处理微信推送消息的处理器接口 | |||
| * @author Daniel Qian | |||
| * | |||
| * @author Daniel Qian | |||
| */ | |||
| public interface WxCpMessageHandler { | |||
| /** | |||
| * | |||
| * @param wxMessage | |||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
| * @param wxCpService | |||
| * @param sessionManager | |||
| * @return xml格式的消息,如果在异步规则里处理的话,可以返回null | |||
| */ | |||
| public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService); | |||
| public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, | |||
| Map<String, Object> context, | |||
| WxCpService wxCpService, | |||
| WxSessionManager sessionManager); | |||
| } | |||
| @@ -1,23 +1,29 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import java.util.Map; | |||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
| import java.util.Map; | |||
| /** | |||
| * 微信消息拦截器,可以用来做验证 | |||
| * @author Daniel Qian | |||
| * | |||
| * @author Daniel Qian | |||
| */ | |||
| public interface WxCpMessageInterceptor { | |||
| /** | |||
| * 拦截微信消息 | |||
| * | |||
| * @param wxMessage | |||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
| * @param wxCpService | |||
| * @return true代表OK,false代表不OK | |||
| * @param sessionManager | |||
| * @return true代表OK,false代表不OK | |||
| */ | |||
| public boolean intercept(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService); | |||
| public boolean intercept(WxCpXmlMessage wxMessage, | |||
| Map<String, Object> context, | |||
| WxCpService wxCpService, | |||
| WxSessionManager sessionManager); | |||
| } | |||
| @@ -1,16 +1,23 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import me.chanjar.weixin.common.session.*; | |||
| import me.chanjar.weixin.common.util.WxMessageDuplicateChecker; | |||
| import me.chanjar.weixin.common.util.WxMessageInMemoryDuplicateChecker; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import java.util.ArrayList; | |||
| import java.util.HashMap; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import java.util.concurrent.ExecutionException; | |||
| import java.util.concurrent.ExecutorService; | |||
| import java.util.concurrent.Executors; | |||
| import java.util.concurrent.Future; | |||
| import java.util.regex.Pattern; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | |||
| /** | |||
| * <pre> | |||
| * 微信消息路由器,通过代码化的配置,把来自微信的消息交给handler处理 | |||
| @@ -41,22 +48,58 @@ import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | |||
| */ | |||
| public class WxCpMessageRouter { | |||
| private static final int DEFAULT_THREAD_POOL_SIZE = 20; | |||
| protected final Logger log = LoggerFactory.getLogger(WxCpMessageRouter.class); | |||
| private final List<Rule> rules = new ArrayList<Rule>(); | |||
| private static final int DEFAULT_THREAD_POOL_SIZE = 100; | |||
| private final ExecutorService executorService; | |||
| private final List<Rule> rules = new ArrayList<Rule>(); | |||
| private final WxCpService wxCpService; | |||
| private ExecutorService executorService; | |||
| private WxMessageDuplicateChecker messageDuplicateChecker; | |||
| private WxSessionManager sessionManager; | |||
| public WxCpMessageRouter(WxCpService wxCpService) { | |||
| this.wxCpService = wxCpService; | |||
| this.executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE); | |||
| this.messageDuplicateChecker = new WxMessageInMemoryDuplicateChecker(); | |||
| this.sessionManager = new StandardSessionManager(); | |||
| } | |||
| public WxCpMessageRouter(WxCpService wxMpService, int threadPoolSize) { | |||
| this.wxCpService = wxMpService; | |||
| this.executorService = Executors.newFixedThreadPool(threadPoolSize); | |||
| /** | |||
| * <pre> | |||
| * 设置自定义的 {@link ExecutorService} | |||
| * 如果不调用该方法,默认使用 Executors.newFixedThreadPool(100) | |||
| * </pre> | |||
| * @param executorService | |||
| */ | |||
| public void setExecutorService(ExecutorService executorService) { | |||
| this.executorService = executorService; | |||
| } | |||
| /** | |||
| * <pre> | |||
| * 设置自定义的 {@link me.chanjar.weixin.common.util.WxMessageDuplicateChecker} | |||
| * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.util.WxMessageInMemoryDuplicateChecker} | |||
| * </pre> | |||
| * @param messageDuplicateChecker | |||
| */ | |||
| public void setMessageDuplicateChecker(WxMessageDuplicateChecker messageDuplicateChecker) { | |||
| this.messageDuplicateChecker = messageDuplicateChecker; | |||
| } | |||
| /** | |||
| * <pre> | |||
| * 设置自定义的{@link me.chanjar.weixin.common.session.WxSessionManager} | |||
| * 如果不调用该方法,默认使用 {@linke SessionManagerImpl} | |||
| * </pre> | |||
| * @param sessionManager | |||
| */ | |||
| public void setSessionManager(WxSessionManager sessionManager) { | |||
| this.sessionManager = sessionManager; | |||
| } | |||
| /** | |||
| @@ -64,7 +107,7 @@ public class WxCpMessageRouter { | |||
| * @return | |||
| */ | |||
| public Rule rule() { | |||
| return new Rule(this, wxCpService); | |||
| return new Rule(this, wxCpService, sessionManager); | |||
| } | |||
| /** | |||
| @@ -72,6 +115,11 @@ public class WxCpMessageRouter { | |||
| * @param wxMessage | |||
| */ | |||
| public WxCpXmlOutMessage route(final WxCpXmlMessage wxMessage) { | |||
| if (isDuplicateMessage(wxMessage)) { | |||
| // 如果是重复消息,那么就不做处理 | |||
| return null; | |||
| } | |||
| final List<Rule> matchRules = new ArrayList<Rule>(); | |||
| // 收集匹配的规则 | |||
| for (final Rule rule : rules) { | |||
| @@ -88,27 +136,84 @@ public class WxCpMessageRouter { | |||
| } | |||
| WxCpXmlOutMessage res = null; | |||
| final List<Future> futures = new ArrayList<Future>(); | |||
| for (final Rule rule : matchRules) { | |||
| // 返回最后一个非异步的rule的执行结果 | |||
| if(rule.async) { | |||
| executorService.submit(new Runnable() { | |||
| public void run() { | |||
| rule.service(wxMessage); | |||
| } | |||
| }); | |||
| futures.add( | |||
| executorService.submit(new Runnable() { | |||
| public void run() { | |||
| rule.service(wxMessage); | |||
| } | |||
| }) | |||
| ); | |||
| } else { | |||
| res = rule.service(wxMessage); | |||
| // 在同步操作结束,session访问结束 | |||
| log.debug("End session access: async=false, sessionId={}", wxMessage.getFromUserName()); | |||
| sessionEndAccess(wxMessage); | |||
| } | |||
| } | |||
| if (futures.size() > 0) { | |||
| executorService.submit(new Runnable() { | |||
| @Override | |||
| public void run() { | |||
| for (Future future : futures) { | |||
| try { | |||
| future.get(); | |||
| log.debug("End session access: async=true, sessionId={}", wxMessage.getFromUserName()); | |||
| // 异步操作结束,session访问结束 | |||
| sessionEndAccess(wxMessage); | |||
| } catch (InterruptedException e) { | |||
| log.error("Error happened when wait task finish", e); | |||
| } catch (ExecutionException e) { | |||
| log.error("Error happened when wait task finish", e); | |||
| } | |||
| } | |||
| } | |||
| }); | |||
| } | |||
| return res; | |||
| } | |||
| protected boolean isDuplicateMessage(WxCpXmlMessage wxMessage) { | |||
| String messageId = ""; | |||
| if (wxMessage.getMsgId() == null) { | |||
| messageId = wxMessage.getFromUserName() + "-" + String.valueOf(wxMessage.getCreateTime()); | |||
| } else { | |||
| messageId = String.valueOf(wxMessage.getMsgId()); | |||
| } | |||
| if (messageDuplicateChecker.isDuplicate(messageId)) { | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| /** | |||
| * 对session的访问结束 | |||
| * @param wxMessage | |||
| */ | |||
| protected void sessionEndAccess(WxCpXmlMessage wxMessage) { | |||
| InternalSession session = ((InternalSessionManager)sessionManager).findSession(wxMessage.getFromUserName()); | |||
| if (session != null) { | |||
| session.endAccess(); | |||
| } | |||
| } | |||
| public static class Rule { | |||
| private final WxCpMessageRouter routerBuilder; | |||
| private final WxCpService wxCpService; | |||
| private final WxSessionManager sessionManager; | |||
| private boolean async = true; | |||
| private String fromUser; | |||
| @@ -131,9 +236,10 @@ public class WxCpMessageRouter { | |||
| private List<WxCpMessageInterceptor> interceptors = new ArrayList<WxCpMessageInterceptor>(); | |||
| protected Rule(WxCpMessageRouter routerBuilder, WxCpService wxCpService) { | |||
| protected Rule(WxCpMessageRouter routerBuilder, WxCpService wxCpService, WxSessionManager sessionManager) { | |||
| this.routerBuilder = routerBuilder; | |||
| this.wxCpService = wxCpService; | |||
| this.sessionManager = sessionManager; | |||
| } | |||
| /** | |||
| @@ -311,7 +417,7 @@ public class WxCpMessageRouter { | |||
| Map<String, Object> context = new HashMap<String, Object>(); | |||
| // 如果拦截器不通过 | |||
| for (WxCpMessageInterceptor interceptor : this.interceptors) { | |||
| if (!interceptor.intercept(wxMessage, context, wxCpService)) { | |||
| if (!interceptor.intercept(wxMessage, context, wxCpService, sessionManager)) { | |||
| return null; | |||
| } | |||
| } | |||
| @@ -320,7 +426,7 @@ public class WxCpMessageRouter { | |||
| WxCpXmlOutMessage res = null; | |||
| for (WxCpMessageHandler handler : this.handlers) { | |||
| // 返回最后handler的结果 | |||
| res = handler.handle(wxMessage, context, wxCpService); | |||
| res = handler.handle(wxMessage, context, wxCpService, sessionManager); | |||
| } | |||
| return res; | |||
| } | |||
| @@ -1,17 +1,20 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import java.io.File; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.util.List; | |||
| import me.chanjar.weixin.common.bean.WxMenu; | |||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import me.chanjar.weixin.common.session.WxSession; | |||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||
| import me.chanjar.weixin.common.util.http.RequestExecutor; | |||
| import me.chanjar.weixin.cp.bean.*; | |||
| import me.chanjar.weixin.cp.bean.WxCpDepart; | |||
| import me.chanjar.weixin.cp.bean.WxCpMessage; | |||
| import me.chanjar.weixin.cp.bean.WxCpTag; | |||
| import me.chanjar.weixin.cp.bean.WxCpUser; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import java.io.File; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.util.List; | |||
| /** | |||
| * 微信API的Service | |||
| @@ -42,6 +45,14 @@ public interface WxCpService { | |||
| */ | |||
| public void userAuthenticated(String userId) throws WxErrorException; | |||
| /** | |||
| * 获取access_token, 不强制刷新access_token | |||
| * @see #getAccessToken(boolean) | |||
| * @return | |||
| * @throws WxErrorException | |||
| */ | |||
| public String getAccessToken() throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 获取access_token,本方法线程安全 | |||
| @@ -50,10 +61,11 @@ public interface WxCpService { | |||
| * 程序员在非必要情况下尽量不要主动调用此方法 | |||
| * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token | |||
| * </pre> | |||
| * | |||
| * @param forceRefresh 强制刷新 | |||
| * @return | |||
| * @throws me.chanjar.weixin.common.exception.WxErrorException | |||
| */ | |||
| public void accessTokenRefresh() throws WxErrorException; | |||
| public String getAccessToken(boolean forceRefresh) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| @@ -343,4 +355,46 @@ public interface WxCpService { | |||
| * @param wxConfigProvider | |||
| */ | |||
| public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider); | |||
| /** | |||
| * <pre> | |||
| * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试 | |||
| * 默认:1000ms | |||
| * </pre> | |||
| * @param retrySleepMillis | |||
| */ | |||
| void setRetrySleepMillis(int retrySleepMillis); | |||
| /** | |||
| * <pre> | |||
| * 设置当微信系统响应系统繁忙时,最大重试次数 | |||
| * 默认:5次 | |||
| * </pre> | |||
| * @param maxRetryTimes | |||
| */ | |||
| void setMaxRetryTimes(int maxRetryTimes); | |||
| /** | |||
| * 获取某个sessionId对应的session,如果sessionId没有对应的session,则新建一个并返回。 | |||
| * @param id id可以为任意字符串,建议使用FromUserName作为id | |||
| * @return | |||
| */ | |||
| WxSession getSession(String id); | |||
| /** | |||
| * 获取某个sessionId对应的session,如果sessionId没有对应的session,若create为true则新建一个,否则返回null。 | |||
| * @param id id可以为任意字符串,建议使用FromUserName作为id | |||
| * @param create | |||
| * @return | |||
| */ | |||
| WxSession getSession(String id, boolean create); | |||
| /** | |||
| * <pre> | |||
| * 设置WxSessionManager,只有当需要使用个性化的WxSessionManager的时候才需要调用此方法, | |||
| * WxCpService默认使用的是{@link me.chanjar.weixin.common.session.StandardSessionManager} | |||
| * </pre> | |||
| * @param sessionManager | |||
| */ | |||
| void setSessionManager(WxSessionManager sessionManager); | |||
| } | |||
| @@ -12,6 +12,9 @@ import me.chanjar.weixin.common.bean.WxMenu; | |||
| import me.chanjar.weixin.common.bean.result.WxError; | |||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import me.chanjar.weixin.common.session.StandardSessionManager; | |||
| import me.chanjar.weixin.common.session.WxSession; | |||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||
| import me.chanjar.weixin.common.util.StringUtils; | |||
| import me.chanjar.weixin.common.util.crypto.SHA1; | |||
| import me.chanjar.weixin.common.util.fs.FileUtils; | |||
| @@ -34,6 +37,8 @@ import org.apache.http.impl.client.BasicCredentialsProvider; | |||
| import org.apache.http.impl.client.BasicResponseHandler; | |||
| import org.apache.http.impl.client.CloseableHttpClient; | |||
| import org.apache.http.impl.client.HttpClients; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import java.io.File; | |||
| import java.io.IOException; | |||
| @@ -41,25 +46,28 @@ import java.io.InputStream; | |||
| import java.io.StringReader; | |||
| import java.util.List; | |||
| import java.util.UUID; | |||
| import java.util.concurrent.atomic.AtomicBoolean; | |||
| public class WxCpServiceImpl implements WxCpService { | |||
| protected final Logger log = LoggerFactory.getLogger(WxCpServiceImpl.class); | |||
| /** | |||
| * 全局的是否正在刷新Access Token的flag | |||
| * true: 正在刷新 | |||
| * false: 没有刷新 | |||
| * 全局的是否正在刷新access token的锁 | |||
| */ | |||
| protected static final AtomicBoolean GLOBAL_ACCESS_TOKEN_REFRESH_FLAG = new AtomicBoolean(false); | |||
| protected static final Object GLOBAL_ACCESS_TOKEN_REFRESH_LOCK = new Object(); | |||
| protected WxCpConfigStorage wxCpConfigStorage; | |||
| protected final ThreadLocal<Integer> retryTimes = new ThreadLocal<Integer>(); | |||
| protected CloseableHttpClient httpClient; | |||
| protected HttpHost httpProxy; | |||
| private int retrySleepMillis = 1000; | |||
| private int maxRetryTimes = 5; | |||
| protected WxSessionManager sessionManager = new StandardSessionManager(); | |||
| public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) { | |||
| try { | |||
| return SHA1.gen(wxCpConfigStorage.getToken(), timestamp, nonce, data).equals(msgSignature); | |||
| @@ -73,45 +81,44 @@ public class WxCpServiceImpl implements WxCpService { | |||
| execute(new SimpleGetRequestExecutor(), url, null); | |||
| } | |||
| public void accessTokenRefresh() throws WxErrorException { | |||
| if (!GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.getAndSet(true)) { | |||
| try { | |||
| String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?" | |||
| + "&corpid=" + wxCpConfigStorage.getCorpId() | |||
| + "&corpsecret=" + wxCpConfigStorage.getCorpSecret(); | |||
| try { | |||
| HttpGet httpGet = new HttpGet(url); | |||
| if (httpProxy != null) { | |||
| RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); | |||
| httpGet.setConfig(config); | |||
| } | |||
| CloseableHttpClient httpclient = getHttpclient(); | |||
| CloseableHttpResponse response = httpclient.execute(httpGet); | |||
| String resultContent = new BasicResponseHandler().handleResponse(response); | |||
| WxError error = WxError.fromJson(resultContent); | |||
| if (error.getErrorCode() != 0) { | |||
| throw new WxErrorException(error); | |||
| public String getAccessToken() throws WxErrorException { | |||
| return getAccessToken(false); | |||
| } | |||
| public String getAccessToken(boolean forceRefresh) throws WxErrorException { | |||
| if (forceRefresh) { | |||
| wxCpConfigStorage.expireAccessToken(); | |||
| } | |||
| if (wxCpConfigStorage.isAccessTokenExpired()) { | |||
| synchronized (GLOBAL_ACCESS_TOKEN_REFRESH_LOCK) { | |||
| if (wxCpConfigStorage.isAccessTokenExpired()) { | |||
| String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?" | |||
| + "&corpid=" + wxCpConfigStorage.getCorpId() | |||
| + "&corpsecret=" + wxCpConfigStorage.getCorpSecret(); | |||
| try { | |||
| HttpGet httpGet = new HttpGet(url); | |||
| if (httpProxy != null) { | |||
| RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); | |||
| httpGet.setConfig(config); | |||
| } | |||
| CloseableHttpClient httpclient = getHttpclient(); | |||
| CloseableHttpResponse response = httpclient.execute(httpGet); | |||
| String resultContent = new BasicResponseHandler().handleResponse(response); | |||
| WxError error = WxError.fromJson(resultContent); | |||
| if (error.getErrorCode() != 0) { | |||
| throw new WxErrorException(error); | |||
| } | |||
| WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); | |||
| wxCpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } catch (ClientProtocolException e) { | |||
| throw new RuntimeException(e); | |||
| } catch (IOException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); | |||
| wxCpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } catch (ClientProtocolException e) { | |||
| throw new RuntimeException(e); | |||
| } catch (IOException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| } finally { | |||
| GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.set(false); | |||
| } | |||
| } else { | |||
| // 每隔100ms检查一下是否刷新完毕了 | |||
| while (GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.get()) { | |||
| try { | |||
| Thread.sleep(100); | |||
| } catch (InterruptedException e) { | |||
| } | |||
| } | |||
| // 刷新完毕了,就没他什么事儿了 | |||
| } | |||
| return wxCpConfigStorage.getAccessToken(); | |||
| } | |||
| public void messageSend(WxCpMessage message) throws WxErrorException { | |||
| @@ -369,10 +376,34 @@ public class WxCpServiceImpl implements WxCpService { | |||
| * @throws WxErrorException | |||
| */ | |||
| public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
| if (StringUtils.isBlank(wxCpConfigStorage.getAccessToken())) { | |||
| accessTokenRefresh(); | |||
| } | |||
| String accessToken = wxCpConfigStorage.getAccessToken(); | |||
| int retryTimes = 0; | |||
| do { | |||
| try { | |||
| return executeInternal(executor, uri, data); | |||
| } catch (WxErrorException e) { | |||
| WxError error = e.getError(); | |||
| /** | |||
| * -1 系统繁忙, 1000ms后重试 | |||
| */ | |||
| if (error.getErrorCode() == -1) { | |||
| int sleepMillis = retrySleepMillis * (1 << retryTimes); | |||
| try { | |||
| log.debug("微信系统繁忙,{}ms 后重试(第{}次)", sleepMillis, retryTimes + 1); | |||
| Thread.sleep(sleepMillis); | |||
| } catch (InterruptedException e1) { | |||
| throw new RuntimeException(e1); | |||
| } | |||
| } else { | |||
| throw e; | |||
| } | |||
| } | |||
| } while(++retryTimes < maxRetryTimes); | |||
| throw new RuntimeException("微信服务端异常,超出重试次数"); | |||
| } | |||
| protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
| String accessToken = getAccessToken(false); | |||
| String uriWithAccessToken = uri; | |||
| uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken; | |||
| @@ -387,30 +418,10 @@ public class WxCpServiceImpl implements WxCpService { | |||
| * 42001 access_token超时 | |||
| */ | |||
| if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { | |||
| accessTokenRefresh(); | |||
| // 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | |||
| wxCpConfigStorage.expireAccessToken(); | |||
| return execute(executor, uri, data); | |||
| } | |||
| /** | |||
| * -1 系统繁忙, 1000ms后重试 | |||
| */ | |||
| if (error.getErrorCode() == -1) { | |||
| if (retryTimes.get() == null) { | |||
| retryTimes.set(0); | |||
| } | |||
| if (retryTimes.get() > 4) { | |||
| retryTimes.set(0); | |||
| throw new RuntimeException("微信服务端异常,超出重试次数"); | |||
| } | |||
| int sleepMillis = 1000 * (1 << retryTimes.get()); | |||
| try { | |||
| System.out.println("微信系统繁忙," + sleepMillis + "ms后重试"); | |||
| Thread.sleep(sleepMillis); | |||
| retryTimes.set(retryTimes.get() + 1); | |||
| return execute(executor, uri, data); | |||
| } catch (InterruptedException e1) { | |||
| throw new RuntimeException(e1); | |||
| } | |||
| } | |||
| if (error.getErrorCode() != 0) { | |||
| throw new WxErrorException(error); | |||
| } | |||
| @@ -421,7 +432,6 @@ public class WxCpServiceImpl implements WxCpService { | |||
| throw new RuntimeException(e); | |||
| } | |||
| } | |||
| protected CloseableHttpClient getHttpclient() { | |||
| return httpClient; | |||
| } | |||
| @@ -456,4 +466,37 @@ public class WxCpServiceImpl implements WxCpService { | |||
| } | |||
| } | |||
| @Override | |||
| public void setRetrySleepMillis(int retrySleepMillis) { | |||
| this.retrySleepMillis = retrySleepMillis; | |||
| } | |||
| @Override | |||
| public void setMaxRetryTimes(int maxRetryTimes) { | |||
| this.maxRetryTimes = maxRetryTimes; | |||
| } | |||
| @Override | |||
| public WxSession getSession(String id) { | |||
| if (sessionManager == null) { | |||
| return null; | |||
| } | |||
| return sessionManager.getSession(id); | |||
| } | |||
| @Override | |||
| public WxSession getSession(String id, boolean create) { | |||
| if (sessionManager == null) { | |||
| return null; | |||
| } | |||
| return sessionManager.getSession(id, create); | |||
| } | |||
| @Override | |||
| public void setSessionManager(WxSessionManager sessionManager) { | |||
| this.sessionManager = sessionManager; | |||
| } | |||
| } | |||
| @@ -1,11 +1,11 @@ | |||
| package me.chanjar.weixin.cp.bean; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import me.chanjar.weixin.cp.bean.messagebuilder.*; | |||
| import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| /** | |||
| * 消息 | |||
| * @author Daniel Qian | |||
| @@ -21,6 +21,8 @@ public class WxCpUser { | |||
| private String tel; | |||
| private String email; | |||
| private String weiXinId; | |||
| private String avatar; | |||
| private Integer status; | |||
| private final List<Attr> extAttrs = new ArrayList<Attr>(); | |||
| public String getUserId() { | |||
| @@ -95,6 +97,22 @@ public class WxCpUser { | |||
| this.weiXinId = weiXinId; | |||
| } | |||
| public String getAvatar() { | |||
| return avatar; | |||
| } | |||
| public void setAvatar(String avatar) { | |||
| this.avatar = avatar; | |||
| } | |||
| public Integer getStatus() { | |||
| return status; | |||
| } | |||
| public void setStatus(Integer status) { | |||
| this.status = status; | |||
| } | |||
| public void addExtAttr(String name, String value) { | |||
| this.extAttrs.add(new Attr(name, value)); | |||
| } | |||
| @@ -1,14 +1,13 @@ | |||
| package me.chanjar.weixin.cp.bean; | |||
| import me.chanjar.weixin.common.util.xml.AdapterCDATA; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; | |||
| import me.chanjar.weixin.cp.api.WxCpConfigStorage; | |||
| import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil; | |||
| import me.chanjar.weixin.cp.util.xml.XmlTransformer; | |||
| import me.chanjar.weixin.cp.util.xml.XStreamTransformer; | |||
| import org.apache.commons.io.IOUtils; | |||
| import javax.xml.bind.JAXBException; | |||
| import javax.xml.bind.annotation.*; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.util.ArrayList; | |||
| @@ -25,103 +24,102 @@ import java.util.List; | |||
| * | |||
| * @author Daniel Qian | |||
| */ | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxCpXmlMessage { | |||
| /////////////////////// | |||
| // 以下都是微信推送过来的消息的xml的element所对应的属性 | |||
| /////////////////////// | |||
| @XmlElement(name="AgentID") | |||
| @XStreamAlias("AgentID") | |||
| private Integer agentId; | |||
| @XmlElement(name = "ToUserName") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("ToUserName") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String toUserName; | |||
| @XmlElement(name = "FromUserName") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("FromUserName") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String fromUserName; | |||
| @XmlElement(name = "CreateTime") | |||
| @XStreamAlias("CreateTime") | |||
| private Long createTime; | |||
| @XmlElement(name = "MsgType") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("MsgType") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String msgType; | |||
| @XmlElement(name = "Content") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Content") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String content; | |||
| @XmlElement(name = "MsgId") | |||
| @XStreamAlias("MsgId") | |||
| private Long msgId; | |||
| @XmlElement(name = "PicUrl") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("PicUrl") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String picUrl; | |||
| @XmlElement(name = "MediaId") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("MediaId") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String mediaId; | |||
| @XmlElement(name = "Format") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Format") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String format; | |||
| @XmlElement(name = "ThumbMediaId") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("ThumbMediaId") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String thumbMediaId; | |||
| @XmlElement(name = "Location_X") | |||
| @XStreamAlias("Location_X") | |||
| private Double locationX; | |||
| @XmlElement(name = "Location_Y") | |||
| @XStreamAlias("Location_Y") | |||
| private Double locationY; | |||
| @XmlElement(name = "Scale") | |||
| @XStreamAlias("Scale") | |||
| private Double scale; | |||
| @XmlElement(name = "Label") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Label") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String label; | |||
| @XmlElement(name = "Title") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Title") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String title; | |||
| @XmlElement(name = "Description") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Description") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String description; | |||
| @XmlElement(name = "Url") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Url") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String url; | |||
| @XmlElement(name = "Event") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Event") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String event; | |||
| @XmlElement(name = "EventKey") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("EventKey") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String eventKey; | |||
| @XmlElement(name = "Ticket") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Ticket") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String ticket; | |||
| @XmlElement(name = "Latitude") | |||
| @XStreamAlias("Latitude") | |||
| private Double latitude; | |||
| @XmlElement(name = "Longitude") | |||
| @XStreamAlias("Longitude") | |||
| private Double longitude; | |||
| @XmlElement(name = "Precision") | |||
| @XStreamAlias("Precision") | |||
| private Double precision; | |||
| @XmlElement(name = "Recognition") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Recognition") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String recognition; | |||
| /////////////////////////////////////// | |||
| @@ -130,37 +128,37 @@ public class WxCpXmlMessage { | |||
| /** | |||
| * 群发的结果 | |||
| */ | |||
| @XmlElement(name = "Status") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Status") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String status; | |||
| /** | |||
| * group_id下粉丝数;或者openid_list中的粉丝数 | |||
| */ | |||
| @XmlElement(name = "TotalCount") | |||
| @XStreamAlias("TotalCount") | |||
| private Integer totalCount; | |||
| /** | |||
| * 过滤(过滤是指特定地区、性别的过滤、用户设置拒收的过滤,用户接收已超4条的过滤)后,准备发送的粉丝数,原则上,filterCount = sentCount + errorCount | |||
| */ | |||
| @XmlElement(name = "FilterCount") | |||
| @XStreamAlias("FilterCount") | |||
| private Integer filterCount; | |||
| /** | |||
| * 发送成功的粉丝数 | |||
| */ | |||
| @XmlElement(name = "SentCount") | |||
| @XStreamAlias("SentCount") | |||
| private Integer sentCount; | |||
| /** | |||
| * 发送失败的粉丝数 | |||
| */ | |||
| @XmlElement(name = "ErrorCount") | |||
| @XStreamAlias("ErrorCount") | |||
| private Integer errorCount; | |||
| @XmlElement(name = "ScanCodeInfo") | |||
| @XStreamAlias("ScanCodeInfo") | |||
| private ScanCodeInfo scanCodeInfo = new ScanCodeInfo(); | |||
| @XmlElement(name = "SendPicsInfo") | |||
| @XStreamAlias("SendPicsInfo") | |||
| private SendPicsInfo sendPicsInfo = new SendPicsInfo(); | |||
| @XmlElement(name = "SendLocationInfo") | |||
| @XStreamAlias("SendLocationInfo") | |||
| private SendLocationInfo sendLocationInfo = new SendLocationInfo(); | |||
| public Integer getAgentId() { | |||
| @@ -390,19 +388,11 @@ public class WxCpXmlMessage { | |||
| } | |||
| protected static WxCpXmlMessage fromXml(String xml) { | |||
| try { | |||
| return XmlTransformer.fromXml(WxCpXmlMessage.class, xml); | |||
| } catch (JAXBException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| return XStreamTransformer.fromXml(WxCpXmlMessage.class, xml); | |||
| } | |||
| protected static WxCpXmlMessage fromXml(InputStream is) { | |||
| try { | |||
| return XmlTransformer.fromXml(WxCpXmlMessage.class, is); | |||
| } catch (JAXBException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| return XStreamTransformer.fromXml(WxCpXmlMessage.class, is); | |||
| } | |||
| /** | |||
| @@ -499,16 +489,15 @@ public class WxCpXmlMessage { | |||
| this.sendLocationInfo = sendLocationInfo; | |||
| } | |||
| @XmlRootElement(name = "ScanCodeInfo") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("ScanCodeInfo") | |||
| public static class ScanCodeInfo { | |||
| @XmlElement(name = "ScanType") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("ScanType") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String scanType; | |||
| @XmlElement(name = "ScanResult") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("ScanResult") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String scanResult; | |||
| /** | |||
| @@ -538,15 +527,13 @@ public class WxCpXmlMessage { | |||
| } | |||
| @XmlRootElement(name = "SendPicsInfo") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("SendPicsInfo") | |||
| public static class SendPicsInfo { | |||
| @XmlElement(name = "Count") | |||
| @XStreamAlias("Count") | |||
| private Long count; | |||
| @XmlElementWrapper(name="PicList") | |||
| @XmlElement(name = "item") | |||
| @XStreamAlias("PicList") | |||
| protected final List<Item> picList = new ArrayList<Item>(); | |||
| public Long getCount() { | |||
| @@ -561,13 +548,11 @@ public class WxCpXmlMessage { | |||
| return picList; | |||
| } | |||
| @XmlRootElement(name = "item") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XmlType(name = "WxXmlMessage.SendPicsInfo.Item") | |||
| @XStreamAlias("item") | |||
| public static class Item { | |||
| @XmlElement(name = "PicMd5Sum") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("PicMd5Sum") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String PicMd5Sum; | |||
| public String getPicMd5Sum() { | |||
| @@ -580,28 +565,27 @@ public class WxCpXmlMessage { | |||
| } | |||
| } | |||
| @XmlRootElement(name = "SendLocationInfo") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("SendLocationInfo") | |||
| public static class SendLocationInfo { | |||
| @XmlElement(name = "Location_X") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Location_X") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String locationX; | |||
| @XmlElement(name = "Location_Y") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Location_Y") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String locationY; | |||
| @XmlElement(name = "Scale") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Scale") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String scale; | |||
| @XmlElement(name = "Label") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Label") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String label; | |||
| @XmlElement(name = "Poiname") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Poiname") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String poiname; | |||
| public String getLocationX() { | |||
| @@ -1,20 +1,15 @@ | |||
| package me.chanjar.weixin.cp.bean; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.MediaIdMarshaller; | |||
| import me.chanjar.weixin.common.util.xml.XStreamMediaIdConverter; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlElement; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxCpXmlOutImageMessage extends WxCpXmlOutMessage { | |||
| @XmlElement(name="Image") | |||
| @XmlJavaTypeAdapter(MediaIdMarshaller.class) | |||
| @XStreamAlias("Image") | |||
| @XStreamConverter(value=XStreamMediaIdConverter.class) | |||
| private String mediaId; | |||
| public WxCpXmlOutImageMessage() { | |||
| @@ -1,35 +1,29 @@ | |||
| package me.chanjar.weixin.cp.bean; | |||
| import me.chanjar.weixin.common.util.xml.AdapterCDATA; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; | |||
| import me.chanjar.weixin.cp.api.WxCpConfigStorage; | |||
| import me.chanjar.weixin.cp.bean.outxmlbuilder.*; | |||
| import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil; | |||
| import me.chanjar.weixin.cp.util.xml.XmlTransformer; | |||
| import me.chanjar.weixin.cp.util.xml.XStreamTransformer; | |||
| import javax.xml.bind.JAXBException; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlElement; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| @XStreamAlias("xml") | |||
| public abstract class WxCpXmlOutMessage { | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| public class WxCpXmlOutMessage { | |||
| @XmlElement(name="ToUserName") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("ToUserName") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| protected String toUserName; | |||
| @XmlElement(name="FromUserName") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("FromUserName") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| protected String fromUserName; | |||
| @XmlElement(name="CreateTime") | |||
| @XStreamAlias("CreateTime") | |||
| protected Long createTime; | |||
| @XmlElement(name="MsgType") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("MsgType") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| protected String msgType; | |||
| public String getToUserName() { | |||
| @@ -65,11 +59,7 @@ public class WxCpXmlOutMessage { | |||
| } | |||
| protected String toXml() { | |||
| try { | |||
| return XmlTransformer.toXml((Class)this.getClass(), this); | |||
| } catch (JAXBException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| return XStreamTransformer.toXml((Class)this.getClass(), this); | |||
| } | |||
| /** | |||
| @@ -1,22 +1,20 @@ | |||
| package me.chanjar.weixin.cp.bean; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.AdapterCDATA; | |||
| import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; | |||
| import javax.xml.bind.annotation.*; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxCpXmlOutNewsMessage extends WxCpXmlOutMessage { | |||
| @XmlElement(name = "ArticleCount") | |||
| @XStreamAlias("ArticleCount") | |||
| protected int articleCount; | |||
| @XmlElementWrapper(name="Articles") | |||
| @XmlElement(name = "item") | |||
| @XStreamAlias("Articles") | |||
| protected final List<Item> articles = new ArrayList<Item>(); | |||
| public WxCpXmlOutNewsMessage() { | |||
| @@ -37,24 +35,23 @@ public class WxCpXmlOutNewsMessage extends WxCpXmlOutMessage { | |||
| } | |||
| @XmlRootElement(name = "Item") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("item") | |||
| public static class Item { | |||
| @XmlElement(name = "Title") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Title") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String Title; | |||
| @XmlElement(name = "Description") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Description") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String Description; | |||
| @XmlElement(name="PicUrl") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("PicUrl") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String PicUrl; | |||
| @XmlElement(name="Url") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Url") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String Url; | |||
| public String getTitle() { | |||
| @@ -1,20 +1,15 @@ | |||
| package me.chanjar.weixin.cp.bean; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlElement; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.AdapterCDATA; | |||
| import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxCpXmlOutTextMessage extends WxCpXmlOutMessage { | |||
| @XmlElement(name="Content") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Content") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String content; | |||
| public WxCpXmlOutTextMessage() { | |||
| @@ -1,19 +1,14 @@ | |||
| package me.chanjar.weixin.cp.bean; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlElement; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.AdapterCDATA; | |||
| import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxCpXmlOutVideoMessage extends WxCpXmlOutMessage { | |||
| @XmlElement(name = "Video") | |||
| @XStreamAlias("Video") | |||
| protected final Video video = new Video(); | |||
| public WxCpXmlOutVideoMessage() { | |||
| @@ -45,20 +40,19 @@ public class WxCpXmlOutVideoMessage extends WxCpXmlOutMessage { | |||
| } | |||
| @XmlRootElement(name = "Video") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| private static class Video { | |||
| @XStreamAlias("Video") | |||
| public static class Video { | |||
| @XmlElement(name = "MediaId") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("MediaId") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String mediaId; | |||
| @XmlElement(name = "Title") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Title") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String title; | |||
| @XmlElement(name = "Description") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Description") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String description; | |||
| public String getMediaId() { | |||
| @@ -1,20 +1,15 @@ | |||
| package me.chanjar.weixin.cp.bean; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlElement; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.MediaIdMarshaller; | |||
| import me.chanjar.weixin.common.util.xml.XStreamMediaIdConverter; | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxCpXmlOutVoiceMessage extends WxCpXmlOutMessage { | |||
| @XmlElement(name="Voice") | |||
| @XmlJavaTypeAdapter(MediaIdMarshaller.class) | |||
| @XStreamAlias("Voice") | |||
| @XStreamConverter(value=XStreamMediaIdConverter.class) | |||
| private String mediaId; | |||
| public WxCpXmlOutVoiceMessage() { | |||
| @@ -1,11 +1,11 @@ | |||
| package me.chanjar.weixin.cp.bean.outxmlbuilder; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutNewsMessage; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutNewsMessage.Item; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| /** | |||
| * 图文消息builder | |||
| * @author Daniel Qian | |||
| @@ -8,18 +8,11 @@ | |||
| */ | |||
| package me.chanjar.weixin.cp.util.json; | |||
| import java.lang.reflect.Type; | |||
| import com.google.gson.*; | |||
| import me.chanjar.weixin.common.util.json.GsonHelper; | |||
| import me.chanjar.weixin.cp.bean.WxCpDepart; | |||
| import com.google.gson.JsonDeserializationContext; | |||
| import com.google.gson.JsonDeserializer; | |||
| import com.google.gson.JsonElement; | |||
| import com.google.gson.JsonObject; | |||
| import com.google.gson.JsonParseException; | |||
| import com.google.gson.JsonSerializationContext; | |||
| import com.google.gson.JsonSerializer; | |||
| import java.lang.reflect.Type; | |||
| /** | |||
| * @author Daniel Qian | |||
| @@ -2,11 +2,12 @@ package me.chanjar.weixin.cp.util.json; | |||
| import com.google.gson.Gson; | |||
| import com.google.gson.GsonBuilder; | |||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | |||
| import me.chanjar.weixin.common.bean.result.WxError; | |||
| import me.chanjar.weixin.common.util.json.WxMediaUploadResultAdapter; | |||
| import me.chanjar.weixin.common.util.json.WxErrorAdapter; | |||
| import me.chanjar.weixin.cp.bean.*; | |||
| import me.chanjar.weixin.cp.bean.WxCpDepart; | |||
| import me.chanjar.weixin.cp.bean.WxCpMessage; | |||
| import me.chanjar.weixin.cp.bean.WxCpTag; | |||
| import me.chanjar.weixin.cp.bean.WxCpUser; | |||
| public class WxCpGsonBuilder { | |||
| @@ -8,12 +8,12 @@ | |||
| */ | |||
| package me.chanjar.weixin.cp.util.json; | |||
| import java.lang.reflect.Type; | |||
| import com.google.gson.*; | |||
| import me.chanjar.weixin.common.util.json.GsonHelper; | |||
| import me.chanjar.weixin.cp.bean.WxCpUser; | |||
| import java.lang.reflect.Type; | |||
| /** | |||
| * @author Daniel Qian | |||
| */ | |||
| @@ -49,6 +49,8 @@ public class WxCpUserGsonAdapter implements JsonDeserializer<WxCpUser>, JsonSeri | |||
| user.setTel(GsonHelper.getString(o, "tel")); | |||
| user.setEmail(GsonHelper.getString(o, "email")); | |||
| user.setWeiXinId(GsonHelper.getString(o, "weixinid")); | |||
| user.setAvatar(GsonHelper.getString(o, "avatar")); | |||
| user.setStatus(GsonHelper.getInteger(o, "status")); | |||
| if (GsonHelper.isNotNull(o.get("extattr"))) { | |||
| JsonArray attrJsonElements = o.get("extattr").getAsJsonObject().get("attrs").getAsJsonArray(); | |||
| @@ -97,6 +99,12 @@ public class WxCpUserGsonAdapter implements JsonDeserializer<WxCpUser>, JsonSeri | |||
| if (user.getWeiXinId() != null) { | |||
| o.addProperty("weixinid", user.getWeiXinId()); | |||
| } | |||
| if (user.getAvatar() != null) { | |||
| o.addProperty("avatar", user.getAvatar()); | |||
| } | |||
| if (user.getStatus() != null) { | |||
| o.addProperty("status", user.getStatus()); | |||
| } | |||
| if (user.getExtAttrs().size() > 0) { | |||
| JsonArray attrsJsonArray = new JsonArray(); | |||
| for (WxCpUser.Attr attr : user.getExtAttrs()) { | |||
| @@ -0,0 +1,103 @@ | |||
| package me.chanjar.weixin.cp.util.xml; | |||
| import com.thoughtworks.xstream.XStream; | |||
| import me.chanjar.weixin.common.util.xml.XStreamInitializer; | |||
| import me.chanjar.weixin.cp.bean.*; | |||
| import java.io.InputStream; | |||
| import java.util.HashMap; | |||
| import java.util.Map; | |||
| public class XStreamTransformer { | |||
| protected static final Map<Class, XStream> CLASS_2_XSTREAM_INSTANCE = configXStreamInstance(); | |||
| /** | |||
| * xml -> pojo | |||
| * | |||
| * @param clazz | |||
| * @param xml | |||
| * @return | |||
| */ | |||
| @SuppressWarnings("unchecked") | |||
| public static <T> T fromXml(Class<T> clazz, String xml) { | |||
| T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(xml); | |||
| return object; | |||
| } | |||
| @SuppressWarnings("unchecked") | |||
| public static <T> T fromXml(Class<T> clazz, InputStream is) { | |||
| T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(is); | |||
| return object; | |||
| } | |||
| /** | |||
| * pojo -> xml | |||
| * | |||
| * @param clazz | |||
| * @param object | |||
| * @return | |||
| */ | |||
| public static <T> String toXml(Class<T> clazz, T object) { | |||
| return CLASS_2_XSTREAM_INSTANCE.get(clazz).toXML(object); | |||
| } | |||
| private static Map<Class, XStream> configXStreamInstance() { | |||
| Map<Class, XStream> map = new HashMap<Class, XStream>(); | |||
| map.put(WxCpXmlMessage.class, config_WxCpXmlMessage()); | |||
| map.put(WxCpXmlOutNewsMessage.class, config_WxCpXmlOutNewsMessage()); | |||
| map.put(WxCpXmlOutTextMessage.class, config_WxCpXmlOutTextMessage()); | |||
| map.put(WxCpXmlOutImageMessage.class, config_WxCpXmlOutImageMessage()); | |||
| map.put(WxCpXmlOutVideoMessage.class, config_WxCpXmlOutVideoMessage()); | |||
| map.put(WxCpXmlOutVoiceMessage.class, config_WxCpXmlOutVoiceMessage()); | |||
| return map; | |||
| } | |||
| private static XStream config_WxCpXmlMessage() { | |||
| XStream xstream = XStreamInitializer.getInstance(); | |||
| xstream.processAnnotations(WxCpXmlMessage.class); | |||
| xstream.processAnnotations(WxCpXmlMessage.ScanCodeInfo.class); | |||
| xstream.processAnnotations(WxCpXmlMessage.SendPicsInfo.class); | |||
| xstream.processAnnotations(WxCpXmlMessage.SendPicsInfo.Item.class); | |||
| xstream.processAnnotations(WxCpXmlMessage.SendLocationInfo.class); | |||
| return xstream; | |||
| } | |||
| private static XStream config_WxCpXmlOutImageMessage() { | |||
| XStream xstream = XStreamInitializer.getInstance(); | |||
| xstream.processAnnotations(WxCpXmlOutMessage.class); | |||
| xstream.processAnnotations(WxCpXmlOutImageMessage.class); | |||
| return xstream; | |||
| } | |||
| private static XStream config_WxCpXmlOutNewsMessage() { | |||
| XStream xstream = XStreamInitializer.getInstance(); | |||
| xstream.processAnnotations(WxCpXmlOutMessage.class); | |||
| xstream.processAnnotations(WxCpXmlOutNewsMessage.class); | |||
| xstream.processAnnotations(WxCpXmlOutNewsMessage.Item.class); | |||
| return xstream; | |||
| } | |||
| private static XStream config_WxCpXmlOutTextMessage() { | |||
| XStream xstream = XStreamInitializer.getInstance(); | |||
| xstream.processAnnotations(WxCpXmlOutMessage.class); | |||
| xstream.processAnnotations(WxCpXmlOutTextMessage.class); | |||
| return xstream; | |||
| } | |||
| private static XStream config_WxCpXmlOutVideoMessage() { | |||
| XStream xstream = XStreamInitializer.getInstance(); | |||
| xstream.processAnnotations(WxCpXmlOutMessage.class); | |||
| xstream.processAnnotations(WxCpXmlOutVideoMessage.class); | |||
| xstream.processAnnotations(WxCpXmlOutVideoMessage.Video.class); | |||
| return xstream; | |||
| } | |||
| private static XStream config_WxCpXmlOutVoiceMessage() { | |||
| XStream xstream = XStreamInitializer.getInstance(); | |||
| xstream.processAnnotations(WxCpXmlOutMessage.class); | |||
| xstream.processAnnotations(WxCpXmlOutVoiceMessage.class); | |||
| return xstream; | |||
| } | |||
| } | |||
| @@ -1,93 +0,0 @@ | |||
| package me.chanjar.weixin.cp.util.xml; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.io.StringReader; | |||
| import java.io.StringWriter; | |||
| import java.io.Writer; | |||
| import javax.xml.bind.*; | |||
| import me.chanjar.weixin.cp.bean.*; | |||
| import org.xml.sax.InputSource; | |||
| import com.sun.xml.bind.marshaller.CharacterEscapeHandler; | |||
| public class XmlTransformer { | |||
| protected static final JAXBContext JAXB_CONTEXT = initJAXBContext(); | |||
| /** | |||
| * xml -> pojo | |||
| * | |||
| * @param clazz | |||
| * @param xml | |||
| * @return | |||
| * @throws JAXBException | |||
| */ | |||
| @SuppressWarnings("unchecked") | |||
| public static <T> T fromXml(Class<T> clazz, String xml) throws JAXBException { | |||
| Unmarshaller um = JAXB_CONTEXT.createUnmarshaller(); | |||
| T object = (T) um.unmarshal(new StringReader(xml)); | |||
| return object; | |||
| } | |||
| @SuppressWarnings("unchecked") | |||
| public static <T> T fromXml(Class<T> clazz, InputStream is) throws JAXBException { | |||
| Unmarshaller um = JAXB_CONTEXT.createUnmarshaller(); | |||
| InputSource inputSource = new InputSource(is); | |||
| inputSource.setEncoding("utf-8"); | |||
| T object = (T) um.unmarshal(inputSource); | |||
| return object; | |||
| } | |||
| /** | |||
| * pojo -> xml | |||
| * | |||
| * @param clazz | |||
| * @param object | |||
| * @return | |||
| * @throws JAXBException | |||
| */ | |||
| public static <T> String toXml(Class<T> clazz, T object) throws JAXBException { | |||
| StringWriter stringWriter = new StringWriter(); | |||
| toXml(clazz, object, stringWriter); | |||
| return stringWriter.getBuffer().toString(); | |||
| } | |||
| public static <T> void toXml(Class<T> clazz, T object, Writer writer) throws JAXBException { | |||
| Marshaller m = JAXB_CONTEXT.createMarshaller(); | |||
| m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); | |||
| m.setProperty(CharacterEscapeHandler.class.getName(), CHAR_ESCAPE_HANDLER); | |||
| m.setProperty(Marshaller.JAXB_FRAGMENT, true); | |||
| m.marshal(object, writer); | |||
| } | |||
| protected static final CharacterEscapeHandler CHAR_ESCAPE_HANDLER = new CharacterUnescapeHandler(); | |||
| protected static class CharacterUnescapeHandler implements CharacterEscapeHandler { | |||
| public void escape(char[] ac, int i, int j, boolean flag, Writer writer) throws IOException { | |||
| writer.write(ac, i, j); | |||
| } | |||
| } | |||
| private static JAXBContext initJAXBContext() { | |||
| /* | |||
| * JAXBContext对象是线程安全的,根据官方文档的建议将对象作为全局实例 | |||
| * https://jaxb.java.net/guide/Performance_and_thread_safety.html | |||
| */ | |||
| try { | |||
| return JAXBContext.newInstance( | |||
| WxCpXmlOutMessage.class, | |||
| WxCpXmlOutImageMessage.class, | |||
| WxCpXmlOutNewsMessage.class, | |||
| WxCpXmlOutTextMessage.class, | |||
| WxCpXmlOutVideoMessage.class, | |||
| WxCpXmlOutVoiceMessage.class, | |||
| WxCpXmlMessage.class); | |||
| } catch (JAXBException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,23 +1,17 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import java.io.InputStream; | |||
| import javax.xml.bind.JAXBContext; | |||
| import javax.xml.bind.JAXBException; | |||
| import javax.xml.bind.Unmarshaller; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import com.google.inject.Binder; | |||
| import com.google.inject.Module; | |||
| import org.xml.sax.InputSource; | |||
| import com.thoughtworks.xstream.XStream; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import me.chanjar.weixin.common.util.xml.XStreamInitializer; | |||
| import java.io.InputStream; | |||
| public class ApiTestModule implements Module { | |||
| @Override | |||
| public void configure(Binder binder) { | |||
| try { | |||
| InputStream is1 = ClassLoader.getSystemResourceAsStream("test-config.xml"); | |||
| WxXmlCpInMemoryConfigStorage config = fromXml(WxXmlCpInMemoryConfigStorage.class, is1); | |||
| WxCpServiceImpl wxService = new WxCpServiceImpl(); | |||
| @@ -25,21 +19,16 @@ public class ApiTestModule implements Module { | |||
| binder.bind(WxCpServiceImpl.class).toInstance(wxService); | |||
| binder.bind(WxCpConfigStorage.class).toInstance(config); | |||
| } catch (JAXBException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| } | |||
| public static <T> T fromXml(Class<T> clazz, InputStream is) throws JAXBException { | |||
| Unmarshaller um = JAXBContext.newInstance(clazz).createUnmarshaller(); | |||
| InputSource inputSource = new InputSource(is); | |||
| inputSource.setEncoding("utf-8"); | |||
| T object = (T) um.unmarshal(inputSource); | |||
| return object; | |||
| public static <T> T fromXml(Class<T> clazz, InputStream is) { | |||
| XStream xstream = XStreamInitializer.getInstance(); | |||
| xstream.alias("xml", clazz); | |||
| xstream.processAnnotations(clazz); | |||
| return (T) xstream.fromXML(is); | |||
| } | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public static class WxXmlCpInMemoryConfigStorage extends WxCpInMemoryConfigStorage { | |||
| protected String userId; | |||
| @@ -1,14 +1,12 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import com.google.inject.Inject; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import me.chanjar.weixin.common.util.StringUtils; | |||
| import org.testng.Assert; | |||
| import org.testng.annotations.Guice; | |||
| import org.testng.annotations.Test; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import com.google.inject.Inject; | |||
| /** | |||
| * 基础API测试 | |||
| * @author Daniel Qian | |||
| @@ -24,7 +22,7 @@ public class WxCpBaseAPITest { | |||
| public void testRefreshAccessToken() throws WxErrorException { | |||
| WxCpConfigStorage configStorage = wxService.wxCpConfigStorage; | |||
| String before = configStorage.getAccessToken(); | |||
| wxService.accessTokenRefresh(); | |||
| wxService.getAccessToken(false); | |||
| String after = configStorage.getAccessToken(); | |||
| @@ -0,0 +1,66 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import me.chanjar.weixin.common.bean.result.WxError; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import me.chanjar.weixin.common.util.http.RequestExecutor; | |||
| import org.testng.annotations.DataProvider; | |||
| import org.testng.annotations.Test; | |||
| import java.util.concurrent.ExecutionException; | |||
| import java.util.concurrent.ExecutorService; | |||
| import java.util.concurrent.Executors; | |||
| import java.util.concurrent.Future; | |||
| @Test | |||
| public class WxCpBusyRetryTest { | |||
| @DataProvider(name="getService") | |||
| public Object[][] getService() { | |||
| WxCpService service = new WxCpServiceImpl() { | |||
| @Override | |||
| protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
| WxError error = new WxError(); | |||
| error.setErrorCode(-1); | |||
| throw new WxErrorException(error); | |||
| } | |||
| }; | |||
| service.setMaxRetryTimes(3); | |||
| service.setRetrySleepMillis(500); | |||
| return new Object[][] { | |||
| new Object[] { service } | |||
| }; | |||
| } | |||
| @Test(dataProvider = "getService", expectedExceptions = RuntimeException.class) | |||
| public void testRetry(WxCpService service) throws WxErrorException { | |||
| service.execute(null, null, null); | |||
| } | |||
| @Test(dataProvider = "getService") | |||
| public void testRetryInThreadPool(final WxCpService service) throws InterruptedException, ExecutionException { | |||
| // 当线程池中的线程复用的时候,还是能保证相同的重试次数 | |||
| ExecutorService executorService = Executors.newFixedThreadPool(1); | |||
| Runnable runnable = new Runnable() { | |||
| @Override | |||
| public void run() { | |||
| try { | |||
| System.out.println("====================="); | |||
| System.out.println(Thread.currentThread().getName() + ": testRetry"); | |||
| service.execute(null, null, null); | |||
| } catch (WxErrorException e) { | |||
| throw new RuntimeException(e); | |||
| } catch (RuntimeException e) { | |||
| // OK | |||
| } | |||
| } | |||
| }; | |||
| Future<?> submit1 = executorService.submit(runnable); | |||
| Future<?> submit2 = executorService.submit(runnable); | |||
| submit1.get(); | |||
| submit2.get(); | |||
| } | |||
| } | |||
| @@ -1,15 +1,13 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import java.util.List; | |||
| import com.google.inject.Inject; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import me.chanjar.weixin.cp.bean.WxCpDepart; | |||
| import org.testng.Assert; | |||
| import org.testng.annotations.Guice; | |||
| import org.testng.annotations.Test; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import com.google.inject.Inject; | |||
| import java.util.List; | |||
| /** | |||
| * 测试部门接口 | |||
| @@ -1,20 +1,18 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| import com.google.inject.Inject; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import org.testng.Assert; | |||
| import org.testng.annotations.DataProvider; | |||
| import org.testng.annotations.Guice; | |||
| import org.testng.annotations.Test; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import com.google.inject.Inject; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| /** | |||
| * 测试多媒体文件上传下载 | |||
| @@ -1,14 +1,12 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import com.google.inject.Inject; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import me.chanjar.weixin.cp.bean.WxCpMessage; | |||
| import org.testng.annotations.Guice; | |||
| import org.testng.annotations.Test; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import com.google.inject.Inject; | |||
| /*** | |||
| * 测试发送消息 | |||
| * @author Daniel Qian | |||
| @@ -1,14 +1,16 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import java.util.Map; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.session.StandardSessionManager; | |||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | |||
| import org.testng.Assert; | |||
| import org.testng.annotations.DataProvider; | |||
| import org.testng.annotations.Test; | |||
| import java.util.Map; | |||
| /** | |||
| * 测试消息路由器 | |||
| * @author Daniel Qian | |||
| @@ -67,7 +69,8 @@ public class WxCpMessageRouterTest { | |||
| final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
| router.rule().handler(new WxCpMessageHandler() { | |||
| @Override | |||
| public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService) { | |||
| public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService, | |||
| WxSessionManager sessionManager) { | |||
| return null; | |||
| } | |||
| }).end(); | |||
| @@ -149,11 +152,145 @@ public class WxCpMessageRouterTest { | |||
| } | |||
| @Override | |||
| public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService) { | |||
| public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService, | |||
| WxSessionManager sessionManager) { | |||
| sb.append(this.echoStr).append(','); | |||
| return null; | |||
| } | |||
| } | |||
| @DataProvider | |||
| public Object[][] standardSessionManager() { | |||
| // 故意把session存活时间变短,清理更频繁 | |||
| StandardSessionManager ism = new StandardSessionManager(); | |||
| ism.setMaxInactiveInterval(1); | |||
| ism.setProcessExpiresFrequency(1); | |||
| ism.setBackgroundProcessorDelay(1); | |||
| return new Object[][] { | |||
| new Object[] { ism } | |||
| }; | |||
| } | |||
| @Test(dataProvider = "standardSessionManager") | |||
| public void testSessionClean1(StandardSessionManager ism) throws InterruptedException { | |||
| // 两个同步请求,看是否处理完毕后会被清理掉 | |||
| final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
| router.setSessionManager(ism); | |||
| router | |||
| .rule().async(false).handler(new WxSessionMessageHandler()).next() | |||
| .rule().async(false).handler(new WxSessionMessageHandler()).end(); | |||
| WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
| msg.setFromUserName("abc"); | |||
| router.route(msg); | |||
| Thread.sleep(2000l); | |||
| Assert.assertEquals(ism.getActiveSessions(), 0); | |||
| } | |||
| @Test(dataProvider = "standardSessionManager") | |||
| public void testSessionClean2(StandardSessionManager ism) throws InterruptedException { | |||
| // 1个同步,1个异步请求,看是否处理完毕后会被清理掉 | |||
| { | |||
| final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
| router.setSessionManager(ism); | |||
| router | |||
| .rule().async(false).handler(new WxSessionMessageHandler()).next() | |||
| .rule().async(true).handler(new WxSessionMessageHandler()).end(); | |||
| WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
| msg.setFromUserName("abc"); | |||
| router.route(msg); | |||
| Thread.sleep(2000l); | |||
| Assert.assertEquals(ism.getActiveSessions(), 0); | |||
| } | |||
| { | |||
| final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
| router.setSessionManager(ism); | |||
| router | |||
| .rule().async(true).handler(new WxSessionMessageHandler()).next() | |||
| .rule().async(false).handler(new WxSessionMessageHandler()).end(); | |||
| WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
| msg.setFromUserName("abc"); | |||
| router.route(msg); | |||
| Thread.sleep(2000l); | |||
| Assert.assertEquals(ism.getActiveSessions(), 0); | |||
| } | |||
| } | |||
| @Test(dataProvider = "standardSessionManager") | |||
| public void testSessionClean3(StandardSessionManager ism) throws InterruptedException { | |||
| // 2个异步请求,看是否处理完毕后会被清理掉 | |||
| final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
| router.setSessionManager(ism); | |||
| router | |||
| .rule().async(true).handler(new WxSessionMessageHandler()).next() | |||
| .rule().async(true).handler(new WxSessionMessageHandler()).end(); | |||
| WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
| msg.setFromUserName("abc"); | |||
| router.route(msg); | |||
| Thread.sleep(2000l); | |||
| Assert.assertEquals(ism.getActiveSessions(), 0); | |||
| } | |||
| @Test(dataProvider = "standardSessionManager") | |||
| public void testSessionClean4(StandardSessionManager ism) throws InterruptedException { | |||
| // 一个同步请求,看是否处理完毕后会被清理掉 | |||
| { | |||
| final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
| router.setSessionManager(ism); | |||
| router | |||
| .rule().async(false).handler(new WxSessionMessageHandler()).end(); | |||
| WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
| msg.setFromUserName("abc"); | |||
| router.route(msg); | |||
| Thread.sleep(2000l); | |||
| Assert.assertEquals(ism.getActiveSessions(), 0); | |||
| } | |||
| { | |||
| final WxCpMessageRouter router = new WxCpMessageRouter(null); | |||
| router.setSessionManager(ism); | |||
| router | |||
| .rule().async(true).handler(new WxSessionMessageHandler()).end(); | |||
| WxCpXmlMessage msg = new WxCpXmlMessage(); | |||
| msg.setFromUserName("abc"); | |||
| router.route(msg); | |||
| Thread.sleep(2000l); | |||
| Assert.assertEquals(ism.getActiveSessions(), 0); | |||
| } | |||
| } | |||
| public static class WxSessionMessageHandler implements WxCpMessageHandler { | |||
| @Override | |||
| public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService wxCpService, | |||
| WxSessionManager sessionManager) { | |||
| sessionManager.getSession(wxMessage.getFromUserName()); | |||
| return null; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,9 +1,9 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import com.google.inject.Inject; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import me.chanjar.weixin.cp.bean.WxCpDepart; | |||
| import me.chanjar.weixin.cp.bean.WxCpUser; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import org.testng.Assert; | |||
| import org.testng.annotations.Guice; | |||
| import org.testng.annotations.Test; | |||
| @@ -26,30 +26,30 @@ public class WxCpUserAPITest { | |||
| public void testUserCreate() throws WxErrorException { | |||
| WxCpUser user = new WxCpUser(); | |||
| user.setUserId("xiaohe.yang"); | |||
| user.setName("杨宝"); | |||
| user.setUserId("some.woman"); | |||
| user.setName("Some Woman"); | |||
| user.setDepartIds(new Integer[] { 9, 8 }); | |||
| user.setEmail("yangxiaohe@ddd.com"); | |||
| user.setEmail("none@none.com"); | |||
| user.setGender("女"); | |||
| user.setMobile("13564684979"); | |||
| user.setPosition("老婆"); | |||
| user.setMobile("13560084979"); | |||
| user.setPosition("woman"); | |||
| user.setTel("3300393"); | |||
| user.addExtAttr("爱好", "老公"); | |||
| user.addExtAttr("爱好", "table"); | |||
| wxCpService.userCreate(user); | |||
| } | |||
| @Test(dependsOnMethods = "testUserCreate") | |||
| public void testUserUpdate() throws WxErrorException { | |||
| WxCpUser user = new WxCpUser(); | |||
| user.setUserId("xiaohe.yang"); | |||
| user.setName("杨宝"); | |||
| user.addExtAttr("爱好", "老公2"); | |||
| user.setUserId("some.woman"); | |||
| user.setName("Some Woman"); | |||
| user.addExtAttr("爱好", "table2"); | |||
| wxCpService.userUpdate(user); | |||
| } | |||
| @Test(dependsOnMethods = "testUserUpdate") | |||
| public void testUserGet() throws WxErrorException { | |||
| WxCpUser user = wxCpService.userGet("xiaohe.yang"); | |||
| WxCpUser user = wxCpService.userGet("some.woman"); | |||
| Assert.assertNotNull(user); | |||
| } | |||
| @@ -61,6 +61,6 @@ public class WxCpUserAPITest { | |||
| @Test(dependsOnMethods = "testDepartGetUsers") | |||
| public void testUserDelete() throws WxErrorException { | |||
| wxCpService.userDelete("xiaohe.yang"); | |||
| wxCpService.userDelete("some.woman"); | |||
| } | |||
| } | |||
| @@ -1,19 +1,15 @@ | |||
| package me.chanjar.weixin.cp.api; | |||
| import javax.xml.bind.JAXBException; | |||
| import com.google.inject.Inject; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.bean.WxMenu; | |||
| import me.chanjar.weixin.common.bean.WxMenu.WxMenuButton; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import org.testng.Assert; | |||
| import org.testng.annotations.DataProvider; | |||
| import org.testng.annotations.Guice; | |||
| import org.testng.annotations.Test; | |||
| import com.google.inject.Inject; | |||
| import me.chanjar.weixin.common.bean.WxMenu.WxMenuButton; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| /** | |||
| * 测试菜单 | |||
| * @author Daniel Qian | |||
| @@ -42,7 +38,7 @@ public class WxMenuAPITest { | |||
| } | |||
| @DataProvider(name="menu") | |||
| public Object[][] getMenu() throws JAXBException { | |||
| public Object[][] getMenu() { | |||
| WxMenu menu = new WxMenu(); | |||
| WxMenuButton button1 = new WxMenuButton(); | |||
| button1.setType(WxConsts.BUTTON_CLICK); | |||
| @@ -1,11 +1,10 @@ | |||
| package me.chanjar.weixin.cp.bean; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.cp.bean.WxCpMessage.WxArticle; | |||
| import org.testng.Assert; | |||
| import org.testng.annotations.Test; | |||
| import me.chanjar.weixin.cp.bean.WxCpMessage.WxArticle; | |||
| @Test | |||
| public class WxCpMessageTest { | |||
| @@ -1,35 +1,29 @@ | |||
| package me.chanjar.weixin.cp.demo; | |||
| import com.thoughtworks.xstream.XStream; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import me.chanjar.weixin.common.util.xml.XStreamInitializer; | |||
| import me.chanjar.weixin.cp.api.WxCpInMemoryConfigStorage; | |||
| import org.xml.sax.InputSource; | |||
| import javax.xml.bind.JAXBContext; | |||
| import javax.xml.bind.JAXBException; | |||
| import javax.xml.bind.Unmarshaller; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import java.io.InputStream; | |||
| /** | |||
| * @author Daniel Qian | |||
| */ | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| class WxCpDemoInMemoryConfigStorage extends WxCpInMemoryConfigStorage { | |||
| @Override | |||
| public String toString() { | |||
| return "SimpleWxConfigProvider [appidOrCorpid=" + corpId + ", corpSecret=" + corpSecret + ", accessToken=" + accessToken | |||
| + ", expiresIn=" + expiresIn + ", token=" + token + ", aesKey=" + aesKey + "]"; | |||
| + ", expiresTime=" + expiresTime + ", token=" + token + ", aesKey=" + aesKey + "]"; | |||
| } | |||
| public static WxCpDemoInMemoryConfigStorage fromXml(InputStream is) throws JAXBException { | |||
| Unmarshaller um = JAXBContext.newInstance(WxCpDemoInMemoryConfigStorage.class).createUnmarshaller(); | |||
| InputSource inputSource = new InputSource(is); | |||
| inputSource.setEncoding("utf-8"); | |||
| return (WxCpDemoInMemoryConfigStorage) um.unmarshal(inputSource); | |||
| public static WxCpDemoInMemoryConfigStorage fromXml(InputStream is) { | |||
| XStream xstream = XStreamInitializer.getInstance(); | |||
| xstream.processAnnotations(WxCpDemoInMemoryConfigStorage.class); | |||
| return (WxCpDemoInMemoryConfigStorage) xstream.fromXML(is); | |||
| } | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| package me.chanjar.weixin.cp.demo; | |||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||
| import me.chanjar.weixin.cp.api.*; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | |||
| @@ -8,7 +9,6 @@ import org.eclipse.jetty.server.Server; | |||
| import org.eclipse.jetty.servlet.ServletHandler; | |||
| import org.eclipse.jetty.servlet.ServletHolder; | |||
| import javax.xml.bind.JAXBException; | |||
| import java.io.InputStream; | |||
| import java.util.Map; | |||
| @@ -37,7 +37,6 @@ public class WxCpDemoServer { | |||
| } | |||
| private static void initWeixin() { | |||
| try { | |||
| InputStream is1 = ClassLoader.getSystemResourceAsStream("test-config.xml"); | |||
| WxCpDemoInMemoryConfigStorage config = WxCpDemoInMemoryConfigStorage.fromXml(is1); | |||
| @@ -48,7 +47,7 @@ public class WxCpDemoServer { | |||
| WxCpMessageHandler handler = new WxCpMessageHandler() { | |||
| @Override | |||
| public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, | |||
| WxCpService wxCpService) { | |||
| WxCpService wxCpService, WxSessionManager sessionManager) { | |||
| WxCpXmlOutTextMessage m = WxCpXmlOutMessage | |||
| .TEXT() | |||
| .content("测试加密消息") | |||
| @@ -62,7 +61,7 @@ public class WxCpDemoServer { | |||
| WxCpMessageHandler oauth2handler = new WxCpMessageHandler() { | |||
| @Override | |||
| public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, | |||
| WxCpService wxCpService) { | |||
| WxCpService wxCpService, WxSessionManager sessionManager) { | |||
| String href = "<a href=\"" + wxCpService.oauth2buildAuthorizationUrl(null) | |||
| + "\">测试oauth2</a>"; | |||
| return WxCpXmlOutMessage | |||
| @@ -87,8 +86,5 @@ public class WxCpDemoServer { | |||
| .end() | |||
| ; | |||
| } catch (JAXBException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| } | |||
| } | |||
| @@ -1,20 +1,18 @@ | |||
| package me.chanjar.weixin.cp.demo; | |||
| import me.chanjar.weixin.common.util.StringUtils; | |||
| import me.chanjar.weixin.cp.api.*; | |||
| import me.chanjar.weixin.cp.api.WxCpConfigStorage; | |||
| import me.chanjar.weixin.cp.api.WxCpMessageRouter; | |||
| import me.chanjar.weixin.cp.api.WxCpService; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlMessage; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; | |||
| import me.chanjar.weixin.cp.bean.WxCpXmlOutTextMessage; | |||
| import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil; | |||
| import javax.servlet.ServletException; | |||
| import javax.servlet.http.HttpServlet; | |||
| import javax.servlet.http.HttpServletRequest; | |||
| import javax.servlet.http.HttpServletResponse; | |||
| import javax.xml.bind.JAXBException; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.util.Map; | |||
| /** | |||
| * @author Daniel Qian | |||
| @@ -10,9 +10,6 @@ import javax.servlet.http.HttpServletResponse; | |||
| import java.io.IOException; | |||
| import java.util.Arrays; | |||
| /** | |||
| * Created by qianjia on 14/11/28. | |||
| */ | |||
| public class WxCpOAuth2Servlet extends HttpServlet { | |||
| protected WxCpService wxCpService; | |||
| @@ -0,0 +1,16 @@ | |||
| <configuration> | |||
| <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> | |||
| <!-- encoders are assigned the type | |||
| ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> | |||
| <encoder> | |||
| <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> | |||
| </encoder> | |||
| </appender> | |||
| <root level="info"> | |||
| <appender-ref ref="STDOUT" /> | |||
| </root> | |||
| <logger name="me.chanjar.weixin.cp" level="debug" /> | |||
| </configuration> | |||
| @@ -5,7 +5,7 @@ | |||
| <token>企业号应用Token</token> | |||
| <aesKey>企业号应用EncodingAESKey</aesKey> | |||
| <accessToken>可以不填写</accessToken> | |||
| <expiresIn>可以不填写</expiresIn> | |||
| <expiresTime>可以不填写</expiresTime> | |||
| <userId>企业号通讯录里的某个userid</userId> | |||
| <departmentId>企业号通讯录的某个部门id</departmentId> | |||
| <tagId>企业号通讯录里的某个tagid</tagId> | |||
| @@ -1,28 +1,29 @@ | |||
| <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" > | |||
| <suite name="Weixin-java-tool-suite" verbose="1"> | |||
| <test name="API_Test"> | |||
| <classes> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpBaseAPITest" /> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpMessageAPITest" /> | |||
| <class name="me.chanjar.weixin.cp.api.WxMenuAPITest" /> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpDepartAPITest" /> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpMediaAPITest" /> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpMessageRouterTest" /> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpTagAPITest" /> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpUserAPITest" /> | |||
| </classes> | |||
| </test> | |||
| <test name="API_Test"> | |||
| <classes> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpBusyRetryTest"/> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpBaseAPITest"/> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpMessageAPITest"/> | |||
| <class name="me.chanjar.weixin.cp.api.WxMenuAPITest"/> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpDepartAPITest"/> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpMediaAPITest"/> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpMessageRouterTest"/> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpTagAPITest"/> | |||
| <class name="me.chanjar.weixin.cp.api.WxCpUserAPITest"/> | |||
| </classes> | |||
| </test> | |||
| <test name="Bean_Test"> | |||
| <classes> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpMessageTest" /> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlMessageTest" /> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlOutImageMessageTest" /> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlOutNewsMessageTest" /> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlOutVideoMessageTest" /> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlOutVoiceMessageTest" /> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlOutTextMessageTest" /> | |||
| </classes> | |||
| </test> | |||
| <test name="Bean_Test"> | |||
| <classes> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpMessageTest"/> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlMessageTest"/> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlOutImageMessageTest"/> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlOutNewsMessageTest"/> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlOutVideoMessageTest"/> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlOutVoiceMessageTest"/> | |||
| <class name="me.chanjar.weixin.cp.bean.WxCpXmlOutTextMessageTest"/> | |||
| </classes> | |||
| </test> | |||
| </suite> | |||
| @@ -6,7 +6,7 @@ | |||
| <parent> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-parent</artifactId> | |||
| <version>1.0.6</version> | |||
| <version>1.1.0-SNAPSHOT</version> | |||
| </parent> | |||
| <artifactId>weixin-java-mp</artifactId> | |||
| <name>WeiXin Java Tools - MP</name> | |||
| @@ -1,72 +0,0 @@ | |||
| <?xml version="1.0"?> | |||
| <project | |||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||
| <modelVersion>4.0.0</modelVersion> | |||
| <parent> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-parent</artifactId> | |||
| <version>1.0.5-SNAPSHOT</version> | |||
| </parent> | |||
| <artifactId>weixin-java-mp</artifactId> | |||
| <name>WeiXin Java Tools - MP</name> | |||
| <description>微信公众号Java SDK</description> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>me.chanjar</groupId> | |||
| <artifactId>weixin-java-common</artifactId> | |||
| <version>${project.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>junit</groupId> | |||
| <artifactId>junit</artifactId> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.testng</groupId> | |||
| <artifactId>testng</artifactId> | |||
| <version>6.8.7</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.mockito</groupId> | |||
| <artifactId>mockito-all</artifactId> | |||
| <version>1.9.5</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.inject</groupId> | |||
| <artifactId>guice</artifactId> | |||
| <version>3.0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-server</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.eclipse.jetty</groupId> | |||
| <artifactId>jetty-servlet</artifactId> | |||
| <version>9.3.0.M0</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| </dependencies> | |||
| <build> | |||
| <plugins> | |||
| <plugin> | |||
| <groupId>org.apache.maven.plugins</groupId> | |||
| <artifactId>maven-surefire-plugin</artifactId> | |||
| <configuration> | |||
| <suiteXmlFiles> | |||
| <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile> | |||
| </suiteXmlFiles> | |||
| </configuration> | |||
| </plugin> | |||
| </plugins> | |||
| </build> | |||
| </project> | |||
| @@ -9,21 +9,52 @@ import me.chanjar.weixin.common.bean.WxAccessToken; | |||
| */ | |||
| public interface WxMpConfigStorage { | |||
| public String getAccessToken(); | |||
| public boolean isAccessTokenExpired(); | |||
| /** | |||
| * 强制将access token过期掉 | |||
| */ | |||
| public void expireAccessToken(); | |||
| /** | |||
| * 应该是线程安全的 | |||
| * @param accessToken | |||
| */ | |||
| public void updateAccessToken(WxAccessToken accessToken); | |||
| /** | |||
| * 应该是线程安全的 | |||
| * @param accessToken | |||
| * @param expiresIn | |||
| */ | |||
| public void updateAccessToken(String accessToken, int expiresIn); | |||
| public String getAccessToken(); | |||
| public String getJsapiTicket(); | |||
| public boolean isJsapiTicketExpired(); | |||
| /** | |||
| * 强制将jsapi ticket过期掉 | |||
| */ | |||
| public void expireJsapiTicket(); | |||
| /** | |||
| * 应该是线程安全的 | |||
| * @param jsapiTicket | |||
| */ | |||
| public void updateJsapiTicket(String jsapiTicket, int expiresInSeconds); | |||
| public String getAppId(); | |||
| public String getSecret(); | |||
| public String getToken(); | |||
| public String getAesKey(); | |||
| public int getExpiresIn(); | |||
| public long getExpiresTime(); | |||
| public String getOauth2redirectUri(); | |||
| @@ -33,6 +64,7 @@ public interface WxMpConfigStorage { | |||
| public String getHttp_proxy_username(); | |||
| public String getHttp_proxy_password(); | |||
| } | |||
| @@ -9,12 +9,14 @@ import me.chanjar.weixin.common.bean.WxAccessToken; | |||
| */ | |||
| public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| private static final long l = 7000 * 1000l; | |||
| protected String appId; | |||
| protected String secret; | |||
| protected String token; | |||
| protected String accessToken; | |||
| protected String aesKey; | |||
| protected int expiresIn; | |||
| protected long expiresTime; | |||
| protected String oauth2redirectUri; | |||
| @@ -23,17 +25,46 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| protected String http_proxy_username; | |||
| protected String http_proxy_password; | |||
| public void updateAccessToken(WxAccessToken accessToken) { | |||
| protected String jsapiTicket; | |||
| protected long jsapiTicketExpiresTime; | |||
| public String getAccessToken() { | |||
| return this.accessToken; | |||
| } | |||
| public boolean isAccessTokenExpired() { | |||
| return System.currentTimeMillis() > this.expiresTime; | |||
| } | |||
| public synchronized void updateAccessToken(WxAccessToken accessToken) { | |||
| updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } | |||
| public void updateAccessToken(String accessToken, int expiresIn) { | |||
| public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) { | |||
| this.accessToken = accessToken; | |||
| this.expiresIn = expiresIn; | |||
| this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l; | |||
| } | |||
| public String getAccessToken() { | |||
| return this.accessToken; | |||
| public void expireAccessToken() { | |||
| this.expiresTime = 0; | |||
| } | |||
| public String getJsapiTicket() { | |||
| return jsapiTicket; | |||
| } | |||
| public boolean isJsapiTicketExpired() { | |||
| return System.currentTimeMillis() > this.jsapiTicketExpiresTime; | |||
| } | |||
| public synchronized void updateJsapiTicket(String jsapiTicket, int expiresInSeconds) { | |||
| this.jsapiTicket = jsapiTicket; | |||
| // 预留200秒的时间 | |||
| this.jsapiTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000l; | |||
| } | |||
| public void expireJsapiTicket() { | |||
| this.jsapiTicketExpiresTime = 0; | |||
| } | |||
| public String getAppId() { | |||
| @@ -48,8 +79,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| return this.token; | |||
| } | |||
| public int getExpiresIn() { | |||
| return this.expiresIn; | |||
| public long getExpiresTime() { | |||
| return this.expiresTime; | |||
| } | |||
| public void setAppId(String appId) { | |||
| @@ -76,8 +107,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| this.accessToken = accessToken; | |||
| } | |||
| public void setExpiresIn(int expiresIn) { | |||
| this.expiresIn = expiresIn; | |||
| public void setExpiresTime(long expiresTime) { | |||
| this.expiresTime = expiresTime; | |||
| } | |||
| @Override | |||
| @@ -129,11 +160,13 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| ", token='" + token + '\'' + | |||
| ", accessToken='" + accessToken + '\'' + | |||
| ", aesKey='" + aesKey + '\'' + | |||
| ", expiresIn=" + expiresIn + | |||
| ", expiresTime=" + expiresTime + | |||
| ", http_proxy_host='" + http_proxy_host + '\'' + | |||
| ", http_proxy_port=" + http_proxy_port + | |||
| ", http_proxy_username='" + http_proxy_username + '\'' + | |||
| ", http_proxy_password='" + http_proxy_password + '\'' + | |||
| ", jsapiTicket='" + jsapiTicket + '\'' + | |||
| ", jsapiTicketExpiresTime='" + jsapiTicketExpiresTime + '\'' + | |||
| '}'; | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| package me.chanjar.weixin.mp.api; | |||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||
| import me.chanjar.weixin.mp.bean.WxMpXmlMessage; | |||
| import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; | |||
| @@ -7,18 +8,21 @@ import java.util.Map; | |||
| /** | |||
| * 处理微信推送消息的处理器接口 | |||
| * @author chanjarster | |||
| * | |||
| * @author Daniel Qian | |||
| */ | |||
| public interface WxMpMessageHandler { | |||
| /** | |||
| * | |||
| * @param wxMessage | |||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
| * @param wxMpService | |||
| * @param sessionManager | |||
| * @return xml格式的消息,如果在异步规则里处理的话,可以返回null | |||
| */ | |||
| public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService); | |||
| public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, | |||
| Map<String, Object> context, | |||
| WxMpService wxMpService, | |||
| WxSessionManager sessionManager); | |||
| } | |||
| @@ -1,23 +1,29 @@ | |||
| package me.chanjar.weixin.mp.api; | |||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||
| import me.chanjar.weixin.mp.bean.WxMpXmlMessage; | |||
| import java.util.Map; | |||
| /** | |||
| * 微信消息拦截器,可以用来做验证 | |||
| * @author qianjia | |||
| * | |||
| * @author Daniel Qian | |||
| */ | |||
| public interface WxMpMessageInterceptor { | |||
| /** | |||
| * 拦截微信消息 | |||
| * | |||
| * @param wxMessage | |||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
| * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 | |||
| * @param wxMpService | |||
| * @return true代表OK,false代表不OK | |||
| * @param sessionManager | |||
| * @return true代表OK,false代表不OK | |||
| */ | |||
| public boolean intercept(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService); | |||
| public boolean intercept(WxMpXmlMessage wxMessage, | |||
| Map<String, Object> context, | |||
| WxMpService wxMpService, | |||
| WxSessionManager sessionManager); | |||
| } | |||
| @@ -1,14 +1,22 @@ | |||
| package me.chanjar.weixin.mp.api; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.session.*; | |||
| import me.chanjar.weixin.common.util.WxMessageDuplicateChecker; | |||
| import me.chanjar.weixin.common.util.WxMessageInMemoryDuplicateChecker; | |||
| import me.chanjar.weixin.mp.bean.WxMpXmlMessage; | |||
| import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import java.util.ArrayList; | |||
| import java.util.HashMap; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| import java.util.concurrent.ExecutionException; | |||
| import java.util.concurrent.ExecutorService; | |||
| import java.util.concurrent.Executors; | |||
| import java.util.concurrent.Future; | |||
| import java.util.regex.Pattern; | |||
| /** | |||
| @@ -36,27 +44,61 @@ import java.util.regex.Pattern; | |||
| * router.route(message); | |||
| * | |||
| * </pre> | |||
| * @author qianjia | |||
| * | |||
| */ | |||
| public class WxMpMessageRouter { | |||
| private static final int DEFAULT_THREAD_POOL_SIZE = 20; | |||
| protected final Logger log = LoggerFactory.getLogger(WxMpMessageRouter.class); | |||
| private final List<Rule> rules = new ArrayList<Rule>(); | |||
| private static final int DEFAULT_THREAD_POOL_SIZE = 100; | |||
| private final ExecutorService executorService; | |||
| private final List<Rule> rules = new ArrayList<Rule>(); | |||
| private final WxMpService wxMpService; | |||
| private ExecutorService executorService; | |||
| private WxMessageDuplicateChecker messageDuplicateChecker; | |||
| private WxSessionManager sessionManager; | |||
| public WxMpMessageRouter(WxMpService wxMpService) { | |||
| this.wxMpService = wxMpService; | |||
| this.executorService = Executors.newFixedThreadPool(DEFAULT_THREAD_POOL_SIZE); | |||
| this.messageDuplicateChecker = new WxMessageInMemoryDuplicateChecker(); | |||
| this.sessionManager = new StandardSessionManager(); | |||
| } | |||
| public WxMpMessageRouter(WxMpService wxMpService, int threadPoolSize) { | |||
| this.wxMpService = wxMpService; | |||
| this.executorService = Executors.newFixedThreadPool(threadPoolSize); | |||
| /** | |||
| * <pre> | |||
| * 设置自定义的 {@link ExecutorService} | |||
| * 如果不调用该方法,默认使用 Executors.newFixedThreadPool(100) | |||
| * </pre> | |||
| * @param executorService | |||
| */ | |||
| public void setExecutorService(ExecutorService executorService) { | |||
| this.executorService = executorService; | |||
| } | |||
| /** | |||
| * <pre> | |||
| * 设置自定义的 {@link me.chanjar.weixin.common.util.WxMessageDuplicateChecker} | |||
| * 如果不调用该方法,默认使用 {@link me.chanjar.weixin.common.util.WxMessageInMemoryDuplicateChecker} | |||
| * </pre> | |||
| * @param messageDuplicateChecker | |||
| */ | |||
| public void setMessageDuplicateChecker(WxMessageDuplicateChecker messageDuplicateChecker) { | |||
| this.messageDuplicateChecker = messageDuplicateChecker; | |||
| } | |||
| /** | |||
| * <pre> | |||
| * 设置自定义的{@link me.chanjar.weixin.common.session.WxSessionManager} | |||
| * 如果不调用该方法,默认使用 {@linke SessionManagerImpl} | |||
| * </pre> | |||
| * @param sessionManager | |||
| */ | |||
| public void setSessionManager(WxSessionManager sessionManager) { | |||
| this.sessionManager = sessionManager; | |||
| } | |||
| /** | |||
| @@ -64,7 +106,7 @@ public class WxMpMessageRouter { | |||
| * @return | |||
| */ | |||
| public Rule rule() { | |||
| return new Rule(this, wxMpService); | |||
| return new Rule(this, wxMpService, sessionManager); | |||
| } | |||
| /** | |||
| @@ -72,6 +114,11 @@ public class WxMpMessageRouter { | |||
| * @param wxMessage | |||
| */ | |||
| public WxMpXmlOutMessage route(final WxMpXmlMessage wxMessage) { | |||
| if (isDuplicateMessage(wxMessage)) { | |||
| // 如果是重复消息,那么就不做处理 | |||
| return null; | |||
| } | |||
| final List<Rule> matchRules = new ArrayList<Rule>(); | |||
| // 收集匹配的规则 | |||
| for (final Rule rule : rules) { | |||
| @@ -82,33 +129,90 @@ public class WxMpMessageRouter { | |||
| } | |||
| } | |||
| } | |||
| if (matchRules.size() == 0) { | |||
| return null; | |||
| } | |||
| WxMpXmlOutMessage res = null; | |||
| final List<Future> futures = new ArrayList<Future>(); | |||
| for (final Rule rule : matchRules) { | |||
| // 返回最后一个非异步的rule的执行结果 | |||
| if(rule.async) { | |||
| executorService.submit(new Runnable() { | |||
| public void run() { | |||
| rule.service(wxMessage); | |||
| } | |||
| }); | |||
| futures.add( | |||
| executorService.submit(new Runnable() { | |||
| public void run() { | |||
| rule.service(wxMessage); | |||
| } | |||
| }) | |||
| ); | |||
| } else { | |||
| res = rule.service(wxMessage); | |||
| // 在同步操作结束,session访问结束 | |||
| log.debug("End session access: async=false, sessionId={}", wxMessage.getFromUserName()); | |||
| sessionEndAccess(wxMessage); | |||
| } | |||
| } | |||
| if (futures.size() > 0) { | |||
| executorService.submit(new Runnable() { | |||
| @Override | |||
| public void run() { | |||
| for (Future future : futures) { | |||
| try { | |||
| future.get(); | |||
| log.debug("End session access: async=true, sessionId={}", wxMessage.getFromUserName()); | |||
| // 异步操作结束,session访问结束 | |||
| sessionEndAccess(wxMessage); | |||
| } catch (InterruptedException e) { | |||
| log.error("Error happened when wait task finish", e); | |||
| } catch (ExecutionException e) { | |||
| log.error("Error happened when wait task finish", e); | |||
| } | |||
| } | |||
| } | |||
| }); | |||
| } | |||
| return res; | |||
| } | |||
| protected boolean isDuplicateMessage(WxMpXmlMessage wxMessage) { | |||
| String messageId = ""; | |||
| if (wxMessage.getMsgId() == null) { | |||
| messageId = wxMessage.getFromUserName() + "-" + String.valueOf(wxMessage.getCreateTime()); | |||
| } else { | |||
| messageId = String.valueOf(wxMessage.getMsgId()); | |||
| } | |||
| if (messageDuplicateChecker.isDuplicate(messageId)) { | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| /** | |||
| * 对session的访问结束 | |||
| * @param wxMessage | |||
| */ | |||
| protected void sessionEndAccess(WxMpXmlMessage wxMessage) { | |||
| InternalSession session = ((InternalSessionManager)sessionManager).findSession(wxMessage.getFromUserName()); | |||
| if (session != null) { | |||
| session.endAccess(); | |||
| } | |||
| } | |||
| public static class Rule { | |||
| private final WxMpMessageRouter routerBuilder; | |||
| private final WxMpService wxMpService; | |||
| private final WxSessionManager sessionManager; | |||
| private boolean async = true; | |||
| private String fromUser; | |||
| @@ -128,10 +232,11 @@ public class WxMpMessageRouter { | |||
| private List<WxMpMessageHandler> handlers = new ArrayList<WxMpMessageHandler>(); | |||
| private List<WxMpMessageInterceptor> interceptors = new ArrayList<WxMpMessageInterceptor>(); | |||
| protected Rule(WxMpMessageRouter routerBuilder, WxMpService wxMpService) { | |||
| protected Rule(WxMpMessageRouter routerBuilder, WxMpService wxMpService, WxSessionManager sessionManager) { | |||
| this.routerBuilder = routerBuilder; | |||
| this.wxMpService = wxMpService; | |||
| this.sessionManager = sessionManager; | |||
| } | |||
| /** | |||
| @@ -297,7 +402,7 @@ public class WxMpMessageRouter { | |||
| Map<String, Object> context = new HashMap<String, Object>(); | |||
| // 如果拦截器不通过 | |||
| for (WxMpMessageInterceptor interceptor : this.interceptors) { | |||
| if (!interceptor.intercept(wxMessage, context, wxMpService)) { | |||
| if (!interceptor.intercept(wxMessage, context, wxMpService, sessionManager)) { | |||
| return null; | |||
| } | |||
| } | |||
| @@ -306,7 +411,7 @@ public class WxMpMessageRouter { | |||
| WxMpXmlOutMessage res = null; | |||
| for (WxMpMessageHandler handler : this.handlers) { | |||
| // 返回最后handler的结果 | |||
| res = handler.handle(wxMessage, context, wxMpService); | |||
| res = handler.handle(wxMessage, context, wxMpService, sessionManager); | |||
| } | |||
| return res; | |||
| } | |||
| @@ -2,10 +2,12 @@ package me.chanjar.weixin.mp.api; | |||
| import me.chanjar.weixin.common.bean.WxMenu; | |||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import me.chanjar.weixin.common.session.WxSession; | |||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||
| import me.chanjar.weixin.common.util.http.RequestExecutor; | |||
| import me.chanjar.weixin.mp.bean.*; | |||
| import me.chanjar.weixin.mp.bean.result.*; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import java.io.File; | |||
| import java.io.IOException; | |||
| @@ -28,7 +30,15 @@ public interface WxMpService { | |||
| * @return | |||
| */ | |||
| public boolean checkSignature(String timestamp, String nonce, String signature); | |||
| /** | |||
| * 获取access_token, 不强制刷新access_token | |||
| * @see #getAccessToken(boolean) | |||
| * @return | |||
| * @throws WxErrorException | |||
| */ | |||
| public String getAccessToken() throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 获取access_token,本方法线程安全 | |||
| @@ -40,20 +50,56 @@ public interface WxMpService { | |||
| * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token | |||
| * </pre> | |||
| * @param forceRefresh 强制刷新 | |||
| * @return | |||
| * @throws me.chanjar.weixin.common.exception.WxErrorException | |||
| */ | |||
| public void accessTokenRefresh() throws WxErrorException; | |||
| public String getAccessToken(boolean forceRefresh) throws WxErrorException; | |||
| /** | |||
| * 获得jsapi_ticket,不强制刷新jsapi_ticket | |||
| * @see #getJsapiTicket(boolean) | |||
| * @return | |||
| * @throws WxErrorException | |||
| */ | |||
| public String getJsapiTicket() throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 获得jsapi_ticket | |||
| * 获得时会检查jsapiToken是否过期,如果过期了,那么就刷新一下,否则就什么都不干 | |||
| * | |||
| * 详情请见:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html#.E9.99.84.E5.BD.951-JS-SDK.E4.BD.BF.E7.94.A8.E6.9D.83.E9.99.90.E7.AD.BE.E5.90.8D.E7.AE.97.E6.B3.95 | |||
| * </pre> | |||
| * @param forceRefresh 强制刷新 | |||
| * @return | |||
| * @throws WxErrorException | |||
| */ | |||
| public String getJsapiTicket(boolean forceRefresh) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 创建调用jsapi时所需要的签名 | |||
| * | |||
| * 详情请见:http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html#.E9.99.84.E5.BD.951-JS-SDK.E4.BD.BF.E7.94.A8.E6.9D.83.E9.99.90.E7.AD.BE.E5.90.8D.E7.AE.97.E6.B3.95 | |||
| * </pre> | |||
| * @param timestamp 时间戳 | |||
| * @param noncestr 用户自己生成的随机字符串 | |||
| * @param url url | |||
| * @return | |||
| */ | |||
| public String createJsapiSignature(long timestamp, String noncestr, String url) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 上传多媒体文件 | |||
| * | |||
| * | |||
| * 上传的多媒体文件有格式和大小限制,如下: | |||
| * 图片(image): 1M,支持JPG格式 | |||
| * 语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式 | |||
| * 视频(video):10MB,支持MP4格式 | |||
| * 缩略图(thumb):64KB,支持JPG格式 | |||
| * | |||
| * | |||
| * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件 | |||
| * </pre> | |||
| * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} | |||
| @@ -412,9 +458,28 @@ public interface WxMpService { | |||
| */ | |||
| public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException; | |||
| /** | |||
| * 注入 {@link WxMpConfigStorage} 的实现 | |||
| * @param wxConfigProvider | |||
| */ | |||
| /** | |||
| * 注入 {@link WxMpConfigStorage} 的实现 | |||
| * @param wxConfigProvider | |||
| */ | |||
| public void setWxMpConfigStorage(WxMpConfigStorage wxConfigProvider); | |||
| /** | |||
| * <pre> | |||
| * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试 | |||
| * 默认:1000ms | |||
| * </pre> | |||
| * @param retrySleepMillis | |||
| */ | |||
| void setRetrySleepMillis(int retrySleepMillis); | |||
| /** | |||
| * <pre> | |||
| * 设置当微信系统响应系统繁忙时,最大重试次数 | |||
| * 默认:5次 | |||
| * </pre> | |||
| * @param maxRetryTimes | |||
| */ | |||
| void setMaxRetryTimes(int maxRetryTimes); | |||
| } | |||
| @@ -10,6 +10,8 @@ import me.chanjar.weixin.common.bean.WxMenu; | |||
| import me.chanjar.weixin.common.bean.result.WxError; | |||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import me.chanjar.weixin.common.session.StandardSessionManager; | |||
| import me.chanjar.weixin.common.session.WxSessionManager; | |||
| import me.chanjar.weixin.common.util.StringUtils; | |||
| import me.chanjar.weixin.common.util.crypto.SHA1; | |||
| import me.chanjar.weixin.common.util.fs.FileUtils; | |||
| @@ -31,32 +33,43 @@ import org.apache.http.impl.client.BasicCredentialsProvider; | |||
| import org.apache.http.impl.client.BasicResponseHandler; | |||
| import org.apache.http.impl.client.CloseableHttpClient; | |||
| import org.apache.http.impl.client.HttpClients; | |||
| import org.slf4j.Logger; | |||
| import org.slf4j.LoggerFactory; | |||
| import java.io.File; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.io.StringReader; | |||
| import java.security.NoSuchAlgorithmException; | |||
| import java.util.List; | |||
| import java.util.UUID; | |||
| import java.util.concurrent.atomic.AtomicBoolean; | |||
| public class WxMpServiceImpl implements WxMpService { | |||
| protected final Logger log = LoggerFactory.getLogger(WxMpServiceImpl.class); | |||
| /** | |||
| * 全局的是否正在刷新Access Token的flag | |||
| * true: 正在刷新 | |||
| * false: 没有刷新 | |||
| * 全局的是否正在刷新access token的锁 | |||
| */ | |||
| protected static final AtomicBoolean GLOBAL_ACCESS_TOKEN_REFRESH_FLAG = new AtomicBoolean(false); | |||
| protected final Object globalAccessTokenRefreshLock = new Object(); | |||
| /** | |||
| * 全局的是否正在刷新jsapi_ticket的锁 | |||
| */ | |||
| protected final Object globalJsapiTicketRefreshLock = new Object(); | |||
| protected WxMpConfigStorage wxMpConfigStorage; | |||
| protected final ThreadLocal<Integer> retryTimes = new ThreadLocal<Integer>(); | |||
| protected CloseableHttpClient httpClient; | |||
| protected HttpHost httpProxy; | |||
| private int retrySleepMillis = 1000; | |||
| private int maxRetryTimes = 5; | |||
| protected WxSessionManager sessionManager = new StandardSessionManager(); | |||
| public boolean checkSignature(String timestamp, String nonce, String signature) { | |||
| try { | |||
| return SHA1.gen(wxMpConfigStorage.getToken(), timestamp, nonce).equals(signature); | |||
| @@ -64,49 +77,86 @@ public class WxMpServiceImpl implements WxMpService { | |||
| return false; | |||
| } | |||
| } | |||
| public void accessTokenRefresh() throws WxErrorException { | |||
| if (!GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.getAndSet(true)) { | |||
| try { | |||
| String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" | |||
| + "&appid=" + wxMpConfigStorage.getAppId() | |||
| + "&secret=" + wxMpConfigStorage.getSecret() | |||
| ; | |||
| try { | |||
| HttpGet httpGet = new HttpGet(url); | |||
| if (httpProxy != null) { | |||
| RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); | |||
| httpGet.setConfig(config); | |||
| } | |||
| CloseableHttpClient httpclient = getHttpclient(); | |||
| CloseableHttpResponse response = httpclient.execute(httpGet); | |||
| String resultContent = new BasicResponseHandler().handleResponse(response); | |||
| WxError error = WxError.fromJson(resultContent); | |||
| if (error.getErrorCode() != 0) { | |||
| throw new WxErrorException(error); | |||
| public String getAccessToken() throws WxErrorException { | |||
| return getAccessToken(false); | |||
| } | |||
| public String getAccessToken(boolean forceRefresh) throws WxErrorException { | |||
| if (forceRefresh) { | |||
| wxMpConfigStorage.expireAccessToken(); | |||
| } | |||
| if (wxMpConfigStorage.isAccessTokenExpired()) { | |||
| synchronized (globalAccessTokenRefreshLock) { | |||
| if (wxMpConfigStorage.isAccessTokenExpired()) { | |||
| String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" | |||
| + "&appid=" + wxMpConfigStorage.getAppId() | |||
| + "&secret=" + wxMpConfigStorage.getSecret() | |||
| ; | |||
| try { | |||
| HttpGet httpGet = new HttpGet(url); | |||
| if (httpProxy != null) { | |||
| RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); | |||
| httpGet.setConfig(config); | |||
| } | |||
| CloseableHttpClient httpclient = getHttpclient(); | |||
| CloseableHttpResponse response = httpclient.execute(httpGet); | |||
| String resultContent = new BasicResponseHandler().handleResponse(response); | |||
| WxError error = WxError.fromJson(resultContent); | |||
| if (error.getErrorCode() != 0) { | |||
| throw new WxErrorException(error); | |||
| } | |||
| WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); | |||
| wxMpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } catch (ClientProtocolException e) { | |||
| throw new RuntimeException(e); | |||
| } catch (IOException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); | |||
| wxMpConfigStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); | |||
| } catch (ClientProtocolException e) { | |||
| throw new RuntimeException(e); | |||
| } catch (IOException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| } finally { | |||
| GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.set(false); | |||
| } | |||
| } else { | |||
| // 每隔100ms检查一下是否刷新完毕了 | |||
| while (GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.get()) { | |||
| try { | |||
| Thread.sleep(100); | |||
| } catch (InterruptedException e) { | |||
| } | |||
| return wxMpConfigStorage.getAccessToken(); | |||
| } | |||
| public String getJsapiTicket() throws WxErrorException { | |||
| return getJsapiTicket(false); | |||
| } | |||
| public String getJsapiTicket(boolean forceRefresh) throws WxErrorException { | |||
| if (forceRefresh) { | |||
| wxMpConfigStorage.expireJsapiTicket(); | |||
| } | |||
| if (wxMpConfigStorage.isJsapiTicketExpired()) { | |||
| synchronized (globalJsapiTicketRefreshLock) { | |||
| if (wxMpConfigStorage.isJsapiTicketExpired()) { | |||
| String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi"; | |||
| String responseContent = execute(new SimpleGetRequestExecutor(), url, null); | |||
| JsonElement tmpJsonElement = Streams.parse(new JsonReader(new StringReader(responseContent))); | |||
| JsonObject tmpJsonObject = tmpJsonElement.getAsJsonObject(); | |||
| String jsapiTicket = tmpJsonObject.get("ticket").getAsString(); | |||
| int expiresInSeconds = tmpJsonObject.get("expires_in").getAsInt(); | |||
| wxMpConfigStorage.updateJsapiTicket(jsapiTicket, expiresInSeconds); | |||
| } | |||
| } | |||
| // 刷新完毕了,就没他什么事儿了 | |||
| } | |||
| return wxMpConfigStorage.getJsapiTicket(); | |||
| } | |||
| public String createJsapiSignature(long timestamp, String noncestr, String url) throws WxErrorException { | |||
| String jsapiTicket = getJsapiTicket(false); | |||
| try { | |||
| return SHA1.genWithAmple( | |||
| "jsapi_ticket=" + jsapiTicket, | |||
| "noncestr=" + noncestr, | |||
| "timestamp=" + timestamp, | |||
| "url=" + url | |||
| ); | |||
| } catch (NoSuchAlgorithmException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| } | |||
| public void customMessageSend(WxMpCustomMessage message) throws WxErrorException { | |||
| String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send"; | |||
| execute(new SimplePostRequestExecutor(), url, message.toJson()); | |||
| @@ -399,7 +449,7 @@ public class WxMpServiceImpl implements WxMpService { | |||
| return execute(new SimplePostRequestExecutor(), url, postData); | |||
| } | |||
| /** | |||
| /** | |||
| * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求 | |||
| * @param executor | |||
| * @param uri | |||
| @@ -408,10 +458,34 @@ public class WxMpServiceImpl implements WxMpService { | |||
| * @throws WxErrorException | |||
| */ | |||
| public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
| if (StringUtils.isBlank(wxMpConfigStorage.getAccessToken())) { | |||
| accessTokenRefresh(); | |||
| } | |||
| String accessToken = wxMpConfigStorage.getAccessToken(); | |||
| int retryTimes = 0; | |||
| do { | |||
| try { | |||
| return executeInternal(executor, uri, data); | |||
| } catch (WxErrorException e) { | |||
| WxError error = e.getError(); | |||
| /** | |||
| * -1 系统繁忙, 1000ms后重试 | |||
| */ | |||
| if (error.getErrorCode() == -1) { | |||
| int sleepMillis = retrySleepMillis * (1 << retryTimes); | |||
| try { | |||
| log.debug("微信系统繁忙,{}ms 后重试(第{}次)", sleepMillis, retryTimes + 1); | |||
| Thread.sleep(sleepMillis); | |||
| } catch (InterruptedException e1) { | |||
| throw new RuntimeException(e1); | |||
| } | |||
| } else { | |||
| throw e; | |||
| } | |||
| } | |||
| } while(++retryTimes < maxRetryTimes); | |||
| throw new RuntimeException("微信服务端异常,超出重试次数"); | |||
| } | |||
| protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException { | |||
| String accessToken = getAccessToken(false); | |||
| String uriWithAccessToken = uri; | |||
| uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken; | |||
| @@ -426,30 +500,10 @@ public class WxMpServiceImpl implements WxMpService { | |||
| * 42001 access_token超时 | |||
| */ | |||
| if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001) { | |||
| accessTokenRefresh(); | |||
| // 强制设置wxMpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token | |||
| wxMpConfigStorage.expireAccessToken(); | |||
| return execute(executor, uri, data); | |||
| } | |||
| /** | |||
| * -1 系统繁忙, 1000ms后重试 | |||
| */ | |||
| if (error.getErrorCode() == -1) { | |||
| if(retryTimes.get() == null) { | |||
| retryTimes.set(0); | |||
| } | |||
| if (retryTimes.get() > 4) { | |||
| retryTimes.set(0); | |||
| throw new RuntimeException("微信服务端异常,超出重试次数"); | |||
| } | |||
| int sleepMillis = 1000 * (1 << retryTimes.get()); | |||
| try { | |||
| System.out.println("微信系统繁忙," + sleepMillis + "ms后重试"); | |||
| Thread.sleep(sleepMillis); | |||
| retryTimes.set(retryTimes.get() + 1); | |||
| return execute(executor, uri, data); | |||
| } catch (InterruptedException e1) { | |||
| throw new RuntimeException(e1); | |||
| } | |||
| } | |||
| if (error.getErrorCode() != 0) { | |||
| throw new WxErrorException(error); | |||
| } | |||
| @@ -495,4 +549,15 @@ public class WxMpServiceImpl implements WxMpService { | |||
| } | |||
| } | |||
| @Override | |||
| public void setRetrySleepMillis(int retrySleepMillis) { | |||
| this.retrySleepMillis = retrySleepMillis; | |||
| } | |||
| @Override | |||
| public void setMaxRetryTimes(int maxRetryTimes) { | |||
| this.maxRetryTimes = maxRetryTimes; | |||
| } | |||
| } | |||
| @@ -1,32 +0,0 @@ | |||
| package me.chanjar.weixin.mp.bean; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.MediaIdMarshaller; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlElement; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| public class WxMpMpXmlOutImageMessage extends WxMpXmlOutMessage { | |||
| @XmlElement(name="Image") | |||
| @XmlJavaTypeAdapter(MediaIdMarshaller.class) | |||
| private String mediaId; | |||
| public WxMpMpXmlOutImageMessage() { | |||
| this.msgType = WxConsts.XML_MSG_IMAGE; | |||
| } | |||
| public String getMediaId() { | |||
| return mediaId; | |||
| } | |||
| public void setMediaId(String mediaId) { | |||
| this.mediaId = mediaId; | |||
| } | |||
| } | |||
| @@ -5,9 +5,6 @@ import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| /** | |||
| * Created by qianjia on 14/11/1. | |||
| */ | |||
| public class WxMpTemplateMessage { | |||
| private String toUser; | |||
| @@ -1,14 +1,13 @@ | |||
| package me.chanjar.weixin.mp.bean; | |||
| import me.chanjar.weixin.common.util.xml.AdapterCDATA; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; | |||
| import me.chanjar.weixin.mp.api.WxMpConfigStorage; | |||
| import me.chanjar.weixin.mp.util.crypto.WxMpCryptUtil; | |||
| import me.chanjar.weixin.mp.util.xml.XmlTransformer; | |||
| import me.chanjar.weixin.mp.util.xml.XStreamTransformer; | |||
| import org.apache.commons.io.IOUtils; | |||
| import javax.xml.bind.JAXBException; | |||
| import javax.xml.bind.annotation.*; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.util.ArrayList; | |||
| @@ -25,100 +24,99 @@ import java.util.List; | |||
| * | |||
| * @author chanjarster | |||
| */ | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxMpXmlMessage { | |||
| /////////////////////// | |||
| // 以下都是微信推送过来的消息的xml的element所对应的属性 | |||
| /////////////////////// | |||
| @XmlElement(name = "ToUserName") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("ToUserName") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String toUserName; | |||
| @XmlElement(name = "FromUserName") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("FromUserName") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String fromUserName; | |||
| @XmlElement(name = "CreateTime") | |||
| @XStreamAlias("CreateTime") | |||
| private Long createTime; | |||
| @XmlElement(name = "MsgType") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("MsgType") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String msgType; | |||
| @XmlElement(name = "Content") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Content") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String content; | |||
| @XmlElement(name = "MsgId") | |||
| @XStreamAlias("MsgId") | |||
| private Long msgId; | |||
| @XmlElement(name = "PicUrl") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("PicUrl") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String picUrl; | |||
| @XmlElement(name = "MediaId") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("MediaId") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String mediaId; | |||
| @XmlElement(name = "Format") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Format") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String format; | |||
| @XmlElement(name = "ThumbMediaId") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("ThumbMediaId") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String thumbMediaId; | |||
| @XmlElement(name = "Location_X") | |||
| @XStreamAlias("Location_X") | |||
| private Double locationX; | |||
| @XmlElement(name = "Location_Y") | |||
| @XStreamAlias("Location_Y") | |||
| private Double locationY; | |||
| @XmlElement(name = "Scale") | |||
| @XStreamAlias("Scale") | |||
| private Double scale; | |||
| @XmlElement(name = "Label") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Label") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String label; | |||
| @XmlElement(name = "Title") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Title") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String title; | |||
| @XmlElement(name = "Description") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Description") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String description; | |||
| @XmlElement(name = "Url") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Url") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String url; | |||
| @XmlElement(name = "Event") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Event") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String event; | |||
| @XmlElement(name = "EventKey") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("EventKey") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String eventKey; | |||
| @XmlElement(name = "Ticket") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Ticket") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String ticket; | |||
| @XmlElement(name = "Latitude") | |||
| @XStreamAlias("Latitude") | |||
| private Double latitude; | |||
| @XmlElement(name = "Longitude") | |||
| @XStreamAlias("Longitude") | |||
| private Double longitude; | |||
| @XmlElement(name = "Precision") | |||
| @XStreamAlias("Precision") | |||
| private Double precision; | |||
| @XmlElement(name = "Recognition") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Recognition") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String recognition; | |||
| /////////////////////////////////////// | |||
| @@ -127,37 +125,37 @@ public class WxMpXmlMessage { | |||
| /** | |||
| * 群发的结果 | |||
| */ | |||
| @XmlElement(name = "Status") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Status") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String status; | |||
| /** | |||
| * group_id下粉丝数;或者openid_list中的粉丝数 | |||
| */ | |||
| @XmlElement(name = "TotalCount") | |||
| @XStreamAlias("TotalCount") | |||
| private Integer totalCount; | |||
| /** | |||
| * 过滤(过滤是指特定地区、性别的过滤、用户设置拒收的过滤,用户接收已超4条的过滤)后,准备发送的粉丝数,原则上,filterCount = sentCount + errorCount | |||
| */ | |||
| @XmlElement(name = "FilterCount") | |||
| @XStreamAlias("FilterCount") | |||
| private Integer filterCount; | |||
| /** | |||
| * 发送成功的粉丝数 | |||
| */ | |||
| @XmlElement(name = "SentCount") | |||
| @XStreamAlias("SentCount") | |||
| private Integer sentCount; | |||
| /** | |||
| * 发送失败的粉丝数 | |||
| */ | |||
| @XmlElement(name = "ErrorCount") | |||
| @XStreamAlias("ErrorCount") | |||
| private Integer errorCount; | |||
| @XmlElement(name = "ScanCodeInfo") | |||
| @XStreamAlias("ScanCodeInfo") | |||
| private ScanCodeInfo scanCodeInfo = new ScanCodeInfo(); | |||
| @XmlElement(name = "SendPicsInfo") | |||
| @XStreamAlias("SendPicsInfo") | |||
| private SendPicsInfo sendPicsInfo = new SendPicsInfo(); | |||
| @XmlElement(name = "SendLocationInfo") | |||
| @XStreamAlias("SendLocationInfo") | |||
| private SendLocationInfo sendLocationInfo = new SendLocationInfo(); | |||
| public String getToUserName() { | |||
| @@ -380,21 +378,11 @@ public class WxMpXmlMessage { | |||
| } | |||
| public static WxMpXmlMessage fromXml(String xml) { | |||
| try { | |||
| // 操蛋的微信,模板消息推送成功的消息是MsgID,其他消息推送过来是MsgId | |||
| xml = xml.replaceAll("<MsgID>", "<MsgId>").replaceAll("</MsgID>", "</MsgId>"); | |||
| return XmlTransformer.fromXml(WxMpXmlMessage.class, xml); | |||
| } catch (JAXBException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| return XStreamTransformer.fromXml(WxMpXmlMessage.class, xml); | |||
| } | |||
| public static WxMpXmlMessage fromXml(InputStream is) { | |||
| try { | |||
| return fromXml(IOUtils.toString(is, "UTF-8")); | |||
| } catch (IOException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| return XStreamTransformer.fromXml(WxMpXmlMessage.class, is); | |||
| } | |||
| /** | |||
| @@ -491,16 +479,15 @@ public class WxMpXmlMessage { | |||
| this.sendLocationInfo = sendLocationInfo; | |||
| } | |||
| @XmlRootElement(name = "ScanCodeInfo") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("ScanCodeInfo") | |||
| public static class ScanCodeInfo { | |||
| @XmlElement(name = "ScanType") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("ScanType") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String scanType; | |||
| @XmlElement(name = "ScanResult") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("ScanResult") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String scanResult; | |||
| /** | |||
| @@ -530,15 +517,13 @@ public class WxMpXmlMessage { | |||
| } | |||
| @XmlRootElement(name = "SendPicsInfo") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("SendPicsInfo") | |||
| public static class SendPicsInfo { | |||
| @XmlElement(name = "Count") | |||
| @XStreamAlias("Count") | |||
| private Long count; | |||
| @XmlElementWrapper(name="PicList") | |||
| @XmlElement(name = "item") | |||
| @XStreamAlias("PicList") | |||
| protected final List<Item> picList = new ArrayList<Item>(); | |||
| public Long getCount() { | |||
| @@ -553,47 +538,44 @@ public class WxMpXmlMessage { | |||
| return picList; | |||
| } | |||
| @XmlRootElement(name = "item") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XmlType(name = "WxXmlMessage.SendPicsInfo.Item") | |||
| @XStreamAlias("item") | |||
| public static class Item { | |||
| @XmlElement(name = "PicMd5Sum") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| private String PicMd5Sum; | |||
| @XStreamAlias("PicMd5Sum") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String picMd5Sum; | |||
| public String getPicMd5Sum() { | |||
| return PicMd5Sum; | |||
| return picMd5Sum; | |||
| } | |||
| public void setPicMd5Sum(String picMd5Sum) { | |||
| PicMd5Sum = picMd5Sum; | |||
| this.picMd5Sum = picMd5Sum; | |||
| } | |||
| } | |||
| } | |||
| @XmlRootElement(name = "SendLocationInfo") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("SendLocationInfo") | |||
| public static class SendLocationInfo { | |||
| @XmlElement(name = "Location_X") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Location_X") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String locationX; | |||
| @XmlElement(name = "Location_Y") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Location_Y") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String locationY; | |||
| @XmlElement(name = "Scale") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Scale") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String scale; | |||
| @XmlElement(name = "Label") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Label") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String label; | |||
| @XmlElement(name = "Poiname") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Poiname") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String poiname; | |||
| public String getLocationX() { | |||
| @@ -0,0 +1,27 @@ | |||
| package me.chanjar.weixin.mp.bean; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.XStreamMediaIdConverter; | |||
| @XStreamAlias("xml") | |||
| public class WxMpXmlOutImageMessage extends WxMpXmlOutMessage { | |||
| @XStreamAlias("Image") | |||
| @XStreamConverter(value = XStreamMediaIdConverter.class) | |||
| private String mediaId; | |||
| public String getMediaId() { | |||
| return mediaId; | |||
| } | |||
| public void setMediaId(String mediaId) { | |||
| this.mediaId = mediaId; | |||
| } | |||
| public WxMpXmlOutImageMessage() { | |||
| this.msgType = WxConsts.XML_MSG_IMAGE; | |||
| } | |||
| } | |||
| @@ -1,35 +1,29 @@ | |||
| package me.chanjar.weixin.mp.bean; | |||
| import me.chanjar.weixin.common.util.xml.AdapterCDATA; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; | |||
| import me.chanjar.weixin.mp.api.WxMpConfigStorage; | |||
| import me.chanjar.weixin.mp.bean.outxmlbuilder.*; | |||
| import me.chanjar.weixin.mp.util.crypto.WxMpCryptUtil; | |||
| import me.chanjar.weixin.mp.util.xml.XmlTransformer; | |||
| import me.chanjar.weixin.mp.util.xml.XStreamTransformer; | |||
| import javax.xml.bind.JAXBException; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlElement; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| @XStreamAlias("xml") | |||
| public abstract class WxMpXmlOutMessage { | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| public class WxMpXmlOutMessage { | |||
| @XmlElement(name="ToUserName") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("ToUserName") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| protected String toUserName; | |||
| @XmlElement(name="FromUserName") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("FromUserName") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| protected String fromUserName; | |||
| @XmlElement(name="CreateTime") | |||
| @XStreamAlias("CreateTime") | |||
| protected Long createTime; | |||
| @XmlElement(name="MsgType") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("MsgType") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| protected String msgType; | |||
| public String getToUserName() { | |||
| @@ -65,11 +59,7 @@ public class WxMpXmlOutMessage { | |||
| } | |||
| public String toXml() { | |||
| try { | |||
| return XmlTransformer.toXml((Class)this.getClass(), this); | |||
| } catch (JAXBException e) { | |||
| throw new RuntimeException(e); | |||
| } | |||
| return XStreamTransformer.toXml((Class) this.getClass(), this); | |||
| } | |||
| /** | |||
| @@ -1,19 +1,14 @@ | |||
| package me.chanjar.weixin.mp.bean; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.AdapterCDATA; | |||
| import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlElement; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxMpXmlOutMusicMessage extends WxMpXmlOutMessage { | |||
| @XmlElement(name = "Music") | |||
| @XStreamAlias("Music") | |||
| protected final Music music = new Music(); | |||
| public WxMpXmlOutMusicMessage() { | |||
| @@ -60,28 +55,27 @@ public class WxMpXmlOutMusicMessage extends WxMpXmlOutMessage { | |||
| music.setHqMusicUrl(hqMusicUrl); | |||
| } | |||
| @XmlRootElement(name = "Music") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| private static class Music { | |||
| @XStreamAlias("Music") | |||
| public static class Music { | |||
| @XmlElement(name = "Title") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Title") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String title; | |||
| @XmlElement(name = "Description") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Description") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String description; | |||
| @XmlElement(name="ThumbMediaId") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("ThumbMediaId") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String thumbMediaId; | |||
| @XmlElement(name="MusicUrl") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("MusicUrl") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String musicUrl; | |||
| @XmlElement(name="HQMusicUrl") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("HQMusicUrl") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String hqMusicUrl; | |||
| public String getTitle() { | |||
| @@ -1,22 +1,20 @@ | |||
| package me.chanjar.weixin.mp.bean; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.AdapterCDATA; | |||
| import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; | |||
| import javax.xml.bind.annotation.*; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxMpXmlOutNewsMessage extends WxMpXmlOutMessage { | |||
| @XmlElement(name = "ArticleCount") | |||
| @XStreamAlias("ArticleCount") | |||
| protected int articleCount; | |||
| @XmlElementWrapper(name="Articles") | |||
| @XmlElement(name = "item") | |||
| @XStreamAlias("Articles") | |||
| protected final List<Item> articles = new ArrayList<Item>(); | |||
| public WxMpXmlOutNewsMessage() { | |||
| @@ -37,24 +35,23 @@ public class WxMpXmlOutNewsMessage extends WxMpXmlOutMessage { | |||
| } | |||
| @XmlRootElement(name = "Item") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("item") | |||
| public static class Item { | |||
| @XmlElement(name = "Title") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Title") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String Title; | |||
| @XmlElement(name = "Description") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Description") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String Description; | |||
| @XmlElement(name="PicUrl") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("PicUrl") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String PicUrl; | |||
| @XmlElement(name="Url") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Url") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String Url; | |||
| public String getTitle() { | |||
| @@ -1,20 +1,15 @@ | |||
| package me.chanjar.weixin.mp.bean; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.AdapterCDATA; | |||
| import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlElement; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxMpXmlOutTextMessage extends WxMpXmlOutMessage { | |||
| @XmlElement(name="Content") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Content") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String content; | |||
| public WxMpXmlOutTextMessage() { | |||
| @@ -1,19 +1,14 @@ | |||
| package me.chanjar.weixin.mp.bean; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.AdapterCDATA; | |||
| import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlElement; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxMpXmlOutVideoMessage extends WxMpXmlOutMessage { | |||
| @XmlElement(name = "Video") | |||
| @XStreamAlias("Video") | |||
| protected final Video video = new Video(); | |||
| public WxMpXmlOutVideoMessage() { | |||
| @@ -45,20 +40,19 @@ public class WxMpXmlOutVideoMessage extends WxMpXmlOutMessage { | |||
| } | |||
| @XmlRootElement(name = "Video") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| private static class Video { | |||
| @XStreamAlias("Video") | |||
| public static class Video { | |||
| @XmlElement(name = "MediaId") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("MediaId") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String mediaId; | |||
| @XmlElement(name = "Title") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Title") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String title; | |||
| @XmlElement(name = "Description") | |||
| @XmlJavaTypeAdapter(AdapterCDATA.class) | |||
| @XStreamAlias("Description") | |||
| @XStreamConverter(value=XStreamCDataConverter.class) | |||
| private String description; | |||
| public String getMediaId() { | |||
| @@ -1,26 +1,21 @@ | |||
| package me.chanjar.weixin.mp.bean; | |||
| import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
| import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
| import me.chanjar.weixin.common.api.WxConsts; | |||
| import me.chanjar.weixin.common.util.xml.MediaIdMarshaller; | |||
| import me.chanjar.weixin.common.util.xml.XStreamMediaIdConverter; | |||
| import javax.xml.bind.annotation.XmlAccessType; | |||
| import javax.xml.bind.annotation.XmlAccessorType; | |||
| import javax.xml.bind.annotation.XmlElement; | |||
| import javax.xml.bind.annotation.XmlRootElement; | |||
| import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; | |||
| @XmlRootElement(name = "xml") | |||
| @XmlAccessorType(XmlAccessType.FIELD) | |||
| @XStreamAlias("xml") | |||
| public class WxMpXmlOutVoiceMessage extends WxMpXmlOutMessage { | |||
| @XmlElement(name="Voice") | |||
| @XmlJavaTypeAdapter(MediaIdMarshaller.class) | |||
| @XStreamAlias("Voice") | |||
| @XStreamConverter(value = XStreamMediaIdConverter.class) | |||
| private String mediaId; | |||
| public WxMpXmlOutVoiceMessage() { | |||
| this.msgType = WxConsts.XML_MSG_VOICE; | |||
| } | |||
| public String getMediaId() { | |||
| return mediaId; | |||
| } | |||
| @@ -28,5 +23,5 @@ public class WxMpXmlOutVoiceMessage extends WxMpXmlOutMessage { | |||
| public void setMediaId(String mediaId) { | |||
| this.mediaId = mediaId; | |||
| } | |||
| } | |||
| @@ -1,12 +1,12 @@ | |||
| package me.chanjar.weixin.mp.bean.outxmlbuilder; | |||
| import me.chanjar.weixin.mp.bean.WxMpMpXmlOutImageMessage; | |||
| import me.chanjar.weixin.mp.bean.WxMpXmlOutImageMessage; | |||
| /** | |||
| * 图片消息builder | |||
| * @author chanjarster | |||
| */ | |||
| public final class ImageBuilder extends BaseBuilder<ImageBuilder, WxMpMpXmlOutImageMessage> { | |||
| public final class ImageBuilder extends BaseBuilder<ImageBuilder, WxMpXmlOutImageMessage> { | |||
| private String mediaId; | |||
| @@ -15,8 +15,8 @@ public final class ImageBuilder extends BaseBuilder<ImageBuilder, WxMpMpXmlOutIm | |||
| return this; | |||
| } | |||
| public WxMpMpXmlOutImageMessage build() { | |||
| WxMpMpXmlOutImageMessage m = new WxMpMpXmlOutImageMessage(); | |||
| public WxMpXmlOutImageMessage build() { | |||
| WxMpXmlOutImageMessage m = new WxMpXmlOutImageMessage(); | |||
| setCommon(m); | |||
| m.setMediaId(this.mediaId); | |||
| return m; | |||
| @@ -1,11 +1,7 @@ | |||
| package me.chanjar.weixin.mp.bean.result; | |||
| import me.chanjar.weixin.common.util.json.WxGsonBuilder; | |||
| import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; | |||
| /** | |||
| * Created by qianjia on 14/11/26. | |||
| */ | |||
| public class WxMpOAuth2AccessToken { | |||
| private String accessToken; | |||
| @@ -66,7 +62,7 @@ public class WxMpOAuth2AccessToken { | |||
| public String toString() { | |||
| return "WxMpOAuth2AccessToken{" + | |||
| "accessToken='" + accessToken + '\'' + | |||
| ", expiresIn=" + expiresIn + | |||
| ", expiresTime=" + expiresIn + | |||
| ", refreshToken='" + refreshToken + '\'' + | |||
| ", openId='" + openId + '\'' + | |||
| ", scope='" + scope + '\'' + | |||
| @@ -1,12 +1,12 @@ | |||
| package me.chanjar.weixin.mp.util.http; | |||
| import me.chanjar.weixin.common.bean.result.WxError; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import me.chanjar.weixin.common.util.fs.FileUtils; | |||
| import me.chanjar.weixin.common.util.http.InputStreamResponseHandler; | |||
| import me.chanjar.weixin.common.util.http.RequestExecutor; | |||
| import me.chanjar.weixin.common.util.http.Utf8ResponseHandler; | |||
| import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket; | |||
| import me.chanjar.weixin.common.exception.WxErrorException; | |||
| import org.apache.http.Header; | |||
| import org.apache.http.HttpHost; | |||
| import org.apache.http.client.ClientProtocolException; | |||
| @@ -14,11 +14,6 @@ import me.chanjar.weixin.mp.bean.WxMpCustomMessage; | |||
| import java.lang.reflect.Type; | |||
| /** | |||
| * | |||
| * @author qianjia | |||
| * | |||
| */ | |||
| public class WxMpCustomMessageGsonAdapter implements JsonSerializer<WxMpCustomMessage> { | |||
| public JsonElement serialize(WxMpCustomMessage message, Type typeOfSrc, JsonSerializationContext context) { | |||
| @@ -14,11 +14,6 @@ import me.chanjar.weixin.mp.bean.WxMpGroup; | |||
| import java.lang.reflect.Type; | |||
| /** | |||
| * | |||
| * @author qianjia | |||
| * | |||
| */ | |||
| public class WxMpGroupGsonAdapter implements JsonSerializer<WxMpGroup>, JsonDeserializer<WxMpGroup> { | |||
| public JsonElement serialize(WxMpGroup group, Type typeOfSrc, JsonSerializationContext context) { | |||
| @@ -4,7 +4,6 @@ import com.google.gson.Gson; | |||
| import com.google.gson.GsonBuilder; | |||
| import me.chanjar.weixin.mp.bean.*; | |||
| import me.chanjar.weixin.mp.bean.result.*; | |||
| import me.chanjar.weixin.mp.bean.result.WxMpOAuth2AccessToken; | |||
| public class WxMpGsonBuilder { | |||
| @@ -17,11 +17,6 @@ import me.chanjar.weixin.mp.bean.WxMpMassGroupMessage; | |||
| import java.lang.reflect.Type; | |||
| /** | |||
| * | |||
| * @author qianjia | |||
| * | |||
| */ | |||
| public class WxMpMassGroupMessageGsonAdapter implements JsonSerializer<WxMpMassGroupMessage> { | |||
| public JsonElement serialize(WxMpMassGroupMessage message, Type typeOfSrc, JsonSerializationContext context) { | |||
| @@ -13,11 +13,6 @@ import me.chanjar.weixin.mp.bean.WxMpMassNews; | |||
| import java.lang.reflect.Type; | |||
| /** | |||
| * | |||
| * @author qianjia | |||
| * | |||
| */ | |||
| public class WxMpMassNewsGsonAdapter implements JsonSerializer<WxMpMassNews> { | |||
| public JsonElement serialize(WxMpMassNews message, Type typeOfSrc, JsonSerializationContext context) { | |||
| @@ -14,11 +14,6 @@ import me.chanjar.weixin.mp.bean.WxMpMassOpenIdsMessage; | |||
| import java.lang.reflect.Type; | |||
| /** | |||
| * | |||
| * @author qianjia | |||
| * | |||
| */ | |||
| public class WxMpMassOpenIdsMessageGsonAdapter implements JsonSerializer<WxMpMassOpenIdsMessage> { | |||
| public JsonElement serialize(WxMpMassOpenIdsMessage message, Type typeOfSrc, JsonSerializationContext context) { | |||
| @@ -1,15 +1,11 @@ | |||
| package me.chanjar.weixin.mp.util.json; | |||
| import com.google.gson.*; | |||
| import me.chanjar.weixin.common.bean.WxAccessToken; | |||
| import me.chanjar.weixin.common.util.json.GsonHelper; | |||
| import me.chanjar.weixin.mp.bean.result.WxMpOAuth2AccessToken; | |||
| import java.lang.reflect.Type; | |||
| /** | |||
| * Created by qianjia on 14/11/28. | |||
| */ | |||
| public class WxMpOAuth2AccessTokenAdapter implements JsonDeserializer<WxMpOAuth2AccessToken> { | |||
| public WxMpOAuth2AccessToken deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws | |||
| @@ -10,8 +10,6 @@ package me.chanjar.weixin.mp.util.json; | |||
| import com.google.gson.*; | |||
| import me.chanjar.weixin.common.util.json.GsonHelper; | |||
| import me.chanjar.weixin.mp.bean.result.WxMpMassSendResult; | |||
| import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket; | |||
| import me.chanjar.weixin.mp.bean.result.WxMpSemanticQueryResult; | |||
| import java.lang.reflect.Type; | |||