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: + *

+ *

+ *

+ * 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 match(WeixinMessageKey messageKey) { + return messageClassMap.get(messageKey); + } + + @Override + public void regist(WeixinMessageKey messageKey, + Class 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 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 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 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 match(WeixinMessageKey messageKey); + + /** + * 注册消息类型「程序没有及时更新而微信又产生了新的消息类型」 + * + * @param messageKey + * 消息key + * @param messageClass + * 消息类型 + */ + public void regist(WeixinMessageKey messageKey, + Class 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... 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 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("<![CDATA[%s]]>", + 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("<![CDATA[%s]]>", + 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编码的字符串). + *
    + *
  1. 第三方回复加密消息给公众平台
  2. + *
  3. 第三方收到公众平台发送的消息,验证消息的安全性,并对消息进行解密。
  4. + *
+ * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案 + *
    + *
  1. 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址: + * http://www.oracle.com/technetwork/java/javase + * /downloads/jce-7-download-432124.html
  2. + *
  3. 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
  4. + *
  5. 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
  6. + *
  7. 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
  8. + *
+ */ +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); + } +}