Java企业微信开发_05_消息推送之被动回复消息
一、本节要点
1.消息的加解密
微信加解密包 下载地址:http://qydev.weixin.qq.com/java.zip ,此包中封装好了AES加解密方法,直接调用方法即可。
其中,解密方法为:
//2.获取消息明文:对加密的请求消息进行解密获得明文 WXBizMsgCrypt wxcpt=new WXBizMsgCrypt(WeiXinParamesUtil.token,WeiXinParamesUtil.encodingAESKey,WeiXinParamesUtil.corpId); result = wxcpt.DecryptMsg(msg_signature, timestamp, nonce, postData);
加密方法为:
//6.加密 WXBizMsgCrypt wxcpt=new WXBizMsgCrypt(WeiXinParamesUtil.token,WeiXinParamesUtil.encodingAESKey,WeiXinParamesUtil.corpId); respMessage = wxcpt.EncryptMsg(respMessage, timestamp, msgType);
2.被动回复消息的流程
用户发送消息之后,微信服务器将消息传递给 第三方服务器,第三方服务器接收到消息后,再对消息做出相应的回复消息。
接收消息:需先从request请求对象的输入流中获取请求参数和已加密的请求消息,再对已加密的请求消息进行解密操作,即可获得明文。
然后就行对明文消息的业务处理了。
回复消息:封装好回复消息后,需先对回复消息进行加密,获得已已加密消息,然后再通过http请求调用被动回复消息的接口,来发送消息。
二、接收消息服务器配置
接受消息服务器配置好后,用户发送消息时,微信服务器会将消息转发到配置的接受消息服务器url上,即以POST方式转发到 CoreServlet 上。
三、接收消息实体类的封装(req)
参见官方文档的说明
3.1 消息基类——BaseMessage
package com.ray.pojo.message.req; /** * 消息基类(普通用户 -> 企业微信) * @author shirayner * */ public class BaseMessage { // 开发者微信号 private String ToUserName; // 发送方帐号(一个OpenID) private String FromUserName; // 消息创建时间 (整型) private long CreateTime; // 消息类型(text/image/location/link) private String MsgType; // 消息id,64位整型 private long MsgId; //企业应用的id,整型。可在应用的设置页面查看 private int AgentID; public String getToUserName() { return ToUserName; } public void setToUserName(String toUserName) { ToUserName = toUserName; } public String getFromUserName() { return FromUserName; } public void setFromUserName(String fromUserName) { FromUserName = fromUserName; } public long getCreateTime() { return CreateTime; } public void setCreateTime(long createTime) { CreateTime = createTime; } public String getMsgType() { return MsgType; } public void setMsgType(String msgType) { MsgType = msgType; } public long getMsgId() { return MsgId; } public void setMsgId(long msgId) { MsgId = msgId; } public int getAgentID() { return AgentID; } public void setAgentID(int agentID) { AgentID = agentID; } }
3.2 文本消息——TextMessage
package com.ray.pojo.message.req; /** * 文本消息 * @author shirayner * */ public class TextMessage extends BaseMessage { // 消息内容 private String Content; public String getContent() { return Content; } public void setContent(String content) { Content = content; } }
3.3图片消息——ImageMessage
package com.ray.pojo.message.req; /** * 图片消息 * @author shirayner * */ public class ImageMessage extends BaseMessage { // 图片链接 private String PicUrl; // 图片媒体文件id,可以调用获取媒体文件接口拉取 private String MediaId; public String getPicUrl() { return PicUrl; } public void setPicUrl(String picUrl) { PicUrl = picUrl; } public String getMediaId() { return MediaId; } public void setMediaId(String mediaId) { MediaId = mediaId; } }
3.4 语音消息——VoiceMessage
package com.ray.pojo.message.req; /** * 语音消息 * @author shirayner * */ public class VoiceMessage extends BaseMessage { // 语音媒体文件id,可以调用获取媒体文件接口拉取数据 private String MediaId; // 语音格式,如amr,speex等 private String Format; public String getMediaId() { return MediaId; } public void setMediaId(String mediaId) { MediaId = mediaId; } public String getFormat() { return Format; } public void setFormat(String format) { Format = format; } }
3.5 视频消息——Video、VideoMessage
Video.java
package com.ray.pojo.message.resp; /** * @desc : 视频 * * @author: shirayner * @date : 2017-8-17 下午2:00:22 */ public class Video { //视频文件id,可以调用获取媒体文件接口拉取 private String MediaId; //视频消息的标题 private String Title; //视频消息的描述 private String Description; public String getMediaId() { return MediaId; } public void setMediaId(String mediaId) { MediaId = mediaId; } public String getTitle() { return Title; } public void setTitle(String title) { Title = title; } public String getDescription() { return Description; } public void setDescription(String description) { Description = description; } }
VideoMessage.java
package com.ray.pojo.message.resp; /** * @desc : 视频消息 * * @author: shirayner * @date : 2017-8-21 上午10:36:33 */ public class VideoMessage extends BaseMessage { // 视频 private Video Video; public Video getVideo() { return Video; } public void setVideo(Video video) { Video = video; } }
3.6 位置消息——LocationMessage
package com.ray.pojo.message.req; /** * 位置消息 * @author shirayner * */ public class LocationMessage extends BaseMessage { // 地理位置维度 private String Location_X; // 地理位置经度 private String Location_Y; // 地图缩放大小 private String Scale; // 地理位置信息 private String Label; public String getLocation_X() { return Location_X; } public void setLocation_X(String location_X) { Location_X = location_X; } public String getLocation_Y() { return Location_Y; } public void setLocation_Y(String location_Y) { Location_Y = location_Y; } public String getScale() { return Scale; } public void setScale(String scale) { Scale = scale; } public String getLabel() { return Label; } public void setLabel(String label) { Label = label; } }
3.7 链接消息——LinkMessage
package com.ray.pojo.message.req; /** * 链接消息 * @author shirayner * */ public class LinkMessage extends BaseMessage { // 消息标题 private String Title; // 消息描述 private String Description; // 封面缩略图的url private String PicUrl; public String getTitle() { return Title; } public void setTitle(String title) { Title = title; } public String getDescription() { return Description; } public void setDescription(String description) { Description = description; } public String getPicUrl() { return PicUrl; } public void setPicUrl(String picUrl) { PicUrl = picUrl; } }
四、被动回复消息的封装(resp)
4.1 消息基类——BaseMessage
package com.ray.pojo.message.resp; /** * 消息基类(企业微信 -> 普通用户) * @author shirayner * */ public class BaseMessage { // 成员UserID private String ToUserName; // 企业微信CorpID private String FromUserName; // 消息创建时间 (整型) private long CreateTime; // 消息类型 private String MsgType; public String getToUserName() { return ToUserName; } public void setToUserName(String toUserName) { ToUserName = toUserName; } public String getFromUserName() { return FromUserName; } public void setFromUserName(String fromUserName) { FromUserName = fromUserName; } public long getCreateTime() { return CreateTime; } public void setCreateTime(long createTime) { CreateTime = createTime; } public String getMsgType() { return MsgType; } public void setMsgType(String msgType) { MsgType = msgType; } }
4.2 文本消息——TextMessage
package com.ray.pojo.message.resp; /** * 文本消息 * @author shirayner * */ public class TextMessage extends BaseMessage { // 回复的消息内容 private String Content; public String getContent() { return Content; } public void setContent(String content) { Content = content; } }
4.3 图片类、语音类——Media
package com.ray.pojo.message.resp; /** * @desc : 图片、语音 * * @author: shirayner * @date : 2017-8-17 下午1:52:19 */ public class Media { private String MediaId; public String getMediaId() { return MediaId; } public void setMediaId(String mediaId) { MediaId = mediaId; } }
4.3.1 图片消息——ImageMessage
package com.ray.pojo.message.resp; /** * @desc : 图片消息 * * @author: shirayner * @date : 2017-8-17 下午1:53:28 */ public class ImageMessage extends BaseMessage { private Media Image; public Media getImage() { return Image; } public void setImage(Media image) { Image = image; } }
4.3.2 语音消息——VoiceMessage
package com.ray.pojo.message.resp; /** * @desc : 语音消息 * * @author: shirayner * @date : 2017-8-17 下午1:57:42 */ public class VoiceMessage extends BaseMessage { // 语音 private Media Voice; public Media getVoice() { return Voice; } public void setVoice(Media voice) { Voice = voice; } }
4.4 视频消息——VoiceMessage
package com.ray.pojo.message.resp; /** * @desc : 语音消息 * * @author: shirayner * @date : 2017-8-17 下午1:57:42 */ public class VoiceMessage extends BaseMessage { // 语音 private Media Voice; public Media getVoice() { return Voice; } public void setVoice(Media voice) { Voice = voice; } }
4.5 图文消息——Article、NewsMessage
Article.java
package com.ray.pojo.message.resp; /** * @desc : 图文 * * @author: shirayner * @date : 2017-8-17 下午2:02:33 */ public class Article { // 图文消息名称 private String Title; // 图文消息描述 private String Description; // 图片链接,支持JPG、PNG格式,较好的效果为大图640*320,小图80*80 private String PicUrl; // 点击图文消息跳转链接 private String Url; public String getTitle() { return Title; } public void setTitle(String title) { Title = title; } public String getDescription() { return null == Description ? "" : Description; } public void setDescription(String description) { Description = description; } public String getPicUrl() { return null == PicUrl ? "" : PicUrl; } public void setPicUrl(String picUrl) { PicUrl = picUrl; } public String getUrl() { return null == Url ? "" : Url; } public void setUrl(String url) { Url = url; } }
NewsMessage.java
package com.ray.pojo.message.resp; import java.util.List; /** * @desc : 图文消息 * * @author: shirayner * @date : 2017-8-17 下午2:03:31 */ public class NewsMessage extends BaseMessage { // 图文消息个数,限制为10条以内 private int ArticleCount; // 多条图文消息信息,默认第一个item为大图 private List<Article> Articles; public int getArticleCount() { return ArticleCount; } public void setArticleCount(int articleCount) { ArticleCount = articleCount; } public List<Article> getArticles() { return Articles; } public void setArticles(List<Article> articles) { Articles = articles; } }
五、消息工具类——MessageUtil
package com.ray.util; import java.io.InputStream; import java.io.Writer; import java.util.HashMap; import java.util.List; import java.util.Map; import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.io.SAXReader; import com.ray.pojo.message.resp.Article; import com.ray.pojo.message.resp.NewsMessage; import com.ray.pojo.message.resp.TextMessage; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.core.util.QuickWriter; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.xml.PrettyPrintWriter; import com.thoughtworks.xstream.io.xml.XppDriver; /** * 消息工具类 * @author shirayner * */ public class MessageUtil { //返回消息类型:文本 public static final String RESP_MESSAGE_TYPE_TEXT = "text"; //返回消息类型:音乐 public static final String RESP_MESSAGE_TYPE_MUSIC = "music"; //返回消息类型:图文 public static final String RESP_MESSAGE_TYPE_NEWS = "news"; //请求消息类型:文本 public static final String REQ_MESSAGE_TYPE_TEXT = "text"; //请求消息类型:图片 public static final String REQ_MESSAGE_TYPE_IMAGE = "image"; //请求消息类型:链接 public static final String REQ_MESSAGE_TYPE_LINK = "link"; //请求消息类型:地理位置 public static final String REQ_MESSAGE_TYPE_LOCATION = "location"; //请求消息类型:音频 public static final String REQ_MESSAGE_TYPE_VOICE = "voice"; //请求消息类型:推送 public static final String REQ_MESSAGE_TYPE_EVENT = "event"; //事件类型:subscribe(订阅) public static final String EVENT_TYPE_SUBSCRIBE = "subscribe"; //事件类型:unsubscribe(取消订阅) public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe"; //事件类型:CLICK(自定义菜单点击事件) public static final String EVENT_TYPE_CLICK = "click"; /** * @desc :1.解析微信发来的请求(XML),获取请求参数 * * @param request * @return * @throws Exception Map<String,String> */ public static Map<String, String> parseXml(HttpServletRequest request) throws Exception { // 将解析结果存储在HashMap中 Map<String, String> map = new HashMap<String, String>(); // 从request中取得输入流 InputStream inputStream = request.getInputStream(); // 读取输入流 SAXReader reader = new SAXReader(); Document document = reader.read(inputStream); // 得到xml根元素 Element root = document.getRootElement(); // 得到根元素的所有子节点 List<Element> elementList = root.elements(); // 遍历所有子节点 for (Element e : elementList) map.put(e.getName(), e.getText()); // 释放资源 inputStream.close(); inputStream = null; return map; } /** * @desc :2.解析微信发来的请求(xmlStr),获取请求参数 * * @param xmlStr * @return * @throws Exception Map<String,String> */ public static Map<String, String> parseXml(String xmlStr) throws Exception { // 将解析结果存储在HashMap中 Map<String, String> map = new HashMap<String, String>(); //1.将字符串转为Document Document document = DocumentHelper.parseText(xmlStr); //2.获取根元素的所有子节点 // 得到xml根元素 Element root = document.getRootElement(); // 得到根元素的所有子节点 List<Element> elementList = root.elements(); //3.遍历所有子节点 for (Element e : elementList) map.put(e.getName(), e.getText()); return map; } /** * 2.文本消息对象转换成xml * * @param textMessage 文本消息对象 * @return xml */ public static String textMessageToXml(TextMessage textMessage) { xstream.alias("xml", textMessage.getClass()); return xstream.toXML(textMessage); } /** * 音乐消息对象转换成xml * * @param musicMessage 音乐消息对象 * @return xml */ /* public static String musicMessageToXml(MusicMessage musicMessage) { xstream.alias("xml", musicMessage.getClass()); return xstream.toXML(musicMessage); } */ /** * 图文消息对象转换成xml * * @param newsMessage 图文消息对象 * @return xml */ public static String newsMessageToXml(NewsMessage newsMessage) { xstream.alias("xml", newsMessage.getClass()); xstream.alias("item", new Article().getClass()); return xstream.toXML(newsMessage); } /** * 扩展xstream,使其支持CDATA块 * * @date 2013-05-19 */ private static XStream xstream = new XStream(new XppDriver() { public HierarchicalStreamWriter createWriter(Writer out) { return new PrettyPrintWriter(out) { // 对所有xml节点的转换都增加CDATA标记 boolean cdata = true; @SuppressWarnings("unchecked") public void startNode(String name, Class clazz) { super.startNode(name, clazz); } protected void writeText(QuickWriter writer, String text) { if (cdata) { writer.write("<![CDATA["); writer.write(text); writer.write("]]>"); } else { writer.write(text); } } }; } }); }
六、消息业务类——MessageService
package com.ray.service; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.Date; import java.util.Map; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import com.qq.weixin.mp.aes.AesException; import com.qq.weixin.mp.aes.WXBizMsgCrypt; import com.ray.pojo.message.resp.TextMessage; import com.ray.util.MessageUtil; import com.ray.util.WeiXinParamesUtil; /** * @desc : 被动回复消息 * * @author: shirayner * @date : 2017-8-17 下午3:37:17 */ public class MessageService { private String msg_signature ; // 微信加密签名 private String timestamp ; // 时间戳 private String nonce ; // 随机数 /** * @desc :获取加密后的回复消息 * * @param request * @return String 返回加密后的回复消息 */ public String getEncryptRespMessage(HttpServletRequest request){ String respMessage = null; try { //1.解密微信发过来的消息 String xmlMsg=this.getDecryptMsg(request); //2.解析微信发来的请求,解析xml字符串 Map<String, String> requestMap= MessageUtil.parseXml(xmlMsg); //3.获取请求参数 //3.1 企业微信CorpID String fromUserName = requestMap.get("FromUserName"); //3.2 成员UserID String toUserName = requestMap.get("ToUserName"); //3.3 消息类型与事件 String msgType = requestMap.get("MsgType"); String eventType = requestMap.get("Event"); String eventKey = requestMap.get("EventKey"); System.out.println("msgType:"+msgType); System.out.println("Event:"+eventType+" eventKey:"+eventKey); //4.组装 回复文本消息 TextMessage textMessage = new TextMessage(); textMessage.setToUserName(fromUserName); textMessage.setFromUserName(toUserName); textMessage.setCreateTime(new Date().getTime()); textMessage.setMsgType(MessageUtil.RESP_MESSAGE_TYPE_TEXT); //4.1.获取回复消息的内容 :消息的分类处理 String respContent=this.getRespContentByMsgType(msgType, eventType, eventKey); textMessage.setContent(respContent); System.out.println("respContent:"+respContent); //5.获取xml字符串: 将(被动回复消息型的)文本消息对象 转成 xml字符串 respMessage = MessageUtil.textMessageToXml(textMessage); //6.加密 WXBizMsgCrypt wxcpt=new WXBizMsgCrypt(WeiXinParamesUtil.token,WeiXinParamesUtil.encodingAESKey,WeiXinParamesUtil.corpId); respMessage = wxcpt.EncryptMsg(respMessage, timestamp, msgType); } catch (Exception e) { e.printStackTrace(); } return respMessage; } /** * @desc :2.从request中获取消息明文 * * @param request * @return String 消息明文 */ public String getDecryptMsg(HttpServletRequest request) { String postData=""; // 密文,对应POST请求的数据 String result=""; // 明文,解密之后的结果 this.msg_signature = request.getParameter("msg_signature"); // 微信加密签名 this.timestamp = request.getParameter("timestamp"); // 时间戳 this.nonce = request.getParameter("nonce"); // 随机数 try { //1.获取加密的请求消息:使用输入流获得加密请求消息postData ServletInputStream in = request.getInputStream(); BufferedReader reader =new BufferedReader(new InputStreamReader(in)); String tempStr=""; //作为输出字符串的临时串,用于判断是否读取完毕 while(null!=(tempStr=reader.readLine())){ postData+=tempStr; } //2.获取消息明文:对加密的请求消息进行解密获得明文 WXBizMsgCrypt wxcpt=new WXBizMsgCrypt(WeiXinParamesUtil.token,WeiXinParamesUtil.encodingAESKey,WeiXinParamesUtil.corpId); result = wxcpt.DecryptMsg(msg_signature, timestamp, nonce, postData); } catch (IOException e) { e.printStackTrace(); } catch (AesException e) { e.printStackTrace(); } return result; } /** * @desc :3.处理消息:根据消息类型获取回复内容 * * @param msgType * @return String */ public String getRespContentByMsgType(String msgType,String eventType,String eventKey){ String respContent=""; //1.文本消息 if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_TEXT)) { respContent = "您发送的是文本消息!"; } //2.图片消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_IMAGE)) { respContent = "您发送的是图片消息!"; } //3.地理位置消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LOCATION)) { System.out.println("消息类型:定位"); } //4.链接消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_LINK)) { respContent = "您发送的是链接消息!"; } //5.音频消息 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_VOICE)) { respContent = "您发送的是音频消息!"; } //6.事件推送 else if (msgType.equals(MessageUtil.REQ_MESSAGE_TYPE_EVENT)) { respContent=this.processEevent(eventType, eventKey); } //7.请求异常 else { respContent="请求处理异常,请稍候尝试!"; } return respContent; } public String processEevent(String eventType,String eventKey){ String respContent=""; // 订阅 if (eventType.equals(MessageUtil.EVENT_TYPE_SUBSCRIBE)) { respContent = "欢迎关注!"; } // 取消订阅 else if (eventType.equals(MessageUtil.EVENT_TYPE_UNSUBSCRIBE)) { // TODO 取消订阅后用户再收不到公众号发送的消息,因此不需要回复消息 } //上报地理位置事件 else if(eventType.equals("LOCATION")){ } // 自定义菜单点击事件 else if (eventType.equals(MessageUtil.EVENT_TYPE_CLICK)) { if (eventKey.equals("12")) { // TODO help } else if (eventKey.equals("13")) { respContent = "周边搜索菜单项被点击!"; } else if (eventKey.equals("14")) { respContent = "历史上的今天菜单项被点击!"; } else if (eventKey.equals("21")) { respContent = "歌曲点播菜单项被点击!"; } else if (eventKey.equals("22")) { respContent = "经典游戏菜单项被点击!"; } else if (eventKey.equals("23")) { respContent = "美女电台菜单项被点击!"; } else if (eventKey.equals("24")) { respContent = "人脸识别菜单项被点击!"; } else if (eventKey.equals("25")) { respContent = "聊天唠嗑菜单项被点击!"; } else if (eventKey.equals("31")) { respContent = "Q友圈菜单项被点击!"; } else if (eventKey.equals("32")) { respContent = "电影排行榜菜单项被点击!"; } else if (eventKey.equals("33")) { respContent = "幽默笑话菜单项被点击!"; } } return respContent; } }
七、CoreServlet
package com.ray.servlet; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.qq.weixin.mp.aes.AesException; import com.qq.weixin.mp.aes.WXBizMsgCrypt; import com.ray.service.MessageService; import com.ray.util.WeiXinParamesUtil; /** * 核心请求处理类 * @author shirayner * */ public class CoreServlet extends HttpServlet { private static final long serialVersionUID = 4440739483644821986L; /** * 确认请求来自微信服务器 */ public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 微信加密签名 String msg_signature = request.getParameter("msg_signature"); // 时间戳 String timestamp = request.getParameter("timestamp"); // 随机数 String nonce = request.getParameter("nonce"); // 随机字符串 String echostr = request.getParameter("echostr"); System.out.println("request=" + request.getRequestURL()); PrintWriter out = response.getWriter(); // 通过检验msg_signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败 String result = null; try { WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(WeiXinParamesUtil.token, WeiXinParamesUtil.encodingAESKey, WeiXinParamesUtil.corpId); result = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr); } catch (AesException e) { e.printStackTrace(); } if (result == null) { result = WeiXinParamesUtil.token; } out.print(result); out.close(); out = null; } /** * 处理微信服务器发来的消息 */ public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //1.将请求、响应的编码均设置为UTF-8(防止中文乱码) request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); //2.调用消息业务类接收消息、处理消息 MessageService msgsv=new MessageService(); String respMessage = msgsv.getEncryptRespMessage(request); //处理表情 // String respMessage = CoreService.processRequest_emoj(request); //处理图文消息 //String respMessage = Test_NewsService.processRequest(request); //3.响应消息 PrintWriter out = response.getWriter(); out.print(respMessage); out.close(); } }
八、web.xml
注册servlet
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <servlet> <servlet-name>coreServlet</servlet-name> <servlet-class> com.ray.servlet.CoreServlet </servlet-class> </servlet> <servlet> <servlet-name>uploadTempMaterialServlet</servlet-name> <servlet-class> com.ray.servlet.UploadTempMaterialServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>uploadTempMaterialServlet</servlet-name> <url-pattern>/uploadTempMaterialServlet</url-pattern> </servlet-mapping> </web-app>