diff --git a/weixin4j-serverX/.classpath b/weixin4j-serverX/.classpath
new file mode 100644
index 00000000..61a75fad
--- /dev/null
+++ b/weixin4j-serverX/.classpath
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/weixin4j-serverX/.gitignore b/weixin4j-serverX/.gitignore
new file mode 100644
index 00000000..b83d2226
--- /dev/null
+++ b/weixin4j-serverX/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/weixin4j-serverX/.project b/weixin4j-serverX/.project
new file mode 100644
index 00000000..f307b322
--- /dev/null
+++ b/weixin4j-serverX/.project
@@ -0,0 +1,23 @@
+
+
+ weixin4j-serverX
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/weixin4j-serverX/.settings/org.eclipse.core.resources.prefs b/weixin4j-serverX/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000..f9fe3459
--- /dev/null
+++ b/weixin4j-serverX/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
+encoding/=UTF-8
diff --git a/weixin4j-serverX/.settings/org.eclipse.jdt.core.prefs b/weixin4j-serverX/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000..60105c1b
--- /dev/null
+++ b/weixin4j-serverX/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,5 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/weixin4j-serverX/.settings/org.eclipse.m2e.core.prefs b/weixin4j-serverX/.settings/org.eclipse.m2e.core.prefs
new file mode 100644
index 00000000..f897a7f1
--- /dev/null
+++ b/weixin4j-serverX/.settings/org.eclipse.m2e.core.prefs
@@ -0,0 +1,4 @@
+activeProfiles=
+eclipse.preferences.version=1
+resolveWorkspaceProjects=true
+version=1
diff --git a/weixin4j-serverX/pom.xml b/weixin4j-serverX/pom.xml
new file mode 100644
index 00000000..5d0682aa
--- /dev/null
+++ b/weixin4j-serverX/pom.xml
@@ -0,0 +1,48 @@
+
+
+ 4.0.0
+
+ com.foxinmy
+ weixin4j
+ 1.7.4
+
+ weixin4j-serverX
+ 0.0.1
+ weixin4j-serverX
+ https://github.com/foxinmy/weixin4j/tree/master/weixin4j-serverX
+ 微信消息接入服务(spring mvc实现)
+
+ 4.2.0.RELEASE
+
+
+
+ junit
+ junit
+
+
+ org.springframework
+ spring-webmvc
+ ${spring.version}
+
+
+ javax.servlet
+ servlet-api
+ 2.5
+ provided
+
+
+ javax.servlet.jsp
+ jsp-api
+ provided
+ 2.1
+
+
+
+
+ 174975857@qq.com
+ Yz
+
+
+
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/annotation/WxMessageHandler.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/annotation/WxMessageHandler.java
new file mode 100644
index 00000000..2fa16943
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/annotation/WxMessageHandler.java
@@ -0,0 +1,19 @@
+package com.zone.weixin4j.annotation;
+
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Created by Yz on 2017/3/13.
+ * WxMessageHandler
+ * WxMessageHandler 注解声明处理消息内容的类
+ */
+@Target({java.lang.annotation.ElementType.TYPE})
+@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@Documented
+@Component
+public @interface WxMessageHandler {
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/annotation/WxMessageInterceptor.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/annotation/WxMessageInterceptor.java
new file mode 100644
index 00000000..ce0f3d62
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/annotation/WxMessageInterceptor.java
@@ -0,0 +1,19 @@
+package com.zone.weixin4j.annotation;
+
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Created by Yz on 2017/3/13.
+ * WxMessageInterceptor
+ * WxMessageInterceptor 注解声明拦截消息内容的类
+ */
+@Target({java.lang.annotation.ElementType.TYPE})
+@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@Documented
+@Component
+public @interface WxMessageInterceptor {
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/base64/Base64.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/base64/Base64.java
new file mode 100644
index 00000000..062e6de3
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/base64/Base64.java
@@ -0,0 +1,888 @@
+/*
+ * 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 com.zone.weixin4j.base64;
+
+import com.zone.weixin4j.util.ServerToolkits;
+
+import java.math.BigInteger;
+
+/**
+ *
+ * reference of apache pivot
+ *
+ *
+ * Provides Base64 encoding and decoding as defined by RFC 2045.
+ *
+ *
+ * This class implements section 6.8. Base64
+ * Content-Transfer-Encoding from RFC 2045 Multipurpose Internet
+ * Mail Extensions (MIME) Part One: Format of Internet Message Bodies by
+ * Freed and Borenstein.
+ *
+ *
+ * The class can be parameterized in the following manner with various
+ * constructors:
+ *
+ * - URL-safe mode: Default off.
+ * - Line length: Default 76. Line length that aren't multiples of 4 will
+ * still essentially end up being multiples of 4 in the encoded data.
+ *
- Line separator: Default is CRLF ("\r\n")
+ *
+ *
+ *
+ * Since this class operates directly on byte streams, and not character
+ * streams, it is hard-coded to only encode/decode character encodings which are
+ * compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8,
+ * etc).
+ *
+ *
+ * This class is thread-safe.
+ *
+ *
+ * @see RFC 2045
+ * @since 1.0
+ * @version $Id: Base64.java 1447577 2013-02-19 02:45:18Z julius $
+ */
+public class Base64 extends BaseNCodec {
+
+ /**
+ * BASE32 characters are 6 bits in length. They are formed by taking a block
+ * of 3 octets to form a 24-bit string, which is converted into 4 BASE64
+ * characters.
+ */
+ private static final int BITS_PER_ENCODED_BYTE = 6;
+ private static final int BYTES_PER_UNENCODED_BLOCK = 3;
+ private static final int BYTES_PER_ENCODED_BLOCK = 4;
+
+ /**
+ * Chunk separator per RFC 2045 section 2.1.
+ *
+ *
+ * N.B. The next major release may break compatibility and make this field
+ * private.
+ *
+ *
+ * @see RFC 2045 section
+ * 2.1
+ */
+ static final byte[] CHUNK_SEPARATOR = { '\r', '\n' };
+
+ /**
+ * This array is a lookup table that translates 6-bit positive integer index
+ * values into their "Base64 Alphabet" equivalents as specified in Table 1
+ * of RFC 2045.
+ *
+ * Thanks to "commons" project in ws.apache.org for this code.
+ * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ */
+ private static final byte[] STANDARD_ENCODE_TABLE = { 'A', 'B', 'C', 'D',
+ 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
+ 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
+ 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
+ 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
+ '4', '5', '6', '7', '8', '9', '+', '/' };
+
+ /**
+ * This is a copy of the STANDARD_ENCODE_TABLE above, but with + and /
+ * changed to - and _ to make the encoded Base64 results more URL-SAFE. This
+ * table is only used when the Base64's mode is set to URL-SAFE.
+ */
+ private static final byte[] URL_SAFE_ENCODE_TABLE = { 'A', 'B', 'C', 'D',
+ 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
+ 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
+ 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
+ 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
+ '4', '5', '6', '7', '8', '9', '-', '_' };
+
+ /**
+ * This array is a lookup table that translates Unicode characters drawn
+ * from the "Base64 Alphabet" (as specified in Table 1 of RFC 2045) into
+ * their 6-bit positive integer equivalents. Characters that are not in the
+ * Base64 alphabet but fall within the bounds of the array are translated to
+ * -1.
+ *
+ * Note: '+' and '-' both decode to 62. '/' and '_' both decode to 63. This
+ * means decoder seamlessly handles both URL_SAFE and STANDARD base64. (The
+ * encoder, on the other hand, needs to know ahead of time what to emit).
+ *
+ * Thanks to "commons" project in ws.apache.org for this code.
+ * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ */
+ private static final byte[] DECODE_TABLE = { -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
+ -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
+ 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1,
+ -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
+ 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 };
+
+ /**
+ * Base64 uses 6-bit fields.
+ */
+ /** Mask used to extract 6 bits, used when encoding */
+ private static final int MASK_6BITS = 0x3f;
+
+ // The static final fields above are used for the original static byte[]
+ // methods on Base64.
+ // The private member fields below are used with the new streaming approach,
+ // which requires
+ // some state be preserved between calls of encode() and decode().
+
+ /**
+ * Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE
+ * above remains static because it is able to decode both STANDARD and
+ * URL_SAFE streams, but the encodeTable must be a member variable so we can
+ * switch between the two modes.
+ */
+ private final byte[] encodeTable;
+
+ // Only one decode table currently; keep for consistency with Base32 code
+ private final byte[] decodeTable = DECODE_TABLE;
+
+ /**
+ * Line separator for encoding. Not used when decoding. Only used if
+ * lineLength > 0.
+ */
+ private final byte[] lineSeparator;
+
+ /**
+ * Convenience variable to help us determine when our buffer is going to run
+ * out of room and needs resizing.
+ * decodeSize = 3 + lineSeparator.length;
+ */
+ private final int decodeSize;
+
+ /**
+ * Convenience variable to help us determine when our buffer is going to run
+ * out of room and needs resizing.
+ * encodeSize = 4 + lineSeparator.length;
+ */
+ private final int encodeSize;
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in
+ * URL-unsafe mode.
+ *
+ * When encoding the line length is 0 (no chunking), and the encoding table
+ * is STANDARD_ENCODE_TABLE.
+ *
+ *
+ *
+ * When decoding all variants are supported.
+ *
+ */
+ public Base64() {
+ this(0);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in the
+ * given URL-safe mode.
+ *
+ * When encoding the line length is 76, the line separator is CRLF, and the
+ * encoding table is STANDARD_ENCODE_TABLE.
+ *
+ *
+ *
+ * When decoding all variants are supported.
+ *
+ *
+ * @param urlSafe
+ * if {@code true}, URL-safe encoding is used. In most cases this
+ * should be set to {@code false}.
+ * @since 1.4
+ */
+ public Base64(final boolean urlSafe) {
+ this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in
+ * URL-unsafe mode.
+ *
+ * When encoding the line length is given in the constructor, the line
+ * separator is CRLF, and the encoding table is STANDARD_ENCODE_TABLE.
+ *
+ *
+ * Line lengths that aren't multiples of 4 will still essentially end up
+ * being multiples of 4 in the encoded data.
+ *
+ *
+ * When decoding all variants are supported.
+ *
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length
+ * (rounded down to nearest multiple of 4). If lineLength <= 0,
+ * then the output will not be divided into lines (chunks).
+ * Ignored when decoding.
+ * @since 1.4
+ */
+ public Base64(final int lineLength) {
+ this(lineLength, CHUNK_SEPARATOR);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in
+ * URL-unsafe mode.
+ *
+ * When encoding the line length and line separator are given in the
+ * constructor, and the encoding table is STANDARD_ENCODE_TABLE.
+ *
+ *
+ * Line lengths that aren't multiples of 4 will still essentially end up
+ * being multiples of 4 in the encoded data.
+ *
+ *
+ * When decoding all variants are supported.
+ *
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length
+ * (rounded down to nearest multiple of 4). If lineLength <= 0,
+ * then the output will not be divided into lines (chunks).
+ * Ignored when decoding.
+ * @param lineSeparator
+ * Each line of encoded data will end with this sequence of
+ * bytes.
+ * @throws IllegalArgumentException
+ * Thrown when the provided lineSeparator included some base64
+ * characters.
+ * @since 1.4
+ */
+ public Base64(final int lineLength, final byte[] lineSeparator) {
+ this(lineLength, lineSeparator, false);
+ }
+
+ /**
+ * Creates a Base64 codec used for decoding (all modes) and encoding in
+ * URL-unsafe mode.
+ *
+ * When encoding the line length and line separator are given in the
+ * constructor, and the encoding table is STANDARD_ENCODE_TABLE.
+ *
+ *
+ * Line lengths that aren't multiples of 4 will still essentially end up
+ * being multiples of 4 in the encoded data.
+ *
+ *
+ * When decoding all variants are supported.
+ *
+ *
+ * @param lineLength
+ * Each line of encoded data will be at most of the given length
+ * (rounded down to nearest multiple of 4). If lineLength <= 0,
+ * then the output will not be divided into lines (chunks).
+ * Ignored when decoding.
+ * @param lineSeparator
+ * Each line of encoded data will end with this sequence of
+ * bytes.
+ * @param urlSafe
+ * Instead of emitting '+' and '/' we emit '-' and '_'
+ * respectively. urlSafe is only applied to encode operations.
+ * Decoding seamlessly handles both modes. Note: no padding is
+ * added when using the URL-safe alphabet.
+ * @throws IllegalArgumentException
+ * The provided lineSeparator included some base64 characters.
+ * That's not going to work!
+ * @since 1.4
+ */
+ public Base64(final int lineLength, final byte[] lineSeparator,
+ final boolean urlSafe) {
+ super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, lineLength,
+ lineSeparator == null ? 0 : lineSeparator.length);
+ // TODO could be simplified if there is no requirement to reject invalid
+ // line sep when length <=0
+ // @see test case Base64Test.testConstructors()
+ if (lineSeparator != null) {
+ if (containsAlphabetOrPad(lineSeparator)) {
+ final String sep = ServerToolkits.newStringUtf8(lineSeparator);
+ throw new IllegalArgumentException(
+ "lineSeparator must not contain base64 characters: ["
+ + sep + "]");
+ }
+ if (lineLength > 0) { // null line-sep forces no chunking rather
+ // than throwing IAE
+ this.encodeSize = BYTES_PER_ENCODED_BLOCK
+ + lineSeparator.length;
+ this.lineSeparator = new byte[lineSeparator.length];
+ System.arraycopy(lineSeparator, 0, this.lineSeparator, 0,
+ lineSeparator.length);
+ } else {
+ this.encodeSize = BYTES_PER_ENCODED_BLOCK;
+ this.lineSeparator = null;
+ }
+ } else {
+ this.encodeSize = BYTES_PER_ENCODED_BLOCK;
+ this.lineSeparator = null;
+ }
+ this.decodeSize = this.encodeSize - 1;
+ this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE
+ : STANDARD_ENCODE_TABLE;
+ }
+
+ /**
+ * Returns our current encode mode. True if we're URL-SAFE, false otherwise.
+ *
+ * @return true if we're in URL-SAFE mode, false otherwise.
+ * @since 1.4
+ */
+ public boolean isUrlSafe() {
+ return this.encodeTable == URL_SAFE_ENCODE_TABLE;
+ }
+
+ /**
+ *
+ * Encodes all of the provided data, starting at inPos, for inAvail bytes.
+ * Must be called at least twice: once with the data to encode, and once
+ * with inAvail set to "-1" to alert encoder that EOF has been reached, to
+ * flush last remaining bytes (if not multiple of 3).
+ *
+ *
+ * Note: no padding is added when encoding using the URL-safe
+ * alphabet.
+ *
+ *
+ * Thanks to "commons" project in ws.apache.org for the bitwise operations,
+ * and general approach.
+ * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ *
+ *
+ * @param in
+ * byte[] array of binary data to base64 encode.
+ * @param inPos
+ * Position to start reading data from.
+ * @param inAvail
+ * Amount of bytes available from input for encoding.
+ * @param context
+ * the context to be used
+ */
+ @Override
+ void encode(final byte[] in, int inPos, final int inAvail,
+ final Context context) {
+ if (context.eof) {
+ return;
+ }
+ // inAvail < 0 is how we're informed of EOF in the underlying data we're
+ // encoding.
+ if (inAvail < 0) {
+ context.eof = true;
+ if (0 == context.modulus && lineLength == 0) {
+ return; // no leftovers to process and not using chunking
+ }
+ final byte[] buffer = ensureBufferSize(encodeSize, context);
+ final int savedPos = context.pos;
+ switch (context.modulus) { // 0-2
+ case 0: // nothing to do here
+ break;
+ case 1: // 8 bits = 6 + 2
+ // top 6 bits:
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 2)
+ & MASK_6BITS];
+ // remaining 2:
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 4)
+ & MASK_6BITS];
+ // URL-SAFE skips the padding to further reduce size.
+ if (encodeTable == STANDARD_ENCODE_TABLE) {
+ buffer[context.pos++] = PAD;
+ buffer[context.pos++] = PAD;
+ }
+ break;
+
+ case 2: // 16 bits = 6 + 6 + 4
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 10)
+ & MASK_6BITS];
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 4)
+ & MASK_6BITS];
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 2)
+ & MASK_6BITS];
+ // URL-SAFE skips the padding to further reduce size.
+ if (encodeTable == STANDARD_ENCODE_TABLE) {
+ buffer[context.pos++] = PAD;
+ }
+ break;
+ default:
+ throw new IllegalStateException("Impossible modulus "
+ + context.modulus);
+ }
+ context.currentLinePos += context.pos - savedPos; // keep track of
+ // current line
+ // position
+ // if currentPos == 0 we are at the start of a line, so don't add
+ // CRLF
+ if (lineLength > 0 && context.currentLinePos > 0) {
+ System.arraycopy(lineSeparator, 0, buffer, context.pos,
+ lineSeparator.length);
+ context.pos += lineSeparator.length;
+ }
+ } else {
+ for (int i = 0; i < inAvail; i++) {
+ final byte[] buffer = ensureBufferSize(encodeSize, context);
+ context.modulus = (context.modulus + 1)
+ % BYTES_PER_UNENCODED_BLOCK;
+ int b = in[inPos++];
+ if (b < 0) {
+ b += 256;
+ }
+ context.ibitWorkArea = (context.ibitWorkArea << 8) + b; // BITS_PER_BYTE
+ if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to
+ // extract
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 18)
+ & MASK_6BITS];
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 12)
+ & MASK_6BITS];
+ buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 6)
+ & MASK_6BITS];
+ buffer[context.pos++] = encodeTable[context.ibitWorkArea
+ & MASK_6BITS];
+ context.currentLinePos += BYTES_PER_ENCODED_BLOCK;
+ if (lineLength > 0 && lineLength <= context.currentLinePos) {
+ System.arraycopy(lineSeparator, 0, buffer, context.pos,
+ lineSeparator.length);
+ context.pos += lineSeparator.length;
+ context.currentLinePos = 0;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ *
+ * Decodes all of the provided data, starting at inPos, for inAvail bytes.
+ * Should be called at least twice: once with the data to decode, and once
+ * with inAvail set to "-1" to alert decoder that EOF has been reached. The
+ * "-1" call is not necessary when decoding, but it doesn't hurt, either.
+ *
+ *
+ * Ignores all non-base64 characters. This is how chunked (e.g. 76
+ * character) data is handled, since CR and LF are silently ignored, but has
+ * implications for other bytes, too. This method subscribes to the
+ * garbage-in, garbage-out philosophy: it will not check the provided data
+ * for validity.
+ *
+ *
+ * Thanks to "commons" project in ws.apache.org for the bitwise operations,
+ * and general approach.
+ * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
+ *
+ *
+ * @param in
+ * byte[] array of ascii data to base64 decode.
+ * @param inPos
+ * Position to start reading data from.
+ * @param inAvail
+ * Amount of bytes available from input for encoding.
+ * @param context
+ * the context to be used
+ */
+ @Override
+ void decode(final byte[] in, int inPos, final int inAvail,
+ final Context context) {
+ if (context.eof) {
+ return;
+ }
+ if (inAvail < 0) {
+ context.eof = true;
+ }
+ for (int i = 0; i < inAvail; i++) {
+ final byte[] buffer = ensureBufferSize(decodeSize, context);
+ final byte b = in[inPos++];
+ if (b == PAD) {
+ // We're done.
+ context.eof = true;
+ break;
+ } else {
+ if (b >= 0 && b < DECODE_TABLE.length) {
+ final int result = DECODE_TABLE[b];
+ if (result >= 0) {
+ context.modulus = (context.modulus + 1)
+ % BYTES_PER_ENCODED_BLOCK;
+ context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE)
+ + result;
+ if (context.modulus == 0) {
+ buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 16) & MASK_8BITS);
+ buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
+ buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS);
+ }
+ }
+ }
+ }
+ }
+
+ // Two forms of EOF as far as base64 decoder is concerned: actual
+ // EOF (-1) and first time '=' character is encountered in stream.
+ // This approach makes the '=' padding characters completely optional.
+ if (context.eof && context.modulus != 0) {
+ final byte[] buffer = ensureBufferSize(decodeSize, context);
+
+ // We have some spare bits remaining
+ // Output all whole multiples of 8 bits and ignore the rest
+ switch (context.modulus) {
+ // case 0 : // impossible, as excluded above
+ case 1: // 6 bits - ignore entirely
+ // TODO not currently tested; perhaps it is impossible?
+ break;
+ case 2: // 12 bits = 8 + 4
+ context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the
+ // extra 4
+ // bits
+ buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
+ break;
+ case 3: // 18 bits = 8 + 8 + 2
+ context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits
+ buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
+ buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
+ break;
+ default:
+ throw new IllegalStateException("Impossible modulus "
+ + context.modulus);
+ }
+ }
+ }
+
+ /**
+ * Tests a given byte array to see if it contains only valid characters
+ * within the Base64 alphabet. Currently the method treats whitespace as
+ * valid.
+ *
+ * @param arrayOctet
+ * byte array to test
+ * @return {@code true} if all bytes are valid characters in the Base64
+ * alphabet or if the byte array is empty; {@code false}, otherwise
+ * @deprecated 1.5 Use {@link #isBase64(byte[])}, will be removed in 2.0.
+ */
+ @Deprecated
+ public static boolean isArrayByteBase64(final byte[] arrayOctet) {
+ return isBase64(arrayOctet);
+ }
+
+ /**
+ * Returns whether or not the octet is in the base 64 alphabet.
+ *
+ * @param octet
+ * The value to test
+ * @return {@code true} if the value is defined in the the base 64 alphabet,
+ * {@code false} otherwise.
+ * @since 1.4
+ */
+ public static boolean isBase64(final byte octet) {
+ return octet == PAD_DEFAULT
+ || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1);
+ }
+
+ /**
+ * Tests a given String to see if it contains only valid characters within
+ * the Base64 alphabet. Currently the method treats whitespace as valid.
+ *
+ * @param base64
+ * String to test
+ * @return {@code true} if all characters in the String are valid characters
+ * in the Base64 alphabet or if the String is empty; {@code false},
+ * otherwise
+ * @since 1.5
+ */
+ public static boolean isBase64(final String base64) {
+ return isBase64(ServerToolkits.getBytesUtf8(base64));
+ }
+
+ /**
+ * Tests a given byte array to see if it contains only valid characters
+ * within the Base64 alphabet. Currently the method treats whitespace as
+ * valid.
+ *
+ * @param arrayOctet
+ * byte array to test
+ * @return {@code true} if all bytes are valid characters in the Base64
+ * alphabet or if the byte array is empty; {@code false}, otherwise
+ * @since 1.5
+ */
+ public static boolean isBase64(final byte[] arrayOctet) {
+ for (int i = 0; i < arrayOctet.length; i++) {
+ if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm but does not chunk the
+ * output.
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return byte[] containing Base64 characters in their UTF-8
+ * representation.
+ */
+ public static byte[] encodeBase64(final byte[] binaryData) {
+ return encodeBase64(binaryData, false);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm but does not chunk the
+ * output.
+ *
+ * NOTE: We changed the behaviour of this method from multi-line chunking
+ * (commons-codec-1.4) to single-line non-chunking (commons-codec-1.5).
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return String containing Base64 characters.
+ * @since 1.4 (NOTE: 1.4 chunked the output, whereas 1.5 does not).
+ */
+ public static String encodeBase64String(final byte[] binaryData) {
+ return ServerToolkits.newStringUtf8(encodeBase64(binaryData, false));
+ }
+
+ /**
+ * Encodes binary data using a URL-safe variation of the base64 algorithm
+ * but does not chunk the output. The url-safe variation emits - and _
+ * instead of + and / characters. Note: no padding is added.
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return byte[] containing Base64 characters in their UTF-8
+ * representation.
+ * @since 1.4
+ */
+ public static byte[] encodeBase64URLSafe(final byte[] binaryData) {
+ return encodeBase64(binaryData, false, true);
+ }
+
+ /**
+ * Encodes binary data using a URL-safe variation of the base64 algorithm
+ * but does not chunk the output. The url-safe variation emits - and _
+ * instead of + and / characters. Note: no padding is added.
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return String containing Base64 characters
+ * @since 1.4
+ */
+ public static String encodeBase64URLSafeString(final byte[] binaryData) {
+ return ServerToolkits.newStringUtf8(encodeBase64(binaryData, false, true));
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm and chunks the encoded
+ * output into 76 character blocks
+ *
+ * @param binaryData
+ * binary data to encode
+ * @return Base64 characters chunked in 76 character blocks
+ */
+ public static byte[] encodeBase64Chunked(final byte[] binaryData) {
+ return encodeBase64(binaryData, true);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm, optionally chunking the
+ * output into 76 character blocks.
+ *
+ * @param binaryData
+ * Array containing binary data to encode.
+ * @param isChunked
+ * if {@code true} this encoder will chunk the base64 output into
+ * 76 character blocks
+ * @return Base64-encoded data.
+ * @throws IllegalArgumentException
+ * Thrown when the input array needs an output array bigger than
+ * {@link Integer#MAX_VALUE}
+ */
+ public static byte[] encodeBase64(final byte[] binaryData,
+ final boolean isChunked) {
+ return encodeBase64(binaryData, isChunked, false);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm, optionally chunking the
+ * output into 76 character blocks.
+ *
+ * @param binaryData
+ * Array containing binary data to encode.
+ * @param isChunked
+ * if {@code true} this encoder will chunk the base64 output into
+ * 76 character blocks
+ * @param urlSafe
+ * if {@code true} this encoder will emit - and _ instead of the
+ * usual + and / characters. Note: no padding is added when
+ * encoding using the URL-safe alphabet.
+ * @return Base64-encoded data.
+ * @throws IllegalArgumentException
+ * Thrown when the input array needs an output array bigger than
+ * {@link Integer#MAX_VALUE}
+ * @since 1.4
+ */
+ public static byte[] encodeBase64(final byte[] binaryData,
+ final boolean isChunked, final boolean urlSafe) {
+ return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Encodes binary data using the base64 algorithm, optionally chunking the
+ * output into 76 character blocks.
+ *
+ * @param binaryData
+ * Array containing binary data to encode.
+ * @param isChunked
+ * if {@code true} this encoder will chunk the base64 output into
+ * 76 character blocks
+ * @param urlSafe
+ * if {@code true} this encoder will emit - and _ instead of the
+ * usual + and / characters. Note: no padding is added when
+ * encoding using the URL-safe alphabet.
+ * @param maxResultSize
+ * The maximum result size to accept.
+ * @return Base64-encoded data.
+ * @throws IllegalArgumentException
+ * Thrown when the input array needs an output array bigger than
+ * maxResultSize
+ * @since 1.4
+ */
+ public static byte[] encodeBase64(final byte[] binaryData,
+ final boolean isChunked, final boolean urlSafe,
+ final int maxResultSize) {
+ if (binaryData == null || binaryData.length == 0) {
+ return binaryData;
+ }
+
+ // Create this so can use the super-class method
+ // Also ensures that the same roundings are performed by the ctor and
+ // the code
+ final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0,
+ CHUNK_SEPARATOR, urlSafe);
+ final long len = b64.getEncodedLength(binaryData);
+ if (len > maxResultSize) {
+ throw new IllegalArgumentException(
+ "Input array too big, the output array would be bigger ("
+ + len + ") than the specified maximum size of "
+ + maxResultSize);
+ }
+
+ return b64.encode(binaryData);
+ }
+
+ /**
+ * Decodes a Base64 String into octets
+ *
+ * @param base64String
+ * String containing Base64 data
+ * @return Array containing decoded data.
+ * @since 1.4
+ */
+ public static byte[] decodeBase64(final String base64String) {
+ return new Base64().decode(base64String);
+ }
+
+ /**
+ * Decodes Base64 data into octets
+ *
+ * @param base64Data
+ * Byte array containing Base64 data
+ * @return Array containing decoded data.
+ */
+ public static byte[] decodeBase64(final byte[] base64Data) {
+ return new Base64().decode(base64Data);
+ }
+
+ // Implementation of the Encoder Interface
+
+ // Implementation of integer encoding used for crypto
+ /**
+ * Decodes a byte64-encoded integer according to crypto standards such as
+ * W3C's XML-Signature
+ *
+ * @param pArray
+ * a byte array containing base64 character data
+ * @return A BigInteger
+ * @since 1.4
+ */
+ public static BigInteger decodeInteger(final byte[] pArray) {
+ return new BigInteger(1, decodeBase64(pArray));
+ }
+
+ /**
+ * Encodes to a byte64-encoded integer according to crypto standards such as
+ * W3C's XML-Signature
+ *
+ * @param bigInt
+ * a BigInteger
+ * @return A byte array containing base64 character data
+ * @throws NullPointerException
+ * if null is passed in
+ * @since 1.4
+ */
+ public static byte[] encodeInteger(final BigInteger bigInt) {
+ if (bigInt == null) {
+ throw new NullPointerException(
+ "encodeInteger called with null parameter");
+ }
+ return encodeBase64(toIntegerBytes(bigInt), false);
+ }
+
+ /**
+ * Returns a byte-array representation of a BigInteger without
+ * sign bit.
+ *
+ * @param bigInt
+ * BigInteger to be converted
+ * @return a byte array representation of the BigInteger parameter
+ */
+ static byte[] toIntegerBytes(final BigInteger bigInt) {
+ int bitlen = bigInt.bitLength();
+ // round bitlen
+ bitlen = ((bitlen + 7) >> 3) << 3;
+ final byte[] bigBytes = bigInt.toByteArray();
+
+ if (((bigInt.bitLength() % 8) != 0)
+ && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) {
+ return bigBytes;
+ }
+ // set up params for copying everything but sign bit
+ int startSrc = 0;
+ int len = bigBytes.length;
+
+ // if bigInt is exactly byte-aligned, just skip signbit in copy
+ if ((bigInt.bitLength() % 8) == 0) {
+ startSrc = 1;
+ len--;
+ }
+ final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec
+ final byte[] resizedBytes = new byte[bitlen / 8];
+ System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len);
+ return resizedBytes;
+ }
+
+ /**
+ * Returns whether or not the octet is in the Base64 alphabet.
+ *
+ * @param octet
+ * The value to test
+ * @return {@code true} if the value is defined in the the Base64 alphabet
+ * {@code false} otherwise.
+ */
+ @Override
+ protected boolean isInAlphabet(final byte octet) {
+ return octet >= 0 && octet < decodeTable.length
+ && decodeTable[octet] != -1;
+ }
+
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/base64/BaseNCodec.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/base64/BaseNCodec.java
new file mode 100644
index 00000000..cc8ba90a
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/base64/BaseNCodec.java
@@ -0,0 +1,521 @@
+/*
+ * 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 com.zone.weixin4j.base64;
+
+import com.zone.weixin4j.util.ServerToolkits;
+
+import java.util.Arrays;
+
+/**
+ *
+ * reference of apache pivot
+ *
+ *
+ * Abstract superclass for Base-N encoders and decoders.
+ *
+ *
+ * This class is thread-safe.
+ *
+ *
+ * @version $Id: BaseNCodec.java 1465182 2013-04-06 04:03:12Z ggregory $
+ */
+public abstract class BaseNCodec {
+
+ /**
+ * Holds thread context so classes can be thread-safe.
+ *
+ * This class is not itself thread-safe; each thread must allocate its own
+ * copy.
+ *
+ * @since 1.7
+ */
+ static class Context {
+
+ /**
+ * Place holder for the bytes we're dealing with for our based logic.
+ * Bitwise operations store and extract the encoding or decoding from
+ * this variable.
+ */
+ int ibitWorkArea;
+
+ /**
+ * Place holder for the bytes we're dealing with for our based logic.
+ * Bitwise operations store and extract the encoding or decoding from
+ * this variable.
+ */
+ long lbitWorkArea;
+
+ /**
+ * Buffer for streaming.
+ */
+ byte[] buffer;
+
+ /**
+ * Position where next character should be written in the buffer.
+ */
+ int pos;
+
+ /**
+ * Position where next character should be read from the buffer.
+ */
+ int readPos;
+
+ /**
+ * Boolean flag to indicate the EOF has been reached. Once EOF has been
+ * reached, this object becomes useless, and must be thrown away.
+ */
+ boolean eof;
+
+ /**
+ * Variable tracks how many characters have been written to the current
+ * line. Only used when encoding. We use it to make sure each encoded
+ * line never goes beyond lineLength (if lineLength > 0).
+ */
+ int currentLinePos;
+
+ /**
+ * Writes to the buffer only occur after every 3/5 reads when encoding,
+ * and every 4/8 reads when decoding. This variable helps track that.
+ */
+ int modulus;
+
+ Context() {
+ }
+
+ /**
+ * Returns a String useful for debugging (especially within a debugger.)
+ *
+ * @return a String useful for debugging.
+ */
+ @SuppressWarnings("boxing")
+ // OK to ignore boxing here
+ @Override
+ public String toString() {
+ return String.format(
+ "%s[buffer=%s, currentLinePos=%s, eof=%s, ibitWorkArea=%s, lbitWorkArea=%s, "
+ + "modulus=%s, pos=%s, readPos=%s]", this
+ .getClass().getSimpleName(), Arrays
+ .toString(buffer), currentLinePos, eof,
+ ibitWorkArea, lbitWorkArea, modulus, pos, readPos);
+ }
+ }
+
+ /**
+ * EOF
+ *
+ * @since 1.7
+ */
+ static final int EOF = -1;
+
+ /**
+ * MIME chunk size per RFC 2045 section 6.8.
+ *
+ *
+ * The {@value} character limit does not count the trailing CRLF, but counts
+ * all other characters, including any equal signs.
+ *
+ *
+ * @see RFC 2045 section
+ * 6.8
+ */
+ public static final int MIME_CHUNK_SIZE = 76;
+
+ /**
+ * PEM chunk size per RFC 1421 section 4.3.2.4.
+ *
+ *
+ * The {@value} character limit does not count the trailing CRLF, but counts
+ * all other characters, including any equal signs.
+ *
+ *
+ * @see RFC 1421 section
+ * 4.3.2.4
+ */
+ public static final int PEM_CHUNK_SIZE = 64;
+
+ private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2;
+
+ /**
+ * Defines the default buffer size - currently {@value} - must be large
+ * enough for at least one encoded block+separator
+ */
+ private static final int DEFAULT_BUFFER_SIZE = 8192;
+
+ /** Mask used to extract 8 bits, used in decoding bytes */
+ protected static final int MASK_8BITS = 0xff;
+
+ /**
+ * Byte used to pad output.
+ */
+ protected static final byte PAD_DEFAULT = '='; // Allow static access to
+ // default
+
+ protected final byte PAD = PAD_DEFAULT; // instance variable just in case it
+ // needs to vary later
+
+ /**
+ * Number of bytes in each full block of unencoded data, e.g. 4 for Base64
+ * and 5 for Base32
+ */
+ private final int unencodedBlockSize;
+
+ /**
+ * Number of bytes in each full block of encoded data, e.g. 3 for Base64 and
+ * 8 for Base32
+ */
+ private final int encodedBlockSize;
+
+ /**
+ * Chunksize for encoding. Not used when decoding. A value of zero or less
+ * implies no chunking of the encoded data. Rounded down to nearest multiple
+ * of encodedBlockSize.
+ */
+ protected final int lineLength;
+
+ /**
+ * Size of chunk separator. Not used unless {@link #lineLength} > 0.
+ */
+ private final int chunkSeparatorLength;
+
+ /**
+ * Note lineLength is rounded down to the nearest multiple of
+ * {@link #encodedBlockSize} If chunkSeparatorLength is zero,
+ * then chunking is disabled.
+ *
+ * @param unencodedBlockSize
+ * the size of an unencoded block (e.g. Base64 = 3)
+ * @param encodedBlockSize
+ * the size of an encoded block (e.g. Base64 = 4)
+ * @param lineLength
+ * if > 0, use chunking with a length lineLength
+ * @param chunkSeparatorLength
+ * the chunk separator length, if relevant
+ */
+ protected BaseNCodec(final int unencodedBlockSize,
+ final int encodedBlockSize, final int lineLength,
+ final int chunkSeparatorLength) {
+ this.unencodedBlockSize = unencodedBlockSize;
+ this.encodedBlockSize = encodedBlockSize;
+ final boolean useChunking = lineLength > 0 && chunkSeparatorLength > 0;
+ this.lineLength = useChunking ? (lineLength / encodedBlockSize)
+ * encodedBlockSize : 0;
+ this.chunkSeparatorLength = chunkSeparatorLength;
+ }
+
+ /**
+ * Returns true if this object has buffered data for reading.
+ *
+ * @param context
+ * the context to be used
+ * @return true if there is data still available for reading.
+ */
+ boolean hasData(final Context context) { // package protected for access
+ // from I/O streams
+ return context.buffer != null;
+ }
+
+ /**
+ * Returns the amount of buffered data available for reading.
+ *
+ * @param context
+ * the context to be used
+ * @return The amount of buffered data available for reading.
+ */
+ int available(final Context context) { // package protected for access from
+ // I/O streams
+ return context.buffer != null ? context.pos - context.readPos : 0;
+ }
+
+ /**
+ * Get the default buffer size. Can be overridden.
+ *
+ * @return {@link #DEFAULT_BUFFER_SIZE}
+ */
+ protected int getDefaultBufferSize() {
+ return DEFAULT_BUFFER_SIZE;
+ }
+
+ /**
+ * Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}.
+ *
+ * @param context
+ * the context to be used
+ */
+ private byte[] resizeBuffer(final Context context) {
+ if (context.buffer == null) {
+ context.buffer = new byte[getDefaultBufferSize()];
+ context.pos = 0;
+ context.readPos = 0;
+ } else {
+ final byte[] b = new byte[context.buffer.length
+ * DEFAULT_BUFFER_RESIZE_FACTOR];
+ System.arraycopy(context.buffer, 0, b, 0, context.buffer.length);
+ context.buffer = b;
+ }
+ return context.buffer;
+ }
+
+ /**
+ * Ensure that the buffer has room for size bytes
+ *
+ * @param size
+ * minimum spare space required
+ * @param context
+ * the context to be used
+ */
+ protected byte[] ensureBufferSize(final int size, final Context context) {
+ if ((context.buffer == null)
+ || (context.buffer.length < context.pos + size)) {
+ return resizeBuffer(context);
+ }
+ return context.buffer;
+ }
+
+ /**
+ * Extracts buffered data into the provided byte[] array, starting at
+ * position bPos, up to a maximum of bAvail bytes. Returns how many bytes
+ * were actually extracted.
+ *
+ * Package protected for access from I/O streams.
+ *
+ * @param b
+ * byte[] array to extract the buffered data into.
+ * @param bPos
+ * position in byte[] array to start extraction at.
+ * @param bAvail
+ * amount of bytes we're allowed to extract. We may extract fewer
+ * (if fewer are available).
+ * @param context
+ * the context to be used
+ * @return The number of bytes successfully extracted into the provided
+ * byte[] array.
+ */
+ int readResults(final byte[] b, final int bPos, final int bAvail,
+ final Context context) {
+ if (context.buffer != null) {
+ final int len = Math.min(available(context), bAvail);
+ System.arraycopy(context.buffer, context.readPos, b, bPos, len);
+ context.readPos += len;
+ if (context.readPos >= context.pos) {
+ context.buffer = null; // so hasData() will return false, and
+ // this method can return -1
+ }
+ return len;
+ }
+ return context.eof ? EOF : 0;
+ }
+
+ /**
+ * Checks if a byte value is whitespace or not. Whitespace is taken to mean:
+ * space, tab, CR, LF
+ *
+ * @param byteToCheck
+ * the byte to check
+ * @return true if byte is whitespace, false otherwise
+ */
+ protected static boolean isWhiteSpace(final byte byteToCheck) {
+ switch (byteToCheck) {
+ case ' ':
+ case '\n':
+ case '\r':
+ case '\t':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Encodes a byte[] containing binary data, into a String containing
+ * characters in the Base-N alphabet. Uses UTF8 encoding.
+ *
+ * @param pArray
+ * a byte array containing binary data
+ * @return A String containing only Base-N character data
+ */
+ public String encodeToString(final byte[] pArray) {
+ return ServerToolkits.newStringUtf8(encode(pArray));
+ }
+
+ /**
+ * Encodes a byte[] containing binary data, into a String containing
+ * characters in the appropriate alphabet. Uses UTF8 encoding.
+ *
+ * @param pArray
+ * a byte array containing binary data
+ * @return String containing only character data in the appropriate
+ * alphabet.
+ */
+ public String encodeAsString(final byte[] pArray) {
+ return ServerToolkits.newStringUtf8(encode(pArray));
+ }
+
+
+
+ /**
+ * Decodes a String containing characters in the Base-N alphabet.
+ *
+ * @param pArray
+ * A String containing Base-N character data
+ * @return a byte array containing binary data
+ */
+ public byte[] decode(final String pArray) {
+ return decode(ServerToolkits.getBytesUtf8(pArray));
+ }
+
+ /**
+ * Decodes a byte[] containing characters in the Base-N alphabet.
+ *
+ * @param pArray
+ * A byte array containing Base-N character data
+ * @return a byte array containing binary data
+ */
+ public byte[] decode(final byte[] pArray) {
+ if (pArray == null || pArray.length == 0) {
+ return pArray;
+ }
+ final Context context = new Context();
+ decode(pArray, 0, pArray.length, context);
+ decode(pArray, 0, EOF, context); // Notify decoder of EOF.
+ final byte[] result = new byte[context.pos];
+ readResults(result, 0, result.length, context);
+ return result;
+ }
+
+ /**
+ * Encodes a byte[] containing binary data, into a byte[] containing
+ * characters in the alphabet.
+ *
+ * @param pArray
+ * a byte array containing binary data
+ * @return A byte array containing only the basen alphabetic character data
+ */
+ public byte[] encode(final byte[] pArray) {
+ if (pArray == null || pArray.length == 0) {
+ return pArray;
+ }
+ final Context context = new Context();
+ encode(pArray, 0, pArray.length, context);
+ encode(pArray, 0, EOF, context); // Notify encoder of EOF.
+ final byte[] buf = new byte[context.pos - context.readPos];
+ readResults(buf, 0, buf.length, context);
+ return buf;
+ }
+
+ // package protected for access from I/O streams
+ abstract void encode(byte[] pArray, int i, int length, Context context);
+
+ // package protected for access from I/O streams
+ abstract void decode(byte[] pArray, int i, int length, Context context);
+
+ /**
+ * Returns whether or not the octet is in the current alphabet.
+ * Does not allow whitespace or pad.
+ *
+ * @param value
+ * The value to test
+ *
+ * @return {@code true} if the value is defined in the current alphabet,
+ * {@code false} otherwise.
+ */
+ protected abstract boolean isInAlphabet(byte value);
+
+ /**
+ * Tests a given byte array to see if it contains only valid characters
+ * within the alphabet. The method optionally treats whitespace and pad as
+ * valid.
+ *
+ * @param arrayOctet
+ * byte array to test
+ * @param allowWSPad
+ * if {@code true}, then whitespace and PAD are also allowed
+ *
+ * @return {@code true} if all bytes are valid characters in the alphabet or
+ * if the byte array is empty; {@code false}, otherwise
+ */
+ public boolean isInAlphabet(final byte[] arrayOctet,
+ final boolean allowWSPad) {
+ for (int i = 0; i < arrayOctet.length; i++) {
+ if (!isInAlphabet(arrayOctet[i])
+ && (!allowWSPad || (arrayOctet[i] != PAD)
+ && !isWhiteSpace(arrayOctet[i]))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Tests a given String to see if it contains only valid characters within
+ * the alphabet. The method treats whitespace and PAD as valid.
+ *
+ * @param basen
+ * String to test
+ * @return {@code true} if all characters in the String are valid characters
+ * in the alphabet or if the String is empty; {@code false},
+ * otherwise
+ * @see #isInAlphabet(byte[], boolean)
+ */
+ public boolean isInAlphabet(final String basen) {
+ return isInAlphabet(ServerToolkits.getBytesUtf8(basen), true);
+ }
+
+ /**
+ * Tests a given byte array to see if it contains any characters within the
+ * alphabet or PAD.
+ *
+ * Intended for use in checking line-ending arrays
+ *
+ * @param arrayOctet
+ * byte array to test
+ * @return {@code true} if any byte is a valid character in the alphabet or
+ * PAD; {@code false} otherwise
+ */
+ protected boolean containsAlphabetOrPad(final byte[] arrayOctet) {
+ if (arrayOctet == null) {
+ return false;
+ }
+ for (final byte element : arrayOctet) {
+ if (PAD == element || isInAlphabet(element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Calculates the amount of space needed to encode the supplied array.
+ *
+ * @param pArray
+ * byte[] array which will later be encoded
+ *
+ * @return amount of space needed to encoded the supplied array. Returns a
+ * long since a max-len array will require > Integer.MAX_VALUE
+ */
+ public long getEncodedLength(final byte[] pArray) {
+ // Calculate non-chunked size - rounded up to allow for padding
+ // cast to long is needed to avoid possibility of overflow
+ long len = ((pArray.length + unencodedBlockSize - 1) / unencodedBlockSize)
+ * (long) encodedBlockSize;
+ if (lineLength > 0) { // We're using chunking
+ // Round up to nearest multiple
+ len += ((len + lineLength - 1) / lineLength) * chunkSeparatorLength;
+ }
+ return len;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/controller/WxController.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/controller/WxController.java
new file mode 100644
index 00000000..714e609b
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/controller/WxController.java
@@ -0,0 +1,109 @@
+package com.zone.weixin4j.controller;
+
+import com.zone.weixin4j.exception.HttpResponseException;
+import com.zone.weixin4j.exception.MessageInterceptorException;
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.response.WeixinResponse;
+import com.zone.weixin4j.service.WeiXin4jContextAware;
+import com.zone.weixin4j.service.WxService;
+import com.zone.weixin4j.util.AesToken;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.*;
+
+/**
+ * Created by Yz on 2017/3/14.
+ * WxController
+ * Spring 主入口需继承的类 / 模版
+ */
+
+public abstract class WxController {
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private final String defaultCharset = "UTF-8";
+
+ @Autowired
+ protected WxService wxService;
+
+ @Autowired
+ protected WeiXin4jContextAware weiXin4jContextAware;
+
+ protected abstract void doRequest(HttpServletRequest request, HttpServletResponse response, @RequestParam(required = false) String encrypt_type, @RequestParam(required = false) String echostr, @RequestParam(required = false) String timestamp, @RequestParam(required = false) String nonce,
+ @RequestParam(required = false) String signature, @RequestParam(required = false) String msg_signature, @RequestParam(required = false) String weixin_id);
+
+ protected void processMessage(HttpServletRequest request, HttpServletResponse response, String encrypt_type, String echostr, String timestamp, String nonce,
+ String signature, String msg_signature, String weixin_id) {
+ String messageContent = getMessageContent(request);
+ logger.info("read original message {}" + messageContent);
+ AesToken aesToken = weiXin4jContextAware.getAesTokenMap().get(StringUtils.isEmpty(weixin_id) ? "" : weixin_id);
+ WeixinResponse weixinResponse = null;
+ try {
+ weixinResponse = wxService.processRequest(request.getRequestURI(), encrypt_type, echostr, timestamp, nonce, signature, msg_signature, messageContent, aesToken, request);
+ response.setCharacterEncoding(defaultCharset);
+ writeMessage(wxService.transferResponse(weixinResponse), response);
+ } catch (WeixinException e) {
+ logger.error("errorCode : " + e.getErrorCode() + " , errorMsg : " + e.getErrorMsg(), e.getCause());
+ e.printStackTrace();
+ writeMessage("", response);
+ } catch (HttpResponseException e) {
+ logger.error(e.getMessage(), e.getCause());
+ response.setStatus(e.getHttpResponseStatus().getCode());
+ response.setContentType(e.getHttpResponseStatus().getReasonPhrase());
+ writeMessage("", response);
+ } catch (MessageInterceptorException e) {
+ logger.error("errorCode : " + e.getErrorCode() + " , errorMsg : " + e.getErrorMsg(), e.getCause());
+ writeMessage("", response);
+ }
+ }
+
+ protected String getMessageContent(HttpServletRequest request) {
+ try {
+ // 从request中取得输入流
+ InputStream inputStream = request.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, defaultCharset));
+ String line;
+ StringBuilder buf = new StringBuilder();
+ while ((line = reader.readLine()) != null) {
+ buf.append(line);
+ }
+ reader.close();
+ inputStream.close();
+ return buf.toString();
+ } catch (IOException e) {
+ logger.error(e.getMessage(), e.getCause());
+ }
+ return "";
+ }
+
+ protected void writeMessage(String result, HttpServletResponse response) {
+ response.setContentType("application/xml");
+ response.setCharacterEncoding("UTF-8");
+ try {
+ PrintWriter writer = response.getWriter();
+ writer.write(result);
+ writer.flush();
+ writer.close();
+ } catch (IOException e) {
+ logger.error(e.getMessage(), e.getCause());
+ }
+ }
+
+ public void setWxService(WxService wxService) {
+ this.wxService = wxService;
+ }
+
+ public void setWeiXin4jContextAware(WeiXin4jContextAware weiXin4jContextAware) {
+ this.weiXin4jContextAware = weiXin4jContextAware;
+ }
+
+ public WeiXin4jContextAware getWeiXin4jContextAware() {
+ return weiXin4jContextAware;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/DefaultMessageMatcher.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/DefaultMessageMatcher.java
new file mode 100644
index 00000000..dc1ebdaf
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/DefaultMessageMatcher.java
@@ -0,0 +1,187 @@
+package com.zone.weixin4j.dispatcher;
+
+import com.zone.weixin4j.message.*;
+import com.zone.weixin4j.message.event.*;
+import com.zone.weixin4j.mp.event.*;
+import com.zone.weixin4j.qy.event.BatchjobresultMessage;
+import com.zone.weixin4j.qy.event.EnterAgentEventMessage;
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.type.AccountType;
+import com.zone.weixin4j.type.EventType;
+import com.zone.weixin4j.type.MessageType;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 默认MessageMatcher实现(可以改进)
+ *
+ * @className DefaultMessageMatcher
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年6月10日
+ * @since JDK 1.6
+ * @see
+ */
+public class DefaultMessageMatcher implements WeixinMessageMatcher {
+
+ private final Map> messageClassMap;
+
+ public DefaultMessageMatcher() {
+ messageClassMap = new HashMap>();
+ initMessageClass();
+ }
+
+ private void initMessageClass() {
+ // /////////////////////////////////////////////////
+ /******************** 普通消息 ********************/
+ // /////////////////////////////////////////////////
+ initGeneralMessageClass();
+ // /////////////////////////////////////////////////
+ /******************** 事件消息 ********************/
+ // /////////////////////////////////////////////////
+ initEventMessageClass();
+ // /////////////////////////////////////////////////
+ /***************** 公众平台事件消息 *****************/
+ // /////////////////////////////////////////////////
+ initMpEventMessageClass();
+ // /////////////////////////////////////////////////
+ /****************** 企业号事件消息 ******************/
+ // /////////////////////////////////////////////////
+ initQyEventMessageClass();
+ }
+
+ private void initGeneralMessageClass() {
+ for (AccountType accountType : AccountType.values()) {
+ messageClassMap.put(new WeixinMessageKey(MessageType.text.name(),
+ null, accountType), TextMessage.class);
+ messageClassMap.put(new WeixinMessageKey(MessageType.image.name(),
+ null, accountType), ImageMessage.class);
+ messageClassMap.put(new WeixinMessageKey(MessageType.voice.name(),
+ null, accountType), VoiceMessage.class);
+ messageClassMap.put(new WeixinMessageKey(MessageType.video.name(),
+ null, accountType), VideoMessage.class);
+ messageClassMap.put(
+ new WeixinMessageKey(MessageType.shortvideo.name(), null,
+ accountType), VideoMessage.class);
+ messageClassMap.put(
+ new WeixinMessageKey(MessageType.location.name(), null,
+ accountType), LocationMessage.class);
+ messageClassMap.put(new WeixinMessageKey(MessageType.link.name(),
+ null, accountType), LinkMessage.class);
+ }
+ }
+
+ private void initEventMessageClass() {
+ String messageType = MessageType.event.name();
+ EventType[] eventTypes = new EventType[] { EventType.subscribe,
+ EventType.unsubscribe };
+ for (EventType eventType : eventTypes) {
+ messageClassMap.put(
+ new WeixinMessageKey(messageType, eventType.name(),
+ AccountType.MP),
+ ScribeEventMessage.class);
+ }
+ for (EventType eventType : eventTypes) {
+ messageClassMap.put(
+ new WeixinMessageKey(messageType, eventType.name(),
+ AccountType.QY),
+ ScribeEventMessage.class);
+ }
+ for (AccountType accountType : AccountType.values()) {
+ messageClassMap.put(new WeixinMessageKey(messageType,
+ EventType.location.name(), accountType),
+ LocationEventMessage.class);
+ messageClassMap.put(new WeixinMessageKey(messageType,
+ EventType.location_select.name(), accountType),
+ MenuLocationEventMessage.class);
+ for (EventType eventType : new EventType[] { EventType.click,
+ EventType.view }) {
+ messageClassMap.put(
+ new WeixinMessageKey(messageType, eventType.name(),
+ accountType), MenuEventMessage.class);
+ }
+ for (EventType eventType : new EventType[] {
+ EventType.scancode_push, EventType.scancode_waitmsg }) {
+ messageClassMap.put(
+ new WeixinMessageKey(messageType, eventType.name(),
+ accountType), MenuScanEventMessage.class);
+ }
+ for (EventType eventType : new EventType[] {
+ EventType.pic_sysphoto, EventType.pic_photo_or_album,
+ EventType.pic_weixin }) {
+ messageClassMap.put(
+ new WeixinMessageKey(messageType, eventType.name(),
+ accountType), MenuPhotoEventMessage.class);
+ }
+ }
+ }
+
+ private void initMpEventMessageClass() {
+ String messageType = MessageType.event.name();
+ AccountType accountType = AccountType.MP;
+ messageClassMap.put(
+ new WeixinMessageKey(messageType, EventType.scan.name(),
+ accountType),
+ ScanEventMessage.class);
+ messageClassMap.put(new WeixinMessageKey(messageType,
+ EventType.masssendjobfinish.name(), accountType),
+ MassEventMessage.class);
+ messageClassMap.put(new WeixinMessageKey(messageType,
+ EventType.templatesendjobfinish.name(), accountType),
+ TemplatesendjobfinishMessage.class);
+ messageClassMap.put(new WeixinMessageKey(messageType,
+ EventType.kf_create_session.name(), accountType),
+ KfCreateEventMessage.class);
+ messageClassMap.put(new WeixinMessageKey(messageType,
+ EventType.kf_close_session.name(), accountType),
+ KfCloseEventMessage.class);
+ messageClassMap.put(new WeixinMessageKey(messageType,
+ EventType.kf_switch_session.name(), accountType),
+ KfSwitchEventMessage.class);
+ EventType[] eventTypes = new EventType[] {
+ EventType.qualification_verify_success,
+ EventType.naming_verify_success, EventType.annual_renew,
+ EventType.verify_expired };
+ for (EventType eventType : eventTypes) {
+ messageClassMap.put(
+ new WeixinMessageKey(messageType, eventType.name(),
+ accountType), VerifyExpireEventMessage.class);
+ }
+ eventTypes = new EventType[] { EventType.qualification_verify_success,
+ EventType.naming_verify_fail };
+ for (EventType eventType : eventTypes) {
+ messageClassMap.put(
+ new WeixinMessageKey(messageType, eventType.name(),
+ accountType), VerifyFailEventMessage.class);
+ }
+ }
+
+ private void initQyEventMessageClass() {
+ String messageType = MessageType.event.name();
+ messageClassMap.put(new WeixinMessageKey(messageType,
+ EventType.batch_job_result.name(), AccountType.QY),
+ BatchjobresultMessage.class);
+ messageClassMap.put(new WeixinMessageKey(messageType,
+ EventType.enter_agent.name(), AccountType.QY),
+ EnterAgentEventMessage.class);
+ //messageClassMap.put(new WeixinMessageKey(messageType,
+ // EventType.suite.name(), AccountType.QY),
+ //SuiteMessage.class);
+ }
+
+ @Override
+ public Class extends WeixinMessage> match(WeixinMessageKey messageKey) {
+ return messageClassMap.get(messageKey);
+ }
+
+ @Override
+ public void regist(WeixinMessageKey messageKey,
+ Class extends WeixinMessage> messageClass) {
+ Class> clazz = match(messageKey);
+ if (clazz != null) {
+ throw new IllegalArgumentException("duplicate messagekey '"
+ + messageKey + "' define for " + clazz);
+ }
+ messageClassMap.put(messageKey, messageClass);
+ }
+}
\ No newline at end of file
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/MessageHandlerExecutor.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/MessageHandlerExecutor.java
new file mode 100644
index 00000000..f3d49ce2
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/MessageHandlerExecutor.java
@@ -0,0 +1,112 @@
+package com.zone.weixin4j.dispatcher;
+
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.handler.WeixinMessageHandler;
+import com.zone.weixin4j.interceptor.WeixinMessageInterceptor;
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.request.WeixinRequest;
+import com.zone.weixin4j.response.WeixinResponse;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * 微信消息的处理执行
+ *
+ * @author jinyu(foxinmy@gmail.com)
+ * @className MessageHandlerExecutor
+ * @date 2015年5月7日
+ * @see com.zone.weixin4j.handler.WeixinMessageHandler
+ * @see com.zone.weixin4j.interceptor.WeixinMessageInterceptor
+ * @since JDK 1.6
+ */
+public class MessageHandlerExecutor {
+
+ private final Log logger = LogFactory.getLog(getClass());
+ /**
+ * 消息处理器
+ */
+ private final WeixinMessageHandler messageHandler;
+
+ /**
+ * 消息拦截器
+ */
+ private final WeixinMessageInterceptor[] messageInterceptors;
+
+ private int interceptorIndex = -1;
+
+ public MessageHandlerExecutor(WeixinMessageHandler messageHandler, WeixinMessageInterceptor[] messageInterceptors) {
+ this.messageHandler = messageHandler;
+ this.messageInterceptors = messageInterceptors;
+ }
+
+ public WeixinMessageHandler getMessageHandler() {
+ return messageHandler;
+ }
+
+ /**
+ * 执行预拦截动作
+ *
+ * @param request 微信请求信息
+ * @param message 微信消息
+ * @return true则继续执行往下执行
+ * @throws WeixinException
+ */
+ public boolean applyPreHandle(WeixinRequest request, WeixinMessage message)
+ throws WeixinException {
+ if (messageInterceptors != null) {
+ for (int i = 0; i < messageInterceptors.length; i++) {
+ WeixinMessageInterceptor interceptor = messageInterceptors[i];
+ if (!interceptor.preHandle(request, message, messageHandler)) {
+ triggerAfterCompletion(request, null, message, null);
+ return false;
+ }
+ this.interceptorIndex = i;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * MessageHandler处理玩请求后的动作
+ *
+ * @param request 微信请求
+ * @param response 处理后的响应
+ * @param message 微信消息
+ * @throws WeixinException
+ */
+ public void applyPostHandle(WeixinRequest request, WeixinResponse response,
+ WeixinMessage message) throws WeixinException {
+ if (messageInterceptors == null) {
+ return;
+ }
+ for (int i = messageInterceptors.length - 1; i >= 0; i--) {
+ WeixinMessageInterceptor interceptor = messageInterceptors[i];
+ interceptor.postHandle(request, response, message, messageHandler);
+ }
+ }
+
+ /**
+ * 全部执行完毕后触发
+ *
+ * @param request 微信请求
+ * @param response 微信响应 可能为空
+ * @param message 微信消息
+ * @param exception 处理时可能的异常
+ * @throws WeixinException
+ */
+ public void triggerAfterCompletion(WeixinRequest request,
+ WeixinResponse response, WeixinMessage message, Exception exception)
+ throws WeixinException {
+ if (messageInterceptors == null) {
+ return;
+ }
+ for (int i = this.interceptorIndex; i >= 0; i--) {
+ WeixinMessageInterceptor interceptor = messageInterceptors[i];
+ try {
+ interceptor.afterCompletion(request, response, message, messageHandler, exception);
+ } catch (WeixinException e) {
+ logger.error("MessageInterceptor.afterCompletion threw exception", e);
+ }
+ }
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/WeixinMessageDispatcher.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/WeixinMessageDispatcher.java
new file mode 100644
index 00000000..8e72cdcf
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/WeixinMessageDispatcher.java
@@ -0,0 +1,337 @@
+package com.zone.weixin4j.dispatcher;
+
+import com.zone.weixin4j.annotation.WxMessageHandler;
+import com.zone.weixin4j.annotation.WxMessageInterceptor;
+import com.zone.weixin4j.exception.HttpResponseException;
+import com.zone.weixin4j.exception.MessageInterceptorException;
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.handler.DebugMessageHandler;
+import com.zone.weixin4j.handler.WeixinMessageHandler;
+import com.zone.weixin4j.interceptor.WeixinMessageInterceptor;
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.request.WeixinRequest;
+import com.zone.weixin4j.response.BlankResponse;
+import com.zone.weixin4j.response.WeixinResponse;
+import com.zone.weixin4j.service.WeiXin4jContextAware;
+import com.zone.weixin4j.service.context.WeiXin4jContextAwareImpl;
+import com.zone.weixin4j.socket.WeixinMessageTransfer;
+import com.zone.weixin4j.util.ServerToolkits;
+import com.zone.weixin4j.xml.MessageTransferHandler;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.transform.Source;
+import javax.xml.transform.stream.StreamSource;
+import java.io.ByteArrayInputStream;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 微信消息分发器
+ *
+ * @author jinyu(foxinmy@gmail.com)
+ * @className WeixinMessageDispatcher
+ * @date 2015年5月7日
+ * @updateBy Yz(174975857@qq.com)
+ * @since JDK 1.6
+ */
+@Component
+@DependsOn({"weiXin4jContextAware"})
+public class WeixinMessageDispatcher {
+
+ @Autowired
+ private WeiXin4jContextAware weiXin4jContextAware;
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ /**
+ * 消息处理器
+ */
+ private List messageHandlerList = new ArrayList();
+ private WeixinMessageHandler[] messageHandlers;
+
+ /**
+ * 消息拦截器
+ */
+ private List messageInterceptorList = new ArrayList();
+ private WeixinMessageInterceptor[] messageInterceptors;
+
+ /**
+ * 消息匹配
+ */
+ private WeixinMessageMatcher messageMatcher;
+ /**
+ * 消息转换
+ */
+ private Map, Unmarshaller> messageUnmarshaller;
+ /**
+ * 是否总是响应请求,如未匹配到MessageHandler时回复空白消息
+ */
+ private boolean alwaysResponse;
+
+ public WeixinMessageDispatcher() {
+ this(new DefaultMessageMatcher());
+ }
+
+ public WeixinMessageDispatcher(WeixinMessageMatcher messageMatcher) {
+ this.messageMatcher = messageMatcher;
+ this.messageUnmarshaller = new ConcurrentHashMap, Unmarshaller>();
+ }
+
+ @PostConstruct
+ public void init() {
+ try {
+ this.getMessageHandlers();
+ this.getMessageInterceptors();
+ if (weiXin4jContextAware.isOpenAlwaysResponse()) {
+ this.openAlwaysResponse();
+ }
+ if (weiXin4jContextAware.isUseDebugMessageHandler()) {
+ if(null == messageHandlerList){
+ messageHandlerList = new ArrayList();
+ messageHandlerList.add(DebugMessageHandler.global);
+ }
+ }
+ this.messageMatcher = weiXin4jContextAware.getWeixinMessageMatcher() == null ? new DefaultMessageMatcher() : weiXin4jContextAware.getWeixinMessageMatcher();
+ this.messageUnmarshaller = new ConcurrentHashMap, Unmarshaller>();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 对消息进行一系列的处理,包括 拦截、匹配、分发等动作
+ *
+ * @param request 微信请求
+ * @throws WeixinException
+ */
+ public WeixinResponse doDispatch(final WeixinRequest request) throws WeixinException, HttpResponseException, MessageInterceptorException {
+ WeixinMessageTransfer messageTransfer = MessageTransferHandler.parser(request);
+ WeiXin4jContextAwareImpl.getWeixinMessageTransfer().set(messageTransfer);
+ WeixinMessageKey messageKey = defineMessageKey(messageTransfer, request);
+ Class extends WeixinMessage> targetClass = messageMatcher.match(messageKey);
+ WeixinMessage message = messageRead(request.getOriginalContent(), targetClass);
+ logger.info(String.format("define %s matched %s", messageKey, targetClass));
+ MessageHandlerExecutor handlerExecutor = getHandlerExecutor(request, messageKey, message, messageTransfer.getNodeNames());
+ if (handlerExecutor == null || handlerExecutor.getMessageHandler() == null) {
+ return noHandlerFound(request, message);
+ }
+ if (!handlerExecutor.applyPreHandle(request, message)) {
+ throw new MessageInterceptorException(" Interceptor Not Accept !! ");
+ }
+ Exception exception = null;
+ WeixinResponse response = null;
+ try {
+ response = handlerExecutor.getMessageHandler().doHandle(request, message, messageTransfer.getNodeNames());
+ handlerExecutor.applyPostHandle(request, response, message);
+ } catch (Exception e) {
+ exception = e;
+ }
+ handlerExecutor.triggerAfterCompletion(request, response, message, exception);
+ return response;
+ }
+
+ /**
+ * 声明messagekey
+ *
+ * @param messageTransfer 基础消息
+ * @param request 请求信息
+ * @return
+ */
+ protected WeixinMessageKey defineMessageKey(
+ WeixinMessageTransfer messageTransfer, WeixinRequest request) {
+ return new WeixinMessageKey(messageTransfer.getMsgType(),
+ messageTransfer.getEventType(),
+ messageTransfer.getAccountType());
+ }
+
+ /**
+ * 未匹配到handler时触发
+ *
+ * @param request 微信请求
+ * @param message 微信消息
+ */
+ protected WeixinResponse noHandlerFound(WeixinRequest request, WeixinMessage message) throws HttpResponseException {
+ logger.warn(String.format("no handler found for %s", request));
+ if (alwaysResponse) {
+ return BlankResponse.global;
+ } else {
+ throw new HttpResponseException(HttpResponseException.HttpResponseStatus.NOT_FOUND);
+ }
+ }
+
+ /**
+ * MessageHandlerExecutor
+ *
+ * @param request 微信请求
+ * @param messageKey 消息的key
+ * @param message 微信消息
+ * @param nodeNames 节点名称集合
+ * @return MessageHandlerExecutor
+ * @throws WeixinException
+ * @see MessageHandlerExecutor
+ */
+ protected MessageHandlerExecutor getHandlerExecutor(
+ WeixinRequest request,
+ WeixinMessageKey messageKey, WeixinMessage message,
+ Set nodeNames) throws WeixinException {
+ WeixinMessageHandler[] messageHandlers = getMessageHandlers();
+ if (messageHandlers == null) {
+ return null;
+ }
+ logger.info(String.format("resolve message handlers %s", this.messageHandlerList));
+ List matchedMessageHandlers = new ArrayList();
+ for (WeixinMessageHandler handler : messageHandlers) {
+ if (handler.canHandle(request, message, nodeNames)) {
+ matchedMessageHandlers.add(handler);
+ }
+ }
+ if (matchedMessageHandlers.isEmpty()) {
+ return null;
+ }
+ Collections.sort(matchedMessageHandlers,
+ new Comparator() {
+ @Override
+ public int compare(WeixinMessageHandler m1,
+ WeixinMessageHandler m2) {
+ return m2.weight() - m1.weight();
+ }
+ });
+ logger.info(String.format("matched message handlers %s", matchedMessageHandlers));
+ return new MessageHandlerExecutor(matchedMessageHandlers.get(0), getMessageInterceptors());
+ }
+
+ /**
+ * 获取所有的handler
+ *
+ * @return handler集合
+ * @throws WeixinException
+ * @see com.zone.weixin4j.handler.WeixinMessageHandler
+ */
+ public WeixinMessageHandler[] getMessageHandlers() throws WeixinException {
+ if (this.messageHandlers == null) {
+ String[] beanNamesForAnnotation = this.weiXin4jContextAware.getApplicationContext().getBeanNamesForAnnotation(WxMessageHandler.class);
+ for (String str : beanNamesForAnnotation) {
+ Object bean = this.weiXin4jContextAware.getApplicationContext().getBean(str);
+ if (bean instanceof WeixinMessageHandler) {
+ this.messageHandlerList.add((WeixinMessageHandler) this.weiXin4jContextAware.getApplicationContext().getBean(str));
+ }
+ }
+ }
+ if (messageHandlerList != null && !this.messageHandlerList.isEmpty()) {
+ this.messageHandlers = this.messageHandlerList.toArray(new WeixinMessageHandler[this.messageHandlerList.size()]);
+ }
+ return this.messageHandlers;
+ }
+
+ /**
+ * 获取所有的interceptor
+ *
+ * @return interceptor集合
+ * @throws WeixinException
+ */
+ public WeixinMessageInterceptor[] getMessageInterceptors()
+ throws WeixinException {
+ if (this.messageInterceptors == null) {
+ String[] beanNamesForAnnotation = this.weiXin4jContextAware.getApplicationContext().getBeanNamesForAnnotation(WxMessageInterceptor.class);
+ for (String str : beanNamesForAnnotation) {
+ Object bean = this.weiXin4jContextAware.getApplicationContext().getBean(str);
+ if (bean instanceof WeixinMessageInterceptor) {
+ this.messageInterceptorList.add((WeixinMessageInterceptor) this.weiXin4jContextAware.getApplicationContext().getBean(str));
+ }
+ }
+ }
+ if (this.messageInterceptorList != null && !this.messageInterceptorList.isEmpty()) {
+ Collections.sort(messageInterceptorList,
+ new Comparator() {
+ @Override
+ public int compare(WeixinMessageInterceptor m1, WeixinMessageInterceptor m2) {
+ return m2.weight() - m1.weight();
+ }
+ });
+ this.messageInterceptors = this.messageInterceptorList.toArray(new WeixinMessageInterceptor[this.messageInterceptorList.size()]);
+ }
+ logger.info(String.format("resolve message interceptors %s", this.messageInterceptorList));
+ return this.messageInterceptors;
+ }
+
+ /**
+ * jaxb读取微信消息
+ *
+ * @param message xml消息
+ * @param clazz 消息类型
+ * @return 消息对象
+ * @throws WeixinException
+ */
+ protected M messageRead(String message,
+ Class clazz) throws WeixinException {
+ if (clazz == null) {
+ return null;
+ }
+ try {
+ Source source = new StreamSource(new ByteArrayInputStream(
+ ServerToolkits.getBytesUtf8(message)));
+ JAXBElement jaxbElement = getUnmarshaller(clazz).unmarshal(
+ source, clazz);
+ return jaxbElement.getValue();
+ } catch (JAXBException e) {
+ throw new WeixinException(e);
+ }
+ }
+
+ /**
+ * xml消息转换器
+ *
+ * @param clazz 消息类型
+ * @return 消息转换器
+ * @throws WeixinException
+ */
+ protected Unmarshaller getUnmarshaller(Class extends WeixinMessage> clazz)
+ throws WeixinException {
+ Unmarshaller unmarshaller = messageUnmarshaller.get(clazz);
+ if (unmarshaller == null) {
+ try {
+ JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
+ unmarshaller = jaxbContext.createUnmarshaller();
+ messageUnmarshaller.put(clazz, unmarshaller);
+ } catch (JAXBException e) {
+ throw new WeixinException(e);
+ }
+ }
+ return unmarshaller;
+ }
+
+ public void setMessageHandlerList(
+ List messageHandlerList) {
+ this.messageHandlerList = messageHandlerList;
+ }
+
+ public void setMessageInterceptorList(
+ List messageInterceptorList) {
+ this.messageInterceptorList = messageInterceptorList;
+ }
+
+ public void registMessageClass(WeixinMessageKey messageKey,
+ Class extends WeixinMessage> messageClass) {
+ messageMatcher.regist(messageKey, messageClass);
+ }
+
+ public WeixinMessageMatcher getMessageMatcher() {
+ return this.messageMatcher;
+ }
+
+ /**
+ * 打开总是响应开关,如未匹配到MessageHandler时回复空白消息
+ */
+ public void openAlwaysResponse() {
+ this.alwaysResponse = true;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/WeixinMessageKey.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/WeixinMessageKey.java
new file mode 100644
index 00000000..8231894e
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/WeixinMessageKey.java
@@ -0,0 +1,88 @@
+package com.zone.weixin4j.dispatcher;
+
+import com.zone.weixin4j.type.AccountType;
+import com.zone.weixin4j.util.ServerToolkits;
+
+import java.io.Serializable;
+
+/**
+ * 微信消息key
+ *
+ * @className WeixinMessageKey
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年6月9日
+ * @since JDK 1.6
+ * @see
+ */
+public class WeixinMessageKey implements Serializable {
+
+ private static final long serialVersionUID = -691330687850400289L;
+
+ private String messageType;
+ private String eventType;
+ private AccountType accountType;
+
+ public WeixinMessageKey(String messageType, String eventType,
+ AccountType accountType) {
+ this.messageType = messageType;
+ this.eventType = eventType;
+ this.accountType = accountType;
+ }
+
+ public String getMessageType() {
+ return messageType;
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ public AccountType getAccountType() {
+ return accountType;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result
+ + ((accountType == null) ? 0 : accountType.hashCode());
+ result = prime * result
+ + ((ServerToolkits.isBlank(eventType)) ? 0 : eventType.hashCode());
+ result = prime
+ * result
+ + ((ServerToolkits.isBlank(messageType)) ? 0 : messageType
+ .hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ WeixinMessageKey other = (WeixinMessageKey) obj;
+ if (accountType != other.accountType)
+ return false;
+ if (eventType == null) {
+ if (other.eventType != null)
+ return false;
+ } else if (!eventType.equalsIgnoreCase(other.eventType))
+ return false;
+ if (messageType == null) {
+ if (other.messageType != null)
+ return false;
+ } else if (!messageType.equalsIgnoreCase(other.messageType))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "WeixinMessageKey [messageType=" + messageType + ", eventType="
+ + eventType + ", accountType=" + accountType + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/WeixinMessageMatcher.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/WeixinMessageMatcher.java
new file mode 100644
index 00000000..4d8afb4d
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/dispatcher/WeixinMessageMatcher.java
@@ -0,0 +1,34 @@
+package com.zone.weixin4j.dispatcher;
+
+import com.zone.weixin4j.request.WeixinMessage;
+
+/**
+ * 微信消息匹配
+ *
+ * @className WeixinMessageMatcher
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月17日
+ * @since JDK 1.6
+ * @see DefaultMessageMatcher
+ */
+public interface WeixinMessageMatcher {
+ /**
+ * 匹配消息类型
+ *
+ * @param messageKey
+ * 消息key
+ * @return 消息类型
+ */
+ public Class extends WeixinMessage> match(WeixinMessageKey messageKey);
+
+ /**
+ * 注册消息类型「程序没有及时更新而微信又产生了新的消息类型」
+ *
+ * @param messageKey
+ * 消息key
+ * @param messageClass
+ * 消息类型
+ */
+ public void regist(WeixinMessageKey messageKey,
+ Class extends WeixinMessage> messageClass);
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/exception/HttpResponseException.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/exception/HttpResponseException.java
new file mode 100644
index 00000000..c0bcefe4
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/exception/HttpResponseException.java
@@ -0,0 +1,359 @@
+package com.zone.weixin4j.exception;
+
+/**
+ * Created by Yz on 2017/3/15.
+ * HttpResponseException -- 异常类
+ */
+public class HttpResponseException extends Exception {
+
+ private static final long serialVersionUID = 1932809035537660697L;
+
+ private HttpResponseStatus httpResponseStatus;
+
+ public HttpResponseException(HttpResponseStatus httpResponseStatus) {
+ this.httpResponseStatus = httpResponseStatus;
+ }
+
+ public HttpResponseStatus getHttpResponseStatus() {
+ return httpResponseStatus;
+ }
+
+ public void setHttpResponseStatus(HttpResponseStatus httpResponseStatus) {
+ this.httpResponseStatus = httpResponseStatus;
+ }
+
+ public static class HttpResponseStatus {
+
+ /**
+ * 100 Continue
+ */
+ public static final HttpResponseStatus CONTINUE = newStatus(100, "Continue");
+
+ /**
+ * 101 Switching Protocols
+ */
+ public static final HttpResponseStatus SWITCHING_PROTOCOLS = newStatus(101, "Switching Protocols");
+
+ /**
+ * 102 Processing (WebDAV, RFC2518)
+ */
+ public static final HttpResponseStatus PROCESSING = newStatus(102, "Processing");
+
+ /**
+ * 200 OK
+ */
+ public static final HttpResponseStatus OK = newStatus(200, "OK");
+
+ /**
+ * 201 Created
+ */
+ public static final HttpResponseStatus CREATED = newStatus(201, "Created");
+
+ /**
+ * 202 Accepted
+ */
+ public static final HttpResponseStatus ACCEPTED = newStatus(202, "Accepted");
+
+ /**
+ * 203 Non-Authoritative Information (since HTTP/1.1)
+ */
+ public static final HttpResponseStatus NON_AUTHORITATIVE_INFORMATION =
+ newStatus(203, "Non-Authoritative Information");
+
+ /**
+ * 204 No Content
+ */
+ public static final HttpResponseStatus NO_CONTENT = newStatus(204, "No Content");
+
+ /**
+ * 205 Reset Content
+ */
+ public static final HttpResponseStatus RESET_CONTENT = newStatus(205, "Reset Content");
+
+ /**
+ * 206 Partial Content
+ */
+ public static final HttpResponseStatus PARTIAL_CONTENT = newStatus(206, "Partial Content");
+
+ /**
+ * 207 Multi-Status (WebDAV, RFC2518)
+ */
+ public static final HttpResponseStatus MULTI_STATUS = newStatus(207, "Multi-Status");
+
+ /**
+ * 300 Multiple Choices
+ */
+ public static final HttpResponseStatus MULTIPLE_CHOICES = newStatus(300, "Multiple Choices");
+
+ /**
+ * 301 Moved Permanently
+ */
+ public static final HttpResponseStatus MOVED_PERMANENTLY = newStatus(301, "Moved Permanently");
+
+ /**
+ * 302 Found
+ */
+ public static final HttpResponseStatus FOUND = newStatus(302, "Found");
+
+ /**
+ * 303 See Other (since HTTP/1.1)
+ */
+ public static final HttpResponseStatus SEE_OTHER = newStatus(303, "See Other");
+
+ /**
+ * 304 Not Modified
+ */
+ public static final HttpResponseStatus NOT_MODIFIED = newStatus(304, "Not Modified");
+
+ /**
+ * 305 Use Proxy (since HTTP/1.1)
+ */
+ public static final HttpResponseStatus USE_PROXY = newStatus(305, "Use Proxy");
+
+ /**
+ * 307 Temporary Redirect (since HTTP/1.1)
+ */
+ public static final HttpResponseStatus TEMPORARY_REDIRECT = newStatus(307, "Temporary Redirect");
+
+ /**
+ * 400 Bad Request
+ */
+ public static final HttpResponseStatus BAD_REQUEST = newStatus(400, "Bad Request");
+
+ /**
+ * 401 Unauthorized
+ */
+ public static final HttpResponseStatus UNAUTHORIZED = newStatus(401, "Unauthorized");
+
+ /**
+ * 402 Payment Required
+ */
+ public static final HttpResponseStatus PAYMENT_REQUIRED = newStatus(402, "Payment Required");
+
+ /**
+ * 403 Forbidden
+ */
+ public static final HttpResponseStatus FORBIDDEN = newStatus(403, "Forbidden");
+
+ /**
+ * 404 Not Found
+ */
+ public static final HttpResponseStatus NOT_FOUND = newStatus(404, "Not Found");
+
+ /**
+ * 405 Method Not Allowed
+ */
+ public static final HttpResponseStatus METHOD_NOT_ALLOWED = newStatus(405, "Method Not Allowed");
+
+ /**
+ * 406 Not Acceptable
+ */
+ public static final HttpResponseStatus NOT_ACCEPTABLE = newStatus(406, "Not Acceptable");
+
+ /**
+ * 407 Proxy Authentication Required
+ */
+ public static final HttpResponseStatus PROXY_AUTHENTICATION_REQUIRED =
+ newStatus(407, "Proxy Authentication Required");
+
+ /**
+ * 408 Request Timeout
+ */
+ public static final HttpResponseStatus REQUEST_TIMEOUT = newStatus(408, "Request Timeout");
+
+ /**
+ * 409 Conflict
+ */
+ public static final HttpResponseStatus CONFLICT = newStatus(409, "Conflict");
+
+ /**
+ * 410 Gone
+ */
+ public static final HttpResponseStatus GONE = newStatus(410, "Gone");
+
+ /**
+ * 411 Length Required
+ */
+ public static final HttpResponseStatus LENGTH_REQUIRED = newStatus(411, "Length Required");
+
+ /**
+ * 412 Precondition Failed
+ */
+ public static final HttpResponseStatus PRECONDITION_FAILED = newStatus(412, "Precondition Failed");
+
+ /**
+ * 413 Request Entity Too Large
+ */
+ public static final HttpResponseStatus REQUEST_ENTITY_TOO_LARGE =
+ newStatus(413, "Request Entity Too Large");
+
+ /**
+ * 414 Request-URI Too Long
+ */
+ public static final HttpResponseStatus REQUEST_URI_TOO_LONG = newStatus(414, "Request-URI Too Long");
+
+ /**
+ * 415 Unsupported Media Type
+ */
+ public static final HttpResponseStatus UNSUPPORTED_MEDIA_TYPE = newStatus(415, "Unsupported Media Type");
+
+ /**
+ * 416 Requested Range Not Satisfiable
+ */
+ public static final HttpResponseStatus REQUESTED_RANGE_NOT_SATISFIABLE =
+ newStatus(416, "Requested Range Not Satisfiable");
+
+ /**
+ * 417 Expectation Failed
+ */
+ public static final HttpResponseStatus EXPECTATION_FAILED = newStatus(417, "Expectation Failed");
+
+ /**
+ * 421 Misdirected Request
+ *
+ * 421 Status Code
+ */
+ public static final HttpResponseStatus MISDIRECTED_REQUEST = newStatus(421, "Misdirected Request");
+
+ /**
+ * 422 Unprocessable Entity (WebDAV, RFC4918)
+ */
+ public static final HttpResponseStatus UNPROCESSABLE_ENTITY = newStatus(422, "Unprocessable Entity");
+
+ /**
+ * 423 Locked (WebDAV, RFC4918)
+ */
+ public static final HttpResponseStatus LOCKED = newStatus(423, "Locked");
+
+ /**
+ * 424 Failed Dependency (WebDAV, RFC4918)
+ */
+ public static final HttpResponseStatus FAILED_DEPENDENCY = newStatus(424, "Failed Dependency");
+
+ /**
+ * 425 Unordered Collection (WebDAV, RFC3648)
+ */
+ public static final HttpResponseStatus UNORDERED_COLLECTION = newStatus(425, "Unordered Collection");
+
+ /**
+ * 426 Upgrade Required (RFC2817)
+ */
+ public static final HttpResponseStatus UPGRADE_REQUIRED = newStatus(426, "Upgrade Required");
+
+ /**
+ * 428 Precondition Required (RFC6585)
+ */
+ public static final HttpResponseStatus PRECONDITION_REQUIRED = newStatus(428, "Precondition Required");
+
+ /**
+ * 429 Too Many Requests (RFC6585)
+ */
+ public static final HttpResponseStatus TOO_MANY_REQUESTS = newStatus(429, "Too Many Requests");
+
+ /**
+ * 431 Request Header Fields Too Large (RFC6585)
+ */
+ public static final HttpResponseStatus REQUEST_HEADER_FIELDS_TOO_LARGE =
+ newStatus(431, "Request Header Fields Too Large");
+
+ /**
+ * 500 Internal Server Error
+ */
+ public static final HttpResponseStatus INTERNAL_SERVER_ERROR = newStatus(500, "Internal Server Error");
+
+ /**
+ * 501 Not Implemented
+ */
+ public static final HttpResponseStatus NOT_IMPLEMENTED = newStatus(501, "Not Implemented");
+
+ /**
+ * 502 Bad Gateway
+ */
+ public static final HttpResponseStatus BAD_GATEWAY = newStatus(502, "Bad Gateway");
+
+ /**
+ * 503 Service Unavailable
+ */
+ public static final HttpResponseStatus SERVICE_UNAVAILABLE = newStatus(503, "Service Unavailable");
+
+ /**
+ * 504 Gateway Timeout
+ */
+ public static final HttpResponseStatus GATEWAY_TIMEOUT = newStatus(504, "Gateway Timeout");
+
+ /**
+ * 505 HTTP Version Not Supported
+ */
+ public static final HttpResponseStatus HTTP_VERSION_NOT_SUPPORTED =
+ newStatus(505, "HTTP Version Not Supported");
+
+ /**
+ * 506 Variant Also Negotiates (RFC2295)
+ */
+ public static final HttpResponseStatus VARIANT_ALSO_NEGOTIATES = newStatus(506, "Variant Also Negotiates");
+
+ /**
+ * 507 Insufficient Storage (WebDAV, RFC4918)
+ */
+ public static final HttpResponseStatus INSUFFICIENT_STORAGE = newStatus(507, "Insufficient Storage");
+
+ /**
+ * 510 Not Extended (RFC2774)
+ */
+ public static final HttpResponseStatus NOT_EXTENDED = newStatus(510, "Not Extended");
+
+ /**
+ * 511 Network Authentication Required (RFC6585)
+ */
+ public static final HttpResponseStatus NETWORK_AUTHENTICATION_REQUIRED =
+ newStatus(511, "Network Authentication Required");
+
+ private static HttpResponseStatus newStatus(int statusCode, String reasonPhrase) {
+ return new HttpResponseStatus(statusCode, reasonPhrase, true);
+ }
+
+ private final int code;
+
+ private final String reasonPhrase;
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getReasonPhrase() {
+ return reasonPhrase;
+ }
+
+ /**
+ * Creates a new instance with the specified {@code code} and its {@code reasonPhrase}.
+ */
+
+ private HttpResponseStatus(int code, String reasonPhrase, boolean bytes) {
+ if (code < 0) {
+ throw new IllegalArgumentException(
+ "code: " + code + " (expected: 0+)");
+ }
+
+ if (reasonPhrase == null) {
+ throw new NullPointerException("reasonPhrase");
+ }
+
+ for (int i = 0; i < reasonPhrase.length(); i++) {
+ char c = reasonPhrase.charAt(i);
+ // Check prohibited characters.
+ switch (c) {
+ case '\n':
+ case '\r':
+ throw new IllegalArgumentException(
+ "reasonPhrase contains one of the following prohibited characters: " +
+ "\\r\\n: " + reasonPhrase);
+ }
+ }
+
+ this.code = code;
+ this.reasonPhrase = reasonPhrase;
+ }
+ }
+
+
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/exception/MessageInterceptorException.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/exception/MessageInterceptorException.java
new file mode 100644
index 00000000..f3238222
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/exception/MessageInterceptorException.java
@@ -0,0 +1,46 @@
+package com.zone.weixin4j.exception;
+
+/**
+ * Created by Yz on 2017/3/15.
+ * 微信消息拦截器异常
+ */
+public class MessageInterceptorException extends Exception {
+
+ private static final long serialVersionUID = -1094109872039360113L;
+
+ private String code;
+ private String msg;
+
+ public MessageInterceptorException(String errorCode, String errorMsg) {
+ this.code = errorCode;
+ this.msg = errorMsg;
+ }
+
+ public MessageInterceptorException(String errorMsg) {
+ this.code = "-1";
+ this.msg = errorMsg;
+ }
+
+ public MessageInterceptorException(Exception e) {
+ super(e);
+ }
+
+ public MessageInterceptorException(String errorMsg, Exception e) {
+ super(e);
+ this.msg = errorMsg;
+ }
+
+ public String getErrorCode() {
+ return code;
+ }
+
+ public String getErrorMsg() {
+ return msg;
+ }
+
+ @Override
+ public String getMessage() {
+ return this.code + "," + this.msg;
+ }
+
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/exception/WeixinException.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/exception/WeixinException.java
new file mode 100644
index 00000000..4c72ccf0
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/exception/WeixinException.java
@@ -0,0 +1,50 @@
+package com.zone.weixin4j.exception;
+
+/**
+ * 微信异常
+ *
+ * @className WeixinException
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月10日
+ * @since JDK 1.6
+ * @see
+ */
+public class WeixinException extends Exception {
+
+ private static final long serialVersionUID = 7148145661883468514L;
+
+ private String errorCode;
+ private String errorMsg;
+
+ public WeixinException(String errorCode, String errorMsg) {
+ this.errorCode = errorCode;
+ this.errorMsg = errorMsg;
+ }
+
+ public WeixinException(String errorMsg) {
+ this.errorCode = "-1";
+ this.errorMsg = errorMsg;
+ }
+
+ public WeixinException(Exception e) {
+ super(e);
+ }
+
+ public WeixinException(String errorMsg, Exception e) {
+ super(e);
+ this.errorMsg = errorMsg;
+ }
+
+ public String getErrorCode() {
+ return errorCode;
+ }
+
+ public String getErrorMsg() {
+ return errorMsg;
+ }
+
+ @Override
+ public String getMessage() {
+ return this.errorCode + "," + this.errorMsg;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/DebugMessageHandler.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/DebugMessageHandler.java
new file mode 100644
index 00000000..a4a47a8d
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/DebugMessageHandler.java
@@ -0,0 +1,47 @@
+package com.zone.weixin4j.handler;
+
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.request.WeixinRequest;
+import com.zone.weixin4j.response.TextResponse;
+import com.zone.weixin4j.response.WeixinResponse;
+
+import java.util.Set;
+
+/**
+ * 调试消息处理器
+ *
+ * @className DebugMessageHandler
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月17日
+ * @since JDK 1.6
+ * @see
+ */
+public class DebugMessageHandler implements WeixinMessageHandler {
+
+ public static final DebugMessageHandler global = new DebugMessageHandler();
+
+ private DebugMessageHandler() {
+
+ }
+
+ @Override
+ public boolean canHandle(WeixinRequest request, WeixinMessage message,
+ Set nodeNames) throws WeixinException {
+ return true;
+ }
+
+ @Override
+ public WeixinResponse doHandle(WeixinRequest request, WeixinMessage message,
+ Set nodeNames) throws WeixinException {
+ String content = message == null ? request.getOriginalContent()
+ .replaceAll("\\!\\[CDATA\\[", "").replaceAll("\\]\\]", "")
+ : message.toString();
+ return new TextResponse(content);
+ }
+
+ @Override
+ public int weight() {
+ return 0;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/MessageHandlerAdapter.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/MessageHandlerAdapter.java
new file mode 100644
index 00000000..601e04d8
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/MessageHandlerAdapter.java
@@ -0,0 +1,73 @@
+package com.zone.weixin4j.handler;
+
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.request.WeixinRequest;
+import com.zone.weixin4j.response.WeixinResponse;
+import com.zone.weixin4j.util.ClassUtil;
+
+import java.util.Set;
+
+/**
+ * 消息适配器:对于特定的消息类型进行适配,如text文本、voice语音消息
+ *
+ * @className MessageHandlerAdapter
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月17日
+ * @since JDK 1.6
+ * @see com.foxinmy.weixin4j.request.WeixinMessage
+ */
+@SuppressWarnings("unchecked")
+public abstract class MessageHandlerAdapter implements
+ WeixinMessageHandler {
+
+ @Override
+ public boolean canHandle(WeixinRequest request, WeixinMessage message,
+ Set nodeNames) throws WeixinException {
+ return message != null
+ && message.getClass() == ClassUtil.getGenericType(getClass())
+ && canHandle0(request, (M) message);
+ }
+
+ /**
+ * 能否处理请求
+ *
+ * @param request
+ * 微信请求
+ * @param message
+ * 微信消息
+ * @return true则执行doHandler0
+ * @throws WeixinException
+ */
+ public boolean canHandle0(WeixinRequest request, M message)
+ throws WeixinException {
+ return true;
+ }
+
+ @Override
+ public WeixinResponse doHandle(WeixinRequest request,
+ WeixinMessage message, Set nodeNames)
+ throws WeixinException {
+ return doHandle0(request, (M) message);
+ }
+
+ /**
+ * 处理请求
+ *
+ * @param request
+ * 微信请求
+ * @param message
+ * 微信消息
+ * @return
+ */
+ public abstract WeixinResponse doHandle0(WeixinRequest request, M message)
+ throws WeixinException;
+
+ /**
+ * 缺省值为1,存在多个匹配到的MessageHandler则比较weight大小
+ */
+ @Override
+ public int weight() {
+ return 1;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/MultipleMessageHandlerAdapter.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/MultipleMessageHandlerAdapter.java
new file mode 100644
index 00000000..e32c2977
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/MultipleMessageHandlerAdapter.java
@@ -0,0 +1,58 @@
+package com.zone.weixin4j.handler;
+
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.request.WeixinRequest;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 多个消息类型适配
+ *
+ * @className MultipleMessageHandlerAdapter
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2016年3月12日
+ * @since JDK 1.6
+ * @see
+ */
+public abstract class MultipleMessageHandlerAdapter implements WeixinMessageHandler {
+
+ private final Set> messageClasses;
+
+ public MultipleMessageHandlerAdapter(Class extends WeixinMessage>... messageClasses) {
+ if (messageClasses == null) {
+ throw new IllegalArgumentException("messageClasses not be empty");
+ }
+ this.messageClasses = new HashSet>(
+ Math.max((int) (messageClasses.length / .75f) + 1, 16));
+ for (Class extends WeixinMessage> clazz : messageClasses) {
+ this.messageClasses.add(clazz);
+ }
+ }
+
+ @Override
+ public boolean canHandle(WeixinRequest request, WeixinMessage message, Set nodeNames)
+ throws WeixinException {
+ return message != null && messageClasses.contains(message.getClass()) && canHandle0(request, message);
+ }
+
+ /**
+ * 能否处理请求
+ *
+ * @param request
+ * 微信请求
+ * @param message
+ * 微信消息
+ * @return true则执行doHandler
+ * @throws WeixinException
+ */
+ public boolean canHandle0(WeixinRequest request, WeixinMessage message) throws WeixinException {
+ return true;
+ }
+
+ @Override
+ public int weight() {
+ return 1;
+ }
+}
\ No newline at end of file
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/WeixinMessageHandler.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/WeixinMessageHandler.java
new file mode 100644
index 00000000..52c0e238
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/handler/WeixinMessageHandler.java
@@ -0,0 +1,56 @@
+package com.zone.weixin4j.handler;
+
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.request.WeixinRequest;
+import com.zone.weixin4j.response.WeixinResponse;
+
+import java.util.Set;
+
+/**
+ * 微信消息处理器
+ *
+ * @className WeixinMessageHandler
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月7日
+ * @since JDK 1.6
+ * @see MessageHandlerAdapter
+ * @see MultipleMessageHandlerAdapter
+ */
+public interface WeixinMessageHandler {
+
+ /**
+ * 能否处理请求
+ *
+ * @param request
+ * 微信请求
+ * @param message
+ * 微信消息
+ * @param nodeNames
+ * 节点名称集合
+ * @return true则执行doHandle
+ */
+ public boolean canHandle(WeixinRequest request, WeixinMessage message,
+ Set nodeNames) throws WeixinException;
+
+ /**
+ * 处理请求
+ *
+ * @param request
+ * 微信请求
+ * @param message
+ * 微信消息
+ * @param nodeNames
+ * 节点名称集合
+ * @return 回复内容
+ */
+ public WeixinResponse doHandle(WeixinRequest request, WeixinMessage message,
+ Set nodeNames) throws WeixinException;
+
+ /**
+ * 用于匹配到多个MessageHandler时权重降序排列,数字越大优先级越高
+ *
+ * @return 权重
+ */
+ public int weight();
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/interceptor/MessageInterceptorAdapter.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/interceptor/MessageInterceptorAdapter.java
new file mode 100644
index 00000000..eb31169d
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/interceptor/MessageInterceptorAdapter.java
@@ -0,0 +1,45 @@
+package com.zone.weixin4j.interceptor;
+
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.handler.WeixinMessageHandler;
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.request.WeixinRequest;
+import com.zone.weixin4j.response.WeixinResponse;
+
+/**
+ * 消息拦截适配
+ *
+ * @author jinyu(foxinmy@gmail.com)
+ * @className MessageInterceptorAdapter
+ * @date 2015年5月14日
+ * @see
+ * @since JDK 1.6
+ */
+public abstract class MessageInterceptorAdapter implements
+ WeixinMessageInterceptor {
+
+ @Override
+ public boolean preHandle(
+ WeixinRequest request, WeixinMessage message, WeixinMessageHandler handler)
+ throws WeixinException {
+ return true;
+ }
+
+ @Override
+ public void postHandle(
+ WeixinRequest request, WeixinResponse response, WeixinMessage message,
+ WeixinMessageHandler handler) throws WeixinException {
+ }
+
+ @Override
+ public void afterCompletion(
+ WeixinRequest request, WeixinResponse response, WeixinMessage message,
+ WeixinMessageHandler handler, Exception exception)
+ throws WeixinException {
+ }
+
+ @Override
+ public int weight() {
+ return 0;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/interceptor/WeixinMessageInterceptor.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/interceptor/WeixinMessageInterceptor.java
new file mode 100644
index 00000000..2e05bfba
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/interceptor/WeixinMessageInterceptor.java
@@ -0,0 +1,66 @@
+package com.zone.weixin4j.interceptor;
+
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.handler.WeixinMessageHandler;
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.request.WeixinRequest;
+import com.zone.weixin4j.response.WeixinResponse;
+
+/**
+ * 微信消息拦截器
+ *
+ * @author jinyu(foxinmy@gmail.com)
+ * @className WeixinMessageInterceptor
+ * @date 2015年5月7日
+ * @see MessageInterceptorAdapter
+ * @since JDK 1.6
+ */
+public interface WeixinMessageInterceptor {
+
+ /**
+ * 执行handler前
+ *
+ * @param request 微信请求
+ * @param message 微信消息
+ * @param handler 消息处理器
+ * @return 返回true执行下一个拦截器
+ * @throws WeixinException
+ */
+ boolean preHandle(WeixinRequest request,
+ WeixinMessage message, WeixinMessageHandler handler)
+ throws WeixinException;
+
+ /**
+ * 执行handler后
+ *
+ * @param request 微信请求
+ * @param response 微信响应
+ * @param message 微信消息
+ * @param handler 消息处理器
+ * @throws WeixinException
+ */
+ void postHandle(WeixinRequest request,
+ WeixinResponse response, WeixinMessage message,
+ WeixinMessageHandler handler) throws WeixinException;
+
+ /**
+ * 全部执行后
+ *
+ * @param request 微信请求
+ * @param message 微信消息
+ * @param handler 消息处理器
+ * @param exception 执行异常
+ * @throws WeixinException
+ */
+ void afterCompletion(WeixinRequest request,
+ WeixinResponse response, WeixinMessage message,
+ WeixinMessageHandler handler, Exception exception)
+ throws WeixinException;
+
+ /**
+ * 用于匹配到多个MessageHandler时权重降序排列,数字越大优先级越高
+ *
+ * @return 权重
+ */
+ int weight();
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/ImageMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/ImageMessage.java
new file mode 100644
index 00000000..6a02eaf0
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/ImageMessage.java
@@ -0,0 +1,52 @@
+package com.zone.weixin4j.message;
+
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.type.MessageType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 图片消息
+ *
+ * @className ImageMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的图片消息
+ * @see 企业号的图片消息
+ */
+public class ImageMessage extends WeixinMessage {
+
+ private static final long serialVersionUID = 8430800898756567016L;
+
+ public ImageMessage() {
+ super(MessageType.image.name());
+ }
+
+ /**
+ * 图片链接
+ */
+ @XmlElement(name = "PicUrl")
+ private String picUrl;
+ /**
+ * 图片消息媒体id,可以调用多媒体文件下载接口拉取数据。
+ */
+ @XmlElement(name = "MediaId")
+ private String mediaId;
+
+ public String getPicUrl() {
+ return picUrl;
+ }
+
+ public String getMediaId() {
+ return mediaId;
+ }
+
+ @Override
+ public String toString() {
+ return "ImageMessage [picUrl=" + picUrl + ", mediaId=" + mediaId + ", "
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/LinkMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/LinkMessage.java
new file mode 100644
index 00000000..bfdd3b70
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/LinkMessage.java
@@ -0,0 +1,63 @@
+package com.zone.weixin4j.message;
+
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.type.MessageType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 链接消息
+ *
+ * @className LinkMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see
+ * 订阅号、服务号的链接消息
+ * @see
+ * 企业号的链接消息
+ */
+public class LinkMessage extends WeixinMessage {
+
+ private static final long serialVersionUID = 754952745115497030L;
+
+ public LinkMessage() {
+ super(MessageType.link.name());
+ }
+
+ /**
+ * 消息标题
+ */
+ @XmlElement(name = "Title")
+ private String title;
+ /**
+ * 消息描述
+ */
+ @XmlElement(name = "Description")
+ private String description;
+ /**
+ * 消息链接
+ */
+ @XmlElement(name = "Url")
+ private String url;
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ @Override
+ public String toString() {
+ return "LinkMessage [title=" + title + ", description=" + description + ", url=" + url + ", " + super.toString()
+ + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/LocationMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/LocationMessage.java
new file mode 100644
index 00000000..8e702ecb
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/LocationMessage.java
@@ -0,0 +1,70 @@
+package com.zone.weixin4j.message;
+
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.type.MessageType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 地理位置消息
+ *
+ * @className LocationMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的地理位置消息
+ * @see 企业号的地理位置消息
+ */
+public class LocationMessage extends WeixinMessage {
+
+ private static final long serialVersionUID = 2866021596599237334L;
+
+ public LocationMessage() {
+ super(MessageType.location.name());
+ }
+
+ /**
+ * 地理位置维度
+ */
+ @XmlElement(name = "Location_X")
+ private double x;
+ /**
+ * 地理位置经度
+ */
+ @XmlElement(name = "Location_Y")
+ private double y;
+ /**
+ * 地图缩放大小
+ */
+ @XmlElement(name = "Scale")
+ private double scale;
+ /**
+ * 地理位置信息
+ */
+ @XmlElement(name = "Label")
+ private String label;
+
+ public double getX() {
+ return x;
+ }
+
+ public double getY() {
+ return y;
+ }
+
+ public double getScale() {
+ return scale;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ @Override
+ public String toString() {
+ return "LocationMessage [x=" + x + ", y=" + y + ", scale=" + scale
+ + ", label=" + label + ", " + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/README.md b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/README.md
new file mode 100644
index 00000000..d273f850
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/README.md
@@ -0,0 +1,10 @@
+普通消息
+-------
+
+当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。
+
+微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次
+
+关于重试的消息排重,推荐使用msgid排重。
+
+假如服务器无法保证在五秒内处理并回复,可以直接回复空串,微信服务器不会对此作任何处理,并且不会发起重试。
\ No newline at end of file
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/TextMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/TextMessage.java
new file mode 100644
index 00000000..23c967a1
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/TextMessage.java
@@ -0,0 +1,43 @@
+package com.zone.weixin4j.message;
+
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.type.MessageType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 文本消息
+ *
+ * @className TextMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的文本消息
+ * @see 企业号的文本消息
+ */
+public class TextMessage extends WeixinMessage {
+
+ private static final long serialVersionUID = -7018053906644190260L;
+
+ public TextMessage() {
+ super(MessageType.text.name());
+ }
+
+ /**
+ * 消息内容
+ */
+ @XmlElement(name = "Content")
+ private String content;
+
+ public String getContent() {
+ return content;
+ }
+
+ @Override
+ public String toString() {
+ return "TextMessage [content=" + content + ", " + super.toString()
+ + "]";
+ }
+}
\ No newline at end of file
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/VideoMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/VideoMessage.java
new file mode 100644
index 00000000..57cfd326
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/VideoMessage.java
@@ -0,0 +1,52 @@
+package com.zone.weixin4j.message;
+
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.type.MessageType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 视频消息
+ *
+ * @className VideoMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的视频消息
+ * @see 企业号的视频消息
+ */
+public class VideoMessage extends WeixinMessage {
+
+ private static final long serialVersionUID = -1013075358679078381L;
+
+ public VideoMessage() {
+ super(MessageType.video.name());
+ }
+
+ /**
+ * 视频消息媒体id,可以调用多媒体文件下载接口拉取数据。
+ */
+ @XmlElement(name = "MediaId")
+ private String mediaId;
+ /**
+ * 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据。
+ */
+ @XmlElement(name = "ThumbMediaId")
+ private String thumbMediaId;
+
+ public String getMediaId() {
+ return mediaId;
+ }
+
+ public String getThumbMediaId() {
+ return thumbMediaId;
+ }
+
+ @Override
+ public String toString() {
+ return "VideoMessage [mediaId=" + mediaId + ", thumbMediaId="
+ + thumbMediaId + ", " + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/VoiceMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/VoiceMessage.java
new file mode 100644
index 00000000..a11eee22
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/VoiceMessage.java
@@ -0,0 +1,65 @@
+package com.zone.weixin4j.message;
+
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.type.MessageType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 语音消息
+ *
+ * 开通语音识别功能,用户每次发送语音给公众号时,微信会在推送的语音消息XML数据包中,赋值到Recongnition字段.
+ *
+ *
+ * @className VoiceMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的语音消息
+ * @see 企业号的语音消息
+ */
+public class VoiceMessage extends WeixinMessage {
+
+ private static final long serialVersionUID = -7988380977182214003L;
+
+ public VoiceMessage() {
+ super(MessageType.voice.name());
+ }
+
+ /**
+ * 语音消息媒体id,可以调用多媒体文件下载接口拉取数据。
+ */
+ @XmlElement(name = "MediaId")
+ private String mediaId;
+ /**
+ * 语音格式,如amr,speex等
+ */
+ @XmlElement(name = "Format")
+ private String format;
+ /**
+ * 语音识别结果,UTF8编码
+ */
+ @XmlElement(name = "Recognition")
+ private String recognition;
+
+ public String getRecognition() {
+ return recognition;
+ }
+
+ public String getMediaId() {
+ return mediaId;
+ }
+
+ public String getFormat() {
+ return format;
+ }
+
+ @Override
+ public String toString() {
+ return "VoiceMessage [mediaId=" + mediaId + ", format=" + format
+ + ", recognition=" + recognition + ", " + super.toString()
+ + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/EventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/EventMessage.java
new file mode 100644
index 00000000..67e9e447
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/EventMessage.java
@@ -0,0 +1,49 @@
+package com.zone.weixin4j.message.event;
+
+import com.zone.weixin4j.request.WeixinMessage;
+import com.zone.weixin4j.type.MessageType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 事件消息基类
+ *
+ * @className EventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的事件推送
+ * @see 企业号的事件消息
+ */
+public class EventMessage extends WeixinMessage {
+
+ private static final long serialVersionUID = 7703667223814088865L;
+
+ protected EventMessage() {
+ // jaxb requried
+ }
+
+ public EventMessage(String eventType) {
+ super(MessageType.event.name());
+ this.eventType = eventType;
+ }
+
+ /**
+ * 事件类型
+ *
+ * @see com.foxinmy.weixin4j.type.EventType
+ */
+ @XmlElement(name = "Event")
+ private String eventType;
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ @Override
+ public String toString() {
+ return "eventType=" + eventType + ", " + super.toString();
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/LocationEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/LocationEventMessage.java
new file mode 100644
index 00000000..83ec1e8f
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/LocationEventMessage.java
@@ -0,0 +1,60 @@
+package com.zone.weixin4j.message.event;
+
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 上报地理位置事件
+ *
+ * @className LocationEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的上报地理位置事件
+ * @see 企业号的上报地理位置事件
+ */
+public class LocationEventMessage extends EventMessage {
+
+ private static final long serialVersionUID = -2030716800669824861L;
+
+ public LocationEventMessage() {
+ super(EventType.location.name());
+ }
+ /**
+ * 地理位置纬度
+ */
+ @XmlElement(name="Latitude")
+ private double latitude;
+ /**
+ * 地理位置经度
+ */
+ @XmlElement(name="Longitude")
+ private double longitude;
+ /**
+ * 地理位置精度
+ */
+ @XmlElement(name="Precision")
+ private double precision;
+
+ public double getLatitude() {
+ return latitude;
+ }
+
+ public double getLongitude() {
+ return longitude;
+ }
+
+ public double getPrecision() {
+ return precision;
+ }
+
+ @Override
+ public String toString() {
+ return "LocationEventMessage [latitude=" + latitude + ", longitude="
+ + longitude + ", precision=" + precision + ", "
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuEventMessage.java
new file mode 100644
index 00000000..7204c1ce
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuEventMessage.java
@@ -0,0 +1,55 @@
+package com.zone.weixin4j.message.event;
+
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 自定义菜单事件(view|click)
+ *
+ * @className MenuEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的菜单事件
+ * @see 企业号的菜单事件
+ */
+public class MenuEventMessage extends EventMessage {
+
+ private static final long serialVersionUID = -1049672447995366063L;
+
+ public MenuEventMessage() {
+ super(EventType.click.name());
+ }
+
+ public MenuEventMessage(EventType eventType) {
+ super(eventType.name());
+ }
+
+ /**
+ * 事件KEY值,与自定义菜单接口中KEY值对应
+ */
+ @XmlElement(name = "EventKey")
+ private String eventKey;
+ /**
+ * 指菜单ID,如果是个性化菜单,则可以通过这个字段,知道是哪个规则的菜单被点击了。
+ */
+ @XmlElement(name = "MenuID")
+ private String menuId;
+
+ public String getEventKey() {
+ return eventKey;
+ }
+
+ public String getMenuId() {
+ return menuId;
+ }
+
+ @Override
+ public String toString() {
+ return "MenuEventMessage [eventKey=" + eventKey + ", menuId=" + menuId
+ + ", " + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuLocationEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuLocationEventMessage.java
new file mode 100644
index 00000000..1caa6533
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuLocationEventMessage.java
@@ -0,0 +1,109 @@
+package com.zone.weixin4j.message.event;
+
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+import java.io.Serializable;
+
+/**
+ * 弹出地理位置选择器的事件推送
+ *
+ * @className MenuLocationEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年9月30日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的弹出地理位置选择事件推送
+ * @see 企业号的弹出地理位置选择事件推送
+ */
+public class MenuLocationEventMessage extends MenuEventMessage {
+
+ private static final long serialVersionUID = 145223888272819563L;
+
+ public MenuLocationEventMessage() {
+ super(EventType.location_select);
+ }
+
+ /**
+ * 发送的位置消息
+ */
+ @XmlElement(name = "SendLocationInfo")
+ private LocationInfo locationInfo;
+
+ public LocationInfo getLocationInfo() {
+ return locationInfo;
+ }
+
+ /**
+ * 地理位置信息
+ *
+ * @className LocationInfo
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年3月29日
+ * @since JDK 1.6
+ * @see
+ */
+ public static class LocationInfo implements Serializable {
+
+ private static final long serialVersionUID = 4904181780216819965L;
+
+ /**
+ * 地理位置维度
+ */
+ @XmlElement(name = "Location_X")
+ private double x;
+ /**
+ * 地理位置经度
+ */
+ @XmlElement(name = "Location_Y")
+ private double y;
+ /**
+ * 地图缩放大小
+ */
+ @XmlElement(name = "Scale")
+ private double scale;
+ /**
+ * 地理位置信息
+ */
+ @XmlElement(name = "Label")
+ private String label;
+ /**
+ * 朋友圈POI的名字,可能为空
+ */
+ @XmlElement(name = "Poiname")
+ private String poiname;
+
+ public double getX() {
+ return x;
+ }
+
+ public double getY() {
+ return y;
+ }
+
+ public double getScale() {
+ return scale;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public String getPoiname() {
+ return poiname;
+ }
+
+ @Override
+ public String toString() {
+ return "LocationInfo [x=" + x + ", y=" + y + ", scale=" + scale
+ + ", label=" + label + ", poiname=" + poiname + "]";
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MenuLocationEventMessage [locationInfo=" + locationInfo + ", "
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuPhotoEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuPhotoEventMessage.java
new file mode 100644
index 00000000..728044fd
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuPhotoEventMessage.java
@@ -0,0 +1,107 @@
+package com.zone.weixin4j.message.event;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 弹出拍照或者相册发图的事件推送(pic_sysphoto|pic_photo_or_album|pic_weixin)
+ *
+ * @className MenuPhotoEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年9月30日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的系统发图的事件推送
+ * @see 企业号的系统发图的事件推送
+ */
+public class MenuPhotoEventMessage extends MenuEventMessage {
+
+ private static final long serialVersionUID = 3142350663022709730L;
+
+ /**
+ * 发送的图片信息
+ */
+ @XmlElement(name = "SendPicsInfo")
+ private PictureInfo pictureInfo;
+
+ public PictureInfo getPictureInfo() {
+ return pictureInfo;
+ }
+
+ /**
+ * 图片信息
+ *
+ * @className PictureInfo
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年3月29日
+ * @since JDK 1.6
+ * @see
+ */
+ public static class PictureInfo implements Serializable {
+
+ private static final long serialVersionUID = -3361375879168233258L;
+
+ /**
+ * 发送的图片数量
+ */
+ @XmlElement(name = "Count")
+ private int count;
+ /**
+ * 图片列表
+ */
+ @XmlElementWrapper(name = "PicList")
+ @XmlElement(name = "item")
+ private List items;
+
+ public int getCount() {
+ return count;
+ }
+
+ public List getItems() {
+ return items;
+ }
+
+ @Override
+ public String toString() {
+ return "PictureInfo [count=" + count + ", items=" + items + "]";
+ }
+ }
+
+ /**
+ * 图片
+ *
+ * @className PictureItem
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年3月29日
+ * @since JDK 1.6
+ * @see
+ */
+ public static class PictureItem implements Serializable {
+
+ private static final long serialVersionUID = -7636697449096645590L;
+
+ /**
+ * 图片的MD5值,开发者若需要,可用于验证接收到图片
+ */
+ @XmlElement(name = "PicMd5Sum")
+ private String md5;
+
+ public String getMd5() {
+ return md5;
+ }
+
+ @Override
+ public String toString() {
+ return "PictureItem [md5=" + md5 + "]";
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MenuPhotoEventMessage [pictureInfo=" + pictureInfo + ", "
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuScanEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuScanEventMessage.java
new file mode 100644
index 00000000..16ec3b1e
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/MenuScanEventMessage.java
@@ -0,0 +1,74 @@
+package com.zone.weixin4j.message.event;
+
+import javax.xml.bind.annotation.XmlElement;
+import java.io.Serializable;
+
+/**
+ * 扫码推事件(scancode_push|scancode_waitmsg)
+ *
+ * @className MenuScanEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年9月30日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的扫码推事件
+ * @see 企业号的的扫码推事件
+ */
+public class MenuScanEventMessage extends MenuEventMessage {
+
+ private static final long serialVersionUID = 3142350663022709730L;
+
+ /**
+ * 扫描信息
+ */
+ @XmlElement(name = "ScanCodeInfo")
+ private ScanInfo scanInfo;
+
+ public ScanInfo getScanInfo() {
+ return scanInfo;
+ }
+
+ /**
+ * 扫描信息
+ *
+ * @className ScanInfo
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年3月29日
+ * @since JDK 1.6
+ * @see
+ */
+ public static class ScanInfo implements Serializable {
+
+ private static final long serialVersionUID = 2237570238164900421L;
+ /**
+ * 扫描类型,一般是qrcode
+ */
+ @XmlElement(name = "ScanType")
+ private String type;
+ /**
+ * 扫描结果,即二维码对应的字符串信息
+ */
+ @XmlElement(name = "ScanResult")
+ private String result;
+
+ public String getType() {
+ return type;
+ }
+
+ public String getResult() {
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ScanInfo [type=" + type + ", result=" + result + "]";
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "MenuScanEventMessage [scanInfo=" + scanInfo + ", "
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/README.md b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/README.md
new file mode 100644
index 00000000..f2651b2b
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/message/event/README.md
@@ -0,0 +1,3 @@
+菜单事件消息
+
+用户点击自定义菜单后,微信会把点击事件推送给开发者,请注意,点击菜单弹出子菜单,不会产生上报。请注意,第3个到第8个的所有事件,仅支持微信iPhone5.4.1以上版本,和Android5.4以上版本的微信用户,旧版本微信用户点击后将没有回应,开发者也不能正常接收到事件推送。
\ No newline at end of file
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/component/ComponentEventType.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/component/ComponentEventType.java
new file mode 100644
index 00000000..2523c760
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/component/ComponentEventType.java
@@ -0,0 +1,28 @@
+package com.zone.weixin4j.mp.component;
+
+/**
+ * 应用组件回调事件
+ *
+ * @className ComponentEventType
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2016年7月5日
+ * @since JDK 1.6
+ */
+public enum ComponentEventType {
+ /**
+ * 推送ticket
+ */
+ component_verify_ticket,
+ /**
+ * 取消授权
+ */
+ unauthorized,
+ /**
+ * 授权成功
+ */
+ authorized,
+ /**
+ * 授权更新
+ */
+ updateauthorized
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/component/ComponentMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/component/ComponentMessage.java
new file mode 100644
index 00000000..e7e13fb7
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/component/ComponentMessage.java
@@ -0,0 +1,105 @@
+package com.zone.weixin4j.mp.component;
+
+import javax.xml.bind.annotation.*;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 组件消息
+ *
+ * @className ComponentMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2016年7月5日
+ * @since JDK 1.6
+ */
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class ComponentMessage implements Serializable {
+
+ private static final long serialVersionUID = -7243616276403632118L;
+ /**
+ * 第三方平台appid
+ */
+ @XmlElement(name = "AppId")
+ private String appId;
+ /**
+ * 事件类型
+ */
+ @XmlElement(name = "InfoType")
+ private String eventType;
+ /**
+ * 时间戳
+ */
+ @XmlElement(name = "CreateTime")
+ private long createTime;
+ /**
+ * Ticket内容
+ */
+ @XmlElement(name = "ComponentVerifyTicket")
+ private String verifyTicket;
+ /**
+ * 授权方的Appid
+ */
+ @XmlElement(name = "AuthorizerAppid")
+ private String authAppId;
+ /**
+ * 授权码,可用于换取公众号的接口调用凭据
+ */
+ @XmlElement(name = "AuthorizationCode")
+ private String authCode;
+ /**
+ * 授权码过期时间
+ */
+ @XmlElement(name = "AuthorizationCodeExpiredTime")
+ private long authCodeExpiredTime;
+
+ public String getAppId() {
+ return appId;
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ @XmlTransient
+ public ComponentEventType getFormatEventType() {
+ return ComponentEventType.valueOf(eventType);
+ }
+
+ public long getCreateTime() {
+ return createTime;
+ }
+
+ @XmlTransient
+ public Date getFormatCreateTime() {
+ return createTime > 0l ? new Date(createTime * 1000l) : null;
+ }
+
+ public String getVerifyTicket() {
+ return verifyTicket;
+ }
+
+ public String getAuthAppId() {
+ return authAppId;
+ }
+
+ public String getAuthCode() {
+ return authCode;
+ }
+
+ public long getAuthCodeExpiredTime() {
+ return authCodeExpiredTime;
+ }
+
+ @XmlTransient
+ public Date getFormatAuthCodeExpiredTime() {
+ return authCodeExpiredTime > 0l ? new Date(authCodeExpiredTime * 1000l) : null;
+ }
+
+ @Override
+ public String toString() {
+ return "ComponentMessage [appId=" + appId + ", eventType=" + eventType + ", createTime=" + createTime
+ + ", verifyTicket=" + verifyTicket + ", authAppId=" + authAppId + ", authCode=" + authCode
+ + ", authCodeExpiredTime=" + authCodeExpiredTime + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/KfCloseEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/KfCloseEventMessage.java
new file mode 100644
index 00000000..a2fcd925
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/KfCloseEventMessage.java
@@ -0,0 +1,41 @@
+package com.zone.weixin4j.mp.event;
+
+import com.zone.weixin4j.message.event.EventMessage;
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 客服关闭会话事件
+ *
+ * @className KfCloseEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年3月22日
+ * @since JDK 1.6
+ * @see 会话状态通知事件
+ */
+public class KfCloseEventMessage extends EventMessage {
+
+ private static final long serialVersionUID = 3644449346935205541L;
+
+ public KfCloseEventMessage() {
+ super(EventType.kf_close_session.name());
+ }
+
+ /**
+ * 客服账号
+ */
+ @XmlElement(name = "KfAccount")
+ private String kfAccount;
+
+ public String getKfAccount() {
+ return kfAccount;
+ }
+
+ @Override
+ public String toString() {
+ return "KfCloseEventMessage [kfAccount=" + kfAccount + ", ="
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/KfCreateEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/KfCreateEventMessage.java
new file mode 100644
index 00000000..32a57010
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/KfCreateEventMessage.java
@@ -0,0 +1,41 @@
+package com.zone.weixin4j.mp.event;
+
+import com.zone.weixin4j.message.event.EventMessage;
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 客服接入会话事件
+ *
+ * @className KfCreateEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年3月22日
+ * @since JDK 1.6
+ * @see 会话状态通知事件
+ */
+public class KfCreateEventMessage extends EventMessage {
+
+ private static final long serialVersionUID = -8968189700999202108L;
+
+ public KfCreateEventMessage() {
+ super(EventType.kf_create_session.name());
+ }
+
+ /**
+ * 客服账号
+ */
+ @XmlElement(name = "KfAccount")
+ private String kfAccount;
+
+ public String getKfAccount() {
+ return kfAccount;
+ }
+
+ @Override
+ public String toString() {
+ return "KfCreateEventMessage [kfAccount=" + kfAccount + ", ="
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/KfSwitchEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/KfSwitchEventMessage.java
new file mode 100644
index 00000000..03c7f711
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/KfSwitchEventMessage.java
@@ -0,0 +1,50 @@
+package com.zone.weixin4j.mp.event;
+
+import com.zone.weixin4j.message.event.EventMessage;
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 客服转接会话事件
+ *
+ * @className KfSwitchEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年3月22日
+ * @since JDK 1.6
+ * @see 会话状态通知事件
+ */
+public class KfSwitchEventMessage extends EventMessage {
+
+ private static final long serialVersionUID = 4319501074109623413L;
+
+ public KfSwitchEventMessage() {
+ super(EventType.kf_switch_session.name());
+ }
+
+ /**
+ * 来自的客服账号
+ */
+ @XmlElement(name = "FromKfAccount")
+ private String fromKfAccount;
+ /**
+ * 转移给客服账号
+ */
+ @XmlElement(name = "ToKfAccount")
+ private String toKfAccount;
+
+ public String getFromKfAccount() {
+ return fromKfAccount;
+ }
+
+ public String getToKfAccount() {
+ return toKfAccount;
+ }
+
+ @Override
+ public String toString() {
+ return "KfSwitchEventMessage [fromKfAccount=" + fromKfAccount
+ + ", toKfAccount=" + toKfAccount + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/MassEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/MassEventMessage.java
new file mode 100644
index 00000000..46ce44ab
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/MassEventMessage.java
@@ -0,0 +1,80 @@
+package com.zone.weixin4j.mp.event;
+
+import com.zone.weixin4j.message.event.EventMessage;
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 群发消息事件推送
+ *
+ * @className MassEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月27日
+ * @since JDK 1.6
+ * @see 群发回调
+ */
+public class MassEventMessage extends EventMessage {
+
+ private static final long serialVersionUID = -1660543255873723895L;
+
+ public MassEventMessage() {
+ super(EventType.masssendjobfinish.name());
+ }
+
+ /**
+ * 群发后的状态信息 为“send success”或“send fail”或“err(num)
+ */
+ @XmlElement(name = "Status")
+ private String status;
+ /**
+ * group_id下粉丝数;或者openid_list中的粉丝数
+ */
+ @XmlElement(name = "TotalCount")
+ private int totalCount;
+ /**
+ * 过滤(过滤是指特定地区、性别的过滤、用户设置拒收的过滤,用户接收已超4条的过滤)后,准备发送的粉丝数,原则上,FilterCount =
+ * SentCount + ErrorCount
+ */
+ @XmlElement(name = "FilterCount")
+ private int filterCount;
+ /**
+ * 发送成功的粉丝数
+ */
+ @XmlElement(name = "SentCount")
+ private int sentCount;
+ /**
+ * 发送失败的粉丝数
+ */
+ @XmlElement(name = "ErrorCount")
+ private int errorCount;
+
+ public String getStatus() {
+ return status;
+ }
+
+ public int getTotalCount() {
+ return totalCount;
+ }
+
+ public int getFilterCount() {
+ return filterCount;
+ }
+
+ public int getSentCount() {
+ return sentCount;
+ }
+
+ public int getErrorCount() {
+ return errorCount;
+ }
+
+ @Override
+ public String toString() {
+ return "MassEventMessage [status=" + status + ", totalCount="
+ + totalCount + ", filterCount=" + filterCount + ", sentCount="
+ + sentCount + ", errorCount=" + errorCount + ", "
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/ScanEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/ScanEventMessage.java
new file mode 100644
index 00000000..561f4c1a
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/ScanEventMessage.java
@@ -0,0 +1,60 @@
+package com.zone.weixin4j.mp.event;
+
+import com.zone.weixin4j.message.event.EventMessage;
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlTransient;
+
+/**
+ * 扫描二维码事件
+ *
+ * @className ScanEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see 扫描二维码事件
+ */
+public class ScanEventMessage extends EventMessage {
+
+ private static final long serialVersionUID = 8078674062833071562L;
+
+ public ScanEventMessage() {
+ super(EventType.scan.name());
+ }
+
+ public ScanEventMessage(String eventType) {
+ super(eventType);
+ }
+
+ /**
+ * 事件KEY值,是一个32位无符号整数,即创建二维码时的二维码scene_id
+ */
+ @XmlElement(name = "EventKey")
+ private String eventKey;
+ /**
+ * 二维码的ticket,可用来换取二维码图片
+ */
+ @XmlElement(name = "Ticket")
+ private String ticket;
+
+ public String getEventKey() {
+ return eventKey;
+ }
+
+ public String getTicket() {
+ return ticket;
+ }
+
+ @XmlTransient
+ public String getParameter() {
+ return eventKey.replaceFirst("qrscene_", "");
+ }
+
+ @Override
+ public String toString() {
+ return "ScanEventMessage [eventKey=" + eventKey + ", ticket=" + ticket
+ + ", " + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/ScribeEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/ScribeEventMessage.java
new file mode 100644
index 00000000..f2025aa3
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/ScribeEventMessage.java
@@ -0,0 +1,27 @@
+package com.zone.weixin4j.mp.event;
+
+import com.zone.weixin4j.type.EventType;
+
+/**
+ * 关注/取消关注事件 包括直接关注与扫描关注
+ *
+ * @className ScribeEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see 订阅号、服务号的关注/取消关注事件
+ */
+public class ScribeEventMessage extends ScanEventMessage {
+
+ private static final long serialVersionUID = -6846321620262204915L;
+
+ public ScribeEventMessage() {
+ super(EventType.subscribe.name());
+ }
+
+ @Override
+ public String toString() {
+ return "ScribeEventMessage [" + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/TemplatesendjobfinishMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/TemplatesendjobfinishMessage.java
new file mode 100644
index 00000000..64dcc080
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/TemplatesendjobfinishMessage.java
@@ -0,0 +1,41 @@
+package com.zone.weixin4j.mp.event;
+
+import com.zone.weixin4j.message.event.EventMessage;
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 模板消息事件推送(公众平台)
+ *
+ * @className TemplatesendjobfinishMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年9月19日
+ * @since JDK 1.6
+ * @see 模板消息事件推送
+ */
+public class TemplatesendjobfinishMessage extends EventMessage {
+
+ private static final long serialVersionUID = -2903359365988594012L;
+
+ public TemplatesendjobfinishMessage() {
+ super(EventType.templatesendjobfinish.name());
+ }
+
+ /**
+ * 推送状态 如failed: system failed
+ */
+ @XmlElement(name = "Status")
+ private String status;
+
+ public String getStatus() {
+ return status;
+ }
+
+ @Override
+ public String toString() {
+ return "TemplatesendjobfinishMessage [status=" + status + ", "
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/VerifyExpireEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/VerifyExpireEventMessage.java
new file mode 100644
index 00000000..73774024
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/VerifyExpireEventMessage.java
@@ -0,0 +1,48 @@
+package com.zone.weixin4j.mp.event;
+
+import com.zone.weixin4j.message.event.EventMessage;
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlTransient;
+import java.util.Date;
+
+/**
+ * 认证通知(资质认证成功/名称认证成功/年审通知/认证过期失效通知)
+ *
+ * @className VerifyExpireEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年10月25日
+ * @since JDK 1.6
+ * @see 认证事件
+ */
+public class VerifyExpireEventMessage extends EventMessage {
+
+ private static final long serialVersionUID = -4309074299189681095L;
+
+ public VerifyExpireEventMessage() {
+ super(EventType.annual_renew.name());
+ }
+
+ /**
+ * 有效期 (整形),指的是时间戳,将于该时间戳认证过期
+ */
+ @XmlElement(name = "EventKey")
+ private long expiredTime;
+
+ public long getExpiredTime() {
+ return expiredTime;
+ }
+
+ @XmlTransient
+ public Date getFormatExpiredTime() {
+ return new Date(expiredTime * 1000l);
+ }
+
+ @Override
+ public String toString() {
+ return "VerifyExpireEventMessage [expiredTime=" + expiredTime + ", "
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/VerifyFailEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/VerifyFailEventMessage.java
new file mode 100644
index 00000000..7a96c381
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/mp/event/VerifyFailEventMessage.java
@@ -0,0 +1,54 @@
+package com.zone.weixin4j.mp.event;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlTransient;
+import java.util.Date;
+
+/**
+ * 认证失败事件(资质认证失败/名称认证失败)
+ *
+ * @className VerifyFailEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年10月25日
+ * @since JDK 1.6
+ * @see
+ * 认证事件
+ */
+public class VerifyFailEventMessage extends VerifyExpireEventMessage {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 2155899086751787490L;
+
+ /**
+ * 失败发生时间 (整形),时间戳
+ */
+ @XmlElement(name = "FailTime")
+ private long failTime;
+ /**
+ * 认证失败的原因
+ */
+ @XmlElement(name = "FailReason")
+ private String failReason;
+
+ public long getFailTime() {
+ return failTime;
+ }
+
+ @XmlTransient
+ public Date getFormatFailTime() {
+ return new Date(failTime * 1000l);
+ }
+
+ public String getFailReason() {
+ return failReason;
+ }
+
+ @Override
+ public String toString() {
+ return "VerifyFailEventMessage [failTime=" + failTime + ", failReason=" + failReason + ", " + super.toString()
+ + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatEventType.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatEventType.java
new file mode 100644
index 00000000..0a5f70d8
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatEventType.java
@@ -0,0 +1,33 @@
+package com.zone.weixin4j.qy.chat;
+
+/**
+ * 会话事件
+ *
+ * @className ChatEventType
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年8月1日
+ * @since JDK 1.6
+ * @see
+ */
+public enum ChatEventType {
+ /**
+ * 创建会话
+ */
+ create_chat,
+ /**
+ * 修改会话
+ */
+ update_chat,
+ /**
+ * 退出会话
+ */
+ quit_chat,
+ /**
+ * 订阅事件
+ */
+ subscribe,
+ /**
+ * 取消订阅事件
+ */
+ unsubscribe;
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatItem.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatItem.java
new file mode 100644
index 00000000..f25ef776
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatItem.java
@@ -0,0 +1,240 @@
+package com.zone.weixin4j.qy.chat;
+
+import com.zone.weixin4j.type.MessageType;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlTransient;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 会话事件或消息
+ *
+ * @className ChatItem
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年8月1日
+ * @since JDK 1.6
+ * @see
+ */
+public class ChatItem implements Serializable {
+
+ private static final long serialVersionUID = -5921235260175596270L;
+
+ private final String LIST_SEPARATOR = "\\|";
+
+ /**
+ * 操作成员UserID
+ */
+ @XmlElement(name = "FromUserName")
+ private String operatorId;
+ /**
+ * 消息创建时间(整型)
+ */
+ @XmlElement(name = "CreateTime")
+ private long createTime;
+ /**
+ * 消息类型
+ *
+ */
+ @XmlElement(name = "MsgType")
+ private String msgType;
+ /**
+ * 事件类型
+ */
+ @XmlElement(name = "Event")
+ private String eventType;
+ /**
+ * 会话id
+ */
+ @XmlElement(name = "ChatId")
+ private String chatId;
+ /**
+ * 会话标题
+ */
+ @XmlElement(name = "Name")
+ private String chatName;
+ /**
+ * 管理员userid
+ */
+ @XmlElement(name = "Owner")
+ private String ownerId;
+ /**
+ * 会话成员列表
+ */
+ @XmlElement(name = "UserList")
+ private String members;
+ /**
+ * 会话新增成员列表
+ */
+ @XmlElement(name = "AddUserList")
+ private String addMembers;
+ /**
+ * 会话删除成员列表
+ */
+ @XmlElement(name = "DelUserList")
+ private String deleteMembers;
+ /**
+ * 消息ID 64位整型
+ */
+ @XmlElement(name = "MsgId")
+ private long msgId;
+ /**
+ * 接收人
+ */
+ @XmlElement(name = "Receiver")
+ private ChatReceiver receiver;
+ /**
+ * 文本消息内容
+ */
+ @XmlElement(name = "Content")
+ private String content;
+ /**
+ * 图片消息链接
+ */
+ @XmlElement(name = "PicUrl")
+ private String picUrl;
+ /**
+ * 链接消息标题
+ */
+ @XmlElement(name = "Title")
+ private String title;
+ /**
+ * 链接消息描述
+ */
+ @XmlElement(name = "Description")
+ private String description;
+ /**
+ * 链接消息链接
+ */
+ @XmlElement(name = "Url")
+ private String url;
+ /**
+ * 图片、语音、文件消息的媒体id,可以调用获取媒体文件接口拉取数据
+ */
+ @XmlElement(name = "MediaId")
+ private String mediaId;
+
+ public String getOperatorId() {
+ return operatorId;
+ }
+
+ public long getCreateTime() {
+ return createTime;
+ }
+
+ @XmlTransient
+ public Date getFormatCreateTime() {
+ return createTime > 0l ? new Date(createTime * 1000l) : null;
+ }
+
+ public String getMsgType() {
+ return msgType;
+ }
+
+ @XmlTransient
+ public MessageType getFormatMsgType() {
+ return msgType != null ? MessageType.valueOf(msgType) : null;
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ @XmlTransient
+ public ChatEventType getFormatEventType() {
+ return eventType != null ? ChatEventType.valueOf(eventType) : null;
+ }
+
+ public String getChatId() {
+ return chatId;
+ }
+
+ public String getChatName() {
+ return chatName;
+ }
+
+ public String getOwnerId() {
+ return ownerId;
+ }
+
+ public String getMembers() {
+ return members;
+ }
+
+ @XmlTransient
+ public List getFormatMembers() {
+ return members != null ? Arrays.asList(members.split(LIST_SEPARATOR))
+ : null;
+ }
+
+ public String getAddMembers() {
+ return addMembers;
+ }
+
+ @XmlTransient
+ public List getFormatAddMembers() {
+ return addMembers != null ? Arrays.asList(addMembers
+ .split(LIST_SEPARATOR)) : null;
+ }
+
+ public String getDeleteMembers() {
+ return deleteMembers;
+ }
+
+ @XmlTransient
+ public List getFormatDeleteMembers() {
+ return deleteMembers != null ? Arrays.asList(deleteMembers
+ .split(LIST_SEPARATOR)) : null;
+ }
+
+ public long getMsgId() {
+ return msgId;
+ }
+
+ public ChatReceiver getReceiver() {
+ return receiver;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public String getPicUrl() {
+ return picUrl;
+ }
+
+ public String getMediaId() {
+ return mediaId;
+ }
+
+ public static long getSerialversionuid() {
+ return serialVersionUID;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ @Override
+ public String toString() {
+ return "ChatItem [operatorId=" + operatorId + ", createTime="
+ + createTime + ", msgType=" + msgType + ", eventType="
+ + eventType + ", chatId=" + chatId + ", chatName=" + chatName
+ + ", ownerId=" + ownerId + ", members=" + members
+ + ", addMembers=" + addMembers + ", deleteMembers="
+ + deleteMembers + ", msgId=" + msgId + ", receiver=" + receiver
+ + ", content=" + content + ", picUrl=" + picUrl + ", title="
+ + title + ", description=" + description + ", url=" + url
+ + ", mediaId=" + mediaId + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatReceiver.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatReceiver.java
new file mode 100644
index 00000000..e450e131
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatReceiver.java
@@ -0,0 +1,49 @@
+package com.zone.weixin4j.qy.chat;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlTransient;
+import java.io.Serializable;
+
+/**
+ * 接收人
+ *
+ * @className ChatReceiver
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年8月1日
+ * @since JDK 1.6
+ * @see
+ */
+public class ChatReceiver implements Serializable {
+
+ private static final long serialVersionUID = -3870813624685620828L;
+ /**
+ * 成员id|会话id
+ */
+ @XmlElement(name = "id")
+ private String targetId;
+ /**
+ * 群聊|单聊|userid|openid
+ */
+ @XmlElement(name = "type")
+ private String chatType;
+
+ public String getTargetId() {
+ return targetId;
+ }
+
+ public String getChatType() {
+ return chatType;
+ }
+
+ @XmlTransient
+ public ChatType getFormatChatType() {
+ return ChatType.valueOf(chatType);
+ }
+
+ @Override
+ public String toString() {
+ return "ChatReceiver [targetId=" + targetId + ", chatType=" + chatType
+ + "]";
+ }
+
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatType.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatType.java
new file mode 100644
index 00000000..3ead9fc5
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/ChatType.java
@@ -0,0 +1,21 @@
+package com.zone.weixin4j.qy.chat;
+
+/**
+ * 会话类型
+ *
+ * @className ChatType
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年7月31日
+ * @since JDK 1.6
+ * @see
+ */
+public enum ChatType {
+ /**
+ * 单聊
+ */
+ single,
+ /**
+ * 群聊
+ */
+ group
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/WeixinChatMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/WeixinChatMessage.java
new file mode 100644
index 00000000..37a908c5
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/chat/WeixinChatMessage.java
@@ -0,0 +1,81 @@
+package com.zone.weixin4j.qy.chat;
+
+import com.zone.weixin4j.type.AgentType;
+
+import javax.xml.bind.annotation.*;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 企业号聊天服务回调消息
+ *
+ * @className WeixinChatMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年8月1日
+ * @since JDK 1.6
+ * @see
+ */
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class WeixinChatMessage implements Serializable {
+
+ private static final long serialVersionUID = 6788124387186831643L;
+
+ /**
+ * 企业号CorpID
+ */
+ @XmlElement(name = "ToUserName")
+ private String corpId;
+ /**
+ * 应用类型
+ */
+ @XmlElement(name = "AgentType")
+ private String agentType;
+ /**
+ * 消息数量
+ */
+ @XmlElement(name = "ItemCount")
+ private int itemCount;
+ /**
+ * 会话事件或消息
+ */
+ @XmlElement(name = "Item")
+ public List items;
+ /**
+ * 回调包ID,uint64类型,企业内唯一
+ */
+ @XmlElement(name = "PackageId")
+ private String packageId;
+
+ public String getCorpId() {
+ return corpId;
+ }
+
+ public String getAgentType() {
+ return agentType;
+ }
+
+ @XmlTransient
+ public AgentType getFormatAgentType() {
+ return AgentType.valueOf(agentType);
+ }
+
+ public int getItemCount() {
+ return itemCount;
+ }
+
+ public List getItems() {
+ return items;
+ }
+
+ public String getPackageId() {
+ return packageId;
+ }
+
+ @Override
+ public String toString() {
+ return "WeixinChatMessage [corpId=" + corpId + ", agentType="
+ + agentType + ", itemCount=" + itemCount + ", items=" + items
+ + ", packageId=" + packageId + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/event/BatchjobresultMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/event/BatchjobresultMessage.java
new file mode 100644
index 00000000..2e763b21
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/event/BatchjobresultMessage.java
@@ -0,0 +1,100 @@
+package com.zone.weixin4j.qy.event;
+
+import com.zone.weixin4j.message.event.EventMessage;
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+import java.io.Serializable;
+
+/**
+ * 异步任务事件完成通知
+ *
+ * @className BatchjobresultMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年3月31日
+ * @since JDK 1.6
+ * @see 异步任务事件完成通知
+ */
+public class BatchjobresultMessage extends EventMessage {
+
+ private static final long serialVersionUID = 8014540441322209657L;
+
+ public BatchjobresultMessage() {
+ super(EventType.batch_job_result.name());
+ }
+
+ /**
+ * 任务信息
+ */
+ @XmlElement(name = "BatchJob")
+ private BatchJob batchJob;
+
+ public BatchJob getBatchJob() {
+ return batchJob;
+ }
+
+ /**
+ * 任务信息
+ *
+ * @className BatchJob
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年4月1日
+ * @since JDK 1.6
+ * @see
+ */
+ public static class BatchJob implements Serializable {
+ private static final long serialVersionUID = -7520032656787156391L;
+ /**
+ * 异步任务id,最大长度为64字符
+ */
+ @XmlElement(name = "JobId")
+ private String jobId;
+ /**
+ * 操作类型,字符串,目前分别有: 1. sync_user(增量更新成员) 2. replace_user(全量覆盖成员) 3.
+ * invite_user(邀请成员关注) 4. replace_party(全量覆盖部门)
+ *
+ * @see com.foxinmy.weixin4j.qy.type.BatchType
+ */
+ @XmlElement(name = "JobType")
+ private String jobType;
+ /**
+ * 返回码
+ */
+ @XmlElement(name = "ErrCode")
+ private String ErrCode;
+ /**
+ * 对返回码的文本描述内容
+ */
+ @XmlElement(name = "ErrMsg")
+ private String errMsg;
+
+ public String getJobId() {
+ return jobId;
+ }
+
+ public String getJobType() {
+ return jobType;
+ }
+
+ public String getErrCode() {
+ return ErrCode;
+ }
+
+ public String getErrMsg() {
+ return errMsg;
+ }
+
+ @Override
+ public String toString() {
+ return "[jobId=" + jobId + ", jobType=" + jobType + ", ErrCode="
+ + ErrCode + ", errMsg=" + errMsg + "]";
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "BatchjobresultMessage [batchJob=" + batchJob + ", "
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/event/EnterAgentEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/event/EnterAgentEventMessage.java
new file mode 100644
index 00000000..0f124e60
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/event/EnterAgentEventMessage.java
@@ -0,0 +1,42 @@
+package com.zone.weixin4j.qy.event;
+
+import com.zone.weixin4j.message.event.EventMessage;
+import com.zone.weixin4j.type.EventType;
+
+import javax.xml.bind.annotation.XmlElement;
+
+/**
+ * 用户进入应用的事件推送(企业号)本事件只有在应用的回调模式中打开上报开关时上报
+ *
+ * @className EnterAgentEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年12月28日
+ * @since JDK 1.6
+ * @see 用户进入应用的事件推送
+ */
+public class EnterAgentEventMessage extends EventMessage {
+
+ private static final long serialVersionUID = 7675732524832500820L;
+
+ public EnterAgentEventMessage() {
+ super(EventType.enter_agent.name());
+ }
+
+ /**
+ * 事件KEY值,与自定义菜单接口中KEY值对应
+ */
+ @XmlElement(name = "EventKey")
+ private String eventKey;
+
+ public String getEventKey() {
+ return eventKey;
+ }
+
+ @Override
+ public String toString() {
+ return "EnterAgentEventMessage [eventKey=" + eventKey + ", "
+ + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/event/ScribeEventMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/event/ScribeEventMessage.java
new file mode 100644
index 00000000..ccdf05b0
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/event/ScribeEventMessage.java
@@ -0,0 +1,28 @@
+package com.zone.weixin4j.qy.event;
+
+import com.zone.weixin4j.message.event.EventMessage;
+import com.zone.weixin4j.type.EventType;
+
+/**
+ * 关注/取消关注事件 包括直接关注与扫描关注
+ *
+ * @className ScribeEventMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年4月6日
+ * @since JDK 1.6
+ * @see 成员关注/取消关注事件
+ */
+public class ScribeEventMessage extends EventMessage {
+
+ private static final long serialVersionUID = -6846321620262204915L;
+
+ public ScribeEventMessage() {
+ super(EventType.subscribe.name());
+ }
+
+ @Override
+ public String toString() {
+ return "ScribeEventMessage [" + super.toString() + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/suite/SuiteEventType.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/suite/SuiteEventType.java
new file mode 100644
index 00000000..25c31bba
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/suite/SuiteEventType.java
@@ -0,0 +1,26 @@
+package com.zone.weixin4j.qy.suite;
+
+/**
+ * 应用套件回调事件
+ *
+ * @className SuiteEventType
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年6月21日
+ * @since JDK 1.6
+ * @see 第三方回调协议
+ */
+public enum SuiteEventType {
+ /**
+ * 推送ticket
+ */
+ suite_ticket,
+ /**
+ * 变更授权
+ */
+ change_auth,
+ /**
+ * 取消授权
+ */
+ cancel_auth;
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/suite/SuiteMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/suite/SuiteMessage.java
new file mode 100644
index 00000000..e2c1425f
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/qy/suite/SuiteMessage.java
@@ -0,0 +1,83 @@
+package com.zone.weixin4j.qy.suite;
+
+import javax.xml.bind.annotation.*;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 套件消息
+ *
+ * @className SuiteMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年6月23日
+ * @since JDK 1.6
+ * @see
+ */
+@XmlRootElement(name = "xml")
+@XmlAccessorType(XmlAccessType.FIELD)
+public class SuiteMessage implements Serializable {
+
+ private static final long serialVersionUID = 6457919241019021514L;
+ /**
+ * 应用套件的SuiteId
+ */
+ @XmlElement(name = "SuiteId")
+ private String suiteId;
+ /**
+ * 事件类型
+ */
+ @XmlElement(name = "InfoType")
+ private String eventType;
+ /**
+ * 时间戳
+ */
+ @XmlElement(name = "TimeStamp")
+ private long timeStamp;
+ /**
+ * Ticket内容
+ */
+ @XmlElement(name = "SuiteTicket")
+ private String suiteTicket;
+ /**
+ * 授权方企业号的corpid
+ */
+ @XmlElement(name = "AuthCorpId")
+ private String authCorpId;
+
+ public String getSuiteId() {
+ return suiteId;
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ @XmlTransient
+ public SuiteEventType getFormatEventType() {
+ return SuiteEventType.valueOf(eventType);
+ }
+
+ public long getTimeStamp() {
+ return timeStamp;
+ }
+
+ @XmlTransient
+ public Date getFormatTimeStamp() {
+ return timeStamp > 0l ? new Date(timeStamp * 1000l) : null;
+ }
+
+ public String getSuiteTicket() {
+ return suiteTicket;
+ }
+
+ public String getAuthCorpId() {
+ return authCorpId;
+ }
+
+ @Override
+ public String toString() {
+ return "SuiteMessage [suiteId=" + suiteId + ", eventType="
+ + eventType + ", timeStamp=" + timeStamp + ", suiteTicket="
+ + suiteTicket + ", authCorpId=" + authCorpId + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/request/WeixinMessage.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/request/WeixinMessage.java
new file mode 100644
index 00000000..e7ebc8f1
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/request/WeixinMessage.java
@@ -0,0 +1,161 @@
+package com.zone.weixin4j.request;
+
+import com.zone.weixin4j.type.MessageType;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlTransient;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 微信消息基类
+ *
+ * @className WeixinMessage
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月6日
+ * @since JDK 1.6
+ * @see com.foxinmy.weixin4j.message.ImageMessage
+ * @see com.foxinmy.weixin4j.message.LinkMessage
+ * @see com.foxinmy.weixin4j.message.LocationMessage
+ * @see com.foxinmy.weixin4j.message.TextMessage
+ * @see com.foxinmy.weixin4j.message.VideoMessage
+ * @see com.foxinmy.weixin4j.message.VoiceMessage
+ * @see com.foxinmy.weixin4j.message.event.EventMessage
+ * @see com.foxinmy.weixin4j.message.event.LocationEventMessage
+ * @see com.foxinmy.weixin4j.message.event.MenuEventMessage
+ * @see com.foxinmy.weixin4j.message.event.MenuLocationEventMessage
+ * @see com.foxinmy.weixin4j.message.event.MenuPhotoEventMessage
+ * @see com.foxinmy.weixin4j.message.event.MenuScanEventMessage
+ * @see com.foxinmy.weixin4j.mp.event.KfCloseEventMessage
+ * @see com.foxinmy.weixin4j.mp.event.KfCreateEventMessage
+ * @see com.foxinmy.weixin4j.mp.event.KfSwitchEventMessage
+ * @see com.foxinmy.weixin4j.mp.event.MassEventMessage
+ * @see com.foxinmy.weixin4j.mp.event.ScanEventMessage
+ * @see com.foxinmy.weixin4j.mp.event.ScribeEventMessage
+ * @see com.foxinmy.weixin4j.mp.event.TemplatesendjobfinishMessage
+ * @see com.foxinmy.weixin4j.qy.event.BatchjobresultMessage
+ * @see com.foxinmy.weixin4j.qy.event.EnterAgentEventMessage
+ * @see com.foxinmy.weixin4j.qy.event.ScribeEventMessage
+ */
+public class WeixinMessage implements Serializable {
+
+ private static final long serialVersionUID = 7761192742840031607L;
+
+ /**
+ * 开发者微信号
+ */
+ @XmlElement(name = "ToUserName")
+ private String toUserName;
+ /**
+ * 发送方账号 即用户的openid
+ */
+ @XmlElement(name = "FromUserName")
+ private String fromUserName;
+ /**
+ * 消息创建时间 系统毫秒数
+ */
+ @XmlElement(name = "CreateTime")
+ private long createTime;
+ /**
+ * 消息类型
+ */
+ @XmlElement(name = "MsgType")
+ private String msgType;
+ /**
+ * 消息ID 可用于排重
+ */
+ @XmlElement(name = "MsgId")
+ private long msgId;
+ /**
+ * 企业号独有的应用ID
+ */
+ @XmlElement(name = "AgentID")
+ private String agentId;
+
+ public WeixinMessage() {
+ // jaxb required
+ }
+
+ public WeixinMessage(String msgType) {
+ this.msgType = msgType;
+ }
+
+ public String getToUserName() {
+ return toUserName;
+ }
+
+ public String getFromUserName() {
+ return fromUserName;
+ }
+
+ public long getCreateTime() {
+ return createTime;
+ }
+
+ @XmlTransient
+ public Date getFormatCreateTime() {
+ return new Date(createTime * 1000l);
+ }
+
+ public String getMsgType() {
+ return msgType;
+ }
+
+ @XmlTransient
+ public MessageType getFormatMsgType() {
+ return MessageType.valueOf(msgType);
+ }
+
+ public long getMsgId() {
+ return msgId;
+ }
+
+ public String getAgentId() {
+ return agentId;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((agentId == null) ? 0 : agentId.hashCode());
+ result = prime * result + (int) (createTime ^ (createTime >>> 32));
+ result = prime * result
+ + ((fromUserName == null) ? 0 : fromUserName.hashCode());
+ result = prime * result + (int) (msgId ^ (msgId >>> 32));
+ result = prime * result + ((msgType == null) ? 0 : msgType.hashCode());
+ result = prime * result
+ + ((toUserName == null) ? 0 : toUserName.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ WeixinMessage other = (WeixinMessage) obj;
+ if (msgId > 0l && other.getMsgId() > 0l) {
+ return msgId == other.getMsgId();
+ }
+ return fromUserName.equals(other.getFromUserName())
+ && createTime == other.getCreateTime();
+ }
+
+ @Override
+ public String toString() {
+ String toString = " toUserName=" + toUserName + ", fromUserName="
+ + fromUserName + ", createTime=" + createTime + ", msgType="
+ + msgType;
+ if (msgId > 0l) {
+ toString += ", msgId=" + msgId;
+ }
+ if (agentId != null) {
+ toString += ", agentId=" + agentId;
+ }
+ return toString;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/request/WeixinRequest.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/request/WeixinRequest.java
new file mode 100644
index 00000000..f6c33fda
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/request/WeixinRequest.java
@@ -0,0 +1,161 @@
+package com.zone.weixin4j.request;
+
+import com.zone.weixin4j.type.EncryptType;
+import com.zone.weixin4j.util.AesToken;
+
+import javax.servlet.http.HttpServletRequest;
+
+
+/**
+ * 微信请求
+ *
+ * @author jinyu(foxinmy@gmail.com)
+ * @className WeixinRequest
+ * @date 2015年3月29日
+ * @see
+ * @since JDK 1.6
+ */
+public class WeixinRequest {
+
+ /**
+ * 请求的URI
+ */
+ private String uri;
+
+ // 以下字段每次被动消息时都会带上
+ /**
+ * 随机字符串
+ */
+ private String echoStr;
+ /**
+ * 时间戳
+ */
+ private String timeStamp;
+ /**
+ * 随机数
+ */
+ private String nonce;
+ /**
+ * 参数签名
+ */
+ private String signature;
+ /**
+ * AES模式下消息签名
+ */
+ private String msgSignature;
+
+ /**
+ * 加密类型(POST时存在)
+ *
+ * @see com.zone.weixin4j.type.EncryptType
+ */
+ private EncryptType encryptType;
+
+ /**
+ * xml消息明文主体
+ */
+ private String originalContent;
+
+ /**
+ * xml消息密文主体(AES时存在)
+ */
+ private String encryptContent;
+ /**
+ * aes & token
+ */
+ private AesToken aesToken;
+
+ private HttpServletRequest request;
+
+ public WeixinRequest(String uri,
+ EncryptType encryptType, String echoStr, String timeStamp,
+ String nonce, String signature, String msgSignature,
+ String originalContent, String encryptContent, AesToken aesToken) {
+ this.uri = uri;
+ this.encryptType = encryptType;
+ this.echoStr = echoStr;
+ this.timeStamp = timeStamp;
+ this.nonce = nonce;
+ this.signature = signature;
+ this.msgSignature = msgSignature;
+ this.originalContent = originalContent;
+ this.encryptContent = encryptContent;
+ this.aesToken = aesToken;
+ }
+
+ public WeixinRequest(String uri,
+ EncryptType encryptType, String echoStr, String timeStamp,
+ String nonce, String signature, String msgSignature,
+ String originalContent, String encryptContent, AesToken aesToken, HttpServletRequest request) {
+ this.uri = uri;
+ this.encryptType = encryptType;
+ this.echoStr = echoStr;
+ this.timeStamp = timeStamp;
+ this.nonce = nonce;
+ this.signature = signature;
+ this.msgSignature = msgSignature;
+ this.originalContent = originalContent;
+ this.encryptContent = encryptContent;
+ this.aesToken = aesToken;
+ this.request = request;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ public String getEchoStr() {
+ return echoStr;
+ }
+
+ public String getTimeStamp() {
+ return timeStamp;
+ }
+
+ public String getNonce() {
+ return nonce;
+ }
+
+ public String getSignature() {
+ return signature;
+ }
+
+ public String getMsgSignature() {
+ return msgSignature;
+ }
+
+ public EncryptType getEncryptType() {
+ return encryptType;
+ }
+
+ public String getOriginalContent() {
+ return originalContent;
+ }
+
+ public String getEncryptContent() {
+ return encryptContent;
+ }
+
+ public AesToken getAesToken() {
+ return aesToken;
+ }
+
+ public HttpServletRequest getHttpServletRequest() {
+ return request;
+ }
+
+ public WeixinRequest setHttpServletRequest(HttpServletRequest httpServletRequest) {
+ this.request = httpServletRequest;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "WeixinRequest [uri=" + uri + ", echoStr=" + echoStr
+ + ", timeStamp=" + timeStamp + ", nonce=" + nonce
+ + ", signature=" + signature + ", msgSignature=" + msgSignature
+ + ", encryptType=" + encryptType + ", originalContent="
+ + originalContent + ", encryptContent=" + encryptContent
+ + ", aesToken=" + aesToken + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/BlankResponse.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/BlankResponse.java
new file mode 100644
index 00000000..5127d88b
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/BlankResponse.java
@@ -0,0 +1,19 @@
+package com.zone.weixin4j.response;
+
+/**
+ * 空白回复(避免微信服务器重复推送消息)
+ *
+ * @className BlankResponse
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月7日
+ * @since JDK 1.6
+ * @see
+ */
+public class BlankResponse extends SingleResponse {
+
+ public static final BlankResponse global = new BlankResponse();
+
+ private BlankResponse() {
+ super("success");
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/ImageResponse.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/ImageResponse.java
new file mode 100644
index 00000000..b8829be8
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/ImageResponse.java
@@ -0,0 +1,37 @@
+package com.zone.weixin4j.response;
+
+/**
+ * 回复图片消息
+ *
+ * @className ImageResponse
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月5日
+ * @since JDK 1.6
+ * @see
+ */
+public class ImageResponse implements WeixinResponse {
+
+ /**
+ * 通过上传多媒体文件,得到的id。
+ */
+ private String mediaId;
+
+ public ImageResponse(String mediaId) {
+ this.mediaId = mediaId;
+ }
+
+ @Override
+ public String toContent() {
+ return String.format(
+ "", mediaId);
+ }
+
+ public String getMediaId() {
+ return mediaId;
+ }
+
+ @Override
+ public String getMsgType() {
+ return "image";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/MusicResponse.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/MusicResponse.java
new file mode 100644
index 00000000..af275845
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/MusicResponse.java
@@ -0,0 +1,98 @@
+package com.zone.weixin4j.response;
+
+/**
+ * 回复音乐消息
+ *
+ * @className MusicResponse
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月5日
+ * @since JDK 1.6
+ * @see
+ */
+public class MusicResponse implements WeixinResponse {
+
+ /**
+ * 缩略图的媒体id,通过上传多媒体文件,得到的id
+ */
+ private String thumbMediaId;
+ /**
+ * 音乐标题
+ */
+ private String title;
+ /**
+ * 音乐描述
+ */
+ private String desc;
+ /**
+ * 音乐链接
+ */
+ private String musicUrl;
+ /**
+ * 高质量音乐链接,WIFI环境优先使用该链接播放音乐
+ */
+ private String hqMusicUrl;
+
+ public MusicResponse(String thumbMediaId) {
+ this.thumbMediaId = thumbMediaId;
+ }
+
+ @Override
+ public String toContent() {
+ StringBuilder content = new StringBuilder();
+ content.append("");
+ content.append(String.format(
+ "", thumbMediaId));
+ content.append(String.format("",
+ title != null ? title : ""));
+ content.append(String.format(
+ "",
+ desc != null ? desc : ""));
+ content.append(String.format("",
+ musicUrl != null ? musicUrl : ""));
+ content.append(String.format("",
+ hqMusicUrl != null ? hqMusicUrl : ""));
+ content.append("");
+ return content.toString();
+ }
+
+ public String getThumbMediaId() {
+ return thumbMediaId;
+ }
+
+ public String getMusicUrl() {
+ return musicUrl;
+ }
+
+ public void setMusicUrl(String musicUrl) {
+ this.musicUrl = musicUrl;
+ }
+
+ public String getHqMusicUrl() {
+ return hqMusicUrl;
+ }
+
+ public void setHqMusicUrl(String hqMusicUrl) {
+ this.hqMusicUrl = hqMusicUrl;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDesc() {
+ return desc;
+ }
+
+ public void setDesc(String desc) {
+ this.desc = desc;
+ }
+
+ @Override
+ public String getMsgType() {
+ return "music";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/NewsResponse.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/NewsResponse.java
new file mode 100644
index 00000000..d3b6c8d5
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/NewsResponse.java
@@ -0,0 +1,159 @@
+package com.zone.weixin4j.response;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 回复图文消息
+ *
+ * @className NewsResponse
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月5日
+ * @since JDK 1.6
+ * @see
+ */
+public class NewsResponse implements WeixinResponse {
+
+ /**
+ * 图文集合
+ */
+ private List articleList;
+
+ public NewsResponse(List articleList) {
+ this.articleList = articleList;
+ }
+
+ public NewsResponse(Article article) {
+ this.articleList = new ArrayList();
+ this.articleList.add(article);
+ }
+
+ public void pushArticle(Article article) {
+ articleList.add(article);
+ }
+
+ public void pushFirstArticle(Article article) {
+ articleList.add(0, article);
+ }
+
+ public void pushLastArticle(Article article) {
+ articleList.add(articleList.size(), article);
+ }
+
+ public Article removeLastArticle() {
+ return articleList.remove(articleList.size() - 1);
+ }
+
+ public Article removeFirstArticle() {
+ return articleList.remove(0);
+ }
+
+ public List getArticleList() {
+ return articleList;
+ }
+
+ @Override
+ public String toContent() {
+ StringBuilder content = new StringBuilder();
+ content.append(String.format("%d",
+ articleList.size()));
+ content.append("");
+ for (Article article : articleList) {
+ content.append("- ");
+ content.append(String.format("",
+ article.getTitle() != null ? article.getTitle() : ""));
+ content.append(String.format(
+ "",
+ article.getDesc() != null ? article.getDesc() : ""));
+ content.append(String.format("",
+ article.getUrl() != null ? article.getUrl() : ""));
+ content.append(String.format("",
+ article.getPicUrl() != null ? article.getPicUrl() : ""));
+ content.append("
");
+ }
+ content.append("");
+ return content.toString();
+ }
+
+ @Override
+ public String getMsgType() {
+ return "news";
+ }
+
+ /**
+ * 图文消息对象
+ *
+ * @className Article
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月5日
+ * @since JDK 1.6
+ * @see
+ */
+ public static class Article {
+ /**
+ * 图文消息标题
+ */
+ private String title;
+ /**
+ * 图文消息描述
+ */
+ private String desc;
+ /**
+ * 点击图文消息跳转链接
+ */
+ private String url;
+ /**
+ * 图片链接,支持JPG、PNG格式,较好的效果为大图360*200,小图200*200
+ */
+ private String picUrl;
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDesc() {
+ return desc;
+ }
+
+ public void setDesc(String desc) {
+ this.desc = desc;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getPicUrl() {
+ return picUrl;
+ }
+
+ public void setPicUrl(String picUrl) {
+ this.picUrl = picUrl;
+ }
+
+ public Article() {
+
+ }
+
+ public Article(String title, String desc, String url, String picUrl) {
+ this.title = title;
+ this.desc = desc;
+ this.url = url;
+ this.picUrl = picUrl;
+ }
+
+ @Override
+ public String toString() {
+ return "Article [title=" + title + ", desc=" + desc + ", url="
+ + url + ", picUrl=" + picUrl + "]";
+ }
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/SingleResponse.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/SingleResponse.java
new file mode 100644
index 00000000..37f26ffe
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/SingleResponse.java
@@ -0,0 +1,34 @@
+package com.zone.weixin4j.response;
+
+/**
+ * 单一内容回复
+ *
+ * @className SingleResponse
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年8月3日
+ * @since JDK 1.6
+ * @see
+ */
+public class SingleResponse implements WeixinResponse {
+
+ private final String content;
+
+ public SingleResponse(String content) {
+ this.content = content;
+ }
+
+ @Override
+ public String toContent() {
+ return content;
+ }
+
+ @Override
+ public String getMsgType() {
+ return "single";
+ }
+
+ @Override
+ public String toString() {
+ return "SingleResponse [content=" + content + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/TextResponse.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/TextResponse.java
new file mode 100644
index 00000000..6f31c558
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/TextResponse.java
@@ -0,0 +1,36 @@
+package com.zone.weixin4j.response;
+
+/**
+ * 回复文本消息
+ *
+ * @className TextResponse
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月5日
+ * @since JDK 1.6
+ * @see
+ */
+public class TextResponse implements WeixinResponse {
+
+ /**
+ * 回复的消息内容(换行:在content中能够换行,微信客户端就支持换行显示)
+ */
+ private String content;
+
+ public TextResponse(String content) {
+ this.content = content;
+ }
+
+ @Override
+ public String toContent() {
+ return String.format("", content);
+ }
+
+ @Override
+ public String getMsgType() {
+ return "text";
+ }
+
+ public String getContent() {
+ return content;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/TransferCustomerResponse.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/TransferCustomerResponse.java
new file mode 100644
index 00000000..260eee83
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/TransferCustomerResponse.java
@@ -0,0 +1,43 @@
+package com.zone.weixin4j.response;
+
+/**
+ * 消息转移到客服
+ *
+ * @className TransferCustomerResponse
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月5日
+ * @since JDK 1.6
+ * @see 转移消息到多客服
+ */
+public class TransferCustomerResponse implements WeixinResponse {
+
+ /**
+ * 指定会话接入的客服账号
+ */
+ private String kfAccount;
+
+ public TransferCustomerResponse(String kfAccount) {
+ this.kfAccount = kfAccount;
+ }
+
+ public String getKfAccount() {
+ return kfAccount;
+ }
+
+ @Override
+ public String toContent() {
+ String content = "";
+ if (kfAccount != null && !kfAccount.trim().isEmpty()) {
+ content = String
+ .format("",
+ kfAccount);
+ }
+ return content;
+ }
+
+ @Override
+ public String getMsgType() {
+ return "transfer_customer_service";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/VideoResponse.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/VideoResponse.java
new file mode 100644
index 00000000..df136022
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/VideoResponse.java
@@ -0,0 +1,76 @@
+package com.zone.weixin4j.response;
+
+/**
+ * 回复视频消息
+ *
+ * @className VideoResponse
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月5日
+ * @since JDK 1.6
+ * @see
+ */
+public class VideoResponse implements WeixinResponse {
+
+ /**
+ * 通过上传多媒体文件,得到的id
+ */
+ private String mediaId;
+ /**
+ * 视频消息标题
+ */
+ private String title;
+ /**
+ * 视频消息描述
+ */
+ private String desc;
+
+ public VideoResponse(String mediaId) {
+ this.mediaId = mediaId;
+ }
+
+ public VideoResponse(String mediaId, String title, String desc) {
+ this.mediaId = mediaId;
+ this.title = title;
+ this.desc = desc;
+ }
+
+ @Override
+ public String toContent() {
+ StringBuilder content = new StringBuilder();
+ content.append("");
+ return content.toString();
+ }
+
+ public String getMediaId() {
+ return mediaId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDesc() {
+ return desc;
+ }
+
+ public void setDesc(String desc) {
+ this.desc = desc;
+ }
+
+ @Override
+ public String getMsgType() {
+ return "video";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/VoiceResponse.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/VoiceResponse.java
new file mode 100644
index 00000000..da0a3712
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/VoiceResponse.java
@@ -0,0 +1,37 @@
+package com.zone.weixin4j.response;
+
+/**
+ * 回复语音消息
+ *
+ * @className VoiceResponse
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月5日
+ * @since JDK 1.6
+ * @see
+ */
+public class VoiceResponse implements WeixinResponse {
+
+ /**
+ * 通过上传多媒体文件,得到的id
+ */
+ private String mediaId;
+
+ public VoiceResponse(String mediaId) {
+ this.mediaId = mediaId;
+ }
+
+ @Override
+ public String toContent() {
+ return String.format(
+ "", mediaId);
+ }
+
+ public String getMediaId() {
+ return mediaId;
+ }
+
+ @Override
+ public String getMsgType() {
+ return "voice";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/WeixinResponse.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/WeixinResponse.java
new file mode 100644
index 00000000..3747c8ce
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/response/WeixinResponse.java
@@ -0,0 +1,39 @@
+package com.zone.weixin4j.response;
+
+
+/**
+ * 微信被动消息回复
+ *
+ * @className WeixinResponse
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月5日
+ * @since JDK 1.6
+ * @see TextResponse
+ * @see ImageResponse
+ * @see MusicResponse
+ * @see VoiceResponse
+ * @see VideoResponse
+ * @see NewsResponse
+ * @see TransferCustomerResponse
+ * @see SingleResponse
+ * @see BlankResponse
+ * @see 订阅号、服务号的被动响应消息
+ * @see 企业号的被动响应消息
+ */
+public interface WeixinResponse {
+ /**
+ * 回复的消息类型
+ *
+ * @return
+ */
+ public String getMsgType();
+
+ /**
+ * 回复的消息内容
+ *
+ * @return
+ */
+ public String toContent();
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/WeiXin4jContextAware.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/WeiXin4jContextAware.java
new file mode 100644
index 00000000..4d0ca3d4
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/WeiXin4jContextAware.java
@@ -0,0 +1,32 @@
+package com.zone.weixin4j.service;
+
+import com.zone.weixin4j.dispatcher.WeixinMessageMatcher;
+import com.zone.weixin4j.util.AesToken;
+import org.springframework.context.ApplicationContext;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by Yz on 2017/3/15.
+ * WeiXin4j上下文
+ */
+public interface WeiXin4jContextAware {
+
+ boolean isOpenAlwaysResponse();
+
+ boolean isUseDebugMessageHandler();
+
+ List getAesTokens();
+
+ ApplicationContext getApplicationContext();
+
+ Map getAesTokenMap();
+
+ void setAesTokenMap(Map aesTokenMap);
+
+ public WeixinMessageMatcher getWeixinMessageMatcher();
+
+ public void setWeixinMessageMatcher(WeixinMessageMatcher weixinMessageMatcher);
+
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/WxService.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/WxService.java
new file mode 100644
index 00000000..227a736a
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/WxService.java
@@ -0,0 +1,23 @@
+package com.zone.weixin4j.service;
+
+import com.zone.weixin4j.exception.HttpResponseException;
+import com.zone.weixin4j.exception.MessageInterceptorException;
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.request.WeixinRequest;
+import com.zone.weixin4j.response.WeixinResponse;
+import com.zone.weixin4j.util.AesToken;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Created by Yz on 2017/3/14.
+ * WxServiceImpl
+ */
+public interface WxService {
+
+ WeixinResponse processRequest(String uri, String encryptType, String echostr, String timestamp, String nonce, String signature, String msg_signature, String messageContent, AesToken aesToken, HttpServletRequest request) throws WeixinException, HttpResponseException, MessageInterceptorException;
+
+ String transferResponse(WeixinResponse weixinResponse) throws WeixinException;
+
+}
+
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/context/WeiXin4jContextAwareImpl.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/context/WeiXin4jContextAwareImpl.java
new file mode 100644
index 00000000..1bc2fbd7
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/context/WeiXin4jContextAwareImpl.java
@@ -0,0 +1,107 @@
+package com.zone.weixin4j.service.context;
+
+import com.zone.weixin4j.dispatcher.WeixinMessageMatcher;
+import com.zone.weixin4j.service.WeiXin4jContextAware;
+import com.zone.weixin4j.socket.WeixinMessageTransfer;
+import com.zone.weixin4j.spring.TokenGenerater;
+import com.zone.weixin4j.util.AesToken;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.NamedThreadLocal;
+import org.springframework.util.StringUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Created by Yz on 2017/3/15.
+ * WeiXin4j上下文
+ */
+public class WeiXin4jContextAwareImpl implements ApplicationContextAware, WeiXin4jContextAware {
+
+ private static final ThreadLocal weixinMessageTransfer = new NamedThreadLocal("WeixinMessageTransfer");
+
+ private ApplicationContext applicationContext;
+
+ private boolean openAlwaysResponse;
+ private boolean useDebugMessageHandler;
+
+ private List aesTokens;
+ private TokenGenerater tokenGenerater;
+
+ private WeixinMessageMatcher weixinMessageMatcher;
+
+ private Map aesTokenMap = new HashMap();
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ this.applicationContext = applicationContext;
+
+ }
+
+ public boolean isOpenAlwaysResponse() {
+ return openAlwaysResponse;
+ }
+
+ public void setOpenAlwaysResponse(boolean openAlwaysResponse) {
+ this.openAlwaysResponse = openAlwaysResponse;
+ }
+
+ public boolean isUseDebugMessageHandler() {
+ return useDebugMessageHandler;
+ }
+
+ public void setUseDebugMessageHandler(boolean useDebugMessageHandler) {
+ this.useDebugMessageHandler = useDebugMessageHandler;
+ }
+
+ public List getAesTokens() {
+ return aesTokens;
+ }
+
+ public void setAesTokens(List aesTokens) {
+ this.aesTokens = aesTokens;
+ }
+
+ public void setTokenGenerater(TokenGenerater tokenGenerater) {
+ this.tokenGenerater = tokenGenerater;
+ }
+
+ public ApplicationContext getApplicationContext() {
+ return applicationContext;
+ }
+
+ public void init() {
+ this.aesTokens = tokenGenerater.getAesTokens();
+ for(AesToken aesToken : this.aesTokens){
+ this.aesTokenMap.put(StringUtils.isEmpty(aesToken.getWeixinId()) ? "" : aesToken.getWeixinId(), aesToken);
+ }
+ }
+
+ public void destroy() {
+ this.applicationContext = null;
+ }
+
+ public static ThreadLocal getWeixinMessageTransfer() {
+ return weixinMessageTransfer;
+ }
+
+ public Map getAesTokenMap() {
+ return aesTokenMap;
+ }
+
+ public void setAesTokenMap(Map aesTokenMap) {
+ this.aesTokenMap = aesTokenMap;
+ }
+
+ public WeixinMessageMatcher getWeixinMessageMatcher() {
+ return weixinMessageMatcher;
+ }
+
+ public void setWeixinMessageMatcher(WeixinMessageMatcher weixinMessageMatcher) {
+ this.weixinMessageMatcher = weixinMessageMatcher;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/impl/WxServiceImpl.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/impl/WxServiceImpl.java
new file mode 100644
index 00000000..45be113f
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/service/impl/WxServiceImpl.java
@@ -0,0 +1,130 @@
+package com.zone.weixin4j.service.impl;
+
+import com.zone.weixin4j.dispatcher.WeixinMessageDispatcher;
+import com.zone.weixin4j.exception.HttpResponseException;
+import com.zone.weixin4j.exception.MessageInterceptorException;
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.request.WeixinRequest;
+import com.zone.weixin4j.response.SingleResponse;
+import com.zone.weixin4j.response.WeixinResponse;
+import com.zone.weixin4j.service.WxService;
+import com.zone.weixin4j.socket.WeixinResponseEncoder;
+import com.zone.weixin4j.type.EncryptType;
+import com.zone.weixin4j.util.AesToken;
+import com.zone.weixin4j.util.MessageUtil;
+import com.zone.weixin4j.util.ServerToolkits;
+import com.zone.weixin4j.xml.EncryptMessageHandler;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Created by Yz on 2017/3/14.
+ * WxServiceImpl
+ */
+
+@Component
+@DependsOn({"weiXin4jContextAware", "weixinMessageDispatcher"})
+public class WxServiceImpl implements WxService {
+
+ private final Log logger = LogFactory.getLog(getClass());
+
+ @Autowired
+ private WeixinMessageDispatcher messageDispatcher;
+
+ @Autowired
+ private WeixinResponseEncoder weixinResponseEncoder;
+
+ /**
+ * 处理Request
+ *
+ * @throws WeixinException
+ * @throws HttpResponseException
+ */
+ @Override
+ public WeixinResponse processRequest(String uri, String encrypt_type, String echostr, String timestamp, String nonce, String signature, String msg_signature, String messageContent, AesToken aesToken, HttpServletRequest request) throws WeixinException, HttpResponseException, MessageInterceptorException {
+ EncryptType encryptType = !StringUtils.isEmpty(encrypt_type) ? EncryptType.valueOf(encrypt_type.toUpperCase()) : EncryptType.RAW;
+ String encryptContent = null;
+ if (!ServerToolkits.isBlank(messageContent) && encryptType == EncryptType.AES) {
+ if (ServerToolkits.isBlank(aesToken.getAesKey())) {
+ logger.error("EncodingAESKey not be empty in safety(AES) mode");
+ }
+ EncryptMessageHandler encryptHandler = EncryptMessageHandler.parser(messageContent);
+ encryptContent = encryptHandler.getEncryptContent();
+ /**
+ * 企业号第三方套件 ╮(╯_╰)╭
+ */
+ if (aesToken.getWeixinId().startsWith("tj")) {
+ aesToken = new AesToken(encryptHandler.getToUserName(), aesToken.getToken(), aesToken.getAesKey());
+ }
+ messageContent = MessageUtil.aesDecrypt(aesToken.getWeixinId(), aesToken.getAesKey(), encryptContent);
+ }
+ WeixinRequest weixinRequest = new WeixinRequest(uri, encryptType, echostr, timestamp, nonce, signature, msg_signature, messageContent, encryptContent, aesToken).setHttpServletRequest(request);
+
+ if (aesToken == null || (ServerToolkits.isBlank(weixinRequest.getSignature()) && ServerToolkits.isBlank(weixinRequest.getMsgSignature()))) {
+ throw new HttpResponseException(HttpResponseException.HttpResponseStatus.BAD_REQUEST);
+ }
+
+ if (request.getMethod().equalsIgnoreCase(RequestMethod.GET.toString())) {
+ return doGet(weixinRequest);
+ } else if (request.getMethod().equalsIgnoreCase(RequestMethod.POST.toString())) {
+ return doPost(weixinRequest);
+ } else {
+ return otherwise(weixinRequest);
+ }
+ }
+
+ /**
+ * 处理Get请求
+ *
+ * @throws WeixinException
+ * @throws HttpResponseException
+ */
+ protected WeixinResponse doGet(WeixinRequest request) throws WeixinException, HttpResponseException {
+ if (!ServerToolkits.isBlank(request.getSignature()) && MessageUtil.signature(request.getAesToken().getToken(), request.getTimeStamp(), request.getNonce()).equals(request.getSignature())) {
+ return new SingleResponse(request.getEchoStr());
+ }
+ // XML消息签名验证
+ if (!ServerToolkits.isBlank(request.getMsgSignature()) && MessageUtil.signature(request.getAesToken().getToken(), request.getTimeStamp(), request.getNonce(), request.getEchoStr()).equals(request.getMsgSignature())) {
+ return new SingleResponse(MessageUtil.aesDecrypt(null, request.getAesToken().getAesKey(), request.getEchoStr()));
+ }
+ throw new HttpResponseException(HttpResponseException.HttpResponseStatus.FORBIDDEN);
+ }
+
+ /**
+ * 处理Post请求
+ *
+ * @throws WeixinException
+ * @throws HttpResponseException
+ */
+ protected WeixinResponse doPost(WeixinRequest request) throws HttpResponseException, MessageInterceptorException, WeixinException {
+ // URL参数签名验证
+ if (!ServerToolkits.isBlank(request.getSignature()) && !MessageUtil.signature(request.getAesToken().getToken(), request.getTimeStamp(), request.getNonce()).equals(request.getSignature())) {
+ throw new HttpResponseException(HttpResponseException.HttpResponseStatus.FORBIDDEN);
+ }
+ // XML消息签名验证
+ if (request.getEncryptType() == EncryptType.AES && !MessageUtil.signature(request.getAesToken().getToken(), request.getTimeStamp(), request.getNonce(), request.getEncryptContent()).equals(request.getMsgSignature())) {
+ throw new HttpResponseException(HttpResponseException.HttpResponseStatus.FORBIDDEN);
+ }
+ return messageDispatcher.doDispatch(request);
+ }
+
+ protected WeixinResponse otherwise(WeixinRequest weixinRequest) throws HttpResponseException {
+ throw new HttpResponseException(HttpResponseException.HttpResponseStatus.METHOD_NOT_ALLOWED);
+ }
+
+ public String transferResponse(WeixinResponse weixinResponse) throws WeixinException {
+ if(weixinResponse instanceof SingleResponse){
+ return weixinResponse.toContent();
+ } else {
+ return weixinResponseEncoder.encode(weixinResponse);
+ }
+ }
+
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/socket/README.md b/weixin4j-serverX/src/main/java/com/zone/weixin4j/socket/README.md
new file mode 100644
index 00000000..ee6f5eb6
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/socket/README.md
@@ -0,0 +1,7 @@
+WeixinMessageDecoder:对微信消息解码
+
+WeixinRequestHandler:微信请求处理类
+
+WeixinResponseEncoder:对微信回复编码
+
+SingleResponseEncoder:对微信单一回复编码
\ No newline at end of file
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/socket/WeixinMessageTransfer.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/socket/WeixinMessageTransfer.java
new file mode 100644
index 00000000..06e52090
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/socket/WeixinMessageTransfer.java
@@ -0,0 +1,109 @@
+package com.zone.weixin4j.socket;
+
+import com.zone.weixin4j.type.AccountType;
+import com.zone.weixin4j.type.EncryptType;
+import com.zone.weixin4j.util.AesToken;
+
+import java.io.Serializable;
+import java.util.Set;
+
+/**
+ * 消息传递
+ *
+ * @className WeixinMessageTransfer
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年6月23日
+ * @since JDK 1.6
+ * @see
+ */
+public class WeixinMessageTransfer implements Serializable {
+
+ private static final long serialVersionUID = 7779948135156353261L;
+
+ /**
+ * aes & token
+ */
+ private AesToken aesToken;
+ /**
+ * 加密类型
+ */
+ private EncryptType encryptType;
+ /**
+ * 消息接收方
+ */
+ private String toUserName;
+ /**
+ * 消息发送方
+ */
+ private String fromUserName;
+ /**
+ * 账号
+ */
+ private AccountType accountType;
+ /**
+ * 消息类型
+ */
+ private String msgType;
+ /**
+ * 事件类型
+ */
+ private String eventType;
+ /**
+ * 节点集合
+ */
+ private Set nodeNames;
+
+ public WeixinMessageTransfer(AesToken aesToken, EncryptType encryptType,
+ String toUserName, String fromUserName, AccountType accountType,
+ String msgType, String eventType, Set nodeNames) {
+ this.aesToken = aesToken;
+ this.encryptType = encryptType;
+ this.toUserName = toUserName;
+ this.fromUserName = fromUserName;
+ this.accountType = accountType;
+ this.msgType = msgType;
+ this.eventType = eventType;
+ this.nodeNames = nodeNames;
+ }
+
+ public AesToken getAesToken() {
+ return aesToken;
+ }
+
+ public EncryptType getEncryptType() {
+ return encryptType;
+ }
+
+ public String getToUserName() {
+ return toUserName;
+ }
+
+ public String getFromUserName() {
+ return fromUserName;
+ }
+
+ public AccountType getAccountType() {
+ return accountType;
+ }
+
+ public String getMsgType() {
+ return msgType;
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ public Set getNodeNames() {
+ return nodeNames;
+ }
+
+ @Override
+ public String toString() {
+ return "WeixinMessageTransfer [aesToken=" + aesToken + ", encryptType="
+ + encryptType + ", toUserName=" + toUserName
+ + ", fromUserName=" + fromUserName + ", accountType="
+ + accountType + ", msgType=" + msgType + ", eventType="
+ + eventType + ", nodeNames=" + nodeNames + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/socket/WeixinResponseEncoder.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/socket/WeixinResponseEncoder.java
new file mode 100644
index 00000000..3dff9aa0
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/socket/WeixinResponseEncoder.java
@@ -0,0 +1,73 @@
+package com.zone.weixin4j.socket;
+
+import com.zone.weixin4j.exception.WeixinException;
+import com.zone.weixin4j.response.WeixinResponse;
+import com.zone.weixin4j.service.context.WeiXin4jContextAwareImpl;
+import com.zone.weixin4j.type.EncryptType;
+import com.zone.weixin4j.util.AesToken;
+import com.zone.weixin4j.util.MessageUtil;
+import com.zone.weixin4j.util.ServerToolkits;
+import org.springframework.stereotype.Component;
+
+/**
+ * 微信回复编码类
+ *
+ * @author jinyu(foxinmy@gmail.com)
+ * @className WeixinResponseEncoder
+ * @date 2014年11月13日
+ * @see 加密接入指引
+ * @see com.zone.weixin4j.response.WeixinResponse
+ * @since JDK 1.6
+ */
+
+@Component
+public class WeixinResponseEncoder {
+
+ private final String XML_START = "";
+ // ---------------明文节点
+ private final String ELEMENT_TOUSERNAME = "";
+ private final String ELEMENT_FROMUSERNAME = "";
+ private final String ELEMENT_CREATETIME = "";
+ private final String ELEMENT_MSGTYPE = "";
+ // ---------------密文节点
+ private final String ELEMENT_MSGSIGNATURE = "";
+ private final String ELEMENT_ENCRYPT = "";
+ private final String ELEMENT_TIMESTAMP = "";
+ private final String ELEMENT_NONCE = "";
+ private final String XML_END = "";
+
+ public String encode(WeixinResponse response) throws WeixinException {
+ WeixinMessageTransfer messageTransfer = WeiXin4jContextAwareImpl.getWeixinMessageTransfer().get();
+ EncryptType encryptType = messageTransfer.getEncryptType();
+ StringBuilder content = new StringBuilder();
+ content.append(XML_START);
+ content.append(String.format(ELEMENT_TOUSERNAME,
+ messageTransfer.getFromUserName()));
+ content.append(String.format(ELEMENT_FROMUSERNAME,
+ messageTransfer.getToUserName()));
+ content.append(String.format(ELEMENT_CREATETIME,
+ System.currentTimeMillis() / 1000l));
+ content.append(String.format(ELEMENT_MSGTYPE, response.getMsgType()));
+ content.append(response.toContent());
+ content.append(XML_END);
+ if (encryptType == EncryptType.AES) {
+ AesToken aesToken = messageTransfer.getAesToken();
+ String nonce = ServerToolkits.generateRandomString(32);
+ String timestamp = Long
+ .toString(System.currentTimeMillis() / 1000l);
+ String encrtypt = MessageUtil.aesEncrypt(aesToken.getWeixinId(),
+ aesToken.getAesKey(), content.toString());
+ String msgSignature = MessageUtil.signature(aesToken.getToken(),
+ nonce, timestamp, encrtypt);
+ content.delete(0, content.length());
+ content.append(XML_START);
+ content.append(String.format(ELEMENT_NONCE, nonce));
+ content.append(String.format(ELEMENT_TIMESTAMP, timestamp));
+ content.append(String.format(ELEMENT_MSGSIGNATURE, msgSignature));
+ content.append(String.format(ELEMENT_ENCRYPT, encrtypt));
+ content.append(XML_END);
+ }
+ return content.toString();
+ }
+}
\ No newline at end of file
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/spring/PropertyTokenGenerater.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/spring/PropertyTokenGenerater.java
new file mode 100644
index 00000000..383647f5
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/spring/PropertyTokenGenerater.java
@@ -0,0 +1,24 @@
+package com.zone.weixin4j.spring;
+
+import com.zone.weixin4j.util.AesToken;
+
+import java.util.List;
+
+/**
+ * Created by Yz on 2017/3/15.
+ * 配置文件中获取Token
+ */
+public class PropertyTokenGenerater extends TokenGenerater {
+
+ private List aesTokens;
+
+ @Override
+ public List getAesTokens() {
+ return aesTokens;
+ }
+
+ public void setAesTokens(List aesTokens) {
+ this.aesTokens = aesTokens;
+ }
+
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/spring/TokenGenerater.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/spring/TokenGenerater.java
new file mode 100644
index 00000000..a3950645
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/spring/TokenGenerater.java
@@ -0,0 +1,15 @@
+package com.zone.weixin4j.spring;
+
+import com.zone.weixin4j.util.AesToken;
+
+import java.util.List;
+
+/**
+ * Created by Yz on 2017/3/15.
+ * 生成Token
+ */
+public abstract class TokenGenerater {
+
+ abstract public List getAesTokens();
+
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/AccountType.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/AccountType.java
new file mode 100644
index 00000000..02b9594d
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/AccountType.java
@@ -0,0 +1,21 @@
+package com.zone.weixin4j.type;
+
+/**
+ * 账号类型
+ *
+ * @className AccountType
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月18日
+ * @since JDK 1.6
+ * @see
+ */
+public enum AccountType {
+ /**
+ * 公众号
+ */
+ MP,
+ /**
+ * 企业号
+ */
+ QY
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/AgentType.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/AgentType.java
new file mode 100644
index 00000000..8e9c8cc5
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/AgentType.java
@@ -0,0 +1,26 @@
+package com.zone.weixin4j.type;
+
+/**
+ * 应用类型
+ *
+ * @className AgentType
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年8月1日
+ * @since JDK 1.6
+ * @see
+ */
+public enum AgentType {
+ /**
+ * 聊天应用
+ */
+ chat,
+ // 企业客服回调
+ /**
+ * 企业号内部客服,客户为企业号通讯录成员
+ */
+ kf_internal,
+ /**
+ * 企业号外部客服,客户为服务号openid
+ */
+ kf_external
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/EncryptType.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/EncryptType.java
new file mode 100644
index 00000000..b6a3a313
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/EncryptType.java
@@ -0,0 +1,21 @@
+package com.zone.weixin4j.type;
+
+/**
+ * 消息加密类型
+ *
+ * @className EncryptType
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年11月23日
+ * @since JDK 1.6
+ * @see
+ */
+public enum EncryptType {
+ /**
+ * 明文模式
+ */
+ RAW,
+ /**
+ * 密文模式
+ */
+ AES;
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/EventType.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/EventType.java
new file mode 100644
index 00000000..7692591b
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/EventType.java
@@ -0,0 +1,159 @@
+package com.zone.weixin4j.type;
+
+/**
+ * 事件类型
+ *
+ * @className EventType
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年9月30日
+ * @since JDK 1.6
+ * @see
+ */
+public enum EventType {
+ /**
+ * 关注事件
+ *
+ */
+ subscribe,
+ /**
+ * 取消关注事件
+ *
+ */
+ unsubscribe,
+ /**
+ * 上报地理位置事件
+ *
+ * @see com.foxinmy.weixin4j.message.event.LocationEventMessage
+ */
+ location,
+ /**
+ * 菜单点击关键字事件
+ *
+ * @see com.foxinmy.weixin4j.message.event.MenuEventMessage
+ */
+ view,
+ /**
+ * 菜单点击链接事件
+ *
+ * @see com.foxinmy.weixin4j.message.event.MenuEventMessage
+ */
+ click,
+ /**
+ * 菜单扫描事件
+ *
+ * @see com.foxinmy.weixin4j.message.event.MenuScanEventMessage
+ */
+ scancode_push,
+ /**
+ * 菜单扫描并调出等待界面事件
+ *
+ * @see com.foxinmy.weixin4j.message.event.MenuScanEventMessage
+ */
+ scancode_waitmsg,
+ /**
+ * 菜单弹出拍照事件
+ *
+ * @see com.foxinmy.weixin4j.message.event.MenuPhotoEventMessage
+ */
+ pic_sysphoto,
+ /**
+ * 菜单弹出发图事件
+ *
+ * @see com.foxinmy.weixin4j.message.event.MenuPhotoEventMessage
+ */
+ pic_photo_or_album,
+ /**
+ * 菜单弹出发图事件
+ *
+ * @see com.foxinmy.weixin4j.message.event.MenuPhotoEventMessage
+ */
+ pic_weixin,
+ /**
+ * 菜单发送地理位置事件
+ *
+ * @see com.foxinmy.weixin4j.message.event.MenuLocationEventMessage
+ */
+ location_select,
+
+ // ------------------------------公众平台特有------------------------------
+
+ /**
+ * 二维码扫描事件
+ *
+ * @see com.foxinmy.weixin4j.mp.event.ScanEventMessage
+ */
+ scan,
+ /**
+ * 群发消息事件
+ *
+ * @see com.foxinmy.weixin4j.mp.event.MassEventMessage
+ */
+ masssendjobfinish,
+ /**
+ * 模板消息事件
+ *
+ * @see com.foxinmy.weixin4j.mp.event.TemplatesendjobfinishMessage
+ */
+ templatesendjobfinish,
+ /**
+ * 客服接入会话事件
+ *
+ * @see com.foxinmy.weixin4j.mp.event.KfCreateEventMessage
+ */
+ kf_create_session,
+ /**
+ * 客服关闭会话事件
+ *
+ * @see com.foxinmy.weixin4j.mp.event.KfCloseEventMessage
+ */
+ kf_close_session,
+ /**
+ * 客服转接会话事件
+ *
+ * @see com.foxinmy.weixin4j.mp.event.KfSwitchEventMessage
+ */
+ kf_switch_session,
+ /**
+ * 资质认证成功事件
+ */
+ qualification_verify_success,
+ /**
+ * 资质认证失败事件
+ */
+ qualification_verify_fail,
+ /**
+ * 名称认证成功事件
+ */
+ naming_verify_success,
+ /**
+ * 名称认证失败事件
+ */
+ naming_verify_fail,
+ /**
+ * 年审通知事件
+ */
+ annual_renew,
+ /**
+ * 认证过期失效通知
+ */
+ verify_expired,
+
+ // ------------------------------企业号特有------------------------------
+ /**
+ * 异步任务完成事件
+ *
+ * @see com.foxinmy.weixin4j.qy.event.BatchjobresultMessage
+ */
+ batch_job_result,
+ /**
+ * 进入企业号应用事件
+ *
+ * @see com.foxinmy.weixin4j.qy.event.EnterAgentEventMessage
+ */
+ enter_agent,
+ /**
+ * 第三方应用套件消息
+ * @see com.foxinmy.weixin4j.qy.suite.WeixinSuiteMessage
+ */
+ suite;
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/MessageType.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/MessageType.java
new file mode 100644
index 00000000..10e24db8
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/type/MessageType.java
@@ -0,0 +1,60 @@
+package com.zone.weixin4j.type;
+
+
+/**
+ *
+ * 消息类型
+ *
+ * @author jinyu(foxinmy@gmail.com)
+ *
+ */
+public enum MessageType {
+ /**
+ * 文字消息
+ *
+ * @see com.foxinmy.weixin4j.message.TextMessage
+ */
+ text,
+ /**
+ * 图片消息
+ *
+ * @see com.foxinmy.weixin4j.message.ImageMessage
+ */
+ image,
+ /**
+ * 语音消息
+ *
+ * @see com.foxinmy.weixin4j.message.VoiceMessage
+ */
+ voice,
+ /**
+ * 视频消息
+ *
+ * @see com.foxinmy.weixin4j.message.VideoMessage
+ */
+ video,
+ /**
+ * 小视频消息
+ *
+ * @see com.foxinmy.weixin4j.message.VideoMessage
+ */
+ shortvideo,
+ /**
+ * 位置消息
+ *
+ * @see com.foxinmy.weixin4j.message.LocationMessage
+ */
+ location,
+ /**
+ * 链接消息
+ *
+ * @see com.foxinmy.weixin4j.message.LinkMessage
+ */
+ link,
+ /**
+ * 事件消息
+ *
+ * @see com.foxinmy.weixin4j.message.event.EventMessage
+ */
+ event;
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/AesToken.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/AesToken.java
new file mode 100644
index 00000000..2cbebcbd
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/AesToken.java
@@ -0,0 +1,74 @@
+package com.zone.weixin4j.util;
+
+import java.io.Serializable;
+
+/**
+ * aes & token
+ *
+ * @className AesToken
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月6日
+ * @since JDK 1.6
+ * @see
+ */
+public class AesToken implements Serializable {
+
+ private static final long serialVersionUID = -6001008896414323534L;
+
+ /**
+ * 账号ID(原始id/appid/corpid)
+ */
+ private String weixinId;
+ /**
+ * 开发者的token
+ */
+ private String token;
+ /**
+ * 安全模式下的加密密钥
+ */
+ private String aesKey;
+
+ /**
+ * 一般为明文模式
+ *
+ * @param token
+ * 开发者的Token
+ */
+ public AesToken(String token) {
+ this(null, token, null);
+ }
+
+ /**
+ * 一般为AES加密模式
+ *
+ * @param weixinId
+ * 公众号的应用ID(原始id/appid/corpid)
+ * @param token
+ * 开发者Token
+ * @param aesKey
+ * 解密的EncodingAESKey
+ */
+ public AesToken(String weixinId, String token, String aesKey) {
+ this.weixinId = weixinId;
+ this.token = token;
+ this.aesKey = aesKey;
+ }
+
+ public String getWeixinId() {
+ return weixinId;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public String getAesKey() {
+ return aesKey;
+ }
+
+ @Override
+ public String toString() {
+ return "AesToken [weixinId=" + weixinId + ", token=" + token
+ + ", aesKey=" + aesKey + "]";
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/ClassUtil.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/ClassUtil.java
new file mode 100644
index 00000000..f82ce601
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/ClassUtil.java
@@ -0,0 +1,212 @@
+package com.zone.weixin4j.util;
+
+import com.zone.weixin4j.exception.WeixinException;
+
+import java.io.*;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.net.JarURLConnection;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/**
+ * 对class的获取
+ *
+ * @className ClassUtil
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年10月31日
+ * @since JDK 1.6
+ * @see
+ */
+public final class ClassUtil {
+ private final static String POINT = ".";
+ private final static String CLASS = ".class";
+
+ /**
+ * 获取某个包下所有的class信息
+ *
+ * @param packageName
+ * 包名
+ * @return
+ */
+ public static List> getClasses(String packageName)
+ throws WeixinException {
+ String packageFileName = packageName.replace(POINT, File.separator);
+ URL fullPath = getDefaultClassLoader().getResource(packageFileName);
+ String protocol = fullPath.getProtocol();
+ if (protocol.equals(ServerToolkits.PROTOCOL_FILE)) {
+ try {
+ File dir = new File(fullPath.toURI());
+ return findClassesByFile(dir, packageName);
+ } catch (URISyntaxException e) {
+ throw new WeixinException(e);
+ }
+ } else if (protocol.equals(ServerToolkits.PROTOCOL_JAR)) {
+ try {
+ return findClassesByJar(
+ ((JarURLConnection) fullPath.openConnection())
+ .getJarFile(),
+ packageName);
+ } catch (IOException e) {
+ throw new WeixinException(e);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 扫描目录下所有的class对象
+ *
+ * @param dir
+ * 文件目录
+ * @param packageName
+ * 包的全限类名
+ * @return
+ */
+ private static List> findClassesByFile(File dir, String packageName) {
+ List> classes = new ArrayList>();
+ File[] files = dir.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File file, String name) {
+ return file.isDirectory() || file.getName().endsWith(CLASS);
+ }
+ });
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ classes.addAll(findClassesByFile(file, packageName + POINT
+ + file.getName()));
+ } else {
+ try {
+ classes.add(Class.forName(packageName + POINT
+ + file.getName().replace(CLASS, "")));
+ } catch (ClassNotFoundException e) {
+ ;
+ }
+ }
+ }
+ }
+ return classes;
+ }
+
+ /**
+ * 扫描jar包下所有的class对象
+ *
+ * @param jar
+ * jar包对象
+ * @param packageName
+ * 包的全限类名
+ * @return
+ */
+ private static List> findClassesByJar(JarFile jar,
+ String packageName) {
+ List> classes = new ArrayList>();
+ Enumeration jarEntries = jar.entries();
+ while (jarEntries.hasMoreElements()) {
+ JarEntry jarEntry = jarEntries.nextElement();
+ if (jarEntry.isDirectory()) {
+ continue;
+ }
+ String className = jarEntry.getName()
+ .replace(File.separator, POINT);
+ if (!className.startsWith(packageName)
+ || !className.endsWith(CLASS)) {
+ continue;
+ }
+ try {
+ classes.add(Class.forName(className.replace(CLASS, "")));
+ } catch (ClassNotFoundException e) {
+ ;
+ }
+ }
+ return classes;
+ }
+
+ public static Object deepClone(Object obj) throws WeixinException {
+ ByteArrayOutputStream bos = null;
+ ObjectOutputStream oos = null;
+ ByteArrayInputStream bis = null;
+ ObjectInputStream ois = null;
+ try {
+ bos = new ByteArrayOutputStream();
+ oos = new ObjectOutputStream(bos);
+ oos.writeObject(obj);
+ bis = new ByteArrayInputStream(bos.toByteArray());
+ ois = new ObjectInputStream(bis);
+ return ois.readObject();
+ } catch (IOException e) {
+ throw new WeixinException(e);
+ } catch (ClassNotFoundException e) {
+ throw new WeixinException(e);
+ } finally {
+ try {
+ if (bos != null) {
+ bos.close();
+ }
+ if (oos != null) {
+ oos.close();
+ }
+ if (bis != null) {
+ bis.close();
+ }
+ if (ois != null) {
+ ois.close();
+ }
+ } catch (IOException e) {
+ ;// ignore
+ }
+ }
+ }
+
+ /**
+ * 获得泛型类型
+ *
+ * @param object
+ * @return
+ */
+ public static Class> getGenericType(Class> clazz) {
+ if(clazz == Object.class){
+ return null;
+ }
+ Type type = clazz.getGenericSuperclass();
+ if (type instanceof ParameterizedType) {
+ ParameterizedType ptype = ((ParameterizedType) type);
+ Type[] args = ptype.getActualTypeArguments();
+ return (Class>) args[0];
+ }
+ return getGenericType(clazz.getSuperclass());
+ }
+
+ public static ClassLoader getDefaultClassLoader() {
+ ClassLoader cl = null;
+ try {
+ cl = Thread.currentThread().getContextClassLoader();
+ } catch (Throwable ex) {
+ // Cannot access thread context ClassLoader - falling back...
+ }
+ if (cl == null) {
+ // No thread context class loader -> use class loader of this class.
+ cl = ClassUtil.class.getClassLoader();
+ if (cl == null) {
+ // getClassLoader() returning null indicates the bootstrap
+ // ClassLoader
+ try {
+ cl = ClassLoader.getSystemClassLoader();
+ } catch (Throwable ex) {
+ // Cannot access system ClassLoader - oh well, maybe the
+ // caller can live with null...
+ }
+ }
+ }
+ return cl;
+ }
+
+ public static void main(String[] args) throws WeixinException {
+ System.err.println(getClasses("com.foxinmy.weixin4j.qy.event"));
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/HexUtil.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/HexUtil.java
new file mode 100644
index 00000000..a607c28c
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/HexUtil.java
@@ -0,0 +1,43 @@
+package com.zone.weixin4j.util;
+
+/**
+ * Hex工具类
+ *
+ * @className HexUtil
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月17日
+ * @since JDK 1.6
+ * @see
+ */
+public final class HexUtil {
+ /**
+ * Used to build output as Hex
+ */
+ private static final char[] DIGITS_LOWER = { '0', '1', '2', '3', '4', '5',
+ '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+ /**
+ * Used to build output as Hex
+ */
+ private static final char[] DIGITS_UPPER = { '0', '1', '2', '3', '4', '5',
+ '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+
+ private static char[] encodeHex(final byte[] data, final char[] toDigits) {
+ final int l = data.length;
+ final char[] out = new char[l << 1];
+ // two characters form the hex value.
+ for (int i = 0, j = 0; i < l; i++) {
+ out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
+ out[j++] = toDigits[0x0F & data[i]];
+ }
+ return out;
+ }
+
+ public static String encodeHexString(final byte[] data) {
+ return new String(encodeHex(data, true));
+ }
+
+ public static char[] encodeHex(final byte[] data, final boolean toLowerCase) {
+ return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/MessageUtil.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/MessageUtil.java
new file mode 100644
index 00000000..6b8e0279
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/MessageUtil.java
@@ -0,0 +1,167 @@
+package com.zone.weixin4j.util;
+
+import com.zone.weixin4j.base64.Base64;
+import com.zone.weixin4j.exception.WeixinException;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.util.Arrays;
+
+/**
+ * 消息工具类
+ *
+ * @className MessageUtil
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2014年10月31日
+ * @since JDK 1.6
+ * @see
+ */
+public final class MessageUtil {
+ /**
+ * 验证微信签名
+ *
+ * @param signature
+ * 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数
+ * @return 开发者通过检验signature对请求进行相关校验。若确认此次GET请求来自微信服务器
+ * 请原样返回echostr参数内容,则接入生效 成为开发者成功,否则接入失败
+ * @see 接入指南
+ */
+ public static String signature(String... para) {
+ Arrays.sort(para);
+ StringBuffer sb = new StringBuffer();
+ for (String str : para) {
+ sb.append(str);
+ }
+ return ServerToolkits.digestSHA1(sb.toString());
+ }
+
+ /**
+ * 对xml消息加密
+ *
+ * @param appId
+ * 应用ID
+ * @param encodingAesKey
+ * 加密密钥
+ * @param xmlContent
+ * 原始消息体
+ * @return aes加密后的消息体
+ * @throws WeixinException
+ */
+ public static String aesEncrypt(String appId, String encodingAesKey,
+ String xmlContent) throws WeixinException {
+ /**
+ * 其中,msg_encrypt=Base64_Encode(AES_Encrypt [random(16B)+ msg_len(4B) +
+ * msg + $AppId])
+ *
+ * random(16B)为16字节的随机字符串;msg_len为msg长度,占4个字节(网络字节序),$AppId为公众账号的AppId
+ */
+ byte[] randomBytes = ServerToolkits.getBytesUtf8(ServerToolkits
+ .generateRandomString(16));
+ byte[] xmlBytes = ServerToolkits.getBytesUtf8(xmlContent);
+ int xmlLength = xmlBytes.length;
+ byte[] orderBytes = new byte[4];
+ orderBytes[3] = (byte) (xmlLength & 0xFF);
+ orderBytes[2] = (byte) (xmlLength >> 8 & 0xFF);
+ orderBytes[1] = (byte) (xmlLength >> 16 & 0xFF);
+ orderBytes[0] = (byte) (xmlLength >> 24 & 0xFF);
+ byte[] appidBytes = ServerToolkits.getBytesUtf8(appId);
+
+ int byteLength = randomBytes.length + xmlLength + orderBytes.length
+ + appidBytes.length;
+ // ... + pad: 使用自定义的填充方式对明文进行补位填充
+ byte[] padBytes = PKCS7Encoder.encode(byteLength);
+ // random + endian + xml + appid + pad 获得最终的字节流
+ byte[] unencrypted = new byte[byteLength + padBytes.length];
+ byteLength = 0;
+ // src:源数组;srcPos:源数组要复制的起始位置;dest:目的数组;destPos:目的数组放置的起始位置;length:复制的长度
+ System.arraycopy(randomBytes, 0, unencrypted, byteLength,
+ randomBytes.length);
+ byteLength += randomBytes.length;
+ System.arraycopy(orderBytes, 0, unencrypted, byteLength,
+ orderBytes.length);
+ byteLength += orderBytes.length;
+ System.arraycopy(xmlBytes, 0, unencrypted, byteLength, xmlBytes.length);
+ byteLength += xmlBytes.length;
+ System.arraycopy(appidBytes, 0, unencrypted, byteLength,
+ appidBytes.length);
+ byteLength += appidBytes.length;
+ System.arraycopy(padBytes, 0, unencrypted, byteLength, padBytes.length);
+ try {
+ byte[] aesKey = Base64.decodeBase64(encodingAesKey + "=");
+ // 设置加密模式为AES的CBC模式
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ SecretKeySpec keySpec = new SecretKeySpec(aesKey, ServerToolkits.AES);
+ IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
+ // 加密
+ byte[] encrypted = cipher.doFinal(unencrypted);
+ // 使用BASE64对加密后的字符串进行编码
+ // return Base64.encodeBase64String(encrypted);
+ return Base64
+ .encodeBase64String(encrypted);
+ } catch (Exception e) {
+ throw new WeixinException("-40006", "AES加密失败:" + e.getMessage());
+ }
+ }
+
+ /**
+ * 对AES消息解密
+ *
+ * @param appId
+ * @param encodingAesKey
+ * aes加密的密钥
+ * @param encryptContent
+ * 加密的消息体
+ * @return 解密后的字符
+ * @throws WeixinException
+ */
+ public static String aesDecrypt(String appId, String encodingAesKey,
+ String encryptContent) throws WeixinException {
+ byte[] aesKey = Base64.decodeBase64(encodingAesKey + "=");
+ byte[] original;
+ try {
+ // 设置解密模式为AES的CBC模式
+ Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
+ SecretKeySpec key_spec = new SecretKeySpec(aesKey, ServerToolkits.AES);
+ IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey,
+ 0, 16));
+ cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);
+ // 使用BASE64对密文进行解码
+ byte[] encrypted = Base64.decodeBase64(encryptContent);
+ // 解密
+ original = cipher.doFinal(encrypted);
+ } catch (Exception e) {
+ throw new WeixinException("-40007", "AES解密失败:" + e.getMessage());
+ }
+ String xmlContent, fromAppId;
+ try {
+ // 去除补位字符
+ byte[] bytes = PKCS7Encoder.decode(original);
+ /**
+ * AES加密的buf由16个字节的随机字符串、4个字节的msg_len(网络字节序)、msg和$AppId组成,
+ * 其中msg_len为msg的长度,$AppId为公众帐号的AppId
+ */
+ // 获取表示xml长度的字节数组
+ byte[] lengthByte = Arrays.copyOfRange(bytes, 16, 20);
+ // 获取xml消息主体的长度(byte[]2int)
+ // http://my.oschina.net/u/169390/blog/97495
+ int xmlLength = lengthByte[3] & 0xff | (lengthByte[2] & 0xff) << 8
+ | (lengthByte[1] & 0xff) << 16
+ | (lengthByte[0] & 0xff) << 24;
+ xmlContent = ServerToolkits.newStringUtf8(Arrays.copyOfRange(bytes, 20,
+ 20 + xmlLength));
+ fromAppId = ServerToolkits.newStringUtf8(Arrays.copyOfRange(bytes,
+ 20 + xmlLength, bytes.length));
+ } catch (Exception e) {
+ throw new WeixinException("-40008", "xml内容不合法:" + e.getMessage());
+ }
+ // 校验appId是否一致
+ if (appId != null && !fromAppId.trim().equals(appId)) {
+ throw new WeixinException("-40005", "校验AppID失败,expect " + appId
+ + ",but actual is " + fromAppId);
+ }
+ return xmlContent;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/PKCS7Encoder.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/PKCS7Encoder.java
new file mode 100644
index 00000000..a9000a1a
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/PKCS7Encoder.java
@@ -0,0 +1,70 @@
+/**
+ * 对公众平台发送给公众账号的消息加解密示例代码.
+ *
+ * @copyright Copyright (c) 1998-2014 Tencent Inc.
+ */
+
+// ------------------------------------------------------------------------
+
+package com.zone.weixin4j.util;
+
+import java.util.Arrays;
+
+/**
+ * 提供基于PKCS7算法的加解密接口
+ * 提供接收和推送给公众平台消息的加解密接口(UTF8编码的字符串).
+ *
+ * - 第三方回复加密消息给公众平台
+ * - 第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。
+ *
+ * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
+ *
+ * - 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:
+ * http://www.oracle.com/technetwork/java/javase
+ * /downloads/jce-7-download-432124.html
+ * - 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
+ * - 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
+ * - 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
+ *
+ */
+public class PKCS7Encoder {
+ private final static int BLOCK_SIZE = 32;
+
+ /**
+ * 获得对明文进行补位填充的字节.
+ *
+ * @param count
+ * 需要进行填充补位操作的明文字节个数
+ * @return 补齐用的字节数组
+ */
+ public static byte[] encode(int count) {
+ // 计算需要填充的位数
+ int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
+ if (amountToPad == 0) {
+ amountToPad = BLOCK_SIZE;
+ }
+ // 获得补位所用的字符
+ byte target = (byte) (amountToPad & 0xFF);
+ char padChr = (char) target;
+ StringBuilder tmp = new StringBuilder();
+ for (int index = 0; index < amountToPad; index++) {
+ tmp.append(padChr);
+ }
+ return tmp.toString().getBytes(ServerToolkits.UTF_8);
+ }
+
+ /**
+ * 删除解密后明文的补位字符
+ *
+ * @param decrypted
+ * 解密后的明文
+ * @return 删除补位字符后的明文
+ */
+ public static byte[] decode(byte[] decrypted) {
+ int pad = (int) decrypted[decrypted.length - 1];
+ if (pad < 1 || pad > 32) {
+ pad = 0;
+ }
+ return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
+ }
+}
\ No newline at end of file
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/ServerToolkits.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/ServerToolkits.java
new file mode 100644
index 00000000..cfd5a8d0
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/util/ServerToolkits.java
@@ -0,0 +1,105 @@
+package com.zone.weixin4j.util;
+
+import com.zone.weixin4j.socket.WeixinMessageTransfer;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Random;
+
+/**
+ * 工具包
+ *
+ * @className ServerToolkits
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年12月26日
+ * @since JDK 1.7
+ * @see
+ */
+public final class ServerToolkits {
+ private static final String ALLCHAR = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ public static final Charset UTF_8 = Charset.forName("UTF-8");
+ public static final String AES = "AES";
+ public static final String SHA1 = "SHA-1";
+ public static final String PROTOCOL_FILE = "file";
+ public static final String PROTOCOL_JAR = "jar";
+ public static final String CONTENTTYPE$APPLICATION_XML = "application/xml";
+ public static final String CONTENTTYPE$TEXT_PLAIN = "text/plain";
+
+ /**
+ * 返回一个定长的随机字符串(包含数字和大小写字母)
+ *
+ * @param length
+ * 随机数的长度
+ * @return
+ */
+ public static String generateRandomString(int length) {
+ StringBuilder sb = new StringBuilder(length);
+ Random random = new Random();
+ for (int i = 0; i < length; i++) {
+ sb.append(ALLCHAR.charAt(random.nextInt(ALLCHAR.length())));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 构造器设置为可见
+ *
+ * @param ctor
+ */
+ public static void makeConstructorAccessible(Constructor> ctor) {
+ if ((!Modifier.isPublic(ctor.getModifiers()) || !Modifier.isPublic(ctor
+ .getDeclaringClass().getModifiers())) && !ctor.isAccessible()) {
+ ctor.setAccessible(true);
+ }
+ }
+
+ /**
+ * SHA1签名
+ *
+ * @param content
+ * 待签名字符串
+ * @return 签名后的字符串
+ */
+ public static String digestSHA1(String content) {
+ byte[] data = ServerToolkits.getBytesUtf8(content);
+ try {
+ return HexUtil.encodeHexString(MessageDigest.getInstance(SHA1)
+ .digest(data));
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ }
+
+ private static String newString(final byte[] bytes, final Charset charset) {
+ return bytes == null ? null : new String(bytes, charset);
+ }
+
+ public static byte[] getBytesUtf8(final String content) {
+ return content != null ? content.getBytes(UTF_8) : null;
+ }
+
+ public static String newStringUtf8(final byte[] bytes) {
+ return newString(bytes, UTF_8);
+ }
+
+ /**
+ * 判断字符串是否为空
+ *
+ * @param cs
+ * @return
+ */
+ public static boolean isBlank(final CharSequence cs) {
+ int strLen;
+ if (cs == null || (strLen = cs.length()) == 0) {
+ return true;
+ }
+ for (int i = 0; i < strLen; i++) {
+ if (Character.isWhitespace(cs.charAt(i)) == false) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/xml/EncryptMessageHandler.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/xml/EncryptMessageHandler.java
new file mode 100644
index 00000000..9a6a0b49
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/xml/EncryptMessageHandler.java
@@ -0,0 +1,81 @@
+package com.zone.weixin4j.xml;
+
+import com.zone.weixin4j.util.ServerToolkits;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+/**
+ * 获取加密的密文内容
+ *
+ * @className EncryptMessageHandler
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月17日
+ * @since JDK 1.6
+ * @see
+ */
+public class EncryptMessageHandler extends DefaultHandler {
+
+ private String toUserName;
+ private String encryptContent;
+ private String content;
+
+ @Override
+ public void startDocument() throws SAXException {
+ toUserName = null;
+ encryptContent = null;
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName,
+ Attributes attributes) throws SAXException {
+
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName)
+ throws SAXException {
+ if (localName.equalsIgnoreCase("encrypt")) {
+ encryptContent = content;
+ } else if (localName.equalsIgnoreCase("tousername")) {
+ toUserName = content;
+ }
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length)
+ throws SAXException {
+ this.content = new String(ch, start, length);
+ }
+
+ public String getToUserName() {
+ return toUserName;
+ }
+
+ public String getEncryptContent() {
+ return encryptContent;
+ }
+
+ private final static EncryptMessageHandler global = new EncryptMessageHandler();
+
+ public static EncryptMessageHandler parser(String xmlContent)
+ throws RuntimeException {
+ try {
+ XMLReader xmlReader = XMLReaderFactory.createXMLReader();
+ xmlReader.setContentHandler(global);
+ xmlReader.parse(new InputSource(new ByteArrayInputStream(xmlContent
+ .getBytes(ServerToolkits.UTF_8))));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } catch (SAXException e) {
+ throw new RuntimeException(e);
+ }
+ return global;
+ }
+}
diff --git a/weixin4j-serverX/src/main/java/com/zone/weixin4j/xml/MessageTransferHandler.java b/weixin4j-serverX/src/main/java/com/zone/weixin4j/xml/MessageTransferHandler.java
new file mode 100644
index 00000000..54d04a47
--- /dev/null
+++ b/weixin4j-serverX/src/main/java/com/zone/weixin4j/xml/MessageTransferHandler.java
@@ -0,0 +1,104 @@
+package com.zone.weixin4j.xml;
+
+import com.zone.weixin4j.request.WeixinRequest;
+import com.zone.weixin4j.socket.WeixinMessageTransfer;
+import com.zone.weixin4j.type.AccountType;
+import com.zone.weixin4j.util.ServerToolkits;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * 微信消息
+ *
+ * @className MessageTransferHandler
+ * @author jinyu(foxinmy@gmail.com)
+ * @date 2015年5月17日
+ * @since JDK 1.6
+ * @see
+ */
+public class MessageTransferHandler extends DefaultHandler {
+
+ private String fromUserName;
+ private String toUserName;
+ private String msgType;
+ private String eventType;
+ private boolean isQY;
+ private Set nodeNames;
+
+ private String content;
+
+ @Override
+ public void startDocument() throws SAXException {
+ fromUserName = null;
+ toUserName = null;
+ msgType = null;
+ eventType = null;
+ isQY = false;
+ nodeNames = new HashSet();
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName)
+ throws SAXException {
+ nodeNames.add(localName);
+ localName = localName.toLowerCase();
+ if (localName.equals("fromusername")) {
+ fromUserName = content;
+ } else if (localName.equals("tousername")) {
+ toUserName = content;
+ } else if (localName.equals("msgtype")) {
+ msgType = content.toLowerCase();
+ } else if (localName.equals("event")) {
+ eventType = content.toLowerCase();
+ } else if (localName.startsWith("agent") // 应用信息
+ || localName.startsWith("suite") // 套件信息
+ || localName.equals("batchjob")) { // 批量任务
+ isQY = true;
+ }
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length)
+ throws SAXException {
+ this.content = new String(ch, start, length);
+ }
+
+ private AccountType getAccountType() {
+ if (isQY) {
+ return AccountType.QY;
+ }
+ if (ServerToolkits.isBlank(msgType)
+ && ServerToolkits.isBlank(eventType)) {
+ return null;
+ }
+ return AccountType.MP;
+ }
+
+ private static MessageTransferHandler global = new MessageTransferHandler();
+
+ public static WeixinMessageTransfer parser(WeixinRequest request)
+ throws RuntimeException {
+ try {
+ XMLReader xmlReader = XMLReaderFactory.createXMLReader();
+ xmlReader.setContentHandler(global);
+ xmlReader.parse(new InputSource(new ByteArrayInputStream(request
+ .getOriginalContent().getBytes(ServerToolkits.UTF_8))));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } catch (SAXException e) {
+ throw new RuntimeException(e);
+ }
+ return new WeixinMessageTransfer(request.getAesToken(),
+ request.getEncryptType(), global.toUserName,
+ global.fromUserName, global.getAccountType(), global.msgType,
+ global.eventType, global.nodeNames);
+ }
+}