[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: 'Please check if there are similar issues before submitting'\nlabels: ''\nassignees: ''\n\n---\n\n**Bug Description**\nA clear and concise description of what the bug is.\n\n**Reproduce**\nstep:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected results**\nA clear and concise description of what you expected to happen.\n\n**Screenshot**\nIf applicable, add screenshots to help explain your problem.\n\n\n**Additional Information**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Multi-Platform Docker Build\n\non:\n  push:\n    tags: ['image-*']\n  workflow_dispatch:    # keep this to allow manual triggering of the workflow\n\njobs:\n  build-multi-arch:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n      id-token: write  # multipass needs this permission to authenticate with the registry\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Docker Buildx\n        uses: docker/setup-buildx-action@v3\n        with:\n          driver: docker-container\n          platforms: linux/amd64,linux/arm64,linux/arm/v7  # set the platforms you want to build for\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build and Push Multi-Arch Image\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: docker/allin1-ubuntu.Dockerfile\n          platforms: linux/amd64,linux/arm64,linux/arm/v7\n          push: true\n          tags: |\n            ghcr.io/crossoverjie/allin1-ubuntu:latest\n            ghcr.io/crossoverjie/allin1-ubuntu:${{ github.run_id }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max"
  },
  {
    "path": ".github/workflows/maven.yml",
    "content": "# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven\n\n# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\nname: Java CI with Maven\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    # Run on all pull requests regardless of target branch to support stacked PRs\n    branches: [ \"**\" ]\n\njobs:\n  build:\n    uses: ./.github/workflows/reusable_run_tests.yml\n    secrets:\n      codecov_token: ${{ secrets.CODECOV_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/reusable_run_tests.yml",
    "content": "name: A reusable workflow to build and run the unit test suite\n\non:\n  workflow_call:\n    secrets:\n      codecov_token:\n        required: true\n  workflow_dispatch:\n\njobs:\n  build_and_test:\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up JDK 17\n        uses: actions/setup-java@v4\n        with:\n          java-version: '17'\n          distribution: 'temurin'\n          cache: maven\n      - name: Run Checkstyle\n        run: mvn checkstyle:check --file pom.xml\n        continue-on-error: true\n      - name: Build with Maven\n        run: mvn -B package --file pom.xml\n      - name: Upload coverage reports to Codecov\n        uses: codecov/codecov-action@v4\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          verbose: true"
  },
  {
    "path": ".gitignore",
    "content": "# Created by .ignore support plugin (hsz.mobi)\n### macOS template\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n### Maven template\ntarget/\npom.xml.tag\npom.xml.releaseBackup\npom.xml.versionsBackup\npom.xml.next\nrelease.properties\ndependency-reduced-pom.xml\nbuildNumber.properties\n.mvn/timing.properties\n\n# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)\n!/.mvn/wrapper/maven-wrapper.jar\n### Java template\n# Compiled class file\n*.class\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n*.war\n*.nar\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n### JetBrains template\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/tasks.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n\n# CMake\ncmake-build-debug/\ncmake-build-release/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n.idea/\n*.iml\n\n# Eclipse Project \nbin\n*.project\n*.settings/\n*.classpath\n*.factorypath\n.vscode/\n/.metadata/\n\n\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CIM Project Guide\n\nCIM (Cross-platform Instant Messaging) is a Java-based instant messaging framework.\n\n## Requirements\n\n- **Minimum JDK Version**: JDK 17\n- **Build Tool**: Maven\n- **Spring Boot Version**: 3.3.0\n\n## Environment Setup\n\nBefore running compile or test commands, set the correct JDK version:\n\n```bash\nexport JAVA_HOME=$JAVA_17_HOME\n```\n\n`JAVA_17_HOME` is defined in `~/.zshrc`.\n\n## Common Commands\n\n### Compile the project\n```bash\nmvn clean compile\n```\n\n### Run tests\n```bash\nmvn test\n```\n\n### Package the project\n```bash\nmvn clean package -DskipTests\n```\n\n### Full build (with tests)\n```bash\nmvn clean install\n```\n\n## Notes\n\n- Checkstyle code style checks run automatically during Maven's `validate` phase\n- Ensure code passes Checkstyle checks before committing\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 crossoverJie\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "IMAGE_NAME = allin1-ubuntu\nREGISTRY = ghcr.io\nOWNER = $(shell git config --get remote.origin.url | sed -e 's/.*github.com[:/]\\([^/]*\\)\\/.*/\\1/')\nTAG_PREFIX = image\n\n# make tag VERSION=v1.0\ntag:\n\t$(if $(VERSION),,$(error VERSION variable not set. Usage: make tag VERSION=x.y.z))\n\tgit tag -f $(TAG_PREFIX)-$(VERSION)\n\tgit push origin $(TAG_PREFIX)-$(VERSION)\n\n# list all tags with the prefix\nlist-tags:\n\tgit tag -l \"$(TAG_PREFIX)-*\" | sort -V\n\n# get the latest tag\nget-latest:\n\t@git describe --abbrev=0 --tags --match=\"$(TAG_PREFIX)-*\" 2>/dev/null | sed 's/$(TAG_PREFIX)-//' || echo \"No tags found\"\n\n# make version TYPE=major|minor|patch\nversion:\n\t$(if $(TYPE),,$(error TYPE variable not set. Options: major, minor, patch))\n\t$(eval CURRENT := $(shell make get-latest))\n\t$(if $(CURRENT),,$(error No existing tags found. Create first tag with: make tag VERSION=1.0.0))\n\n\t$(eval MAJOR := $(shell echo $(CURRENT) | cut -d. -f1))\n\t$(eval MINOR := $(shell echo $(CURRENT) | cut -d. -f2))\n\t$(eval PATCH := $(shell echo $(CURRENT) | cut -d. -f3))\n\n\t$(if $(filter $(TYPE),major),$(eval NEW_VERSION := $(($(MAJOR)+1)).0.0))\n\t$(if $(filter $(TYPE),minor),$(eval NEW_VERSION := $(MAJOR).$(($(MINOR)+1)).0))\n\t$(if $(filter $(TYPE),patch),$(eval NEW_VERSION := $(MAJOR).$(MINOR).$(($(PATCH)+1))))\n\n\tmake tag VERSION=$(NEW_VERSION)\n\n.PHONY: tag list-tags get-latest version"
  },
  {
    "path": "README-zh.md",
    "content": "\n\n\n<div align=\"center\">\n\n<img src=\"https://i.loli.net/2020/02/21/rfOGvKlTcHCmM92.png\"  />\n<br/>\n\n[![codecov](https://codecov.io/gh/crossoverJie/cim/graph/badge.svg?token=oW5Gd1oKmf)](https://codecov.io/gh/crossoverJie/cim)\n[![Build Status](https://img.shields.io/badge/cim-cross--im-brightgreen.svg)](https://github.com/crossoverJie/cim)\n[![](https://badge.juejin.im/entry/5c2c000e6fb9a049f5713e26/likes.svg?style=flat-square)](https://juejin.im/post/5c2bffdc51882509181395d7)\n\n📘[介绍](#介绍) |📽[视频演示](#视频演示) | 🏖[TODO LIST](#todo-list) | 🌈[系统架构](#系统架构) |💡[流程图](#流程图)|🌁[快速启动](#快速启动)|👨🏻‍✈️[内置命令](#客户端内置命令)|🎤[通信](#群聊私聊)|❓[QA](https://github.com/crossoverJie/cim/blob/master/doc/QA.md)|💌[联系作者](#联系作者)\n\n[English](README.md)\n\n</div>\n<br/>\n\n# V2.0\n- [x] 升级至 JDK17 & springboot3.0\n- [x] Client SDK\n- [ ] 客户端使用 [picocli](https://picocli.info/) 替代 springboot\n- [x] 支持集成测试\n- [ ] 集成 OpenTelemetry\n- [ ] 支持单节点启动（不依赖外部组件）\n- [ ] 第三方组件支持替换（Redis/Zookeeper 等）\n- [ ] 支持 Web 客户端（websocket）\n- [x] 支持 Docker 容器\n- [ ] 支持 Kubernetes 部署\n- [ ] 支持二进制客户端（使用 golang 构建）\n\n## 介绍\n\n`CIM(CROSS-IM)` 是面向开发者的 `IM（即时通讯）` 系统；同时提供了一些组件帮助开发者构建自己可扩展的 `IM`。\n借助 `CIM` 你可以实现以下需求：\n- `IM` 即时通讯系统。\n- `APP` 消息推送中间件。\n- `IOT` 海量连接场景中的消息中间件。\n\n> 如果在使用或开发过程中有任何问题，可以[联系作者](#联系作者)。\n\n## 视频演示\n\n> 点击下方链接可以查看视频版 Demo。\n\n| YouTube | Bilibili|\n| :------:| :------: |\n| [群聊](https://youtu.be/_9a4lIkQ5_o) [私聊](https://youtu.be/kfEfQFPLBTQ) | [群聊](https://www.bilibili.com/video/av39405501) [私聊](https://www.bilibili.com/video/av39405821) |\n| <img src=\"https://i.loli.net//2019//05//08//5cd1d9e788004.jpg\"  height=\"295px\" />  | <img src=\"https://i.loli.net//2019//05//08//5cd1da2f943c5.jpg\" height=\"295px\" />\n\n![demo.gif](pic/demo.gif)\n\n## TODO LIST\n\n* [x] [群聊](#群聊)\n* [x] [私聊](#私聊)\n* [x] [内置命令](#客户端内置命令)\n* [x] [聊天记录查询](#聊天记录查询)\n* [x] [一键开启 AI 模式](#ai-模式)\n* [x] 使用 `Google Protocol Buffer` 高效编解码\n* [x] 根据实际情况灵活的水平扩容、缩容\n* [x] 服务端自动剔除离线客户端\n* [x] 客户端自动重连\n* [x] [延时消息](#延时消息)\n* [x] SDK 开发包\n* [ ] 分组群聊\n* [ ] 离线消息\n* [ ] 消息加密\n\n\n\n## 系统架构\n\n![](pic/architecture.png)\n\n- `CIM` 中的各个组件均采用 `SpringBoot` 构建\n  - 客户端基于 [cim-client-sdk](https://github.com/crossoverJie/cim/tree/master/cim-client-sdk) 构建\n- 采用 `Netty` 构建底层通信\n- `MetaStore` 用于 `IM-server` 服务的注册与发现\n\n\n### cim-server\nIM 服务端，用于接收客户端连接、消息转发、消息推送等功能。\n支持集群部署。\n\n### cim-route\n\n路由服务器；用于处理消息路由、消息转发、用户登录、用户下线以及一些运维工具（获取在线用户数等）。\n\n### cim-client\nIM 客户端终端，一个命令即可启动并与其他人进行通信（群聊、私聊）。\n\n## 流程图\n\n![](https://s2.loli.net/2024/10/13/8teMn7BSa5VWuvi.png)\n\n- Server 注册到 `MetaStore`\n- Route 订阅 `MetaStore`\n- Client 登录到 Route\n  - Route 从 `MetaStore` 获取 Server 信息\n- Client 与 Server 建立连接\n- Client1 发送消息到 Route\n- Route 选择 Server 并将消息转发给 Server\n- Server 将消息推送给 Client2\n\n\n## 快速启动\n\n### Docker\n\n`allin1` 镜像内置了 Zookeeper、Redis、cim-server、cim-forward-route 四个服务，使用 [Supervisor](http://supervisord.org/) 统一管理，开箱即用。\n\n**支持平台：** linux/amd64, linux/arm64, linux/arm/v7\n\n**端口说明：**\n\n| 端口 | 服务 | 说明 |\n|------|---------|-------------|\n| 2181 | Zookeeper | 服务注册与发现 |\n| 6379 | Redis | 数据缓存 |\n| 8083 | Route Server | HTTP API 路由服务 |\n\n拉取镜像并启动：\n\n```shell\ndocker pull ghcr.io/crossoverjie/allin1-ubuntu:latest\ndocker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 ghcr.io/crossoverjie/allin1-ubuntu:latest\n```\n\n容器启动后，可参考下方 [注册账号](#注册账号) 和 [启动客户端](#启动客户端) 章节快速体验完整的 IM 流程。\n\n### 本地构建 Docker 镜像\n\n如果需要从源码构建镜像：\n\n```shell\n# 在项目根目录执行\ndocker build -t cim-allin1:latest -f docker/allin1-ubuntu.Dockerfile .\ndocker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 cim-allin1:latest\n```\n\n### 本地编译\n\n首先需要安装 `Zookeeper`、`Redis` 并保证网络通畅。\n\n```shell\ndocker run --rm --name zookeeper -d -p 2181:2181 zookeeper:3.9.2\ndocker run --rm --name redis -d -p 6379:6379 redis:7.4.0\n```\n\n```shell\ngit clone https://github.com/crossoverJie/cim.git\ncd cim\nmvn clean install -DskipTests=true\ncd cim-server && cim-client && cim-forward-route\nmvn clean package spring-boot:repackage -DskipTests=true\n```\n\n### 部署 IM-server（cim-server）\n\n```shell\ncp /cim/cim-server/target/cim-server-1.0.0-SNAPSHOT.jar /xx/work/server0/\ncd /xx/work/server0/\nnohup java -jar  /root/work/server0/cim-server-1.0.0-SNAPSHOT.jar --cim.server.port=9000 --app.zk.addr=zk地址  > /root/work/server0/log.file 2>&1 &\n```\n\n> cim-server 集群部署同理，只要保证 Zookeeper 地址相同即可。\n\n### 部署路由服务器（cim-forward-route）\n\n```shell\ncp /cim/cim-server/cim-forward-route/target/cim-forward-route-1.0.0-SNAPSHOT.jar /xx/work/route0/\ncd /xx/work/route0/\nnohup java -jar  /root/work/route0/cim-forward-route-1.0.0-SNAPSHOT.jar --app.zk.addr=zk地址 --spring.redis.host=redis地址 --spring.redis.port=6379  > /root/work/route/log.file 2>&1 &\n```\n\n> cim-forward-route 本身就是无状态，可以部署多台；使用 Nginx 代理即可。\n\n\n### 启动客户端\n\n```shell\ncp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/\ncd /xx/work/route0/\njava -jar cim-client-1.0.0-SNAPSHOT.jar --server.port=8084 --cim.user.id=唯一客户端ID --cim.user.userName=用户名 --cim.route.url=http://路由服务器:8083/\n```\n\n![](https://ws2.sinaimg.cn/large/006tNbRwly1fylgxjgshfj31vo04m7p9.jpg)\n![](https://ws1.sinaimg.cn/large/006tNbRwly1fylgxu0x4uj31hy04q75z.jpg)\n\n如上图，启动两个客户端可以互相通信即可。\n\n### 本地启动客户端\n\n#### 注册账号\n```shell\ncurl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{\n  \"reqNo\": \"1234567890\",\n  \"timeStamp\": 0,\n  \"userName\": \"zhangsan\"\n}' 'http://路由服务器:8083/registerAccount'\n```\n\n从返回结果中获取 `userId`\n\n```json\n{\n    \"code\":\"9000\",\n    \"message\":\"成功\",\n    \"reqNo\":null,\n    \"dataBody\":{\n        \"userId\":1547028929407,\n        \"userName\":\"test\"\n    }\n}\n```\n\n#### 启动本地客户端\n```shell\n# 启动本地客户端\ncp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/\ncd /xx/work/route0/\njava -jar cim-client-1.0.0-SNAPSHOT.jar --server.port=8084 --cim.user.id=上方返回的userId --cim.user.userName=用户名 --cim.route.url=http://路由服务器:8083/\n```\n\n## 客户端内置命令\n\n| 命令 | 描述|\n| ------ | ------ |\n| `:q!` | 退出客户端|\n| `:olu` | 获取所有在线用户信息 |\n| `:all` | 获取所有命令 |\n| `:q [option]` | 【:q 关键字】查询聊天记录 |\n| `:ai` | 开启 AI 模式 |\n| `:qai` | 关闭 AI 模式 |\n| `:pu` | 模糊匹配用户 |\n| `:info` | 获取客户端信息 |\n| `:emoji [option]` | 查询表情包 [option:页码] |\n| `:delay [msg] [delayTime]` | 发送延时消息 |\n| `:` | 更多命令正在开发中。。 |\n\n![](https://ws3.sinaimg.cn/large/006tNbRwly1fylh7bdlo6g30go01shdt.gif)\n\n### 聊天记录查询\n\n![](https://i.loli.net/2019/05/08/5cd1c310cb796.jpg)\n\n使用命令 `:q 关键字` 即可查询与个人相关的聊天记录。\n\n> 客户端聊天记录默认存放在 `/opt/logs/cim/`，所以需要这个目录的写入权限。也可在启动命令中加入 `--cim.msg.logger.path = /自定义` 参数自定义目录。\n\n\n\n### AI 模式\n\n![](https://i.loli.net/2019/05/08/5cd1c30e47d95.jpg)\n\n使用命令 `:ai` 开启 AI 模式，之后所有的消息都会由 `AI` 响应。\n\n`:qai` 退出 AI 模式。\n\n### 前缀匹配用户名\n\n![](https://i.loli.net/2019/05/08/5cd1c32ac3397.jpg)\n\n使用命令 `:qu prefix` 可以按照前缀的方式搜索用户信息。\n\n> 该功能主要用于在移动端中的输入框中搜索用户。\n\n### 群聊/私聊\n\n#### 群聊\n\n![](https://ws1.sinaimg.cn/large/006tNbRwly1fyli54e8e1j31t0056x11.jpg)\n![](https://ws3.sinaimg.cn/large/006tNbRwly1fyli5yyspmj31im06atb8.jpg)\n![](https://ws3.sinaimg.cn/large/006tNbRwly1fyli6sn3c8j31ss06qmzq.jpg)\n\n群聊只需要在控制台里输入消息回车后即可发送，同时所有在线客户端都可收到消息。\n\n#### 私聊\n\n私聊首先需要知道对方的 `userID` 才能进行。\n\n输入命令 `:olu` 可列出所有在线用户。\n\n![](https://ws4.sinaimg.cn/large/006tNbRwly1fyli98mlf3j31ta06mwhv.jpg)\n\n接着使用 `userId;;消息内容` 的格式即可发送私聊消息。\n\n![](https://ws4.sinaimg.cn/large/006tNbRwly1fylib08qlnj31sk082zo6.jpg)\n![](https://ws1.sinaimg.cn/large/006tNbRwly1fylibc13etj31wa0564lp.jpg)\n![](https://ws3.sinaimg.cn/large/006tNbRwly1fylicmjj6cj31wg07c4qp.jpg)\n![](https://ws1.sinaimg.cn/large/006tNbRwly1fylicwhe04j31ua03ejsv.jpg)\n\n同时另一个账号收不到消息。\n![](https://ws3.sinaimg.cn/large/006tNbRwly1fylie727jaj31t20dq1ky.jpg)\n\n\n\n### emoji 表情支持\n\n使用命令 `:emoji 1` 查询出所有表情列表，使用表情别名即可发送表情。\n\n![](https://tva1.sinaimg.cn/large/006y8mN6ly1g6j910cqrzj30dn05qjw9.jpg)\n![](https://tva1.sinaimg.cn/large/006y8mN6ly1g6j99hazg6j30ax03hq35.jpg)\n\n### 延时消息\n\n发送 10s 的延时消息：\n\n```shell\n:delay delayMsg 10\n```\n\n![](pic/delay.gif)\n\n## 联系作者\n\n## 贡献指南\n\n欢迎贡献代码！提交 PR 前，请确保代码通过 Checkstyle 检查。\n\n### 代码风格\n\n本项目使用 [Checkstyle](https://checkstyle.org/) 来规范代码风格，规则定义在 `checkstyle/checkstyle.xml` 中。\n\n**提交前在本地运行 Checkstyle：**\n\n```shell\nmvn checkstyle:check\n```\n\n**主要规则：**\n- `{`、`}` 和运算符前后使用空格\n- 行尾不能有空格\n- 文件必须以换行符结尾\n- 删除未使用的 import\n- 常量（`static final`）必须使用 `UPPER_SNAKE_CASE` 命名\n- 使用 Java 风格的数组声明：`String[] args`（而非 `String args[]`）\n\n**快速构建时跳过 Checkstyle：**\n\n```shell\nmvn package -Dcheckstyle.skip=true\n```\n\n<div align=\"center\">\n\n<a href=\"https://t.zsxq.com/odQDJ\" target=\"_blank\"><img src=\"https://s2.loli.net/2024/05/17/zRkabDu2SKfChLX.png\" alt=\"202405171520366.png\"></a>\n</div>\n\n最近开通了知识星球，感谢大家对 CIM 的支持，为大家提供 100 份 10 元优惠券，也就是 69-10=59 元，具体福利大家可以扫码参考再决定是否加入。\n\n> PS: 后续会在星球开始 V2.0 版本的重构，感兴趣的可以加入星球当面催更（当然代码依然会开源）。\n\n- [crossoverJie@gmail.com](mailto:crossoverJie@gmail.com)\n- 微信公众号\n\n![index.jpg](https://i.loli.net/2021/10/12/ckQW9LYXSxFogJZ.jpg)\n"
  },
  {
    "path": "README.md",
    "content": "\n\n\n<div align=\"center\">\n\n<img src=\"https://i.loli.net/2020/02/21/rfOGvKlTcHCmM92.png\"  />\n<br/>\n\n[![codecov](https://codecov.io/gh/crossoverJie/cim/graph/badge.svg?token=oW5Gd1oKmf)](https://codecov.io/gh/crossoverJie/cim)\n[![Build Status](https://img.shields.io/badge/cim-cross--im-brightgreen.svg)](https://github.com/crossoverJie/cim)\n[![](https://badge.juejin.im/entry/5c2c000e6fb9a049f5713e26/likes.svg?style=flat-square)](https://juejin.im/post/5c2bffdc51882509181395d7)\n\n📘[Introduction](#introduction) |📽[Video Demo](#video-demo) | 🏖[TODO LIST](#todo-list) | 🌈[Architecture](#architecture) |💡[Flow Chart](#flow-chart)|🌁[Quick Start](#quick-start)|👨🏻‍✈️[Built-in Commands](#built-in-commands)|🎤[Chat](#group-chatprivate-chat)|❓[QA](https://github.com/crossoverJie/cim/blob/master/doc/QA.md)|💌[Contact](#contact)\n\n[中文文档](README-zh.md)\n\n</div>\n<br/>\n\n# V2.0\n- [x] Upgrade to JDK17 & springboot3.0\n- [x] Client SDK\n- [ ] Client use [picocli](https://picocli.info/) instead of springboot.\n- [x] Support integration testing.\n- [ ] Integrate OpenTelemetry .\n- [ ] Support single node startup(Contains no components).\n- [ ] Third-party components support replacement(Redis/Zookeeper, etc.).\n- [ ] Support web client(websocket).\n- [x] Support docker container.\n- [ ] Support kubernetes operation.\n- [ ] Supports binary client(build with golang).\n\n## Introduction\n\n`CIM(CROSS-IM)` is an `IM (instant messaging)` system for developers; it also provides some components to help developers build their own scalable `IM`.\nUsing `CIM`, you can achieve the following requirements:\n- `IM` instant messaging system.\n- Message push middleware for `APP`.\n- Message middleware for `IOT` massive connection scenarios.\n\n> If you have any questions during use or development, you can [contact the author](#contact).\n\n## Video Demo\n\n> Click the links below to watch the video demo.\n\n| YouTube | Bilibili|\n| :------:| :------: |\n| [Group Chat](https://youtu.be/_9a4lIkQ5_o) [Private Chat](https://youtu.be/kfEfQFPLBTQ) | [Group Chat](https://www.bilibili.com/video/av39405501) [Private Chat](https://www.bilibili.com/video/av39405821) |\n| <img src=\"https://i.loli.net//2019//05//08//5cd1d9e788004.jpg\"  height=\"295px\" />  | <img src=\"https://i.loli.net//2019//05//08//5cd1da2f943c5.jpg\" height=\"295px\" />\n\n![demo.gif](pic/demo.gif)\n\n## TODO LIST\n\n* [x] [Group Chat](#group-chat)\n* [x] [Private Chat](#private-chat)\n* [x] [Built-in Commands](#built-in-commands)\n* [x] [Chat History Query](#chat-history-query)\n* [x] [AI Mode](#ai-mode)\n* [x] Efficient encoding/decoding with `Google Protocol Buffer`\n* [x] Flexible horizontal scaling based on actual needs\n* [x] Server-side automatic removal of offline clients\n* [x] Client automatic reconnection\n* [x] [Delayed Messages](#delayed-messages)\n* [x] SDK development package\n* [ ] Group categorization\n* [ ] Offline messages\n* [ ] Message encryption\n\n\n\n## Architecture\n\n![](pic/architecture.png)\n\n- Each component in `CIM` is built using `SpringBoot`\n  - Client build with [cim-client-sdk](https://github.com/crossoverJie/cim/tree/master/cim-client-sdk)\n- Use `Netty` to build the underlying communication.\n- `MetaStore` is used for registration and discovery of `IM-server` services.\n\n\n### cim-server\nIM server is used to receive client connections, message forwarding, message push, etc.\nSupport cluster deployment.\n\n### cim-route\n\nRoute server; used to process message routing, message forwarding, user login, user offline, and some operation tools (get the number of online users, etc.).\n\n### cim-client\nIM client terminal, a command can be started and initiated to communicate with others (group chat, private chat).\n\n## Flow Chart\n\n![](https://s2.loli.net/2024/10/13/8teMn7BSa5VWuvi.png)\n\n- Server register to `MetaStore`\n- Route subscribe `MetaStore`\n- Client login to Route\n  - Route get Server info from `MetaStore`\n- Client open connection to Server\n- Client1 send message to Route\n- Route select Server and forward message to Server\n- Server push message to Client2\n\n\n## Quick Start\n\n### Docker\n\nThe `allin1` image comes with Zookeeper, Redis, cim-server, and cim-forward-route pre-installed, all managed by [Supervisor](http://supervisord.org/) for an out-of-the-box experience.\n\n**Supported platforms:** linux/amd64, linux/arm64, linux/arm/v7\n\n**Port mapping:**\n\n| Port | Service | Description |\n|------|---------|-------------|\n| 2181 | Zookeeper | Service registration & discovery |\n| 6379 | Redis | Data caching |\n| 8083 | Route Server | HTTP API routing service |\n\nPull the image and start the container:\n\n```shell\ndocker pull ghcr.io/crossoverjie/allin1-ubuntu:latest\ndocker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 ghcr.io/crossoverjie/allin1-ubuntu:latest\n```\n\nAfter the container starts, refer to the [Register Account](#register-account) and [Start Client](#start-client) sections below to experience the full IM workflow.\n\n### Build Docker Image Locally\n\nTo build the Docker image from source:\n\n```shell\n# Run from the project root directory\ndocker build -t cim-allin1:latest -f docker/allin1-ubuntu.Dockerfile .\ndocker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 cim-allin1:latest\n```\n\n### Build from Source\n\nFirst, install `Zookeeper` and `Redis` and ensure the network is accessible.\n\n```shell\ndocker run --rm --name zookeeper -d -p 2181:2181 zookeeper:3.9.2\ndocker run --rm --name redis -d -p 6379:6379 redis:7.4.0\n```\n\n```shell\ngit clone https://github.com/crossoverJie/cim.git\ncd cim\nmvn clean install -DskipTests=true\ncd cim-server && cim-client && cim-forward-route\nmvn clean package spring-boot:repackage -DskipTests=true\n```\n\n### Deploy IM-server (cim-server)\n\n```shell\ncp /cim/cim-server/target/cim-server-1.0.0-SNAPSHOT.jar /xx/work/server0/\ncd /xx/work/server0/\nnohup java -jar  /root/work/server0/cim-server-1.0.0-SNAPSHOT.jar --cim.server.port=9000 --app.zk.addr=<zk-address>  > /root/work/server0/log.file 2>&1 &\n```\n\n> For cim-server cluster deployment, just ensure all instances point to the same Zookeeper address.\n\n### Deploy Route Server (cim-forward-route)\n\n```shell\ncp /cim/cim-server/cim-forward-route/target/cim-forward-route-1.0.0-SNAPSHOT.jar /xx/work/route0/\ncd /xx/work/route0/\nnohup java -jar  /root/work/route0/cim-forward-route-1.0.0-SNAPSHOT.jar --app.zk.addr=<zk-address> --spring.redis.host=<redis-address> --spring.redis.port=6379  > /root/work/route/log.file 2>&1 &\n```\n\n> cim-forward-route is stateless and can be deployed on multiple nodes; use Nginx as a reverse proxy.\n\n\n### Start Client\n\n```shell\ncp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/\ncd /xx/work/route0/\njava -jar cim-client-1.0.0-SNAPSHOT.jar --server.port=8084 --cim.user.id=<unique-client-id> --cim.user.userName=<username> --cim.route.url=http://<route-server>:8083/\n```\n\n![](https://ws2.sinaimg.cn/large/006tNbRwly1fylgxjgshfj31vo04m7p9.jpg)\n![](https://ws1.sinaimg.cn/large/006tNbRwly1fylgxu0x4uj31hy04q75z.jpg)\n\nAs shown above, two clients can communicate with each other.\n\n### Local Client Startup\n\n#### Register Account\n```shell\ncurl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{\n  \"reqNo\": \"1234567890\",\n  \"timeStamp\": 0,\n  \"userName\": \"zhangsan\"\n}' 'http://<route-server>:8083/registerAccount'\n```\n\nGet the `userId` from the response:\n\n```json\n{\n    \"code\":\"9000\",\n    \"message\":\"success\",\n    \"reqNo\":null,\n    \"dataBody\":{\n        \"userId\":1547028929407,\n        \"userName\":\"test\"\n    }\n}\n```\n\n#### Start Local Client\n```shell\n# Start local client\ncp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/\ncd /xx/work/route0/\njava -jar cim-client-1.0.0-SNAPSHOT.jar --server.port=8084 --cim.user.id=<userId-from-above> --cim.user.userName=<username> --cim.route.url=http://<route-server>:8083/\n```\n\n## Built-in Commands\n\n| Command | Description |\n| ------ | ------ |\n| `:q!` | Quit the client |\n| `:olu` | List all online users |\n| `:all` | Show all available commands |\n| `:q [keyword]` | Search chat history by keyword |\n| `:ai` | Enable AI mode |\n| `:qai` | Disable AI mode |\n| `:pu` | Fuzzy search users |\n| `:info` | Show client information |\n| `:emoji [option]` | Browse emoji list [option: page number] |\n| `:delay [msg] [delayTime]` | Send a delayed message |\n| `:` | More commands are under development... |\n\n![](https://ws3.sinaimg.cn/large/006tNbRwly1fylh7bdlo6g30go01shdt.gif)\n\n### Chat History Query\n\n![](https://i.loli.net/2019/05/08/5cd1c310cb796.jpg)\n\nUse the command `:q keyword` to search chat history related to you.\n\n> Client chat history is stored in `/opt/logs/cim/` by default, so write permission is required for this directory. You can also customize the directory by adding `--cim.msg.logger.path=/custom/path` to the startup command.\n\n\n\n### AI Mode\n\n![](https://i.loli.net/2019/05/08/5cd1c30e47d95.jpg)\n\nUse the command `:ai` to enable AI mode. After that, all messages will be responded to by `AI`.\n\nUse `:qai` to exit AI mode.\n\n### Prefix Match Username\n\n![](https://i.loli.net/2019/05/08/5cd1c32ac3397.jpg)\n\nUse the command `:qu prefix` to search user information by prefix.\n\n> This feature is primarily designed for searching users in input fields on mobile clients.\n\n### Group Chat/Private Chat\n\n#### Group Chat\n\n![](https://ws1.sinaimg.cn/large/006tNbRwly1fyli54e8e1j31t0056x11.jpg)\n![](https://ws3.sinaimg.cn/large/006tNbRwly1fyli5yyspmj31im06atb8.jpg)\n![](https://ws3.sinaimg.cn/large/006tNbRwly1fyli6sn3c8j31ss06qmzq.jpg)\n\nFor group chat, simply type a message in the console and press Enter to send. All online clients will receive the message.\n\n#### Private Chat\n\nTo send a private message, you need to know the recipient's `userID`.\n\nUse the command `:olu` to list all online users.\n\n![](https://ws4.sinaimg.cn/large/006tNbRwly1fyli98mlf3j31ta06mwhv.jpg)\n\nThen use the format `userId;;message content` to send a private message.\n\n![](https://ws4.sinaimg.cn/large/006tNbRwly1fylib08qlnj31sk082zo6.jpg)\n![](https://ws1.sinaimg.cn/large/006tNbRwly1fylibc13etj31wa0564lp.jpg)\n![](https://ws3.sinaimg.cn/large/006tNbRwly1fylicmjj6cj31wg07c4qp.jpg)\n![](https://ws1.sinaimg.cn/large/006tNbRwly1fylicwhe04j31ua03ejsv.jpg)\n\nMeanwhile, the other account will not receive the message.\n![](https://ws3.sinaimg.cn/large/006tNbRwly1fylie727jaj31t20dq1ky.jpg)\n\n\n\n### Emoji Support\n\nUse the command `:emoji 1` to list all available emojis. Use the emoji alias to send an emoji.\n\n![](https://tva1.sinaimg.cn/large/006y8mN6ly1g6j910cqrzj30dn05qjw9.jpg)\n![](https://tva1.sinaimg.cn/large/006y8mN6ly1g6j99hazg6j30ax03hq35.jpg)\n\n### Delayed Messages\n\nSend a message with a 10-second delay:\n\n```shell\n:delay delayMsg 10\n```\n\n![](pic/delay.gif)\n\n## Contact\n\n## Contributing\n\nWe welcome contributions! Before submitting a PR, please ensure your code passes the Checkstyle check.\n\n### Code Style\n\nThis project uses [Checkstyle](https://checkstyle.org/) to enforce code style. The rules are defined in `checkstyle/checkstyle.xml`.\n\n**Run Checkstyle locally before committing:**\n\n```shell\nmvn checkstyle:check\n```\n\n**Key rules:**\n- Use spaces around `{`, `}`, and operators\n- No trailing whitespace\n- Files must end with a newline\n- Remove unused imports\n- Constants (`static final`) must be `UPPER_SNAKE_CASE`\n- Use Java-style array declarations: `String[] args` (not `String args[]`)\n\n**Skip Checkstyle for quick builds:**\n\n```shell\nmvn package -Dcheckstyle.skip=true\n```\n\n- [crossoverJie@gmail.com](mailto:crossoverJie@gmail.com)\n"
  },
  {
    "path": "checkstyle/checkstyle.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC\n        \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\"\n        \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n\n<!--\n    CIM Project Checkstyle Configuration\n    Based on Google Java Style with customizations\n-->\n<module name=\"Checker\">\n    <property name=\"charset\" value=\"UTF-8\"/>\n    <property name=\"severity\" value=\"error\"/>\n    <property name=\"fileExtensions\" value=\"java\"/>\n\n    <!-- Suppressions -->\n    <module name=\"SuppressionFilter\">\n        <property name=\"file\" value=\"${org.checkstyle.google.suppressionfilter.config}\"\n                  default=\"checkstyle/suppressions.xml\"/>\n        <property name=\"optional\" value=\"true\"/>\n    </module>\n\n    <!-- File Length -->\n    <module name=\"FileLength\">\n        <property name=\"max\" value=\"2000\"/>\n    </module>\n\n    <!-- No Trailing Whitespace -->\n    <module name=\"RegexpSingleline\">\n        <property name=\"format\" value=\"\\s+$\"/>\n        <property name=\"minimum\" value=\"0\"/>\n        <property name=\"maximum\" value=\"0\"/>\n        <property name=\"message\" value=\"Line has trailing spaces.\"/>\n    </module>\n\n    <!-- Newline at end of file -->\n    <module name=\"NewlineAtEndOfFile\">\n        <property name=\"lineSeparator\" value=\"lf\"/>\n    </module>\n\n    <!-- Line Length (must be at Checker level in Checkstyle 10.x) -->\n    <module name=\"LineLength\">\n        <property name=\"max\" value=\"150\"/>\n        <property name=\"ignorePattern\" value=\"^package.*|^import.*|a href|href|http://|https://|ftp://\"/>\n    </module>\n\n    <module name=\"TreeWalker\">\n        <!-- Naming Conventions -->\n        <module name=\"TypeName\">\n            <property name=\"format\" value=\"^[A-Z][a-zA-Z0-9]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Type name ''{0}'' must match pattern ''{1}'' (UpperCamelCase).\"/>\n        </module>\n        <module name=\"MemberName\">\n            <property name=\"format\" value=\"^[a-z][a-zA-Z0-9]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Member name ''{0}'' must match pattern ''{1}'' (lowerCamelCase).\"/>\n        </module>\n        <module name=\"MethodName\">\n            <property name=\"format\" value=\"^[a-z][a-zA-Z0-9]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Method name ''{0}'' must match pattern ''{1}'' (lowerCamelCase).\"/>\n        </module>\n        <module name=\"ParameterName\">\n            <property name=\"format\" value=\"^[a-z][a-zA-Z0-9]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Parameter name ''{0}'' must match pattern ''{1}'' (lowerCamelCase).\"/>\n        </module>\n        <module name=\"LocalVariableName\">\n            <property name=\"format\" value=\"^[a-z][a-zA-Z0-9]*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Local variable name ''{0}'' must match pattern ''{1}'' (lowerCamelCase).\"/>\n        </module>\n        <module name=\"ConstantName\">\n            <property name=\"format\" value=\"^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Constant name ''{0}'' must match pattern ''{1}'' (UPPER_SNAKE_CASE).\"/>\n        </module>\n        <module name=\"PackageName\">\n            <property name=\"format\" value=\"^[a-z]+(\\.[a-z][a-z0-9]*)*$\"/>\n            <message key=\"name.invalidPattern\"\n                     value=\"Package name ''{0}'' must match pattern ''{1}'' (all lowercase).\"/>\n        </module>\n\n        <!-- Import Checks -->\n        <module name=\"AvoidStarImport\">\n            <property name=\"excludes\" value=\"java.io,java.net,java.lang.Math\"/>\n            <property name=\"allowClassImports\" value=\"false\"/>\n            <property name=\"allowStaticMemberImports\" value=\"false\"/>\n        </module>\n        <module name=\"RedundantImport\"/>\n        <module name=\"UnusedImports\"/>\n        <module name=\"IllegalImport\">\n            <property name=\"illegalPkgs\" value=\"sun\"/>\n        </module>\n\n        <!-- Braces -->\n        <module name=\"LeftCurly\">\n            <property name=\"option\" value=\"eol\"/>\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"option\" value=\"same\"/>\n            <property name=\"tokens\" value=\"LITERAL_TRY,LITERAL_CATCH,LITERAL_FINALLY,LITERAL_IF,LITERAL_ELSE\"/>\n        </module>\n        <module name=\"NeedBraces\">\n            <property name=\"tokens\" value=\"LITERAL_DO,LITERAL_ELSE,LITERAL_FOR,LITERAL_IF,LITERAL_WHILE\"/>\n        </module>\n\n        <!-- Whitespace -->\n        <module name=\"GenericWhitespace\"/>\n        <module name=\"WhitespaceAround\">\n            <property name=\"allowEmptyConstructors\" value=\"true\"/>\n            <property name=\"allowEmptyMethods\" value=\"true\"/>\n            <property name=\"allowEmptyTypes\" value=\"true\"/>\n            <property name=\"allowEmptyLoops\" value=\"true\"/>\n        </module>\n        <module name=\"NoWhitespaceBefore\"/>\n        <module name=\"NoWhitespaceAfter\">\n            <property name=\"tokens\" value=\"AT,INC,DEC,UNARY_MINUS,UNARY_PLUS,BNOT,LNOT,DOT,ARRAY_DECLARATOR,INDEX_OP\"/>\n        </module>\n\n        <!-- Coding -->\n        <module name=\"ArrayTypeStyle\"/>\n        <module name=\"MissingSwitchDefault\"/>\n        <module name=\"ModifierOrder\"/>\n        <module name=\"EmptyBlock\">\n            <property name=\"option\" value=\"TEXT\"/>\n            <property name=\"tokens\" value=\"LITERAL_TRY,LITERAL_FINALLY,LITERAL_IF,LITERAL_ELSE,LITERAL_SWITCH\"/>\n        </module>\n        <module name=\"EmptyStatement\"/>\n        <module name=\"EqualsHashCode\"/>\n        <module name=\"SimplifyBooleanExpression\"/>\n        <module name=\"SimplifyBooleanReturn\"/>\n        <module name=\"StringLiteralEquality\"/>\n        <module name=\"UpperEll\"/>\n        <module name=\"OneStatementPerLine\"/>\n        <module name=\"MultipleVariableDeclarations\"/>\n\n        <!-- Javadoc (warnings only for now) -->\n        <module name=\"JavadocMethod\">\n            <property name=\"severity\" value=\"ignore\"/>\n        </module>\n        <module name=\"JavadocType\">\n            <property name=\"severity\" value=\"ignore\"/>\n        </module>\n\n        <!-- Suppressions via annotation -->\n        <module name=\"SuppressWarningsHolder\"/>\n    </module>\n\n    <module name=\"SuppressWarningsFilter\"/>\n</module>\n"
  },
  {
    "path": "checkstyle/suppressions.xml",
    "content": "<?xml version=\"1.0\"?>\n<!DOCTYPE suppressions PUBLIC\n        \"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN\"\n        \"https://checkstyle.org/dtds/suppressions_1_2.dtd\">\n\n<!--\n    CIM Project Checkstyle Suppressions\n    Exemptions for generated code and specific patterns\n-->\n<suppressions>\n    <!-- Suppress all checks for generated protobuf files -->\n    <suppress files=\".*[/\\\\]generated[/\\\\].*\" checks=\".*\"/>\n    <suppress files=\".*[/\\\\]target[/\\\\].*\" checks=\".*\"/>\n\n    <!-- Suppress naming checks for protobuf generated classes -->\n    <suppress files=\".*Proto\\.java$\" checks=\".*\"/>\n    <suppress files=\".*Grpc\\.java$\" checks=\".*\"/>\n\n    <!-- Suppress line length for long string literals in tests -->\n    <suppress files=\".*Test\\.java$\" checks=\"LineLength\"/>\n    <suppress files=\".*Tests\\.java$\" checks=\"LineLength\"/>\n\n    <!-- Suppress star import check for test files (allow static imports for assertions) -->\n    <suppress files=\".*Test\\.java$\" checks=\"AvoidStarImport\"/>\n    <suppress files=\".*Tests\\.java$\" checks=\"AvoidStarImport\"/>\n</suppressions>\n"
  },
  {
    "path": "cim-client/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.crossoverjie.netty</groupId>\n        <artifactId>cim</artifactId>\n        <version>1.0.0-SNAPSHOT</version>\n    </parent>\n    <artifactId>cim-client</artifactId>\n    <packaging>jar</packaging>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <java.version>17</java.version>\n        <swagger.version>2.5.0</swagger.version>\n    </properties>\n\n\n    <dependencies>\n\n\n        <dependency>\n            <groupId>com.google.protobuf</groupId>\n            <artifactId>protobuf-java</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-common</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-client-sdk</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.vintage</groupId>\n            <artifactId>junit-vintage-engine</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-configuration-processor</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.alibaba</groupId>\n            <artifactId>fastjson</artifactId>\n        </dependency>\n\n\n        <dependency>\n            <groupId>com.vdurmont</groupId>\n            <artifactId>emoji-java</artifactId>\n            <version>5.0.0</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-rout-api</artifactId>\n        </dependency>\n\n    </dependencies>\n\n    <build>\n        <plugins>\n            <!-- spring-boot-maven-plugin (提供了直接运行项目的插件：如果是通过parent方式继承spring-boot-starter-parent则不用此插件) -->\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>repackage</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n\n\n</project>"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/CIMClientApplication.java",
    "content": "package com.crossoverjie.cim.client;\n\nimport com.crossoverjie.cim.client.scanner.Scan;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.CommandLineRunner;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\n/**\n * @author crossoverJie\n */\n@Slf4j\n@SpringBootApplication\npublic class CIMClientApplication implements CommandLineRunner {\n\n\n\tpublic static void main(String[] args) {\n        SpringApplication.run(CIMClientApplication.class, args);\n\t\tlog.info(\"Client start success\");\n\t}\n\n\t@Override\n\tpublic void run(String... args) {\n\t\tScan scan = new Scan();\n\t\tThread thread = new Thread(scan);\n\t\tthread.setName(\"scan-thread\");\n\t\tthread.start();\n\t}\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/config/AppConfiguration.java",
    "content": "package com.crossoverjie.cim.client.config;\n\nimport lombok.Data;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Component;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/8/24 01:43\n * @since JDK 1.8\n */\n@Component\n@Data\npublic class AppConfiguration {\n\n    @Value(\"${cim.user.id}\")\n    private Long userId;\n\n    @Value(\"${cim.user.userName}\")\n    private String userName;\n\n    @Value(\"${cim.msg.logger.path}\")\n    private String msgLoggerPath;\n\n    @Value(\"${cim.heartbeat.time}\")\n    private long heartBeatTime;\n\n    @Value(\"${cim.reconnect.count}\")\n    private int reconnectCount;\n\n    @Value(\"${cim.route.url}\")\n    private String routeUrl;\n    @Value(\"${cim.callback.thread.queue.size}\")\n    private int queueSize;\n    @Value(\"${cim.callback.thread.pool.size}\")\n    private int poolSize;\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/config/BeanConfig.java",
    "content": "package com.crossoverjie.cim.client.config;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;\nimport com.crossoverjie.cim.client.sdk.io.backoff.RandomBackoff;\nimport com.crossoverjie.cim.client.service.MsgLogger;\nimport com.crossoverjie.cim.client.service.ShutDownSign;\nimport com.crossoverjie.cim.client.service.impl.MsgCallBackListener;\nimport com.crossoverjie.cim.common.data.construct.RingBufferWheel;\nimport com.google.common.util.concurrent.ThreadFactoryBuilder;\nimport jakarta.annotation.Resource;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport okhttp3.OkHttpClient;\nimport org.springframework.beans.factory.annotation.Qualifier;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * Function:bean 配置\n *\n * @author crossoverJie\n * Date: 24/05/2018 15:55\n * @since JDK 1.8\n */\n@Configuration\npublic class BeanConfig {\n\n    @Resource\n    private AppConfiguration appConfiguration;\n\n    @Resource\n    private ShutDownSign shutDownSign;\n\n    @Resource\n    private MsgLogger msgLogger;\n\n\n    @Bean\n    public Client buildClient(@Qualifier(\"callBackThreadPool\") ThreadPoolExecutor callbackThreadPool,\n                              Event event) {\n        OkHttpClient okHttpClient = new OkHttpClient.Builder().connectTimeout(3, TimeUnit.SECONDS)\n                .readTimeout(3, TimeUnit.SECONDS)\n                .writeTimeout(3, TimeUnit.SECONDS)\n                .retryOnConnectionFailure(true).build();\n\n        return Client.builder()\n                .auth(ClientConfigurationData.Auth.builder()\n                        .userName(appConfiguration.getUserName())\n                        .userId(appConfiguration.getUserId())\n                        .build())\n                .routeUrl(appConfiguration.getRouteUrl())\n                .loginRetryCount(appConfiguration.getReconnectCount())\n                .event(event)\n                .reconnectCheck(client -> !shutDownSign.checkStatus())\n                .okHttpClient(okHttpClient)\n                .messageListener(new MsgCallBackListener(msgLogger, event))\n                .callbackThreadPool(callbackThreadPool)\n                .backoffStrategy(new RandomBackoff())\n                .build();\n    }\n\n    /**\n     * http client\n     *\n     * @return okHttp\n     */\n    @Bean\n    public OkHttpClient okHttpClient() {\n        OkHttpClient.Builder builder = new OkHttpClient.Builder();\n        builder.connectTimeout(3, TimeUnit.SECONDS)\n                .readTimeout(3, TimeUnit.SECONDS)\n                .writeTimeout(3, TimeUnit.SECONDS)\n                .retryOnConnectionFailure(true);\n        return builder.build();\n    }\n\n\n    /**\n     * Create callback thread pool\n     *\n     * @return\n     */\n    @Bean(\"callBackThreadPool\")\n    public ThreadPoolExecutor buildCallerThread() {\n        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(appConfiguration.getQueueSize());\n        ThreadFactory executor = new ThreadFactoryBuilder()\n                .setNameFormat(\"msg-callback-%d\")\n                .setDaemon(true)\n                .build();\n        return new ThreadPoolExecutor(appConfiguration.getPoolSize(), appConfiguration.getPoolSize(), 1,\n                TimeUnit.MILLISECONDS, queue, executor);\n    }\n\n\n    @Bean\n    public RingBufferWheel bufferWheel() {\n        ExecutorService executorService = Executors.newFixedThreadPool(2);\n        return new RingBufferWheel(executorService);\n    }\n\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/config/SwaggerConfig.java",
    "content": "package com.crossoverjie.cim.client.config;\n\nimport io.swagger.v3.oas.models.OpenAPI;\nimport io.swagger.v3.oas.models.info.Contact;\nimport io.swagger.v3.oas.models.info.Info;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n\n@Configuration\npublic class SwaggerConfig {\n\n    @Bean\n    public OpenAPI createRestApi() {\n        return new OpenAPI()\n                .info(apiInfo());\n    }\n\n    private Info apiInfo() {\n        return new Info()\n                .title(\"cim client\")\n                .description(\"cim client api\")\n                .termsOfService(\"http://crossoverJie.top\")\n                .contact(contact())\n                .version(\"1.0.0\");\n    }\n\n    private Contact contact() {\n        Contact contact = new Contact();\n        contact.setName(\"crossoverJie\");\n        return contact;\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/scanner/Scan.java",
    "content": "package com.crossoverjie.cim.client.scanner;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.service.MsgHandle;\nimport com.crossoverjie.cim.client.service.MsgLogger;\nimport com.crossoverjie.cim.client.util.SpringBeanFactory;\nimport java.util.Scanner;\nimport lombok.SneakyThrows;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/12/21 16:44\n * @since JDK 1.8\n */\npublic class Scan implements Runnable {\n\n\n    private final MsgHandle msgHandle;\n\n    private final MsgLogger msgLogger;\n    private final Event event;\n\n    public Scan() {\n        this.msgHandle = SpringBeanFactory.getBean(MsgHandle.class);\n        this.msgLogger = SpringBeanFactory.getBean(MsgLogger.class);\n        this.event = SpringBeanFactory.getBean(Event.class);\n    }\n\n    @SneakyThrows\n    @Override\n    public void run() {\n        Scanner sc = new Scanner(System.in);\n        while (true) {\n            String msg = sc.nextLine();\n\n            if (msgHandle.checkMsg(msg)) {\n                continue;\n            }\n\n            // internal cmd\n            if (msgHandle.innerCommand(msg)) {\n                continue;\n            }\n\n            msgHandle.sendMsg(msg);\n\n            // write to log\n            msgLogger.log(msg);\n\n            event.info(msg);\n        }\n    }\n\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/InnerCommand.java",
    "content": "package com.crossoverjie.cim.client.service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:26\n * @since JDK 1.8\n */\npublic interface InnerCommand {\n\n    /**\n     * 执行\n     * @param msg\n     */\n    void process(String msg) throws Exception;\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/InnerCommandContext.java",
    "content": "package com.crossoverjie.cim.client.service;\n\nimport com.crossoverjie.cim.client.service.impl.command.PrintAllCommand;\nimport com.crossoverjie.cim.client.util.SpringBeanFactory;\nimport com.crossoverjie.cim.common.enums.SystemCommandEnum;\nimport com.crossoverjie.cim.common.util.StringUtil;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\n\nimport java.util.Map;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:39\n * @since JDK 1.8\n */\n@Slf4j\n@Component\npublic class InnerCommandContext {\n\n    /**\n     * 获取执行器实例\n     * @param command 执行器实例\n     * @return\n     */\n    public InnerCommand getInstance(String command) {\n\n        Map<String, String> allClazz = SystemCommandEnum.getAllClazz();\n\n        //兼容需要命令后接参数的数据 :q cross\n        String[] trim = command.trim().split(\" \");\n        String clazz = allClazz.get(trim[0]);\n        InnerCommand innerCommand = null;\n        try {\n            if (StringUtil.isEmpty(clazz)) {\n                clazz = PrintAllCommand.class.getName();\n            }\n            innerCommand = (InnerCommand) SpringBeanFactory.getBean(Class.forName(clazz));\n        } catch (Exception e) {\n            log.error(\"Exception\", e);\n        }\n\n        return innerCommand;\n    }\n\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/MsgHandle.java",
    "content": "package com.crossoverjie.cim.client.service;\n\n/**\n * Function:消息处理器\n *\n * @author crossoverJie\n * Date: 2018/12/26 11:11\n * @since JDK 1.8\n */\npublic interface MsgHandle {\n\n    /**\n     * 统一的发送接口，包含了 groupChat p2pChat\n     *\n     * @param msg\n     */\n    void sendMsg(String msg) throws Exception;\n\n\n    /**\n     * 校验消息\n     *\n     * @param msg\n     * @return 不能为空，后续可以加上一些敏感词\n     * @throws Exception\n     */\n    boolean checkMsg(String msg);\n\n    /**\n     * 执行内部命令\n     *\n     * @param msg\n     * @return 是否应当跳过当前消息（包含了\":\" 就需要跳过）\n     */\n    boolean innerCommand(String msg) throws Exception;\n\n\n    /**\n     * 开启 AI 模式\n     */\n    void openAIModel();\n\n    /**\n     * 关闭 AI 模式\n     */\n    void closeAIModel();\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/MsgLogger.java",
    "content": "package com.crossoverjie.cim.client.service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2019/1/6 15:23\n * @since JDK 1.8\n */\npublic interface MsgLogger {\n\n    /**\n     * write log\n     * @param msg\n     */\n    void log(String msg);\n\n\n    /**\n     * 停止写入\n     */\n    void stop();\n\n    /**\n     * 查询聊天记录\n     * @param key 关键字\n     * @return\n     */\n    String query(String key);\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/ShutDownSign.java",
    "content": "package com.crossoverjie.cim.client.service;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-02-27 16:17\n * @since JDK 1.8\n */\n@Component\npublic class ShutDownSign {\n    private boolean isCommand;\n\n    /**\n     * Set user exit sign.\n     */\n    public void shutdown() {\n        isCommand = true;\n    }\n\n    public boolean checkStatus() {\n        return isCommand;\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/AsyncMsgLogger.java",
    "content": "package com.crossoverjie.cim.client.service.impl;\n\nimport com.crossoverjie.cim.client.config.AppConfiguration;\nimport com.crossoverjie.cim.client.service.MsgLogger;\nimport jakarta.annotation.Resource;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Collections;\nimport lombok.Cleanup;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.LinkOption;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardOpenOption;\nimport java.time.LocalDate;\nimport java.util.List;\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.stream.Stream;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2019/1/6 15:26\n * @since JDK 1.8\n */\n@Slf4j\n@Service\npublic class AsyncMsgLogger implements MsgLogger {\n\n\n    /**\n     * The default buffer size.\n     */\n    private static final int DEFAULT_QUEUE_SIZE = 16;\n    private final BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(DEFAULT_QUEUE_SIZE);\n\n    private volatile boolean started = false;\n    private final Worker worker = new Worker();\n\n    @Resource\n    private AppConfiguration appConfiguration;\n\n    @Override\n    public void log(String msg) {\n        // start worker\n        startMsgLogger();\n        try {\n            // TODO: 2019/1/6 消息堆满是否阻塞线程？\n            blockingQueue.put(msg);\n        } catch (InterruptedException e) {\n            log.error(\"InterruptedException\", e);\n        }\n    }\n\n    private class Worker extends Thread {\n\n\n        @Override\n        public void run() {\n            while (started) {\n                try {\n                    String msg = blockingQueue.take();\n                    writeLog(msg);\n                } catch (InterruptedException e) {\n                    break;\n                }\n            }\n        }\n\n    }\n\n\n    private void writeLog(String msg) {\n\n        LocalDate today = LocalDate.now();\n        int year = today.getYear();\n        int month = today.getMonthValue();\n        int day = today.getDayOfMonth();\n\n        String dir = appConfiguration.getMsgLoggerPath() + appConfiguration.getUserName() + \"/\";\n        String fileName = dir + year + month + day + \".log\";\n\n        Path file = Paths.get(fileName);\n        boolean exists = Files.exists(Paths.get(dir), LinkOption.NOFOLLOW_LINKS);\n        try {\n            if (!exists) {\n                Files.createDirectories(Paths.get(dir));\n            }\n\n            List<String> lines = Collections.singletonList(msg);\n\n            Files.write(file, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);\n        } catch (IOException e) {\n            log.info(\"IOException\", e);\n        }\n\n    }\n\n    /**\n     * Begin worker\n     */\n    private void startMsgLogger() {\n        if (started) {\n            return;\n        }\n\n        worker.setDaemon(true);\n        worker.setName(\"AsyncMsgLogger-Worker\");\n        started = true;\n        worker.start();\n    }\n\n\n    @Override\n    public void stop() {\n        started = false;\n        worker.interrupt();\n    }\n\n    @Override\n    public String query(String key) {\n        StringBuilder sb = new StringBuilder();\n\n        Path path = Paths.get(appConfiguration.getMsgLoggerPath() + appConfiguration.getUserName() + \"/\");\n\n        try {\n            @Cleanup\n            Stream<Path> list = Files.list(path);\n            List<Path> collect = list.toList();\n            for (Path file : collect) {\n                List<String> strings = Files.readAllLines(file);\n                for (String msg : strings) {\n                    if (msg.trim().contains(key)) {\n                        sb.append(msg).append(\"\\n\");\n                    }\n                }\n\n            }\n        } catch (IOException e) {\n            log.info(\"IOException\", e);\n        }\n\n        return sb.toString().replace(key, \"\\033[31;4m\" + key + \"\\033[0m\");\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/EchoServiceImpl.java",
    "content": "package com.crossoverjie.cim.client.service.impl;\n\nimport com.crossoverjie.cim.client.config.AppConfiguration;\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.service.MsgLogger;\nimport com.vdurmont.emoji.EmojiParser;\nimport jakarta.annotation.Resource;\nimport java.time.LocalDate;\nimport java.time.LocalTime;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-08-27 22:37\n * @since JDK 1.8\n */\n@Service\npublic class EchoServiceImpl implements Event {\n\n    private static final String PREFIX = \"$\";\n\n    @Resource\n    private AppConfiguration appConfiguration;\n\n    @Resource\n    private MsgLogger msgLogger;\n\n    @Override\n    public void debug(String msg, Object... replace) {\n        msgLogger.log(String.format(\"Debug[%s]\", msg));\n    }\n\n    @Override\n    public void info(String msg, Object... replace) {\n        // Make terminal can display the emoji\n        msg = EmojiParser.parseToUnicode(msg);\n        String date = LocalDate.now() + \" \" + LocalTime.now().withNano(0).toString();\n\n        msg = \"[\" + date + \"] \\033[31;4m\" + appConfiguration.getUserName() + PREFIX + \"\\033[0m\" + \" \" + msg;\n\n        String log = print(msg, replace);\n\n        System.out.println(log);\n    }\n\n    @Override\n    public void warn(String msg, Object... replace) {\n        info(String.format(\"Warn##%s##\", msg), replace);\n    }\n\n    @Override\n    public void error(String msg, Object... replace) {\n        info(String.format(\"Error!!%s!!\", msg), replace);\n    }\n\n    @Override\n    public void fatal(Client client) {\n        info(\"{} fatal error, shutdown client\", client.getAuth());\n    }\n\n\n    /**\n     * print msg\n     *\n     * @param msg\n     * @param place\n     * @return\n     */\n    private String print(String msg, Object... place) {\n        StringBuilder sb = new StringBuilder();\n        int k = 0;\n        for (int i = 0; i < place.length; i++) {\n            int index = msg.indexOf(\"{}\", k);\n\n            if (index == -1) {\n                return msg;\n            }\n\n            if (index != 0) {\n                sb.append(msg, k, index);\n                sb.append(place[i]);\n\n                if (place.length == 1) {\n                    sb.append(msg, index + 2, msg.length());\n                }\n\n            } else {\n                sb.append(place[i]);\n                if (place.length == 1) {\n                    sb.append(msg, index + 2, msg.length());\n                }\n            }\n\n            k = index + 2;\n        }\n        if (sb.toString().equals(\"\")) {\n            return msg;\n        } else {\n            return sb.toString();\n        }\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/MsgCallBackListener.java",
    "content": "package com.crossoverjie.cim.client.service.impl;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.sdk.io.MessageListener;\nimport com.crossoverjie.cim.client.service.MsgLogger;\nimport com.crossoverjie.cim.common.constant.Constants;\nimport java.util.Map;\n\n/**\n * Function:自定义收到消息回调\n *\n * @author crossoverJie\n * Date: 2019/1/6 17:49\n * @since JDK 1.8\n */\npublic class MsgCallBackListener implements MessageListener {\n\n\n    private final MsgLogger msgLogger;\n    private final Event event;\n\n    public MsgCallBackListener(MsgLogger msgLogger, Event event) {\n        this.msgLogger = msgLogger;\n        this.event = event;\n    }\n\n\n    @Override\n    public void received(Client client, Map<String, String> properties, String msg) {\n        String sendUserName = properties.getOrDefault(Constants.MetaKey.SEND_USER_NAME, \"nobody\");\n        this.msgLogger.log(sendUserName + \":\" + msg);\n        this.event.info(sendUserName + \":\" + msg);\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/MsgHandler.java",
    "content": "package com.crossoverjie.cim.client.service.impl;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimport com.crossoverjie.cim.client.service.InnerCommandContext;\nimport com.crossoverjie.cim.client.service.MsgHandle;\nimport com.crossoverjie.cim.common.util.StringUtil;\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2018/12/26 11:15\n * @since JDK 1.8\n */\n@Slf4j\n@Service\npublic class MsgHandler implements MsgHandle {\n\n\n    @Resource\n    private InnerCommandContext innerCommandContext;\n\n    @Resource\n    private Client client;\n\n    private boolean aiModel = false;\n\n    @Override\n    public void sendMsg(String msg) throws Exception {\n        if (aiModel) {\n            aiChat(msg);\n        } else {\n            normalChat(msg);\n        }\n    }\n\n    private void normalChat(String msg) throws Exception {\n        String[] totalMsg = msg.split(\";;\");\n        if (totalMsg.length > 1) {\n            P2PReqVO p2PReqVO = new P2PReqVO();\n            p2PReqVO.setReceiveUserId(Long.parseLong(totalMsg[0]));\n            p2PReqVO.setMsg(totalMsg[1]);\n            client.sendP2P(p2PReqVO);\n\n        } else {\n            client.sendGroup(msg);\n        }\n    }\n\n    /**\n     * AI model\n     *\n     * @param msg\n     */\n    private void aiChat(String msg) {\n        msg = msg.replace(\"吗\", \"\");\n        msg = msg.replace(\"嘛\", \"\");\n        msg = msg.replace(\"?\", \"!\");\n        msg = msg.replace(\"？\", \"!\");\n        msg = msg.replace(\"你\", \"我\");\n        System.out.println(\"AI:\\033[31;4m\" + msg + \"\\033[0m\");\n    }\n\n    @Override\n    public boolean checkMsg(String msg) {\n        if (StringUtil.isEmpty(msg)) {\n            log.warn(\"不能发送空消息！\");\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public boolean innerCommand(String msg) throws Exception {\n\n        if (msg.startsWith(\":\")) {\n\n            InnerCommand instance = innerCommandContext.getInstance(msg);\n            instance.process(msg);\n\n            return true;\n\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public void openAIModel() {\n        aiModel = true;\n    }\n\n    @Override\n    public void closeAIModel() {\n        aiModel = false;\n    }\n\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/CloseAIModelCommand.java",
    "content": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimport com.crossoverjie.cim.client.service.MsgHandle;\nimport jakarta.annotation.Resource;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:37\n * @since JDK 1.8\n */\n@Service\npublic class CloseAIModelCommand implements InnerCommand {\n\n\n    @Resource\n    private MsgHandle msgHandle;\n\n    @Resource\n    private Event event;\n\n    @Override\n    public void process(String msg) {\n        msgHandle.closeAIModel();\n\n        event.info(\"\\033[31;4m\" + \"｡ﾟ(ﾟ´ω`ﾟ)ﾟ｡  AI 下线了！\" + \"\\033[0m\");\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/DelayMsgCommand.java",
    "content": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimport com.crossoverjie.cim.client.service.MsgHandle;\nimport com.crossoverjie.cim.common.data.construct.RingBufferWheel;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-09-25 00:37\n * @since JDK 1.8\n */\n@Service\n@Slf4j\npublic class DelayMsgCommand implements InnerCommand {\n\n    @Resource\n    private Event event;\n\n    @Resource\n    private MsgHandle msgHandle;\n\n    @Resource\n    private RingBufferWheel ringBufferWheel;\n\n    @Override\n    public void process(String msg) {\n        if (msg.split(\" \").length <= 2) {\n            event.info(\"incorrect commond, :delay [msg] [delayTime]\");\n            return;\n        }\n\n        String message = msg.split(\" \")[1];\n        int delayTime = Integer.parseInt(msg.split(\" \")[2]);\n\n        RingBufferWheel.Task task = new DelayMsgJob(message);\n        task.setKey(delayTime);\n        ringBufferWheel.addTask(task);\n        event.info(msg);\n    }\n\n\n\n    private class DelayMsgJob extends RingBufferWheel.Task {\n\n        private String msg;\n\n        public DelayMsgJob(String msg) {\n            this.msg = msg;\n        }\n\n        @Override\n        public void run() {\n            try {\n                msgHandle.sendMsg(msg);\n            } catch (Exception e) {\n                log.error(\"Delay message send error\", e);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/EchoInfoCommand.java",
    "content": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimport jakarta.annotation.Resource;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:37\n * @since JDK 1.8\n */\n@Service\npublic class EchoInfoCommand implements InnerCommand {\n\n    @Resource\n    private Client client;\n\n    @Resource\n    private Event event;\n\n    @Override\n    public void process(String msg) {\n        event.info(\"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\");\n        event.info(\"client info={}\", client.getAuth());\n        event.info(\"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\");\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/EmojiCommand.java",
    "content": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimport com.vdurmont.emoji.Emoji;\nimport com.vdurmont.emoji.EmojiManager;\nimport com.vdurmont.emoji.EmojiParser;\nimport jakarta.annotation.Resource;\nimport java.util.List;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:37\n * @since JDK 1.8\n */\n@Service\npublic class EmojiCommand implements InnerCommand {\n\n    @Resource\n    private Event event;\n\n\n    @Override\n    public void process(String msg) {\n        if (msg.split(\" \").length <= 1) {\n            event.info(\"incorrect commond, :emoji [option]\");\n            return;\n        }\n        String value = msg.split(\" \")[1];\n        if (value != null) {\n            int index = Integer.parseInt(value);\n            List<Emoji> all = (List<Emoji>) EmojiManager.getAll();\n            all = all.subList(5 * index, 5 * index + 5);\n\n            for (Emoji emoji : all) {\n                event.info(EmojiParser.parseToAliases(emoji.getUnicode()) + \"--->\" + emoji.getUnicode());\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/OpenAIModelCommand.java",
    "content": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimport com.crossoverjie.cim.client.service.MsgHandle;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:37\n * @since JDK 1.8\n */\n@Service\npublic class OpenAIModelCommand implements InnerCommand {\n\n\n    @Autowired\n    private MsgHandle msgHandle;\n\n    @Override\n    public void process(String msg) {\n        msgHandle.openAIModel();\n        System.out.println(\"\\033[31;4m\" + \"Hello,我是估值两亿的 AI 机器人！\" + \"\\033[0m\");\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrefixSearchCommand.java",
    "content": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimport com.crossoverjie.cim.common.data.construct.TrieTree;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport jakarta.annotation.Resource;\nimport java.util.List;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:37\n * @since JDK 1.8\n */\n@Slf4j\n@Service\npublic class PrefixSearchCommand implements InnerCommand {\n\n\n    @Resource\n    private Client client;\n    @Resource\n    private Event event;\n\n    @Override\n    public void process(String msg) {\n        try {\n            Set<CIMUserInfo> onlineUsers = client.getOnlineUser();\n            TrieTree trieTree = new TrieTree();\n            for (CIMUserInfo onlineUser : onlineUsers) {\n                trieTree.insert(onlineUser.getUserName());\n            }\n\n            String[] split = msg.split(\" \");\n            String key = split[1];\n            List<String> list = trieTree.prefixSearch(key);\n\n            for (String res : list) {\n                res = res.replace(key, \"\\033[31;4m\" + key + \"\\033[0m\");\n                event.info(res);\n            }\n\n        } catch (Exception e) {\n            log.error(\"Exception\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrintAllCommand.java",
    "content": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimport com.crossoverjie.cim.common.enums.SystemCommandEnum;\nimport jakarta.annotation.Resource;\nimport java.util.Map;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:37\n * @since JDK 1.8\n */\n@Service\npublic class PrintAllCommand implements InnerCommand {\n\n\n    @Resource\n    private Event event;\n\n    @Override\n    public void process(String msg) {\n        Map<String, String> allStatusCode = SystemCommandEnum.getAllStatusCode();\n        event.info(\"====================================\");\n        for (Map.Entry<String, String> stringStringEntry : allStatusCode.entrySet()) {\n            String key = stringStringEntry.getKey();\n            String value = stringStringEntry.getValue();\n            event.info(key + \"----->\" + value);\n        }\n        event.info(\"====================================\");\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrintOnlineUsersCommand.java",
    "content": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport jakarta.annotation.Resource;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:37\n * @since JDK 1.8\n */\n@Slf4j\n@Service\npublic class PrintOnlineUsersCommand implements InnerCommand {\n\n    @Resource\n    private Client client;\n\n    @Resource\n    private Event event;\n\n    @Override\n    public void process(String msg) {\n        try {\n            Set<CIMUserInfo> onlineUsers = client.getOnlineUser();\n\n            event.info(\"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\");\n            for (CIMUserInfo onlineUser : onlineUsers) {\n                event.info(\"userId={}=====userName={}\", onlineUser.getUserId(), onlineUser.getUserName());\n            }\n            event.info(\"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\");\n\n        } catch (Exception e) {\n            log.error(\"Exception\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/QueryHistoryCommand.java",
    "content": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimport com.crossoverjie.cim.client.service.MsgLogger;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:37\n * @since JDK 1.8\n */\n@Slf4j\n@Service\npublic class QueryHistoryCommand implements InnerCommand {\n\n    @Resource\n    private MsgLogger msgLogger;\n\n    @Resource\n    private Event event;\n\n    @Override\n    public void process(String msg) {\n        String[] split = msg.split(\" \");\n        if (split.length < 2) {\n            return;\n        }\n        String res = msgLogger.query(split[1]);\n        event.info(res);\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/ShutDownCommand.java",
    "content": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimport com.crossoverjie.cim.client.service.MsgLogger;\nimport com.crossoverjie.cim.client.service.ShutDownSign;\nimport com.crossoverjie.cim.common.data.construct.RingBufferWheel;\nimport jakarta.annotation.Resource;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:28\n * @since JDK 1.8\n */\n@Slf4j\n@Service\n@ConditionalOnWebApplication\npublic class ShutDownCommand implements InnerCommand {\n\n    @Resource\n    private Client cimClient;\n\n    @Resource\n    private MsgLogger msgLogger;\n\n    @Resource(name = \"callBackThreadPool\")\n    private ThreadPoolExecutor callBackExecutor;\n\n    @Resource\n    private Event event;\n\n    @Resource\n    private ShutDownSign shutDownSign;\n\n    @Resource\n    private RingBufferWheel ringBufferWheel;\n\n    @Override\n    public void process(String msg) throws Exception {\n        event.info(\"cim client closing...\");\n        cimClient.close();\n        shutDownSign.shutdown();\n        msgLogger.stop();\n        callBackExecutor.shutdown();\n        ringBufferWheel.stop(false);\n        try {\n            while (!callBackExecutor.awaitTermination(1, TimeUnit.SECONDS)) {\n                event.info(\"thread pool closing\");\n            }\n        } catch (Exception e) {\n            log.error(\"exception\", e);\n        }\n        event.info(\"cim close success!\");\n        System.exit(0);\n    }\n}\n"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/util/SpringBeanFactory.java",
    "content": "package com.crossoverjie.cim.client.util;\n\nimport org.springframework.beans.BeansException;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic final class SpringBeanFactory implements ApplicationContextAware {\n    private static ApplicationContext context;\n\n    public static <T> T getBean(Class<T> c) {\n        return context.getBean(c);\n    }\n\n\n    public static <T> T getBean(String name, Class<T> clazz) {\n        return context.getBean(name, clazz);\n    }\n\n    @Override\n    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\n        context = applicationContext;\n    }\n\n\n}\n"
  },
  {
    "path": "cim-client/src/main/resources/application.yaml",
    "content": "spring:\n  application:\n    name: cim-client\n\n# web port\nserver:\n  port: 8082\n\nlogging:\n  level:\n    root: error\n\n# enable swagger\nspringdoc:\n  swagger-ui:\n    enabled: true\n\n# log path\ncim:\n  msg:\n    logger:\n      path: /opt/logs/cim/\n  route:\n    url: http://localhost:8083 # route url suggested that this is Nginx address\n  user: # cim userId and userName\n    id: 1747309836375\n    userName: yuge\n  callback:\n    thread:\n      queue:\n        size: 10\n      pool:\n        size: 1\n  heartbeat:\n    time: 60 # cim heartbeat time (seconds)\n  reconnect:\n    count: 3\n"
  },
  {
    "path": "cim-client/src/main/resources/banner.txt",
    "content": "      _              ___          __\n ____(_)_ _     ____/ (_)__ ___  / /_\n/ __/ /  ' \\   / __/ / / -_) _ \\/ __/\n\\__/_/_/_/_/   \\__/_/_/\\__/_//_/\\__/\n Power by @crossoverJie\n\n\n\n"
  },
  {
    "path": "cim-client/src/test/java/com/crossoverjie/cim/client/service/InnerCommandContextTest.java",
    "content": "package com.crossoverjie.cim.client.service;\n\nimport com.crossoverjie.cim.client.CIMClientApplication;\nimport com.crossoverjie.cim.common.enums.SystemCommandEnum;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.junit4.SpringRunner;\n\n@SpringBootTest(classes = CIMClientApplication.class)\n@RunWith(SpringRunner.class)\npublic class InnerCommandContextTest {\n\n    @Autowired\n    private InnerCommandContext context;\n\n    @Test\n    public void execute() throws Exception {\n        String msg = \":all\";\n        InnerCommand execute = context.getInstance(msg);\n        execute.process(msg);\n    }\n\n//    @Test\n    public void execute3() throws Exception {\n        // TODO: 2024/8/31 Integration test\n        String msg = SystemCommandEnum.ONLINE_USER.getCommandType();\n        InnerCommand execute = context.getInstance(msg);\n        execute.process(msg);\n    }\n\n    @Test\n    public void execute4() throws Exception {\n        String msg = \":q 天气\";\n        InnerCommand execute = context.getInstance(msg);\n        execute.process(msg);\n    }\n\n    @Test\n    public void execute5() throws Exception {\n        String msg = \":q crossoverJie\";\n        InnerCommand execute = context.getInstance(msg);\n        execute.process(msg);\n    }\n\n    @Test\n    public void execute6() throws Exception {\n        String msg = SystemCommandEnum.AI.getCommandType();\n        InnerCommand execute = context.getInstance(msg);\n        execute.process(msg);\n    }\n\n    @Test\n    public void execute7() throws Exception {\n        String msg = SystemCommandEnum.QAI.getCommandType();\n        InnerCommand execute = context.getInstance(msg);\n        execute.process(msg);\n    }\n\n//    @Test\n    public void execute8() throws Exception {\n        // TODO: 2024/8/31 Integration test\n        String msg = \":pu cross\";\n        InnerCommand execute = context.getInstance(msg);\n        execute.process(msg);\n    }\n\n    @Test\n    public void execute9() throws Exception {\n        String msg = SystemCommandEnum.INFO.getCommandType();\n        InnerCommand execute = context.getInstance(msg);\n        execute.process(msg);\n    }\n\n    @Test\n    public void execute10() throws Exception {\n        String msg = \"dsds\";\n        InnerCommand execute = context.getInstance(msg);\n        execute.process(msg);\n    }\n\n\n\n   // @Test\n    public void quit() throws Exception {\n        String msg = \":q!\";\n        InnerCommand execute = context.getInstance(msg);\n        execute.process(msg);\n    }\n}\n"
  },
  {
    "path": "cim-client/src/test/java/com/crossoverjie/cim/client/service/impl/AsyncMsgLoggerTest.java",
    "content": "package com.crossoverjie.cim.client.service.impl;\n\nimport com.crossoverjie.cim.client.CIMClientApplication;\nimport com.crossoverjie.cim.client.service.MsgLogger;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.test.context.junit4.SpringRunner;\n\nimport java.util.concurrent.TimeUnit;\n\n@SpringBootTest(classes = CIMClientApplication.class)\n@RunWith(SpringRunner.class)\npublic class AsyncMsgLoggerTest {\n\n\n\n    @Autowired\n    private MsgLogger msgLogger;\n\n    @Test\n    public void writeLog() throws Exception {\n        for (int i = 0; i < 10; i++) {\n            msgLogger.log(\"zhangsan:【asdsd】\" + i);\n        }\n\n        TimeUnit.SECONDS.sleep(2);\n    }\n\n\n\n    @Test\n    public void query() {\n        String crossoverJie = msgLogger.query(\"crossoverJie\");\n        System.out.println(crossoverJie);\n    }\n\n}\n"
  },
  {
    "path": "cim-client/src/test/java/com/crossoverjie/cim/server/test/CommonTest.java",
    "content": "package com.crossoverjie.cim.server.test;\n\n\nimport com.crossoverjie.cim.common.core.proxy.RpcProxyManager;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.common.res.BaseResponse;\nimport com.crossoverjie.cim.route.api.RouteApi;\nimport com.crossoverjie.cim.route.api.vo.req.LoginReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.vdurmont.emoji.EmojiParser;\nimport java.io.IOException;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Type;\nimport java.nio.charset.Charset;\nimport java.nio.file.Files;\nimport java.nio.file.LinkOption;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.nio.file.StandardOpenOption;\nimport java.time.LocalDate;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport okhttp3.OkHttpClient;\nimport org.junit.Test;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 22/05/2018 18:44\n * @since JDK 1.8\n */\n@Slf4j\npublic class CommonTest {\n\n\n\n\n    @Test\n    public void searchMsg2() {\n        StringBuilder sb = new StringBuilder();\n        String allMsg = \"于是在之前的基础上我完善了一些内容，先来看看这个项目的介绍吧：\\n\" +\n                \"\\n\" +\n                \"CIM(CROSS-IM) 一款面向开发者的 IM(即时通讯)系统；同时提供了一些组件帮助开发者构建一款属于自己可水平扩展的 IM 。\\n\" +\n                \"\\n\" +\n                \"借助 CIM 你可以实现以下需求：\";\n\n        String key = \"CIM\";\n\n        String[] split = allMsg.split(\"\\n\");\n        for (String msg : split) {\n            if (msg.trim().contains(key)) {\n                sb.append(msg).append(\"\\n\");\n            }\n        }\n        int pos = 0;\n\n        String result = sb.toString();\n\n        int count = 1;\n        int multiple = 2;\n        while ((pos = result.indexOf(key, pos)) >= 0) {\n\n            log.info(\"{},{}\",pos, pos + key.length());\n\n            pos += key.length();\n\n\n            count++;\n        }\n\n        System.out.println(sb.toString());\n        System.out.println(sb.toString().replace(key, \"\\033[31;4m\" + key + \"\\033[0m\"));\n    }\n\n    @Test\n    public void log() {\n        String msg = \"hahahdsadsd\";\n        LocalDate today = LocalDate.now();\n        int year = today.getYear();\n        int month = today.getMonthValue();\n        int day = today.getDayOfMonth();\n\n        String dir = \"/opt/logs/cim/zhangsan\" + \"/\";\n        String fileName = dir + year + month + day + \".log\";\n        log.info(\"fileName={}\", fileName);\n\n        Path file = Paths.get(fileName);\n        boolean exists = Files.exists(Paths.get(dir), LinkOption.NOFOLLOW_LINKS);\n        try {\n            if (!exists) {\n                Files.createDirectories(Paths.get(dir));\n            }\n\n            List<String> lines = Arrays.asList(msg);\n\n            Files.write(file, lines, Charset.forName(\"UTF-8\"), StandardOpenOption.CREATE, StandardOpenOption.APPEND);\n        } catch (IOException e) {\n            log.info(\"IOException\", e);\n        }\n\n    }\n\n    @Test\n    public void emoji() throws Exception {\n        String str = \"An :grinning:awesome :smiley:string &#128516;with a few :wink:emojis!\";\n        String result = EmojiParser.parseToUnicode(str);\n        System.out.println(result);\n\n\n        result = EmojiParser.parseToAliases(str);\n        System.out.println(result);\n//\n//        Collection<Emoji> all = EmojiManager.getAll();\n//        for (Emoji emoji : all) {\n//            System.out.println(EmojiParser.parseToAliases(emoji.getUnicode())  + \"--->\" + emoji.getUnicode() );\n//        }\n\n    }\n\n    @Test\n    public void emoji2() {\n        String emostring = \"😂\";\n\n        String faceWithTearsOfJoy = emostring.replaceAll(\"\\uD83D\\uDE02\", \"face with tears of joy\");\n        System.out.println(faceWithTearsOfJoy);\n\n        System.out.println(\"======\" + faceWithTearsOfJoy.replaceAll(\"face with tears of joy\",\"\\uD83D\\uDE02\"));\n    }\n\n//    @Test\n    public void deSerialize() throws Exception {\n        RouteApi routeApi = RpcProxyManager.create(RouteApi.class, \"http://localhost:8083\", new OkHttpClient());\n\n        BaseResponse<com.crossoverjie.cim.route.api.vo.res.CIMServerResVO> login =\n                routeApi.login(new LoginReqVO(1725722966520L, \"cj\"));\n        System.out.println(login.getDataBody());\n\n        BaseResponse<Set<CIMUserInfo>> setBaseResponse = routeApi.onlineUser();\n        log.info(\"setBaseResponse={}\",setBaseResponse.getDataBody());\n    }\n\n    @Test\n    public void json() throws JsonProcessingException, ClassNotFoundException {\n        String json = \"{\\\"code\\\":\\\"9000\\\",\\\"message\\\":\\\"成功\\\",\\\"reqNo\\\":null,\\\"dataBody\\\":{\\\"ip\\\":\\\"127.0.0.1\\\",\\\"cimServerPort\\\":11211,\\\"httpPort\\\":8081}}\";\n\n        ObjectMapper objectMapper = new ObjectMapper();\n        Class<?> generic = null;\n        for (Method declaredMethod : RouteApi.class.getDeclaredMethods()) {\n            if (declaredMethod.getName().equals(\"login\")) {\n                Type returnType = declaredMethod.getGenericReturnType();\n\n                // check if the return type is a parameterized type\n                if (returnType instanceof ParameterizedType) {\n                    ParameterizedType parameterizedType = (ParameterizedType) returnType;\n\n                    Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();\n\n                    for (Type typeArgument : actualTypeArguments) {\n                        System.out.println(\"generic: \" + typeArgument.getTypeName());\n                        generic = Class.forName(typeArgument.getTypeName());\n                        break;\n                    }\n                } else {\n                    System.out.println(\"not a generic type\");\n                }\n            }\n        }\n        BaseResponse<com.crossoverjie.cim.route.api.vo.res.CIMServerResVO> response = objectMapper.readValue(json,\n                objectMapper.getTypeFactory().constructParametricType(BaseResponse.class, generic));\n        System.out.println(response.getDataBody().getIp());\n    }\n\n\n    private static class Gen<T, R> {\n        private T t;\n        private R r;\n    }\n\n    interface TestInterface {\n        Gen<String, P2PReqVO> login();\n    }\n\n\n    @Test\n    public void test1() throws JsonProcessingException {\n        String json = \"{\\\"code\\\":\\\"200\\\",\\\"message\\\":\\\"Success\\\",\\\"reqNo\\\":null,\\\"dataBody\\\":[{\\\"userId\\\":\\\"123\\\",\\\"userName\\\":\\\"Alice\\\"}, {\\\"userId\\\":\\\"456\\\",\\\"userName\\\":\\\"Bob\\\"}]}\";\n\n        ObjectMapper objectMapper = new ObjectMapper();\n\n        // 获取 BaseResponse<Set<CIMUserInfo>> 的泛型参数\n        Type setType = getGenericTypeOfBaseResponse();\n\n        // 将泛型类型传递给 ObjectMapper 进行反序列化\n        BaseResponse<Set<CIMUserInfo>> response = objectMapper.readValue(json,\n                objectMapper.getTypeFactory().constructParametricType(BaseResponse.class, objectMapper.getTypeFactory().constructType(setType)));\n\n        System.out.println(\"Response Code: \" + response.getCode());\n        System.out.println(\"Online Users: \");\n        for (CIMUserInfo user : response.getDataBody()) {\n            System.out.println(\"User ID: \" + user.getUserId() + \", User Name: \" + user.getUserName());\n        }\n    }\n\n    // 通过反射获取 BaseResponse<Set<CIMUserInfo>> 中的泛型类型\n    public static Type getGenericTypeOfBaseResponse() {\n        // 这里模拟你需要处理的 BaseResponse<Set<CIMUserInfo>>\n        ParameterizedType baseResponseType = (ParameterizedType) new TypeReference<BaseResponse<Set<CIMUserInfo>>>() {}.getType();\n\n        // 获取 BaseResponse 的泛型参数，即 Set<CIMUserInfo>\n        Type[] actualTypeArguments = baseResponseType.getActualTypeArguments();\n\n        // 返回第一个泛型参数 (Set<CIMUserInfo>)\n        return actualTypeArguments[0];\n    }\n}\n"
  },
  {
    "path": "cim-client/src/test/java/com/crossoverjie/cim/server/test/EchoTest.java",
    "content": "package com.crossoverjie.cim.server.test;\n\nimport org.junit.Assert;\nimport org.junit.Test;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-08-28 01:47\n * @since JDK 1.8\n */\npublic class EchoTest {\n    @Test\n    public void echo() {\n        String msg = \"{} say,you {}\";\n        String[] place = {\"zhangsan\", \"haha\"};\n\n        String log = log(msg, place);\n        System.out.println(log);\n        Assert.assertEquals(log,\"zhangsan say,you haha\");\n    }\n\n    @Test\n    public void echo2() {\n        String msg = \"{} say,you {},zhangsan say {}\";\n        String[] place = {\"zhangsan\", \"haha\", \"nihao\"};\n\n        String log = log(msg, place);\n        System.out.println(log);\n        Assert.assertEquals(log,\"zhangsan say,you haha,zhangsan say nihao\");\n    }\n\n    @Test\n    public void echo3() {\n        String msg = \"see you {},zhangsan say\";\n        String[] place = {\"zhangsan\"};\n\n        String log = log(msg, place);\n        System.out.println(log);\n        Assert.assertEquals(log,\"see you zhangsan,zhangsan say\");\n    }\n    @Test\n    public void echo4() {\n        String msg = \"{}see you,zhangsan say\";\n        String[] place = {\"!!!\"};\n\n        String log = log(msg, place);\n        System.out.println(log);\n        Assert.assertEquals(log,\"!!!see you,zhangsan say\");\n    }\n    @Test\n    public void echo5() {\n        String msg = \"see you,zhangsan say{}\";\n        String[] place = {\"!!!\"};\n\n        String log = log(msg, place);\n        System.out.println(log);\n        Assert.assertEquals(log,\"see you,zhangsan say!!!\");\n    }\n\n    @Test\n    public void echo6() {\n        String msg = \"see you,zhangsan say\";\n        String[] place = {\"\"};\n\n        String log = log(msg, place);\n        System.out.println(log);\n        Assert.assertEquals(log,\"see you,zhangsan say\");\n    }\n\n    private String log(String msg, String... place) {\n        StringBuilder sb = new StringBuilder();\n        int k = 0;\n        for (int i = 0; i < place.length; i++) {\n            int index = msg.indexOf(\"{}\", k);\n\n            if (index == -1) {\n                return msg;\n            }\n\n            if (index != 0) {\n                sb.append(msg, k, index);\n                sb.append(place[i]);\n\n                if (place.length == 1) {\n                    sb.append(msg, index + 2, msg.length());\n                }\n\n            } else {\n                sb.append(place[i]);\n                if (place.length == 1) {\n                    sb.append(msg, index + 2, msg.length());\n                }\n            }\n\n            k = index + 2;\n        }\n        return sb.toString();\n    }\n}\n"
  },
  {
    "path": "cim-client/src/test/resources/application.yaml",
    "content": "spring:\n  application:\n    name: cim-client\n  main:\n    # this will not be used to create real spring context, because don't need this context in test case.\n    web-application-type: none\n\n# web port\nserver:\n  port: 8082\n\nlogging:\n  level:\n    root: error\n\n# enable swagger\nspringdoc:\n  swagger-ui:\n    enabled: true\n\n# log path\ncim:\n  msg:\n    logger:\n      path: /opt/logs/cim/\n  route:\n    url: http://localhost:8083 # route url suggested that this is Nginx address\n  user: # cim userId and userName\n    id: 1722343979085\n    userName: zhangsan\n  callback:\n    thread:\n      queue:\n        size: 1000\n      pool:\n        size: 2\n  heartbeat:\n    time: 60 # cim heartbeat time (seconds)\n  reconnect:\n    count: 3"
  },
  {
    "path": "cim-client-sdk/README.md",
    "content": "\n```java\n    var auth1 = ClientConfigurationData.Auth.builder()\n    .userId(id)\n    .userName(cj)\n    .build();\n    \n    Client client1 = Client.builder()\n                    .auth(auth1)\n                    .routeUrl(routeUrl)\n                    .build();\n    \n    ClientState.State state = client1.getState();\n    Awaitility.await().atMost(10, TimeUnit.SECONDS)\n    .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));\n    Optional<CIMServerResVO> serverInfo = client1.getServerInfo();\n    Assertions.assertTrue(serverInfo.isPresent());\n    \n    // send msg\n    String msg = \"hello\";\n    client1.sendGroup(msg);\n    \n    // get oline user\n    Set<CIMUserInfo> onlineUser = client1.getOnlineUser();\n```"
  },
  {
    "path": "cim-client-sdk/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n  <parent>\n    <groupId>com.crossoverjie.netty</groupId>\n    <artifactId>cim</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>cim-client-sdk</artifactId>\n\n  <properties>\n    <maven.compiler.source>17</maven.compiler.source>\n    <maven.compiler.target>17</maven.compiler.target>\n    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n  </properties>\n\n\n  <dependencies>\n    <dependency>\n      <groupId>com.crossoverjie.netty</groupId>\n      <artifactId>cim-common</artifactId>\n    </dependency>\n\n    <dependency>\n      <groupId>com.crossoverjie.netty</groupId>\n      <artifactId>cim-rout-api</artifactId>\n    </dependency>\n\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter</artifactId>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.junit.vintage</groupId>\n      <artifactId>junit-vintage-engine</artifactId>\n      <scope>test</scope>\n    </dependency>\n    <dependency>\n      <groupId>com.crossoverjie.netty</groupId>\n      <artifactId>cim-integration-test</artifactId>\n      <version>${project.version}</version>\n      <scope>test</scope>\n    </dependency>\n  </dependencies>\n\n</project>"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/Client.java",
    "content": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientBuilderImpl;\nimport com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\nimport java.io.Closeable;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\n\npublic interface Client extends Closeable {\n\n    static ClientBuilder builder() {\n        return new ClientBuilderImpl();\n    }\n\n    default void sendP2P(P2PReqVO p2PReqVO) throws Exception {\n        sendP2PAsync(p2PReqVO).get();\n    }\n\n    CompletableFuture<Void> sendP2PAsync(P2PReqVO p2PReqVO);\n\n    default void sendGroup(String msg) throws Exception {\n        sendGroupAsync(msg).get();\n    }\n\n    CompletableFuture<Void> sendGroupAsync(String msg);\n\n    ClientState.State getState();\n\n    ClientConfigurationData.Auth getAuth();\n\n    Set<CIMUserInfo> getOnlineUser() throws Exception;\n\n    Optional<CIMServerResVO> getServerInfo();\n\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ClientBuilder.java",
    "content": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;\nimport com.crossoverjie.cim.client.sdk.io.MessageListener;\nimport com.crossoverjie.cim.client.sdk.io.ReconnectCheck;\nimport java.util.concurrent.ThreadPoolExecutor;\n\nimport com.crossoverjie.cim.client.sdk.io.backoff.BackoffStrategy;\nimport okhttp3.OkHttpClient;\n\n/**\n * @author crossoverJie\n */\npublic interface ClientBuilder {\n\n    Client build();\n    ClientBuilder auth(ClientConfigurationData.Auth auth);\n    ClientBuilder routeUrl(String routeUrl);\n    ClientBuilder loginRetryCount(int loginRetryCount);\n    ClientBuilder event(Event event);\n    ClientBuilder reconnectCheck(ReconnectCheck reconnectCheck);\n    ClientBuilder okHttpClient(OkHttpClient okHttpClient);\n    ClientBuilder messageListener(MessageListener messageListener);\n    ClientBuilder callbackThreadPool(ThreadPoolExecutor callbackThreadPool);\n    ClientBuilder backoffStrategy(BackoffStrategy backoffStrategy);\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ClientState.java",
    "content": "package com.crossoverjie.cim.client.sdk;\n\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic abstract class ClientState {\n\n    private static final AtomicReference<State> STATE = new AtomicReference<>(State.Initialized);\n\n    public enum State {\n        /**\n         * Client state\n         */\n        Initialized, Reconnecting, Ready, Closed, Failed\n    }\n\n    public void setState(State s) {\n        STATE.set(s);\n    }\n\n    public State getState() {\n        return STATE.get();\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/Event.java",
    "content": "package com.crossoverjie.cim.client.sdk;\n\npublic interface Event {\n    void debug(String msg, Object... replace);\n    void info(String msg, Object... replace);\n    void warn(String msg, Object... replace);\n    void error(String msg, Object... replace);\n    void fatal(Client client);\n\n    class DefaultEvent implements Event {\n        @Override\n        public void debug(String msg, Object... replace) {\n            System.out.println(msg);\n        }\n\n        @Override\n        public void info(String msg, Object... replace) {\n            System.out.println(msg);\n        }\n\n        @Override\n        public void warn(String msg, Object... replace) {\n            System.out.println(msg);\n        }\n\n        @Override\n        public void error(String msg, Object... replace) {\n            System.err.println(msg);\n        }\n\n        @Override\n        public void fatal(Client client) {\n\n        }\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/FetchOfflineMsgJob.java",
    "content": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;\nimport com.crossoverjie.cim.common.data.construct.RingBufferWheel;\n\npublic class FetchOfflineMsgJob extends RingBufferWheel.Task {\n    private static final int INITIAL_DELAY_SECONDS = 5;\n\n    private RouteManager routeManager;\n    private ClientConfigurationData conf;\n\n    public FetchOfflineMsgJob(RouteManager routeManager, ClientConfigurationData conf) {\n        this.routeManager = routeManager;\n        this.conf = conf;\n        setKey(INITIAL_DELAY_SECONDS); //It will be sent with a 5-second delay\n    }\n\n    @Override\n    public void run() {\n        routeManager.fetchOfflineMsgs(conf.getAuth().getUserId());\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ReConnectManager.java",
    "content": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientImpl;\nimport com.crossoverjie.cim.common.kit.HeartBeatHandler;\nimport com.google.common.util.concurrent.ThreadFactoryBuilder;\nimport io.netty.channel.ChannelHandlerContext;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.ScheduledThreadPoolExecutor;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.TimeUnit;\n\npublic final class ReConnectManager {\n\n    private ScheduledExecutorService scheduledExecutorService;\n\n    /**\n     * Trigger reconnect job\n     *\n     * @param ctx\n     */\n    public void reConnect(ChannelHandlerContext ctx) {\n        buildExecutor();\n        scheduledExecutorService.scheduleAtFixedRate(() -> {\n                    try {\n                        ClientImpl.getClient().getHeartBeatHandler().process(ctx);\n                    } catch (Exception e) {\n                        ClientImpl.getClient().getConf().getEvent().error(\"ReConnectManager reConnect error\", e);\n                    }\n                },\n                0, 10, TimeUnit.SECONDS);\n    }\n\n    /**\n     * Close reconnect job if reconnect success.\n     */\n    public void reConnectSuccess() {\n        scheduledExecutorService.shutdown();\n    }\n\n\n    /***\n     * build a thread executor\n     */\n    private void buildExecutor() {\n        if (scheduledExecutorService == null || scheduledExecutorService.isShutdown()) {\n            ThreadFactory factory = new ThreadFactoryBuilder()\n                    .setNameFormat(\"reConnect-job-%d\")\n                    .setDaemon(true)\n                    .build();\n            scheduledExecutorService = new ScheduledThreadPoolExecutor(1, factory);\n        }\n    }\n\n    private static class ClientHeartBeatHandle implements HeartBeatHandler {\n\n        @Override\n        public void process(ChannelHandlerContext ctx) throws Exception {\n            ClientImpl.getClient().reconnect();\n        }\n    }\n\n    public static ReConnectManager createReConnectManager() {\n        return new ReConnectManager();\n    }\n\n    public static HeartBeatHandler createHeartBeatHandler() {\n        return new ClientHeartBeatHandle();\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/RouteManager.java",
    "content": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientImpl;\nimport com.crossoverjie.cim.common.core.proxy.RpcProxyManager;\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.common.res.BaseResponse;\nimport com.crossoverjie.cim.common.res.NULLBody;\nimport com.crossoverjie.cim.route.api.RouteApi;\nimport com.crossoverjie.cim.route.api.vo.req.ChatReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.LoginReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.OfflineMsgReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport okhttp3.OkHttpClient;\n\npublic class RouteManager {\n\n\n    private final RouteApi routeApi;\n    private final Event event;\n\n    public RouteManager(String routeUrl, OkHttpClient okHttpClient, Event event) {\n        routeApi = RpcProxyManager.create(RouteApi.class, routeUrl, okHttpClient);\n        this.event = event;\n    }\n\n    public CIMServerResVO getServer(LoginReqVO loginReqVO) throws Exception {\n        BaseResponse<CIMServerResVO> cimServerResVO = routeApi.login(loginReqVO);\n\n        // repeat fail\n        if (!cimServerResVO.getCode().equals(StatusEnum.SUCCESS.getCode())) {\n            event.info(cimServerResVO.getMessage());\n\n            // when client in Reconnecting state, could exit.\n            if (ClientImpl.getClient().getState() == ClientState.State.Reconnecting) {\n                event.warn(\"###{}###\", StatusEnum.RECONNECT_FAIL.getMessage());\n                throw new CIMException(StatusEnum.RECONNECT_FAIL);\n            }\n        }\n        return cimServerResVO.getDataBody();\n    }\n\n    public CompletableFuture<Void> sendP2P(CompletableFuture<Void> future, P2PReqVO p2PReqVO) {\n        return CompletableFuture.runAsync(() -> {\n            try {\n                BaseResponse<NULLBody> response = routeApi.p2pRoute(p2PReqVO);\n                if (response.getCode().equals(StatusEnum.OFF_LINE.getCode())) {\n                    future.completeExceptionally(new CIMException(StatusEnum.OFF_LINE));\n                }\n                future.complete(null);\n            } catch (Exception e) {\n                future.completeExceptionally(e);\n                event.error(\"send p2p msg error\", e);\n            }\n        });\n    }\n\n    public CompletableFuture<Void> sendGroupMsg(ChatReqVO chatReqVO) {\n        return CompletableFuture.runAsync(() -> {\n            try {\n                routeApi.groupRoute(chatReqVO);\n            } catch (Exception e) {\n                event.error(\"send group msg error\", e);\n            }\n        });\n    }\n\n    public void offLine(Long userId) {\n        ChatReqVO vo = new ChatReqVO(userId, \"offLine\", null);\n        routeApi.offLine(vo);\n    }\n\n    public Set<CIMUserInfo> onlineUser() throws Exception {\n        BaseResponse<Set<CIMUserInfo>> onlineUsersResVO = routeApi.onlineUser();\n        return onlineUsersResVO.getDataBody();\n    }\n\n    public void fetchOfflineMsgs(Long userId) {\n        OfflineMsgReqVO offlineMsgReqVO = OfflineMsgReqVO.builder().receiveUserId(userId).build();\n        routeApi.fetchOfflineMsgs(offlineMsgReqVO);\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientBuilderImpl.java",
    "content": "package com.crossoverjie.cim.client.sdk.impl;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.cim.client.sdk.ClientBuilder;\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.sdk.io.MessageListener;\nimport com.crossoverjie.cim.client.sdk.io.ReconnectCheck;\nimport com.crossoverjie.cim.client.sdk.io.backoff.BackoffStrategy;\nimport com.crossoverjie.cim.common.util.StringUtil;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport okhttp3.OkHttpClient;\n\npublic class ClientBuilderImpl implements ClientBuilder {\n\n\n    private final ClientConfigurationData conf;\n\n    public ClientBuilderImpl() {\n        this(new ClientConfigurationData());\n    }\n    public ClientBuilderImpl(ClientConfigurationData conf) {\n        this.conf = conf;\n    }\n\n    @Override\n    public Client build() {\n        return new ClientImpl(conf);\n    }\n\n    @Override\n    public ClientBuilder auth(ClientConfigurationData.Auth auth) {\n        if (auth.getUserId() <= 0 || StringUtil.isEmpty(auth.getUserName())) {\n            throw new IllegalArgumentException(\"userId and userName must be set\");\n        }\n        this.conf.setAuth(auth);\n        return this;\n    }\n\n    @Override\n    public ClientBuilder routeUrl(String routeUrl) {\n        if (StringUtil.isEmpty(routeUrl)) {\n            throw new IllegalArgumentException(\"routeUrl must be set\");\n        }\n        this.conf.setRouteUrl(routeUrl);\n        return this;\n    }\n\n    @Override\n    public ClientBuilder loginRetryCount(int loginRetryCount) {\n        this.conf.setLoginRetryCount(loginRetryCount);\n        return this;\n    }\n\n    @Override\n    public ClientBuilder event(Event event) {\n        this.conf.setEvent(event);\n        return this;\n    }\n\n    @Override\n    public ClientBuilder reconnectCheck(ReconnectCheck reconnectCheck) {\n        this.conf.setReconnectCheck(reconnectCheck);\n        return this;\n    }\n\n    @Override\n    public ClientBuilder okHttpClient(OkHttpClient okHttpClient) {\n        this.conf.setOkHttpClient(okHttpClient);\n        return this;\n    }\n\n    @Override\n    public ClientBuilder messageListener(MessageListener messageListener) {\n        this.conf.setMessageListener(messageListener);\n        return this;\n    }\n\n    @Override\n    public ClientBuilder callbackThreadPool(ThreadPoolExecutor callbackThreadPool) {\n        this.conf.setCallbackThreadPool(callbackThreadPool);\n        return this;\n    }\n\n    @Override\n    public ClientBuilder backoffStrategy(BackoffStrategy backoffStrategy) {\n        this.conf.setBackoffStrategy(backoffStrategy);\n        return this;\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientConfigurationData.java",
    "content": "package com.crossoverjie.cim.client.sdk.impl;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim.client.sdk.io.backoff.BackoffStrategy;\nimport com.crossoverjie.cim.client.sdk.io.MessageListener;\nimport com.crossoverjie.cim.client.sdk.io.backoff.RandomBackoff;\nimport com.crossoverjie.cim.client.sdk.io.ReconnectCheck;\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport okhttp3.OkHttpClient;\n\n@Data\n@NoArgsConstructor\n@AllArgsConstructor\n@JsonIgnoreProperties(ignoreUnknown = true)\npublic class ClientConfigurationData {\n\n    private Auth auth;\n\n    @Data\n    @AllArgsConstructor\n    @Builder\n    public static class Auth {\n        private long userId;\n        private String userName;\n    }\n\n    private String routeUrl;\n    private int loginRetryCount = 5;\n\n    @JsonIgnore\n    private Event event = new Event.DefaultEvent();\n\n    @JsonIgnore\n    private MessageListener messageListener =\n            (client, properties, msg) -> System.out.printf(\"id:[%s] msg:[%s]%n \\n\", client.getAuth(), msg);\n\n    @JsonIgnore\n    private OkHttpClient okHttpClient = new OkHttpClient();\n\n    @JsonIgnore\n    private ThreadPoolExecutor callbackThreadPool;\n\n    @JsonIgnore\n    private ReconnectCheck reconnectCheck = (__) -> true;\n\n    @JsonIgnore\n    private BackoffStrategy backoffStrategy = new RandomBackoff();\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientImpl.java",
    "content": "package com.crossoverjie.cim.client.sdk.impl;\n\nimport static com.crossoverjie.cim.common.enums.StatusEnum.RECONNECT_FAIL;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.cim.client.sdk.ClientState;\nimport com.crossoverjie.cim.client.sdk.FetchOfflineMsgJob;\nimport com.crossoverjie.cim.client.sdk.ReConnectManager;\nimport com.crossoverjie.cim.client.sdk.RouteManager;\nimport com.crossoverjie.cim.client.sdk.io.CIMClientHandleInitializer;\nimport com.crossoverjie.cim.common.data.construct.RingBufferWheel;\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossoverjie.cim.common.kit.HeartBeatHandler;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.common.protocol.BaseCommand;\nimport com.crossoverjie.cim.common.protocol.Request;\nimport com.crossoverjie.cim.route.api.vo.req.ChatReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.LoginReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\nimport com.google.common.util.concurrent.ThreadFactoryBuilder;\nimport io.netty.bootstrap.Bootstrap;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.EventLoopGroup;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.SocketChannel;\nimport io.netty.channel.socket.nio.NioSocketChannel;\nimport io.netty.util.concurrent.DefaultThreadFactory;\n\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.function.Consumer;\nimport lombok.Getter;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\npublic class ClientImpl extends ClientState implements Client {\n\n    @Getter\n    private final ClientConfigurationData conf;\n    private static final int CALLBACK_QUEUE_SIZE = 1024;\n    private static final int CALLBACK_POOL_SIZE = 10;\n\n    // ======= private ========\n    private int errorCount;\n    private SocketChannel channel;\n\n    private final RouteManager routeManager;\n\n    @Getter\n    private final HeartBeatHandler heartBeatHandler = ReConnectManager.createHeartBeatHandler();\n    @Getter\n    private final ReConnectManager reConnectManager = ReConnectManager.createReConnectManager();\n\n    @Getter\n    private static ClientImpl client;\n    @Getter\n    private static Map<Long, ClientImpl> clientMap = new ConcurrentHashMap<>();\n    @Getter\n    private final Request heartBeatPacket;\n\n    private RingBufferWheel ringBufferWheel;\n\n    // Client connected server info\n    private CIMServerResVO serverInfo;\n\n    public ClientImpl(ClientConfigurationData conf) {\n        this.conf = conf;\n\n        if (this.conf.getCallbackThreadPool() == null) {\n            BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(CALLBACK_QUEUE_SIZE);\n            ThreadFactory factory = new ThreadFactoryBuilder()\n                    .setNameFormat(\"msg-callback-%d\")\n                    .setDaemon(true)\n                    .build();\n            this.conf.setCallbackThreadPool(\n                    new ThreadPoolExecutor(CALLBACK_POOL_SIZE, CALLBACK_POOL_SIZE, 1, TimeUnit.SECONDS, queue,\n                            factory));\n        }\n\n        routeManager = new RouteManager(conf.getRouteUrl(), conf.getOkHttpClient(), conf.getEvent());\n\n        heartBeatPacket = Request.newBuilder()\n                .setRequestId(this.conf.getAuth().getUserId())\n                .setReqMsg(\"ping\")\n                .setCmd(BaseCommand.PING)\n                .build();\n        client = this;\n        clientMap.put(conf.getAuth().getUserId(), this);\n\n        connectServer(v -> this.conf.getEvent().info(\"Login success!\"));\n\n        postConnectionSetup();\n    }\n\n    /**\n     * 1. Pull offline messages from the server\n     */\n    private void postConnectionSetup() {\n        ringBufferWheel = new RingBufferWheel(Executors.newFixedThreadPool(1));\n        ringBufferWheel.addTask(new FetchOfflineMsgJob(routeManager, conf));\n    }\n\n    private void connectServer(Consumer<Void> success) {\n        this.doConnectServer().whenComplete((r, e) -> {\n            if (r) {\n                success.accept(null);\n            }\n            if (e != null) {\n                if (e instanceof CIMException cimException && cimException.getErrorCode()\n                        .equals(RECONNECT_FAIL.getCode())) {\n                    this.conf.getEvent().fatal(this);\n                } else {\n                    if (errorCount++ >= this.conf.getLoginRetryCount()) {\n                        this.conf.getEvent()\n                                .error(\"The maximum number of reconnections has been reached[{}]times, exit cim client!\",\n                                        errorCount);\n                        this.conf.getEvent().fatal(this);\n                    }\n                }\n            }\n\n        });\n    }\n\n    /**\n     * 1. User login and get target server\n     * 2. Connect target server\n     * 3. send login cmd to server\n     */\n    private CompletableFuture<Boolean> doConnectServer() {\n        CompletableFuture<Boolean> future = new CompletableFuture<>();\n        this.userLogin(future).ifPresentOrElse((cimServer) -> {\n            this.doConnectServer(cimServer, future);\n            this.loginServer();\n            this.serverInfo = cimServer;\n            future.complete(true);\n        }, () -> {\n            this.conf.getEvent().error(\"Login fail!, cannot get server info!\");\n            this.conf.getEvent().fatal(this);\n            future.complete(false);\n        });\n        return future;\n    }\n\n    /**\n     * Login and get server info\n     *\n     * @return Server info\n     */\n    private Optional<CIMServerResVO> userLogin(CompletableFuture<Boolean> future) {\n        LoginReqVO loginReqVO = new LoginReqVO(conf.getAuth().getUserId(),\n                conf.getAuth().getUserName());\n\n        CIMServerResVO cimServer = null;\n        try {\n            cimServer = routeManager.getServer(loginReqVO);\n            log.info(\"cimServer=[{}]\", cimServer);\n        } catch (Exception e) {\n            log.error(\"login fail\", e);\n            future.completeExceptionally(e);\n        }\n        return Optional.ofNullable(cimServer);\n    }\n\n    private final EventLoopGroup group = new NioEventLoopGroup(0, new DefaultThreadFactory(\"cim-work\"));\n\n    private void doConnectServer(CIMServerResVO cimServer, CompletableFuture<Boolean> future) {\n        Bootstrap bootstrap = new Bootstrap();\n        bootstrap.group(group)\n                .channel(NioSocketChannel.class)\n                .handler(new CIMClientHandleInitializer());\n        ChannelFuture sync;\n        try {\n            sync = bootstrap.connect(cimServer.getIp(), cimServer.getCimServerPort()).sync();\n            if (sync.isSuccess()) {\n                this.conf.getEvent().info(\"Start cim client success!\");\n                channel = (SocketChannel) sync.channel();\n            }\n        } catch (InterruptedException e) {\n            future.completeExceptionally(e);\n        }\n    }\n\n    /**\n     * Send login cmd to server\n     */\n    private void loginServer() {\n        Request login = Request.newBuilder()\n                .setRequestId(this.conf.getAuth().getUserId())\n                .setReqMsg(this.conf.getAuth().getUserName())\n                .setCmd(BaseCommand.LOGIN_REQUEST)\n                .build();\n        channel.writeAndFlush(login)\n                .addListener((ChannelFutureListener) channelFuture ->\n                        this.conf.getEvent().info(\"Registry cim server success!\")\n                );\n    }\n\n    /**\n     * 1. clear route information.\n     * 2. reconnect.\n     * 3. shutdown reconnect job.\n     * 4. reset reconnect state.\n     * @throws Exception\n     */\n    public void reconnect() throws Exception {\n        if (channel != null && channel.isActive()) {\n            return;\n        }\n        this.serverInfo = null;\n        // clear route information.\n        this.routeManager.offLine(this.getConf().getAuth().getUserId());\n\n        this.conf.getEvent().info(\"cim trigger reconnecting....\");\n\n        this.conf.getBackoffStrategy().runBackoff();\n\n        // don't set State ready, because when connect success, the State will be set to ready automate.\n        connectServer(v -> {\n            this.reConnectManager.reConnectSuccess();\n            this.conf.getEvent().info(\"Great! reConnect success!!!\");\n        });\n    }\n\n    @Override\n    public void close() {\n        if (channel != null) {\n            channel.close();\n            channel = null;\n        }\n        super.setState(ClientState.State.Closed);\n        this.routeManager.offLine(this.getAuth().getUserId());\n        this.clientMap.remove(this.getAuth().getUserId());\n        ringBufferWheel.stop(true);\n    }\n\n    @Override\n    public void sendP2P(P2PReqVO p2PReqVO) throws Exception {\n        recordSendLog(sendP2PAsync(p2PReqVO), \"P2P\");\n    }\n\n    @Override\n    public void sendGroup(String msg) throws Exception {\n        recordSendLog(sendGroupAsync(msg), \"GROUP\");\n    }\n\n    private void recordSendLog(CompletableFuture<Void> future, String msgWay) {\n        future.orTimeout(10, TimeUnit.SECONDS)\n                .whenComplete((result, throwable) -> {\n                    if (throwable == null) {\n                        log.info(\"{} message task completed successfully\", msgWay);\n                    } else if (throwable instanceof TimeoutException) {\n                        log.error(\"{} message processing timeout\", msgWay, throwable);\n                    } else {\n                        log.warn(\"{} message task completed with exception\", msgWay, throwable);\n                    }\n                });\n    }\n\n    @Override\n    public CompletableFuture<Void> sendP2PAsync(P2PReqVO p2PReqVO) {\n        CompletableFuture<Void> future = new CompletableFuture<>();\n        p2PReqVO.setUserId(this.conf.getAuth().getUserId());\n        return routeManager.sendP2P(future, p2PReqVO);\n    }\n\n    @Override\n    public CompletableFuture<Void> sendGroupAsync(String msg) {\n        // TODO: 2024/9/12 return messageId\n        return this.routeManager.sendGroupMsg(new ChatReqVO(this.conf.getAuth().getUserId(), msg, null));\n    }\n\n    @Override\n    public ClientConfigurationData.Auth getAuth() {\n        return this.conf.getAuth();\n    }\n\n    @Override\n    public ClientState.State getState() {\n        return super.getState();\n    }\n\n    @Override\n    public Set<CIMUserInfo> getOnlineUser() throws Exception {\n        return routeManager.onlineUser();\n    }\n\n    @Override\n    public Optional<CIMServerResVO> getServerInfo() {\n        return Optional.ofNullable(this.serverInfo);\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/CIMClientHandle.java",
    "content": "package com.crossoverjie.cim.client.sdk.io;\n\nimport com.crossoverjie.cim.client.sdk.ClientState;\nimport com.crossoverjie.cim.client.sdk.impl.ClientImpl;\nimport com.crossoverjie.cim.common.constant.Constants;\nimport com.crossoverjie.cim.common.protocol.BaseCommand;\nimport com.crossoverjie.cim.common.protocol.Response;\nimport com.crossoverjie.cim.common.util.NettyAttrUtil;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.handler.timeout.IdleState;\nimport io.netty.handler.timeout.IdleStateEvent;\nimport lombok.extern.slf4j.Slf4j;\n\n@ChannelHandler.Sharable\n@Slf4j\npublic class CIMClientHandle extends SimpleChannelInboundHandler<Response> {\n\n    @Override\n    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {\n\n        if (evt instanceof IdleStateEvent idleStateEvent) {\n\n            if (idleStateEvent.state() == IdleState.WRITER_IDLE) {\n\n                ctx.writeAndFlush(ClientImpl.getClient().getHeartBeatPacket()).addListeners((ChannelFutureListener) future -> {\n                    if (!future.isSuccess()) {\n                        log.error(\"heart beat error,close Channel\");\n                        ClientImpl.getClient().getConf().getEvent().warn(\"heart beat error,close Channel\");\n                        future.channel().close();\n                    }\n                });\n            }\n\n        }\n\n        super.userEventTriggered(ctx, evt);\n    }\n\n    @Override\n    public void channelActive(ChannelHandlerContext ctx) {\n        ClientImpl.getClient().getConf().getEvent().debug(\"ChannelActive\");\n        ClientImpl.getClient().setState(ClientState.State.Ready);\n    }\n\n    @Override\n    public void channelInactive(ChannelHandlerContext ctx) {\n\n        if (!ClientImpl.getClient().getConf().getReconnectCheck().isNeedReconnect(ClientImpl.getClient())) {\n            return;\n        }\n        ClientImpl.getClient().setState(ClientState.State.Closed);\n\n        ClientImpl.getClient().getConf().getEvent().warn(\"Client inactive, let's reconnect\");\n        ClientImpl.getClient().getReConnectManager().reConnect(ctx);\n    }\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, Response msg) {\n        if (msg.getCmd() == BaseCommand.PING) {\n            ClientImpl.getClient().getConf().getEvent().debug(\"received ping from server\");\n            NettyAttrUtil.updateReaderTime(ctx.channel(), System.currentTimeMillis());\n        }\n\n        if (msg.getCmd() != BaseCommand.PING) {\n            String receiveUserId = msg.getPropertiesMap().get(Constants.MetaKey.RECEIVE_USER_ID);\n            ClientImpl client = ClientImpl.getClientMap().get(Long.valueOf(receiveUserId));\n            if (client == null) {\n                log.error(\"client not found for userId: {}\", receiveUserId);\n                return;\n            }\n            // callback\n            client.getConf().getCallbackThreadPool().execute(() -> {\n                log.info(\"client address: {} :{}\", ctx.channel().remoteAddress(), client);\n                MessageListener messageListener = client.getConf().getMessageListener();\n                if (msg.getBatchResMsgCount() > 0) {\n                    for (int i = 0; i < msg.getBatchResMsgCount(); i++) {\n                        messageListener.received(client, msg.getPropertiesMap(), msg.getBatchResMsg(i));\n                    }\n                } else {\n                    messageListener.received(client, msg.getPropertiesMap(), msg.getResMsg());\n                }\n            });\n        }\n\n    }\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        ClientImpl.getClient().getConf().getEvent().error(cause.getCause().toString());\n        ctx.close();\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/CIMClientHandleInitializer.java",
    "content": "package com.crossoverjie.cim.client.sdk.io;\n\nimport com.crossoverjie.cim.common.protocol.Response;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.handler.codec.protobuf.ProtobufDecoder;\nimport io.netty.handler.codec.protobuf.ProtobufEncoder;\nimport io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;\nimport io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;\nimport io.netty.handler.timeout.IdleStateHandler;\n\npublic class CIMClientHandleInitializer extends ChannelInitializer<Channel> {\n\n    private final CIMClientHandle cimClientHandle = new CIMClientHandle();\n\n    @Override\n    protected void initChannel(Channel ch) {\n        ch.pipeline()\n                .addLast(new IdleStateHandler(0, 10, 0))\n\n                // google Protobuf\n                .addLast(new ProtobufVarint32FrameDecoder())\n                .addLast(new ProtobufDecoder(Response.getDefaultInstance()))\n                .addLast(new ProtobufVarint32LengthFieldPrepender())\n                .addLast(new ProtobufEncoder())\n                .addLast(cimClientHandle);\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/MessageListener.java",
    "content": "package com.crossoverjie.cim.client.sdk.io;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport java.util.Map;\n\npublic interface MessageListener {\n\n    /**\n     * @param client     client\n     * @param properties meta data\n     * @param msg        msgs\n     */\n    void received(Client client, Map<String, String> properties, String msg);\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/ReconnectCheck.java",
    "content": "package com.crossoverjie.cim.client.sdk.io;\n\nimport com.crossoverjie.cim.client.sdk.Client;\n\npublic interface ReconnectCheck {\n\n    /**\n     * By the default, the client will reconnect to the server when the connection is close(inactive).\n     * @return false if the client should not reconnect to the server.\n     */\n    boolean isNeedReconnect(Client client);\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/backoff/BackoffStrategy.java",
    "content": "package com.crossoverjie.cim.client.sdk.io.backoff;\n\nimport java.util.concurrent.TimeUnit;\n\n/**\n * @author:qjj\n * @create: 2024-09-21 12:16\n * @Description: backoff strategy interface\n */\n\npublic interface BackoffStrategy {\n    /**\n     * @return the backoff time in milliseconds\n     */\n    long nextBackoff();\n\n    /**\n     * Run the backoff strategy\n     * @throws InterruptedException\n     */\n    default void runBackoff() throws InterruptedException {\n        TimeUnit.SECONDS.sleep(nextBackoff());\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/backoff/RandomBackoff.java",
    "content": "package com.crossoverjie.cim.client.sdk.io.backoff;\n\n/**\n * @author:qjj\n * @create: 2024-09-21 12:22\n * @Description: random backoff strategy\n */\n\npublic class RandomBackoff implements BackoffStrategy {\n\n    @Override\n    public long nextBackoff() {\n        return (int) (Math.random() * 7 + 3);\n    }\n\n}\n"
  },
  {
    "path": "cim-client-sdk/src/test/java/com/crossoverjie/cim/client/sdk/ClientTest.java",
    "content": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;\nimport com.crossoverjie.cim.client.sdk.impl.ClientImpl;\nimport com.crossoverjie.cim.client.sdk.io.backoff.RandomBackoff;\nimport com.crossoverjie.cim.client.sdk.route.AbstractRouteBaseTest;\nimport com.crossoverjie.cim.common.constant.Constants;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicReference;\n\nimport com.crossoverjie.cim.route.constant.Constant;\nimport lombok.Cleanup;\nimport lombok.extern.slf4j.Slf4j;\nimport org.awaitility.Awaitility;\nimport org.junit.jupiter.api.AfterEach;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\n@Slf4j\npublic class ClientTest extends AbstractRouteBaseTest {\n\n\n    @AfterEach\n    public void tearDown() {\n        super.close();\n    }\n\n    @Test\n    public void groupChat() throws Exception {\n        super.starSingleServer();\n        super.startRoute(Constant.OfflineStoreMode.REDIS);\n        String routeUrl = \"http://localhost:8083\";\n        String cj = \"crossoverJie\";\n        String zs = \"zs\";\n        Long id = super.registerAccount(cj);\n        Long zsId = super.registerAccount(zs);\n        var auth1 = ClientConfigurationData.Auth.builder()\n                .userId(id)\n                .userName(cj)\n                .build();\n        var auth2 = ClientConfigurationData.Auth.builder()\n                .userId(zsId)\n                .userName(zs)\n                .build();\n\n        @Cleanup\n        Client client1 = Client.builder()\n                .auth(auth1)\n                .routeUrl(routeUrl)\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state = client1.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));\n        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();\n        Assertions.assertTrue(serverInfo.isPresent());\n        System.out.println(\"client1 serverInfo = \" + serverInfo.get());\n\n\n        AtomicReference<String> client2Receive = new AtomicReference<>();\n        @Cleanup\n        Client client2 = Client.builder()\n                .auth(auth2)\n                .routeUrl(routeUrl)\n                .messageListener((client, properties, message) -> {\n                    client2Receive.set(message);\n                    Assertions.assertEquals(properties.get(Constants.MetaKey.SEND_USER_ID), String.valueOf(auth1.getUserId()));\n                    Assertions.assertEquals(properties.get(Constants.MetaKey.SEND_USER_NAME), auth1.getUserName());\n                })\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state2 = client2.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));\n\n        Optional<CIMServerResVO> serverInfo2 = client2.getServerInfo();\n        Assertions.assertTrue(serverInfo2.isPresent());\n        System.out.println(\"client2 serverInfo = \" + serverInfo2.get());\n\n        // send msg\n        String msg = \"hello\";\n        client1.sendGroup(msg);\n\n        Set<CIMUserInfo> onlineUser = client1.getOnlineUser();\n        Assertions.assertEquals(onlineUser.size(), 2);\n        onlineUser.forEach(userInfo -> {\n            log.info(\"online user = {}\", userInfo);\n            Long userId = userInfo.getUserId();\n            if (userId.equals(id)) {\n                Assertions.assertEquals(cj, userInfo.getUserName());\n            } else if (userId.equals(zsId)) {\n                Assertions.assertEquals(zs, userInfo.getUserName());\n            }\n        });\n\n        Awaitility.await().untilAsserted(\n                () -> Assertions.assertEquals(msg, client2Receive.get()));\n        super.stopSingle();\n        client1.close();\n        client2.close();\n    }\n\n    @Test\n    public void testP2PChat() throws Exception {\n        super.starSingleServer();\n        super.startRoute(Constant.OfflineStoreMode.REDIS);\n        String routeUrl = \"http://localhost:8083\";\n        String cj = \"cj\";\n        String zs = \"zs\";\n        String ls = \"ls\";\n        Long cjId = super.registerAccount(cj);\n        Long zsId = super.registerAccount(zs);\n        Long lsId = super.registerAccount(ls);\n        var auth1 = ClientConfigurationData.Auth.builder()\n                .userName(cj)\n                .userId(cjId)\n                .build();\n        var auth2 = ClientConfigurationData.Auth.builder()\n                .userName(zs)\n                .userId(zsId)\n                .build();\n        var auth3 = ClientConfigurationData.Auth.builder()\n                .userName(ls)\n                .userId(lsId)\n                .build();\n\n        var client1Receive = new ArrayList<>();\n        @Cleanup\n        Client client1 = Client.builder()\n                .auth(auth1)\n                .routeUrl(routeUrl)\n                .messageListener((__, properties, message) -> {\n                    log.info(\"client1 receive message = {}\", message);\n                    client1Receive.add(message);\n                })\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state = client1.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));\n        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();\n        Assertions.assertTrue(serverInfo.isPresent());\n        System.out.println(\"client1 serverInfo = \" + serverInfo.get());\n\n\n        // client2\n        AtomicReference<String> client2Receive = new AtomicReference<>();\n        @Cleanup\n        Client client2 = Client.builder()\n                .auth(auth2)\n                .routeUrl(routeUrl)\n                .messageListener((client, properties, message) -> client2Receive.set(message))\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state2 = client2.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));\n\n        Optional<CIMServerResVO> serverInfo2 = client2.getServerInfo();\n        Assertions.assertTrue(serverInfo2.isPresent());\n        System.out.println(\"client2 serverInfo = \" + serverInfo2.get());\n\n        // client3\n        AtomicReference<String> client3Receive = new AtomicReference<>();\n        @Cleanup\n        Client client3 = Client.builder()\n                .auth(auth3)\n                .routeUrl(routeUrl)\n                .messageListener((client, properties, message) -> {\n                    log.info(\"client3 receive message = {}\", message);\n                    client3Receive.set(message);\n                })\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state3 = client3.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));\n\n        Optional<CIMServerResVO> serverInfo3 = client3.getServerInfo();\n        Assertions.assertTrue(serverInfo3.isPresent());\n        System.out.println(\"client3 serverInfo = \" + serverInfo3.get());\n\n        // client1 send msg to client3\n        String msg = \"hello\";\n        client1.sendP2P(P2PReqVO.builder()\n                .receiveUserId(lsId)\n                .msg(msg)\n                .build());\n\n        Set<CIMUserInfo> onlineUser = client1.getOnlineUser();\n        Assertions.assertEquals(onlineUser.size(), 3);\n        onlineUser.forEach(userInfo -> {\n            log.info(\"online user = {}\", userInfo);\n            Long userId = userInfo.getUserId();\n            if (userId.equals(cjId)) {\n                Assertions.assertEquals(cj, userInfo.getUserName());\n            } else if (userId.equals(zsId)) {\n                Assertions.assertEquals(zs, userInfo.getUserName());\n            } else if (userId.equals(lsId)) {\n                Assertions.assertEquals(ls, userInfo.getUserName());\n            }\n        });\n\n        // client2 send batch msg to client1\n        var batchMsg = List.of(\"a\",\"b\",\"c\");\n        client2.sendP2P(P2PReqVO.builder()\n                .receiveUserId(cjId)\n                .batchMsg(batchMsg)\n                .build());\n\n        Assertions.assertEquals(ClientImpl.getClientMap().size(), 3);\n        Awaitility.await().untilAsserted(\n                () -> Assertions.assertEquals(msg, client3Receive.get()));\n        Awaitility.await().untilAsserted(\n                () -> Assertions.assertNull(client2Receive.get()));\n        Awaitility.await().untilAsserted(\n                () -> Assertions.assertEquals(batchMsg, client1Receive));\n        super.stopSingle();\n        client1.close();\n        client2.close();\n        client3.close();\n        Assertions.assertEquals(ClientImpl.getClientMap().size(), 0);\n    }\n\n    /**\n     * 1. Start two servers.\n     * 2. Start two client, and send message.\n     * 3. Stop one server which is connected by client1.\n     * 4. Wait for client1 reconnect.\n     * 5. Send message again.\n     *\n     * @throws Exception\n     */\n    @Test\n    public void testReconnect() throws Exception {\n        super.startTwoServer();\n        super.startRoute(Constant.OfflineStoreMode.REDIS);\n\n        String routeUrl = \"http://localhost:8083\";\n        String cj = \"cj\";\n        String zs = \"zs\";\n        Long cjId = super.registerAccount(cj);\n        Long zsId = super.registerAccount(zs);\n        var auth1 = ClientConfigurationData.Auth.builder()\n                .userName(cj)\n                .userId(cjId)\n                .build();\n        var auth2 = ClientConfigurationData.Auth.builder()\n                .userName(zs)\n                .userId(zsId)\n                .build();\n        var backoffStrategy = new RandomBackoff();\n\n        @Cleanup\n        Client client1 = Client.builder()\n                .auth(auth1)\n                .routeUrl(routeUrl)\n                .backoffStrategy(backoffStrategy)\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state = client1.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));\n\n\n        AtomicReference<String> client2Receive = new AtomicReference<>();\n        @Cleanup\n        Client client2 = Client.builder()\n                .auth(auth2)\n                .routeUrl(routeUrl)\n                .messageListener((client, properties, message) -> client2Receive.set(message))\n                .backoffStrategy(backoffStrategy)\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state2 = client2.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));\n\n        Optional<CIMServerResVO> serverInfo2 = client2.getServerInfo();\n        Assertions.assertTrue(serverInfo2.isPresent());\n        System.out.println(\"client2 serverInfo = \" + serverInfo2.get());\n\n        // send msg\n        String msg = \"hello\";\n        client1.sendGroup(msg);\n        Awaitility.await()\n                .untilAsserted(() -> Assertions.assertEquals(msg, client2Receive.get()));\n        client2Receive.set(\"\");\n\n\n        System.out.println(\"ready to restart server\");\n        TimeUnit.SECONDS.sleep(3);\n        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();\n        Assertions.assertTrue(serverInfo.isPresent());\n        System.out.println(\"server info = \" + serverInfo.get());\n\n        super.stopServer(serverInfo.get().getCimServerPort());\n        System.out.println(\"stop server success! \" + serverInfo.get());\n\n\n        // Waiting server stopped, and client reconnect.\n        TimeUnit.SECONDS.sleep(30);\n        System.out.println(\"reconnect state: \" + client1.getState());\n        Awaitility.await().atMost(15, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));\n        serverInfo = client1.getServerInfo();\n        Assertions.assertTrue(serverInfo.isPresent());\n        System.out.println(\"client1 reconnect server info = \" + serverInfo.get());\n\n        // Send message again.\n        log.info(\"send message again, client2Receive = {}\", client2Receive.get());\n        client1.sendGroup(msg);\n        Awaitility.await()\n                .untilAsserted(() -> Assertions.assertEquals(msg, client2Receive.get()));\n        super.stopTwoServer();\n        client1.close();\n        client2.close();\n    }\n\n    @Test\n    public void offLineAndOnline() throws Exception {\n        super.starSingleServer();\n        super.startRoute(Constant.OfflineStoreMode.REDIS);\n        String routeUrl = \"http://localhost:8083\";\n        String cj = \"crossoverJie\";\n        String zs = \"zs\";\n        Long id = super.registerAccount(cj);\n        Long zsId = super.registerAccount(zs);\n        var auth1 = ClientConfigurationData.Auth.builder()\n                .userId(id)\n                .userName(cj)\n                .build();\n        var auth2 = ClientConfigurationData.Auth.builder()\n                .userId(zsId)\n                .userName(zs)\n                .build();\n\n        @Cleanup\n        Client client1 = Client.builder()\n                .auth(auth1)\n                .routeUrl(routeUrl)\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state = client1.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));\n        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();\n        Assertions.assertTrue(serverInfo.isPresent());\n        System.out.println(\"client1 serverInfo = \" + serverInfo.get());\n\n\n        AtomicReference<String> client2Receive = new AtomicReference<>();\n        Client client2 = Client.builder()\n                .auth(auth2)\n                .routeUrl(routeUrl)\n                .messageListener((client, properties, message) -> client2Receive.set(message))\n                // Avoid auto reconnect, this test will manually close client.\n                .reconnectCheck((client) -> false)\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state2 = client2.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));\n\n        Optional<CIMServerResVO> serverInfo2 = client2.getServerInfo();\n        Assertions.assertTrue(serverInfo2.isPresent());\n        System.out.println(\"client2 serverInfo = \" + serverInfo2.get());\n\n        // send msg\n        String msg = \"hello\";\n        client1.sendGroup(msg);\n        Awaitility.await().untilAsserted(\n                () -> Assertions.assertEquals(msg, client2Receive.get()));\n        client2Receive.set(\"\");\n\n        // Manually offline\n        client2.close();\n        TimeUnit.SECONDS.sleep(10);\n        client2 = Client.builder()\n                .auth(auth2)\n                .routeUrl(routeUrl)\n                .messageListener((client, properties, message) -> client2Receive.set(message))\n                // Avoid to auto reconnect, this test will manually close client.\n                .reconnectCheck((client) -> false)\n                .build();\n        ClientState.State state3 = client2.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));\n\n        // send msg again\n        client1.sendGroup(msg);\n        Awaitility.await().untilAsserted(\n                () -> Assertions.assertEquals(msg, client2Receive.get()));\n\n        super.stopSingle();\n        client1.close();\n        client2.close();\n    }\n\n    @Test\n    public void testClose() throws Exception {\n        super.starSingleServer();\n        super.startRoute(Constant.OfflineStoreMode.REDIS);\n        String routeUrl = \"http://localhost:8083\";\n        String cj = \"crossoverJie\";\n        Long id = super.registerAccount(cj);\n        var auth1 = ClientConfigurationData.Auth.builder()\n                .userId(id)\n                .userName(cj)\n                .build();\n\n        Client client1 = Client.builder()\n                .auth(auth1)\n                .routeUrl(routeUrl)\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state = client1.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));\n        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();\n        Assertions.assertTrue(serverInfo.isPresent());\n        System.out.println(\"client1 serverInfo = \" + serverInfo.get());\n\n        client1.close();\n        ClientState.State state1 = client1.getState();\n        Assertions.assertEquals(ClientState.State.Closed, state1);\n        super.stopSingle();\n    }\n\n    @Test\n    public void testIncorrectUser() throws Exception {\n        super.starSingleServer();\n        super.startRoute(Constant.OfflineStoreMode.REDIS);\n        String routeUrl = \"http://localhost:8083\";\n        String cj = \"xx\";\n        long id = 100L;\n        var auth1 = ClientConfigurationData.Auth.builder()\n                .userId(id)\n                .userName(cj)\n                .build();\n\n        Client client1 = Client.builder()\n                .auth(auth1)\n                .routeUrl(routeUrl)\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n\n        Assertions.assertDoesNotThrow(client1::close);\n\n        super.stopSingle();\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/test/java/com/crossoverjie/cim/client/sdk/OfflineMsgTest.java",
    "content": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;\nimport com.crossoverjie.cim.client.sdk.impl.ClientImpl;\nimport com.crossoverjie.cim.client.sdk.route.OfflineMsgStoreRouteBaseTest;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\nimport com.crossoverjie.cim.route.constant.Constant;\nimport lombok.extern.slf4j.Slf4j;\nimport org.awaitility.Awaitility;\nimport org.junit.jupiter.api.*;\n\nimport java.util.ArrayList;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicReference;\n\n@Slf4j\npublic class OfflineMsgTest extends OfflineMsgStoreRouteBaseTest {\n\n    @Test\n    public void testP2POfflineChatRedis() throws Exception {\n        super.starSingleServer();\n        super.startRoute(Constant.OfflineStoreMode.REDIS);\n        String routeUrl = \"http://localhost:8083\";\n        String cj = \"cj\";\n        String ls = \"ls\";\n        Long cjId = super.registerAccount(cj);\n        Long lsId = super.registerAccount(ls);\n        var auth1 = ClientConfigurationData.Auth.builder()\n                .userName(cj)\n                .userId(cjId)\n                .build();\n        var auth3 = ClientConfigurationData.Auth.builder()\n                .userName(ls)\n                .userId(lsId)\n                .build();\n\n        var client1Receive = new ArrayList<>();\n        Client client1 = Client.builder()\n                .auth(auth1)\n                .routeUrl(routeUrl)\n                .messageListener((__, properties, message) -> {\n                    log.info(\"client1 receive message = {}\", message);\n                    client1Receive.add(message);\n                })\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state = client1.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));\n        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();\n        Assertions.assertTrue(serverInfo.isPresent());\n        System.out.println(\"client1 serverInfo = \" + serverInfo.get());\n\n\n        // client3\n        AtomicReference<String> client3Receive = new AtomicReference<>();\n        Client client3 = Client.builder()\n                .auth(auth3)\n                .routeUrl(routeUrl)\n                .messageListener((client, properties, message) -> {\n                    log.info(\"client3 receive message = {}\", message);\n                    client3Receive.set(message);\n                })\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state3 = client3.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));\n\n        Optional<CIMServerResVO> serverInfo3 = client3.getServerInfo();\n        Assertions.assertTrue(serverInfo3.isPresent());\n        System.out.println(\"client3 serverInfo = \" + serverInfo3.get());\n\n        // client1 send msg to client3\n        String msg = \"hello\";\n        client1.sendP2P(P2PReqVO.builder()\n                .receiveUserId(lsId)\n                .msg(msg)\n                .build());\n\n        Set<CIMUserInfo> onlineUser = client1.getOnlineUser();\n        Assertions.assertEquals(onlineUser.size(), 2);\n        onlineUser.forEach(userInfo -> {\n            log.info(\"online user = {}\", userInfo);\n            Long userId = userInfo.getUserId();\n            if (userId.equals(cjId)) {\n                Assertions.assertEquals(cj, userInfo.getUserName());\n            } else if (userId.equals(lsId)) {\n                Assertions.assertEquals(ls, userInfo.getUserName());\n            }\n        });\n\n\n        Awaitility.await().untilAsserted(\n                () -> Assertions.assertEquals(msg, client3Receive.get()));\n\n        // Manually offline client3\n        client3.close();\n        client3Receive.set(null);\n        ClientState.State closeState = client3.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Closed, closeState));\n\n        // client1 send client3 an offline message\n        String offlineMsg = \"offline message\";\n        client1.sendP2P(P2PReqVO.builder()\n                .receiveUserId(lsId)\n                .msg(offlineMsg)\n                .build());\n\n        // online again\n        client3 = Client.builder()\n                .auth(auth3)\n                .routeUrl(routeUrl)\n                .messageListener((client, properties, message) -> {\n                    log.info(\"client3 online again receive message = {}\", message);\n                    client3Receive.set(message);\n                })\n                .build();\n\n        ClientState.State client3AgainState = client3.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, client3AgainState));\n\n\n        // check offline message\n        Awaitility.await().untilAsserted(\n                () -> Assertions.assertEquals(offlineMsg, client3Receive.get()));\n\n        // close\n        client1.close();\n        client3.close();\n        super.close();\n        super.stopSingle();\n        Assertions.assertEquals(ClientImpl.getClientMap().size(), 0);\n    }\n\n    @Test\n    public void testP2POfflineChatMysql() throws Exception {\n        super.starSingleServer();\n        super.startRoute(Constant.OfflineStoreMode.MYSQL);\n        String routeUrl = \"http://localhost:8083\";\n        String cj = \"cj\";\n        String ls = \"ls\";\n        Long cjId = super.registerAccount(cj);\n        Long lsId = super.registerAccount(ls);\n        var auth1 = ClientConfigurationData.Auth.builder()\n                .userName(cj)\n                .userId(cjId)\n                .build();\n        var auth3 = ClientConfigurationData.Auth.builder()\n                .userName(ls)\n                .userId(lsId)\n                .build();\n\n        var client1Receive = new ArrayList<>();\n        Client client1 = Client.builder()\n                .auth(auth1)\n                .routeUrl(routeUrl)\n                .messageListener((__, properties, message) -> {\n                    log.info(\"client1 receive message = {}\", message);\n                    client1Receive.add(message);\n                })\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state = client1.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));\n        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();\n        Assertions.assertTrue(serverInfo.isPresent());\n        System.out.println(\"client1 serverInfo = \" + serverInfo.get());\n\n\n        // client3\n        AtomicReference<String> client3Receive = new AtomicReference<>();\n        Client client3 = Client.builder()\n                .auth(auth3)\n                .routeUrl(routeUrl)\n                .messageListener((client, properties, message) -> {\n                    log.info(\"client3 receive message = {}\", message);\n                    client3Receive.set(message);\n                })\n                .build();\n        TimeUnit.SECONDS.sleep(3);\n        ClientState.State state3 = client3.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));\n\n        Optional<CIMServerResVO> serverInfo3 = client3.getServerInfo();\n        Assertions.assertTrue(serverInfo3.isPresent());\n        System.out.println(\"client3 serverInfo = \" + serverInfo3.get());\n\n        // client1 send msg to client3\n        String msg = \"hello\";\n        client1.sendP2P(P2PReqVO.builder()\n                .receiveUserId(lsId)\n                .msg(msg)\n                .build());\n\n        Set<CIMUserInfo> onlineUser = client1.getOnlineUser();\n        Assertions.assertEquals(onlineUser.size(), 2);\n        onlineUser.forEach(userInfo -> {\n            log.info(\"online user = {}\", userInfo);\n            Long userId = userInfo.getUserId();\n            if (userId.equals(cjId)) {\n                Assertions.assertEquals(cj, userInfo.getUserName());\n            } else if (userId.equals(lsId)) {\n                Assertions.assertEquals(ls, userInfo.getUserName());\n            }\n        });\n\n\n        Awaitility.await().untilAsserted(\n                () -> Assertions.assertEquals(msg, client3Receive.get()));\n\n        // Manually offline client3\n        client3.close();\n        client3Receive.set(null);\n        ClientState.State closeState = client3.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Closed, closeState));\n\n        // client1 send client3 an offline message\n        String offlineMsg = \"offline message\";\n        client1.sendP2P(P2PReqVO.builder()\n                .receiveUserId(lsId)\n                .msg(offlineMsg)\n                .build());\n\n        // online again\n        client3 = Client.builder()\n                .auth(auth3)\n                .routeUrl(routeUrl)\n                .messageListener((client, properties, message) -> {\n                    log.info(\"client3 online again receive message = {}\", message);\n                    client3Receive.set(message);\n                })\n                .build();\n\n        ClientState.State client3AgainState = client3.getState();\n        Awaitility.await().atMost(10, TimeUnit.SECONDS)\n                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, client3AgainState));\n\n\n        // check offline message\n        Awaitility.await().untilAsserted(\n                () -> Assertions.assertEquals(offlineMsg, client3Receive.get()));\n\n        // close\n        client1.close();\n        client3.close();\n        super.close();\n        super.stopSingle();\n        Assertions.assertEquals(ClientImpl.getClientMap().size(), 0);\n    }\n}\n"
  },
  {
    "path": "cim-client-sdk/src/test/resources/application-route.yaml",
    "content": "spring:\n  application:\n    name:\n      cim-forward-route\n  data:\n    redis:\n      host: 127.0.0.1\n      port: 6379\n      jedis:\n        pool:\n          max-active: 100\n          max-idle: 100\n          max-wait: 1000\n          min-idle: 10\n\n\n# web port\nserver:\n  port: 8083\n\nlogging:\n  level:\n    root: info\n\n  # enable swagger\nspringdoc:\n  swagger-ui:\n    enabled: true\n\napp:\n  zk:\n    connect:\n      timeout: 30000\n    root: /route\n\n  # route strategy\n  #app.route.way=com.crossoverjie.cim.common.route.algorithm.loop.LoopHandle\n\n  # route strategy\n  #app.route.way=com.crossoverjie.cim.common.route.algorithm.random.RandomHandle\n\n  # route strategy\n  route:\n    way:\n      handler: com.crossoverjie.cim.common.route.algorithm.loop.LoopHandle\n\n  #app.route.way.consitenthash=com.crossoverjie.cim.common.route.algorithm.consistenthash.SortArrayMapConsistentHash\n\n      consitenthash: com.crossoverjie.cim.common.route.algorithm.consistenthash.TreeMapConsistentHash\n\noffline:\n  store:\n    mode: redis\n    redis:\n      expire:\n        message-ttl-days: 3\n\n"
  },
  {
    "path": "cim-client-sdk/src/test/resources/init.sql",
    "content": "-- 创建表\nCREATE TABLE IF NOT EXISTS `offline_msg`\n(\n    `id`\n                   BIGINT\n        PRIMARY\n            KEY\n        AUTO_INCREMENT,\n    `message_id`\n                   BIGINT\n        NOT\n            NULL,\n    `receive_user_id`\n                   BIGINT\n        NOT\n            NULL,\n    `content`\n                   VARCHAR(2000),\n    `message_type` INT,\n    `status`       TINYINT COMMENT '0: Pending, 1: Acked',\n    `created_at`   DATETIME,\n    `properties`   VARCHAR(2000),\n    INDEX `idx_receive_user_id`\n        (\n         `receive_user_id`\n            )\n);\nCREATE TABLE offline_msg_last_send_record\n(\n    receive_user_id BIGINT NOT NULL PRIMARY KEY,\n    last_message_id BIGINT,\n    updated_at      DATETIME\n) ENGINE = InnoDB\n  DEFAULT CHARSET = utf8mb4;"
  },
  {
    "path": "cim-common/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>cim</artifactId>\n        <groupId>com.crossoverjie.netty</groupId>\n        <version>1.0.0-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n    <version>1.0.0-SNAPSHOT</version>\n\n    <artifactId>cim-common</artifactId>\n\n\n    <dependencies>\n        <dependency>\n            <groupId>org.projectlombok</groupId>\n            <artifactId>lombok</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.google.protobuf</groupId>\n            <artifactId>protobuf-java</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.github.xiaoymin</groupId>\n            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.squareup.okhttp3</groupId>\n            <artifactId>okhttp</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.github.ben-manes.caffeine</groupId>\n            <artifactId>caffeine</artifactId>\n        </dependency>\n        <!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->\n        <dependency>\n            <groupId>com.101tec</groupId>\n            <artifactId>zkclient</artifactId>\n        </dependency>\n\n\n        <dependency>\n            <groupId>org.apache.curator</groupId>\n            <artifactId>curator-recipes</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.apache.zookeeper</groupId>\n            <artifactId>zookeeper</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.vintage</groupId>\n            <artifactId>junit-vintage-engine</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-all</artifactId>\n            <version>${netty.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.alibaba</groupId>\n            <artifactId>fastjson</artifactId>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <extensions>\n            <extension>\n                <groupId>kr.motd.maven</groupId>\n                <artifactId>os-maven-plugin</artifactId>\n                <version>1.5.0.Final</version>\n            </extension>\n        </extensions>\n        <plugins>\n            <plugin>\n                <groupId>org.xolstice.maven.plugins</groupId>\n                <artifactId>protobuf-maven-plugin</artifactId>\n                <version>0.5.1</version>\n                <configuration>\n                    <protocArtifact>com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier}</protocArtifact>\n                    <pluginId>grpc-java</pluginId>\n                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.19.0:exe:${os.detected.classifier}</pluginArtifact>\n                </configuration>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>compile</goal>\n                            <goal>compile-custom</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/constant/Constants.java",
    "content": "package com.crossoverjie.cim.common.constant;\n\n/**\n * Function:常量\n *\n * @author crossoverJie\n *         Date: 28/03/2018 17:41\n * @since JDK 1.8\n */\npublic class Constants {\n\n\n    /**\n     * 服务端手动 push 次数\n     */\n    public static final String COUNTER_SERVER_PUSH_COUNT = \"counter.server.push.count\";\n\n\n    /**\n     * 客户端手动 push 次数\n     */\n    public static final String COUNTER_CLIENT_PUSH_COUNT = \"counter.client.push.count\";\n\n    public static class MetaKey {\n        public static final String SEND_USER_ID = \"sendUserId\";\n        public static final String SEND_USER_NAME = \"sendUserName\";\n        public static final String RECEIVE_USER_ID = \"receiveUserId\";\n        public static final String RECEIVE_USER_NAME = \"receiveUserName\";\n    }\n\n    //从数据库读取离线消息的每次获取量\n    public static final Integer FETCH_OFFLINE_MSG_LIMIT = 100;\n\n    public static final Integer OFFLINE_MSG_PENDING = 0;\n\n    public static final Integer OFFLINE_MSG_DELIVERED = 1;\n\n    public static final Integer MSG_TYPE_TEXT = 0;\n\n    public static final Integer MSG_TYPE_IMAGE = 1;\n\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/DynamicUrl.java",
    "content": "package com.crossoverjie.cim.common.core.proxy;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * @author crossoverJie\n */\n@Target({ElementType.PARAMETER})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface DynamicUrl {\n    boolean useMethodEndpoint() default true;\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/Request.java",
    "content": "package com.crossoverjie.cim.common.core.proxy;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\n\n/**\n * @author crossoverJie\n */\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface Request {\n    String method() default POST;\n    String url() default \"\";\n\n    String GET = \"GET\";\n    String POST = \"POST\";\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/RpcProxyManager.java",
    "content": "package com.crossoverjie.cim.common.core.proxy;\n\nimport static com.crossoverjie.cim.common.enums.StatusEnum.VALIDATION_FAIL;\nimport com.alibaba.fastjson.JSONObject;\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossoverjie.cim.common.util.HttpClient;\nimport com.crossoverjie.cim.common.util.StringUtil;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.lang.annotation.Annotation;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Proxy;\nimport java.lang.reflect.Type;\nimport java.net.URI;\nimport lombok.extern.slf4j.Slf4j;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Response;\n\n/**\n * RpcProxyManager is a proxy manager for creating dynamic proxy instances of interfaces.\n * It handles HTTP requests and responses using OkHttpClient.\n *\n * @param <T> the type of the proxied interface\n */\n@Slf4j\npublic final class RpcProxyManager<T> {\n\n    private Class<T> clazz;\n    private String url;\n    private OkHttpClient okHttpClient;\n    private final ObjectMapper objectMapper = new ObjectMapper();\n\n    /**\n     * Private constructor to initialize RpcProxyManager.\n     *\n     * @param clazz        Proxied interface\n     * @param url          Server provider URL\n     * @param okHttpClient HTTP client\n     */\n    private RpcProxyManager(Class<T> clazz, String url, OkHttpClient okHttpClient) {\n        this.clazz = clazz;\n        this.url = url;\n        this.okHttpClient = okHttpClient;\n    }\n\n    private RpcProxyManager(Class<T> clazz, OkHttpClient okHttpClient) {\n        this(clazz, \"\", okHttpClient);\n    }\n\n    /**\n     * Default private constructor.\n     */\n    private RpcProxyManager() {\n    }\n\n    /**\n     * Creates a proxy instance of the specified interface.\n     *\n     * @param clazz        Proxied interface\n     * @param url          Server provider URL\n     * @param okHttpClient HTTP client\n     * @param <T>          Type of the proxied interface\n     * @return Proxy instance of the specified interface\n     */\n    public static <T> T create(Class<T> clazz, String url, OkHttpClient okHttpClient) {\n        return new RpcProxyManager<>(clazz, url, okHttpClient).getInstance();\n    }\n\n    public static <T> T create(Class<T> clazz, OkHttpClient okHttpClient) {\n        return new RpcProxyManager<>(clazz, okHttpClient).getInstance();\n    }\n\n    /**\n     * Gets the proxy instance of the API.\n     *\n     * @return Proxy instance of the API\n     */\n    @SuppressWarnings(\"unchecked\")\n    public T getInstance() {\n        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{clazz},\n                new ProxyInvocation());\n    }\n\n    /**\n     * ProxyInvocation is an invocation handler for handling method calls on proxy instances.\n     */\n    private class ProxyInvocation implements InvocationHandler {\n\n        /**\n         * Handles method calls on proxy instances.\n         *\n         * @param proxy  The proxy instance\n         * @param method The method being called\n         * @param args   The arguments of the method call\n         * @return The result of the method call\n         * @throws Throwable if an error occurs during method invocation\n         */\n        @Override\n        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {\n\n            Response result = null;\n            String serverUrl = url + \"/\" + method.getName();\n            Request annotation = method.getAnnotation(Request.class);\n            if (annotation != null && StringUtil.isNotEmpty(annotation.url())) {\n                serverUrl = url + \"/\" + annotation.url();\n            }\n            URI serverUri = new URI(serverUrl);\n            serverUrl = serverUri.normalize().toString();\n\n            Object para = null;\n            Class<?> parameterType = null;\n            for (int i = 0; i < method.getParameterAnnotations().length; i++) {\n                Annotation[] annotations = method.getParameterAnnotations()[i];\n                if (annotations.length == 0) {\n                    para = args[i];\n                    parameterType = method.getParameterTypes()[i];\n                }\n\n                for (Annotation ann : annotations) {\n                    if (ann instanceof DynamicUrl) {\n                        if (args[i] instanceof String) {\n                            serverUrl = (String) args[i];\n                            if (((DynamicUrl) ann).useMethodEndpoint()) {\n                                serverUrl = serverUrl + \"/\" + method.getName();\n                            }\n                            break;\n                        } else {\n                            throw new CIMException(\"DynamicUrl must be String type\");\n                        }\n                    }\n                }\n            }\n\n            try {\n                if (annotation != null && annotation.method().equals(Request.GET)) {\n                    result = HttpClient.get(okHttpClient, serverUrl);\n                } else {\n\n                    if (args == null || args.length > 2 || para == null || parameterType == null) {\n                        throw new IllegalArgumentException(VALIDATION_FAIL.message());\n                    }\n                    JSONObject jsonObject = new JSONObject();\n                    for (Field field : parameterType.getDeclaredFields()) {\n                        field.setAccessible(true);\n                        jsonObject.put(field.getName(), field.get(para));\n                    }\n\n                    result = HttpClient.post(okHttpClient, jsonObject.toString(), serverUrl);\n                }\n                if (method.getReturnType() == void.class) {\n                    return null;\n                }\n\n                String json = result.body().string();\n                Type genericTypeOfBaseResponse = getGenericTypeOfBaseResponse(method);\n                if (genericTypeOfBaseResponse == null) {\n                    return objectMapper.readValue(json, method.getReturnType());\n                } else {\n                    return objectMapper.readValue(json, objectMapper.getTypeFactory()\n                            .constructParametricType(method.getReturnType(),\n                                    objectMapper.getTypeFactory().constructType(genericTypeOfBaseResponse)));\n                }\n            } finally {\n                if (result != null) {\n                    result.body().close();\n                }\n            }\n        }\n    }\n\n    private Type getGenericTypeOfBaseResponse(Method declaredMethod) {\n        Type returnType = declaredMethod.getGenericReturnType();\n\n        // check if the return type is a parameterized type\n        if (returnType instanceof ParameterizedType parameterizedType) {\n\n            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();\n\n            for (Type typeArgument : actualTypeArguments) {\n                return typeArgument;\n            }\n        }\n\n        return null;\n\n    }\n\n    /**\n     * Gets the generic type of the BaseResponse.\n     *\n     * @param declaredMethod The method whose return type is being checked\n     * @return The generic type of the BaseResponse, or null if not found\n     * @throws ClassNotFoundException if the class of the generic type is not found\n    private Class<?> getBaseResponseGeneric(Method declaredMethod) throws ClassNotFoundException {\n\n    Type returnType = declaredMethod.getGenericReturnType();\n\n    // check if the return type is a parameterized type\n    if (returnType instanceof ParameterizedType parameterizedType) {\n\n    Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();\n\n    for (Type typeArgument : actualTypeArguments) {\n    // BaseResponse only has one generic type\n    return getClass(typeArgument);\n    }\n    }\n\n    return null;\n    }\n\n    public static Class<?> getClass(Type type) throws ClassNotFoundException {\n    if (type instanceof Class<?>) {\n    // 普通类型，直接返回\n    return (Class<?>) type;\n    } else if (type instanceof ParameterizedType) {\n    // 参数化类型，返回原始类型\n    return getClass(((ParameterizedType) type).getRawType());\n    } else if (type instanceof TypeVariable<?>) {\n    // 类型变量，无法在运行时获取具体类型\n    return Object.class;\n    } else {\n    throw new ClassNotFoundException(\"无法处理的类型: \" + type.toString());\n    }\n    }*/\n\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/RingBufferWheel.java",
    "content": "package com.crossoverjie.cim.common.data.construct;\n\nimport lombok.extern.slf4j.Slf4j;\n\nimport java.util.HashSet;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.locks.Condition;\nimport java.util.concurrent.locks.Lock;\nimport java.util.concurrent.locks.ReentrantLock;\n/**\n * Function:Ring Queue, it can be used to delay task.\n *\n * @author crossoverJie\n * Date: 2019-09-20 14:46\n * @since JDK 1.8\n */\n@Slf4j\npublic final class RingBufferWheel {\n\n\n    /**\n     * default ring buffer size\n     */\n    private static final int STATIC_RING_SIZE = 64;\n\n    private Object[] ringBuffer;\n\n    private int bufferSize;\n\n    /**\n     * business thread pool\n     */\n    private ExecutorService executorService;\n\n    private volatile int size = 0;\n\n    /***\n     * task stop sign\n     */\n    private volatile boolean stop = false;\n\n    /**\n     * task start sign\n     */\n    private volatile AtomicBoolean start = new AtomicBoolean(false);\n\n    /**\n     * total tick times\n     */\n    private AtomicInteger tick = new AtomicInteger();\n\n    private Lock lock = new ReentrantLock();\n    private Condition condition = lock.newCondition();\n\n    private AtomicInteger taskId = new AtomicInteger();\n    private Map<Integer, Task> taskMap = new ConcurrentHashMap<>(16);\n\n    /**\n     * Create a new delay task ring buffer by default size\n     *\n     * @param executorService the business thread pool\n     */\n    public RingBufferWheel(ExecutorService executorService) {\n        this.executorService = executorService;\n        this.bufferSize = STATIC_RING_SIZE;\n        this.ringBuffer = new Object[bufferSize];\n    }\n\n\n    /**\n     * Create a new delay task ring buffer by custom buffer size\n     *\n     * @param executorService the business thread pool\n     * @param bufferSize      custom buffer size\n     */\n    public RingBufferWheel(ExecutorService executorService, int bufferSize) {\n        this(executorService);\n\n        if (!powerOf2(bufferSize)) {\n            throw new RuntimeException(\"bufferSize=[\" + bufferSize + \"] must be a power of 2\");\n        }\n        this.bufferSize = bufferSize;\n        this.ringBuffer = new Object[bufferSize];\n    }\n\n    /**\n     * Add a task into the ring buffer(thread safe)\n     *\n     * @param task business task extends {@link Task}\n     */\n    public int addTask(Task task) {\n        int key = task.getKey();\n        int id;\n\n        try {\n            lock.lock();\n            int index = mod(key, bufferSize);\n            task.setIndex(index);\n            Set<Task> tasks = get(index);\n\n            int cycleNum = cycleNum(key, bufferSize);\n            if (tasks != null) {\n                task.setCycleNum(cycleNum);\n                tasks.add(task);\n            } else {\n                task.setIndex(index);\n                task.setCycleNum(cycleNum);\n                Set<Task> sets = new HashSet<>();\n                sets.add(task);\n                put(key, sets);\n            }\n            id = taskId.incrementAndGet();\n            task.setTaskId(id);\n            taskMap.put(id, task);\n            size++;\n        } finally {\n            lock.unlock();\n        }\n\n        start();\n\n        return id;\n    }\n\n\n    /**\n     * Cancel task by taskId\n     * @param id unique id through {@link #addTask(Task)}\n     * @return\n     */\n    public boolean cancel(int id) {\n\n        boolean flag = false;\n        Set<Task> tempTask = new HashSet<>();\n\n        try {\n            lock.lock();\n            Task task = taskMap.get(id);\n            if (task == null) {\n                return false;\n            }\n\n            Set<Task> tasks = get(task.getIndex());\n            for (Task tk : tasks) {\n                if (tk.getKey() == task.getKey() && tk.getCycleNum() == task.getCycleNum()) {\n                    size--;\n                    flag = true;\n                    taskMap.remove(id);\n                } else {\n                    tempTask.add(tk);\n                }\n\n            }\n            //update origin data\n            ringBuffer[task.getIndex()] = tempTask;\n        } finally {\n            lock.unlock();\n        }\n\n        return flag;\n    }\n\n    /**\n     * Thread safe\n     *\n     * @return the size of ring buffer\n     */\n    public int taskSize() {\n        return size;\n    }\n\n    /**\n     * Same with method {@link #taskSize}\n     * @return\n     */\n    public int taskMapSize() {\n        return taskMap.size();\n    }\n\n    /**\n     * Start background thread to consumer wheel timer, it will always run until you call method {@link #stop}\n     */\n    public void start() {\n        if (!start.get()) {\n\n            if (start.compareAndSet(start.get(), true)) {\n                log.info(\"Delay task is starting\");\n                Thread job = new Thread(new TriggerJob());\n                job.setName(\"consumer RingBuffer thread\");\n                job.start();\n                start.set(true);\n            }\n\n        }\n    }\n\n    /**\n     * Stop consumer ring buffer thread\n     *\n     * @param force True will force close consumer thread and discard all pending tasks\n     *              otherwise the consumer thread waits for all tasks to completes before closing.\n     */\n    public void stop(boolean force) {\n        if (force) {\n            log.info(\"Delay task is forced stop\");\n            stop = true;\n            executorService.shutdownNow();\n        } else {\n            log.info(\"Delay task is stopping\");\n            if (taskSize() > 0) {\n                try {\n                    lock.lock();\n                    condition.await();\n                    stop = true;\n                } catch (InterruptedException e) {\n                    log.error(\"InterruptedException\", e);\n                } finally {\n                    lock.unlock();\n                }\n            }\n            executorService.shutdown();\n        }\n\n\n    }\n\n\n    private Set<Task> get(int index) {\n        return (Set<Task>) ringBuffer[index];\n    }\n\n    private void put(int key, Set<Task> tasks) {\n        int index = mod(key, bufferSize);\n        ringBuffer[index] = tasks;\n    }\n\n    /**\n     * Remove and get task list.\n     * @param key\n     * @return task list\n     */\n    private Set<Task> remove(int key) {\n        Set<Task> tempTask = new HashSet<>();\n        Set<Task> result = new HashSet<>();\n\n        Set<Task> tasks = (Set<Task>) ringBuffer[key];\n        if (tasks == null) {\n            return result;\n        }\n\n        for (Task task : tasks) {\n            if (task.getCycleNum() == 0) {\n                result.add(task);\n\n                size2Notify();\n            } else {\n                // decrement 1 cycle number and update origin data\n                task.setCycleNum(task.getCycleNum() - 1);\n                tempTask.add(task);\n            }\n            // remove task, and free the memory.\n            taskMap.remove(task.getTaskId());\n        }\n\n        //update origin data\n        ringBuffer[key] = tempTask;\n\n        return result;\n    }\n\n    private void size2Notify() {\n        try {\n            lock.lock();\n            size--;\n            if (size == 0) {\n                condition.signal();\n            }\n        } finally {\n            lock.unlock();\n        }\n    }\n\n    private boolean powerOf2(int target) {\n        if (target < 0) {\n            return false;\n        }\n        int value = target & (target - 1);\n        if (value != 0) {\n            return false;\n        }\n\n        return true;\n    }\n\n    private int mod(int target, int mod) {\n        // equals target % mod\n        target = target + tick.get();\n        return target & (mod - 1);\n    }\n\n    private int cycleNum(int target, int mod) {\n        //equals target/mod\n        return target >> Integer.bitCount(mod - 1);\n    }\n\n    /**\n     * An abstract class used to implement business.\n     */\n    public abstract static class Task extends Thread {\n\n        private int index;\n\n        private int cycleNum;\n\n        private int key;\n\n        /**\n         * The unique ID of the task\n         */\n        private int taskId;\n\n        @Override\n        public void run() {\n        }\n\n        public int getKey() {\n            return key;\n        }\n\n        /**\n         *\n         * @param key Delay time(seconds)\n         */\n        public void setKey(int key) {\n            this.key = key;\n        }\n\n        public int getCycleNum() {\n            return cycleNum;\n        }\n\n        private void setCycleNum(int cycleNum) {\n            this.cycleNum = cycleNum;\n        }\n\n        public int getIndex() {\n            return index;\n        }\n\n        private void setIndex(int index) {\n            this.index = index;\n        }\n\n        public int getTaskId() {\n            return taskId;\n        }\n\n        public void setTaskId(int taskId) {\n            this.taskId = taskId;\n        }\n    }\n\n\n    private class TriggerJob implements Runnable {\n\n        @Override\n        public void run() {\n            int index = 0;\n            while (!stop) {\n                try {\n                    Set<Task> tasks = remove(index);\n                    for (Task task : tasks) {\n                        executorService.submit(task);\n                    }\n\n                    if (++index > bufferSize - 1) {\n                        index = 0;\n                    }\n\n                    //Total tick number of records\n                    tick.incrementAndGet();\n                    TimeUnit.SECONDS.sleep(1);\n\n                } catch (Exception e) {\n                    log.error(\"Exception\", e);\n                }\n\n            }\n\n            log.info(\"Delay task has stopped\");\n        }\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/SortArrayMap.java",
    "content": "package com.crossoverjie.cim.common.data.construct;\n\nimport java.util.AbstractMap;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Set;\nimport org.apache.curator.shaded.com.google.common.collect.Sets;\n\n/**\n * Function:根据 key 排序的 Map\n *\n * @author crossoverJie\n * Date: 2019-02-25 18:17\n * @since JDK 1.8\n */\npublic class SortArrayMap extends AbstractMap<String, String> {\n\n    /**\n     * 核心数组\n     */\n    private Node[] buckets;\n\n    private static final int DEFAULT_SIZE = 10;\n\n    /**\n     * 数组大小\n     */\n    private int size = 0;\n\n    public SortArrayMap() {\n        buckets = new Node[DEFAULT_SIZE];\n    }\n\n    /**\n     * 写入数据\n     * @param key\n     * @param value\n     */\n    public void add(Long key, String value) {\n        checkSize(size + 1);\n        Node node = new Node(key, value);\n        buckets[size++] = node;\n    }\n\n    public SortArrayMap remove(String value) {\n        List<Node> list = new ArrayList<>(Arrays.asList(buckets));\n        list.removeIf(next -> next != null && next.value.equals(value));\n        buckets = list.toArray(new Node[0]);\n        return this;\n    }\n\n    /**\n     * 校验是否需要扩容\n     * @param size\n     */\n    private void checkSize(int size) {\n        if (size >= buckets.length) {\n            //扩容自身的 3/2\n            int oldLen = buckets.length;\n            int newLen = oldLen + (oldLen >> 1);\n            buckets = Arrays.copyOf(buckets, newLen);\n        }\n    }\n\n    /**\n     * 顺时针取出数据\n     * @param key\n     * @return\n     */\n    public String firstNodeValue(long key) {\n        if (size == 0) {\n            return null;\n        }\n        for (Node bucket : buckets) {\n            if (bucket == null) {\n                break;\n            }\n            if (bucket.key >= key) {\n                return bucket.value;\n            }\n        }\n\n        return buckets[0].value;\n\n    }\n\n    /**\n     * 排序\n     */\n    public void sort() {\n        Arrays.sort(buckets, 0, size, (o1, o2) -> {\n            if (o1.key > o2.key) {\n                return 1;\n            } else {\n                return 0;\n            }\n        });\n    }\n\n    public void print() {\n        for (Node bucket : buckets) {\n            if (bucket == null) {\n                continue;\n            }\n            System.out.println(bucket.toString());\n        }\n    }\n\n    @Override\n    public int size() {\n        return size;\n    }\n\n    @Override\n    public void clear() {\n        buckets = new Node[DEFAULT_SIZE];\n        size = 0;\n    }\n\n    @Override\n    public Set<Entry<String, String>> entrySet() {\n        Set<Entry<String, String>> set = Sets.newHashSet();\n        for (Node bucket : buckets) {\n            set.add(new SimpleEntry<>(String.valueOf(bucket.key), bucket.value));\n        }\n        return set;\n    }\n\n    @Override\n    public Set<String> keySet() {\n        Set<String> set = Sets.newHashSet();\n        for (Node bucket : buckets) {\n            if (bucket == null) {\n                continue;\n            }\n            set.add(bucket.value);\n        }\n        return set;\n    }\n\n    /**\n     * 数据节点\n     */\n    private class Node {\n        public Long key;\n        public String value;\n\n        public Node(Long key, String value) {\n            this.key = key;\n            this.value = value;\n        }\n\n        @Override\n        public String toString() {\n            return \"Node{\" +\n                    \"key=\" + key +\n                    \", value='\" + value + '\\'' +\n                    '}';\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/TrieTree.java",
    "content": "package com.crossoverjie.cim.common.data.construct;\n\nimport com.crossoverjie.cim.common.util.StringUtil;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * Function:字典树字符前缀模糊匹配\n *\n * @author crossoverJie\n *         Date: 2019/1/7 18:58\n * @since JDK 1.8\n */\npublic class TrieTree {\n\n    /**\n     * 大小写都可保存\n     */\n    private static final int CHILDREN_LENGTH = 26 * 2;\n\n    /**\n     * 存放的最大字符串长度\n     */\n    private static final int MAX_CHAR_LENGTH = 16;\n\n    private static final char UPPERCASE_STAR = 'A';\n\n    /**\n     * 小写就要 -71\n     */\n    private static final char LOWERCASE_STAR = 'G';\n\n    private Node root;\n\n    public TrieTree() {\n        root = new Node();\n    }\n\n    /**\n     * 写入\n     *\n     * @param data\n     */\n    public void insert(String data) {\n        this.insert(this.root, data);\n    }\n\n    private void insert(Node root, String data) {\n        char[] chars = data.toCharArray();\n        for (int i = 0; i < chars.length; i++) {\n            char aChar = chars[i];\n            int index;\n            if (Character.isUpperCase(aChar)) {\n                index = aChar - UPPERCASE_STAR;\n            } else {\n                //小写就要 -71\n                index = aChar - LOWERCASE_STAR;\n            }\n\n\n            if (index >= 0 && index < CHILDREN_LENGTH) {\n                if (root.children[index] == null) {\n                    Node node = new Node();\n                    root.children[index] = node;\n                    root.children[index].data = chars[i];\n\n                }\n\n                //最后一个字符设置标志\n                if (i + 1 == chars.length) {\n                    root.children[index].isEnd = true;\n                }\n\n                //指向下一节点\n                root = root.children[index];\n            }\n\n        }\n    }\n\n\n    /**\n     * 递归深度遍历\n     *\n     * @param key\n     * @return\n     */\n    public List<String> prefixSearch(String key) {\n        List<String> value = new ArrayList<String>();\n        if (StringUtil.isEmpty(key)) {\n            return value;\n        }\n\n        char k = key.charAt(0);\n        int index;\n        if (Character.isUpperCase(k)) {\n            index = k - UPPERCASE_STAR;\n        } else {\n            index = k - LOWERCASE_STAR;\n\n        }\n        if (root.children != null && root.children[index] != null) {\n            return query(root.children[index], value,\n                    key.substring(1), String.valueOf(k));\n        }\n        return value;\n    }\n\n    private List<String> query(Node child, List<String> value, String key, String result) {\n\n        if (child.isEnd && key == null) {\n            value.add(result);\n        }\n        if (StringUtil.isNotEmpty(key)) {\n            char ca = key.charAt(0);\n\n            int index;\n            if (Character.isUpperCase(ca)) {\n                index = ca - UPPERCASE_STAR;\n            } else {\n                index = ca - LOWERCASE_STAR;\n\n            }\n\n            if (child.children[index] != null) {\n                query(child.children[index], value, key.substring(1).equals(\"\") ? null : key.substring(1), result + ca);\n            }\n        } else {\n            for (int i = 0; i < CHILDREN_LENGTH; i++) {\n                if (child.children[i] == null) {\n                    continue;\n                }\n\n                int j;\n                if (Character.isUpperCase(child.children[i].data)) {\n                    j = UPPERCASE_STAR + i;\n                } else {\n                    j = LOWERCASE_STAR + i;\n                }\n\n                char temp = (char) j;\n                query(child.children[i], value, null, result + temp);\n            }\n        }\n\n        return value;\n    }\n\n\n    /**\n     * 查询所有\n     *\n     * @return\n     */\n    public List<String> all() {\n        char[] chars = new char[MAX_CHAR_LENGTH];\n        List<String> value = depth(this.root, new ArrayList<String>(), chars, 0);\n        return value;\n    }\n\n\n    public List<String> depth(Node node, List<String> list, char[] chars, int index) {\n        if (node.children == null || node.children.length == 0) {\n            return list;\n        }\n\n        Node[] children = node.children;\n\n        for (int i = 0; i < children.length; i++) {\n            Node child = children[i];\n\n            if (child == null) {\n                continue;\n            }\n\n            if (child.isEnd) {\n                chars[index] = child.data;\n\n                char[] temp = new char[index + 1];\n                for (int j = 0; j < chars.length; j++) {\n                    if (chars[j] == 0) {\n                        continue;\n                    }\n\n                    temp[j] = chars[j];\n                }\n                list.add(String.valueOf(temp));\n                return list;\n            } else {\n                chars[index] = child.data;\n\n                index++;\n\n                depth(child, list, chars, index);\n\n                index = 0;\n            }\n        }\n\n\n        return list;\n    }\n\n\n    /**\n     * 字典树节点\n     */\n    private class Node {\n        /**\n         * 是否为最后一个字符\n         */\n        public boolean isEnd = false;\n\n        /**\n         * 如果只是查询，则不需要存储数据\n         */\n        public char data;\n\n        public Node[] children = new Node[CHILDREN_LENGTH];\n\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/enums/StatusEnum.java",
    "content": "package com.crossoverjie.cim.common.enums;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * @author crossoverJie\n */\n\npublic enum StatusEnum {\n\n    /** 成功 */\n    SUCCESS(\"9000\", \"Success\"),\n    /** 成功 */\n    FALLBACK(\"8000\", \"FALL_BACK\"),\n    /** 参数校验失败**/\n    VALIDATION_FAIL(\"3000\", \"invalid argument\"),\n    /** 失败 */\n    FAIL(\"4000\", \"Failure\"),\n\n    /** 重复登录 */\n    REPEAT_LOGIN(\"5000\", \"Repeat login, log out an account please!\"),\n\n    /** 请求限流 */\n    REQUEST_LIMIT(\"6000\", \"请求限流\"),\n\n    /** 账号不在线 */\n    OFF_LINE(\"7000\", \"You selected user is offline!, please try again later!\"),\n\n    SERVER_NOT_AVAILABLE(\"7100\", \"cim server is not available, please try again later!\"),\n\n    RECONNECT_FAIL(\"7200\", \"Reconnect fail, continue to retry!\"),\n    /** 登录信息不匹配 */\n    ACCOUNT_NOT_MATCH(\"9100\", \"The User information you have used is incorrect!\"),\n\n    OFFLINE_MESSAGE_STORAGE_ERROR(\"9200\", \"Offline message storage error!\"),\n\n    OFFLINE_MESSAGE_FETCH_ERROR(\"9201\", \"Offline message fetch error!\"),\n\n    OFFLINE_MESSAGE_DELETE_ERROR(\"9202\", \"Offline message delete error!\");\n\n\n    /** 枚举值码 */\n    private final String code;\n\n    /** 枚举描述 */\n    private final String message;\n\n    /**\n     * 构建一个 StatusEnum 。\n     * @param code 枚举值码。\n     * @param message 枚举描述。\n     */\n    private StatusEnum(String code, String message) {\n        this.code = code;\n        this.message = message;\n    }\n\n    /**\n     * 得到枚举值码。\n     * @return 枚举值码。\n     */\n    public String getCode() {\n        return code;\n    }\n\n    /**\n     * 得到枚举描述。\n     * @return 枚举描述。\n     */\n    public String getMessage() {\n        return message;\n    }\n\n    /**\n     * 得到枚举值码。\n     * @return 枚举值码。\n     */\n    public String code() {\n        return code;\n    }\n\n    /**\n     * 得到枚举描述。\n     * @return 枚举描述。\n     */\n    public String message() {\n        return message;\n    }\n\n    /**\n     * 通过枚举值码查找枚举值。\n     * @param code 查找枚举值的枚举值码。\n     * @return 枚举值码对应的枚举值。\n     * @throws IllegalArgumentException 如果 code 没有对应的 StatusEnum 。\n     */\n    public static StatusEnum findStatus(String code) {\n        for (StatusEnum status : values()) {\n            if (status.getCode().equals(code)) {\n                return status;\n            }\n        }\n        throw new IllegalArgumentException(\"ResultInfo StatusEnum not legal:\" + code);\n    }\n\n    /**\n     * 获取全部枚举值。\n     *\n     * @return 全部枚举值。\n     */\n    public static List<StatusEnum> getAllStatus() {\n        List<StatusEnum> list = new ArrayList<StatusEnum>();\n        for (StatusEnum status : values()) {\n            list.add(status);\n        }\n        return list;\n    }\n\n    /**\n     * 获取全部枚举值码。\n     *\n     * @return 全部枚举值码。\n     */\n    public static List<String> getAllStatusCode() {\n        List<String> list = new ArrayList<String>();\n        for (StatusEnum status : values()) {\n            list.add(status.code());\n        }\n        return list;\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/enums/SystemCommandEnum.java",
    "content": "package com.crossoverjie.cim.common.enums;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/12/26 18:38\n * @since JDK 1.8\n */\npublic enum SystemCommandEnum {\n\n            ALL(\":all       \", \"获取所有命令\", \"PrintAllCommand\"),\n    ONLINE_USER(\":olu       \", \"获取所有在线用户\", \"PrintOnlineUsersCommand\"),\n           QUIT(\":q!        \", \"退出程序\", \"ShutDownCommand\"),\n          QUERY(\":q         \", \"【:q 关键字】查询聊天记录\", \"QueryHistoryCommand\"),\n             AI(\":ai        \", \"开启 AI 模式\", \"OpenAIModelCommand\"),\n            QAI(\":qai       \", \"关闭 AI 模式\", \"CloseAIModelCommand\"),\n         PREFIX(\":pu        \", \"模糊匹配用户\", \"PrefixSearchCommand\"),\n          EMOJI(\":emoji     \", \"emoji 表情列表\", \"EmojiCommand\"),\n           INFO(\":info      \", \"获取客户端信息\", \"EchoInfoCommand\"),\n      DELAY_MSG(\":delay     \", \"delay message, :delay [msg] [delayTime]\", \"DelayMsgCommand\");\n\n    /** 枚举值码 */\n    private final String commandType;\n\n    /** 枚举描述 */\n    private final String desc;\n\n    /**\n     * 实现类\n     */\n    private final String clazz;\n\n\n    /**\n     * 构建一个 。\n     * @param commandType 枚举值码。\n     * @param desc 枚举描述。\n     */\n    private SystemCommandEnum(String commandType, String desc, String clazz) {\n        this.commandType = commandType;\n        this.desc = desc;\n        this.clazz = clazz;\n    }\n\n    /**\n     * 得到枚举值码。\n     * @return 枚举值码。\n     */\n    public String getCommandType() {\n        return commandType;\n    }\n    /**\n     * 获取 class。\n     * @return class。\n     */\n    public String getClazz() {\n        return clazz;\n    }\n\n    /**\n     * 得到枚举描述。\n     * @return 枚举描述。\n     */\n    public String getDesc() {\n        return desc;\n    }\n\n    /**\n     * 得到枚举值码。\n     * @return 枚举值码。\n     */\n    public String code() {\n        return commandType;\n    }\n\n    /**\n     * 得到枚举描述。\n     * @return 枚举描述。\n     */\n    public String message() {\n        return desc;\n    }\n\n    /**\n     * 获取全部枚举值码。\n     *\n     * @return 全部枚举值码。\n     */\n    public static Map<String, String> getAllStatusCode() {\n        Map<String, String> map = new HashMap<String, String>(16);\n        for (SystemCommandEnum status : values()) {\n            map.put(status.getCommandType(), status.getDesc());\n        }\n        return map;\n    }\n\n    public static Map<String, String> getAllClazz() {\n        Map<String, String> map = new HashMap<String, String>(16);\n        for (SystemCommandEnum status : values()) {\n            map.put(status.getCommandType().trim(), \"com.crossoverjie.cim.client.service.impl.command.\" + status.getClazz());\n        }\n        return map;\n    }\n\n\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/exception/CIMException.java",
    "content": "package com.crossoverjie.cim.common.exception;\n\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/8/25 15:26\n * @since JDK 1.8\n */\npublic class CIMException extends GenericException {\n\n\n    public CIMException(String errorCode, String errorMessage) {\n        super(errorMessage);\n        this.errorCode = errorCode;\n        this.errorMessage = errorMessage;\n    }\n\n    public CIMException(Exception e, String errorCode, String errorMessage) {\n        super(e, errorMessage);\n        this.errorCode = errorCode;\n        this.errorMessage = errorMessage;\n    }\n\n    public CIMException(String message) {\n        super(message);\n        this.errorMessage = message;\n    }\n\n    public CIMException(StatusEnum statusEnum) {\n        super(statusEnum.getMessage());\n        this.errorMessage = statusEnum.message();\n        this.errorCode = statusEnum.getCode();\n    }\n\n    public CIMException(StatusEnum statusEnum, String message) {\n        super(message);\n        this.errorMessage = message;\n        this.errorCode = statusEnum.getCode();\n    }\n\n    public CIMException(Exception oriEx) {\n        super(oriEx);\n    }\n\n    public CIMException(Throwable oriEx) {\n        super(oriEx);\n    }\n\n    public CIMException(String message, Exception oriEx) {\n        super(message, oriEx);\n        this.errorMessage = message;\n    }\n\n    public CIMException(String message, Throwable oriEx) {\n        super(message, oriEx);\n        this.errorMessage = message;\n    }\n\n\n    public static boolean isResetByPeer(String msg) {\n        if (\"Connection reset by peer\".equals(msg)) {\n            return true;\n        }\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/exception/GenericException.java",
    "content": "package com.crossoverjie.cim.common.exception;\n\nimport java.io.Serializable;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/8/25 15:27\n * @since JDK 1.8\n */\npublic class GenericException extends RuntimeException implements Serializable {\n    private static final long serialVersionUID = 1L;\n    String errorCode;\n    String errorMessage;\n\n    public GenericException() {\n    }\n\n    public GenericException(String message) {\n        super(message);\n    }\n\n    public GenericException(Exception oriEx) {\n        super(oriEx);\n    }\n\n    public GenericException(Exception oriEx, String message) {\n        super(message, oriEx);\n    }\n\n    public GenericException(Throwable oriEx) {\n        super(oriEx);\n    }\n\n    public GenericException(String message, Exception oriEx) {\n        super(message, oriEx);\n    }\n\n    public GenericException(String message, Throwable oriEx) {\n        super(message, oriEx);\n    }\n\n    public String getErrorCode() {\n        return this.errorCode;\n    }\n\n    public void setErrorCode(String errorCode) {\n        this.errorCode = errorCode;\n    }\n\n    public String getErrorMessage() {\n        return this.errorMessage;\n    }\n\n    public void setErrorMessage(String errorMessage) {\n        this.errorMessage = errorMessage;\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/kit/HeartBeatHandler.java",
    "content": "package com.crossoverjie.cim.common.kit;\n\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-20 17:15\n * @since JDK 1.8\n */\npublic interface HeartBeatHandler {\n\n    /**\n     * 处理心跳\n     * @param ctx\n     * @throws Exception\n     */\n    void process(ChannelHandlerContext ctx) throws Exception;\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/metastore/AbstractConfiguration.java",
    "content": "package com.crossoverjie.cim.common.metastore;\n\nimport lombok.Builder;\nimport lombok.Data;\n\n/**\n * @author crossverJie\n */\n@Data\n@Builder\npublic class AbstractConfiguration<RETRY> {\n\n    private String metaServiceUri;\n    private int timeoutMs;\n    private RETRY retryPolicy;\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/metastore/MetaStore.java",
    "content": "package com.crossoverjie.cim.common.metastore;\n\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * @author crossoverJie\n */\npublic interface MetaStore {\n\n    void initialize(AbstractConfiguration<?> configuration) throws Exception;\n\n    /**\n     * Get available server list\n     * @return available server list\n     * @throws Exception exception\n     */\n    Set<String> getAvailableServerList() throws Exception;\n\n    /**\n     * Add server to meta store\n     * @throws Exception exception\n     */\n    void addServer(String ip, int cimServerPort, int httpPort) throws Exception;\n\n    /**\n     * Subscribe server list\n     * @param childListener child listener\n     * @throws Exception exception\n     */\n    void listenServerList(ChildListener childListener) throws Exception;\n\n\n    /**\n     * @throws Exception\n     */\n    void rebuildCache() throws Exception;\n\n    interface ChildListener {\n        /**\n         * Child changed\n         * @param parentPath parent path(eg. for zookeeper: [/cim])\n         * @param currentChildren current children\n         * @throws Exception exception\n         */\n        void childChanged(String parentPath, List<String> currentChildren) throws Exception;\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/metastore/ZkConfiguration.java",
    "content": "package com.crossoverjie.cim.common.metastore;\n\nimport org.apache.curator.RetryPolicy;\n\n/**\n * @author crossoverJie\n */\npublic class ZkConfiguration extends AbstractConfiguration<RetryPolicy> {\n    ZkConfiguration(String metaServiceUri, int timeout, RetryPolicy retryPolicy) {\n        super(metaServiceUri, timeout, retryPolicy);\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/metastore/ZkMetaStoreImpl.java",
    "content": "package com.crossoverjie.cim.common.metastore;\n\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverjie.cim.common.util.RouteInfoParseUtil;\nimport com.github.benmanes.caffeine.cache.Cache;\nimport com.github.benmanes.caffeine.cache.Caffeine;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport lombok.extern.slf4j.Slf4j;\nimport org.I0Itec.zkclient.ZkClient;\nimport org.apache.curator.framework.CuratorFramework;\nimport org.apache.zookeeper.CreateMode;\nimport org.apache.zookeeper.Watcher;\n\n/**\n * @author crossovreJie\n */\n@Slf4j\npublic class ZkMetaStoreImpl implements MetaStore {\n    public static final String ROOT = \"/cim\";\n\n    private ZkClient client;\n\n    Cache<String, String> cache;\n\n    @Override\n    public void initialize(AbstractConfiguration<?> configuration) throws Exception {\n        cache = Caffeine.newBuilder().build();\n        client = new ZkClient(configuration.getMetaServiceUri(), configuration.getTimeoutMs());\n    }\n\n    @Override\n    public Set<String> getAvailableServerList() throws Exception {\n        if (cache.asMap().size() > 0) {\n            return cache.asMap().keySet();\n        }\n        List<String> coll = client.getChildren(ROOT);\n        Map<String, String> voidMap = coll.stream().collect(Collectors.toMap(\n                Function.identity(),\n                Function.identity()\n        ));\n        cache.putAll(voidMap);\n        return voidMap.keySet();\n    }\n\n    @Override\n    public void addServer(String ip, int cimServerPort, int httpPort) throws Exception {\n        boolean exists = client.exists(ROOT);\n        if (!exists) {\n            client.createPersistent(ROOT);\n        }\n        String zkParse = RouteInfoParseUtil.parse(RouteInfo.builder()\n                .ip(ip)\n                .cimServerPort(cimServerPort)\n                .httpPort(httpPort)\n                .build());\n        String serverPath = String.format(\"%s/%s\", ROOT, zkParse);\n        client.createEphemeral(serverPath);\n        log.info(\"Add server to zk [{}]\", serverPath);\n    }\n\n    @Override\n    public void listenServerList(ChildListener childListener) throws Exception {\n        client.subscribeChildChanges(ROOT, (parentPath, currentChildren) -> {\n            log.info(\"Clear and update local cache parentPath=[{}],current server list=[{}]\", parentPath, currentChildren.toString());\n            childListener.childChanged(parentPath, currentChildren);\n\n            // TODO: 2024/8/19 maybe can reuse currentChildren.\n            // Because rebuildCache() will re-fetch the server list from zk.\n            rebuildCache();\n        });\n    }\n\n    @Override\n    public synchronized void rebuildCache() throws Exception {\n        cache.invalidateAll();\n        this.getAvailableServerList();\n\n    }\n\n\n    private List<String> watchedGetChildren(CuratorFramework client, String path) throws Exception {\n        /**\n         * Get children and set a watcher on the node. The watcher notification will come through the\n         * CuratorListener (see setDataAsync() above).\n         */\n        return client.getChildren().watched().forPath(path);\n    }\n\n    private void createEphemeral(CuratorFramework client, String path, byte[] payload) throws Exception {\n        // this will create the given EPHEMERAL ZNode with the given data\n        client.create().withMode(CreateMode.EPHEMERAL).forPath(path, payload);\n    }\n\n    private void create(CuratorFramework client, String path, byte[] payload) throws Exception {\n        // this will create the given ZNode with the given data\n        client.create().forPath(path, payload);\n    }\n\n    private void watchedGetChildren(CuratorFramework client, String path, Watcher watcher)\n            throws Exception {\n        // Get children and set the given watcher on the node.\n        client.getChildren().usingWatcher(watcher).forPath(path);\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/pojo/CIMUserInfo.java",
    "content": "package com.crossoverjie.cim.common.pojo;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n/**\n * Function: 用户信息\n *\n * @author crossoverJie\n *         Date: 2018/12/24 02:33\n * @since JDK 1.8\n */\n@Data\n@AllArgsConstructor\n@NoArgsConstructor\npublic class CIMUserInfo {\n    private Long userId;\n    private String userName;\n\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/pojo/RouteInfo.java",
    "content": "package com.crossoverjie.cim.common.pojo;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2020-04-12 20:48\n * @since JDK 1.8\n */\n@Data\n@AllArgsConstructor\n@Builder\npublic final class RouteInfo {\n\n    private String ip;\n    private Integer cimServerPort;\n    private Integer httpPort;\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/req/BaseRequest.java",
    "content": "package com.crossoverjie.cim.common.req;\n\n\nimport io.swagger.v3.oas.annotations.media.Schema;\n\n/**\n * Function:\n * @author crossoverJie\n * Date: 2017/6/7 下午11:28\n * @since JDK 1.8\n */\npublic class BaseRequest {\n\n\n    @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = \"reqNo\", example = \"1234567890\")\n    private String reqNo;\n\n    @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = \"timestamp\", example = \"0\")\n    private int timeStamp;\n\n\n\n    public BaseRequest() {\n        this.setTimeStamp((int)(System.currentTimeMillis() / 1000));\n    }\n\n    public String getReqNo() {\n        return reqNo;\n    }\n\n    public void setReqNo(String reqNo) {\n        this.reqNo = reqNo;\n    }\n\n    public int getTimeStamp() {\n        return timeStamp;\n    }\n\n    public void setTimeStamp(int timeStamp) {\n        this.timeStamp = timeStamp;\n    }\n\n\n    @Override\n    public String toString() {\n        return \"BaseRequest{\" +\n                \"reqNo='\" + reqNo + '\\'' +\n                \", timeStamp=\" + timeStamp +\n                '}';\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/res/BaseResponse.java",
    "content": "package com.crossoverjie.cim.common.res;\n\n\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverjie.cim.common.util.StringUtil;\n\nimport java.io.Serializable;\n\npublic class BaseResponse<T> implements Serializable {\n\tprivate String code;\n\n\tprivate String message;\n\n\t/**\n\t * 请求号\n\t */\n\tprivate String reqNo;\n\n\tprivate T dataBody;\n\n\tpublic BaseResponse() {}\n\n\tpublic BaseResponse(T dataBody) {\n\t\tthis.dataBody = dataBody;\n\t}\n\n\tpublic BaseResponse(String code, String message) {\n\t\tthis.code = code;\n\t\tthis.message = message;\n\t}\n\n\tpublic BaseResponse(String code, String message, T dataBody) {\n\t\tthis.code = code;\n\t\tthis.message = message;\n\t\tthis.dataBody = dataBody;\n\t}\n\n\tpublic BaseResponse(String code, String message, String reqNo, T dataBody) {\n\t\tthis.code = code;\n\t\tthis.message = message;\n\t\tthis.reqNo = reqNo;\n\t\tthis.dataBody = dataBody;\n\t}\n\n\tpublic static <T> BaseResponse<T> create(T t) {\n\t\treturn new BaseResponse<T>(t);\n\t}\n\n\tpublic static <T> BaseResponse<T> create(T t, StatusEnum statusEnum) {\n\t\treturn new BaseResponse<T>(statusEnum.getCode(), statusEnum.getMessage(), t);\n\t}\n\n\tpublic static <T> BaseResponse<T> createSuccess(T t, String message) {\n\t\tString msg = StringUtil.isNullOrEmpty(message) ? StatusEnum.SUCCESS.getMessage() : message;\n\t\treturn new BaseResponse<T>(StatusEnum.SUCCESS.getCode(), msg, t);\n\t}\n\n\tpublic static <T> BaseResponse<T> createFail(T t, String message) {\n\t\treturn new BaseResponse<T>(StatusEnum.FAIL.getCode(), StringUtil.isNullOrEmpty(message) ? StatusEnum.FAIL.getMessage() : message, t);\n\t}\n\n\tpublic static <T> BaseResponse<T> create(T t, StatusEnum statusEnum, String message) {\n\n\t\treturn new BaseResponse<T>(statusEnum.getCode(), message, t);\n\t}\n\n\n\tpublic String getCode() {\n\t\treturn code;\n\t}\n\n\tpublic void setCode(String code) {\n\t\tthis.code = code;\n\t}\n\n\tpublic String getMessage() {\n\t\treturn message;\n\t}\n\n\tpublic void setMessage(String message) {\n\t\tthis.message = message;\n\t}\n\n\tpublic T getDataBody() {\n\t\treturn dataBody;\n\t}\n\n\tpublic void setDataBody(T dataBody) {\n\t\tthis.dataBody = dataBody;\n\t}\n\n\tpublic String getReqNo() {\n\t\treturn reqNo;\n\t}\n\n\tpublic void setReqNo(String reqNo) {\n\t\tthis.reqNo = reqNo;\n\t}\n\n\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/res/NULLBody.java",
    "content": "package com.crossoverjie.cim.common.res;\n\n/**\n * Function:空对象,用在泛型中,表示没有额外的请求参数或者返回参数\n *\n * @author crossoverJie\n *         Date: 2017/6/7 下午11:57\n * @since JDK 1.8\n */\npublic class NULLBody {\n    public NULLBody() {}\n\n    public static NULLBody create() {\n        return new NULLBody();\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/RouteHandle.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm;\n\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport java.util.List;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-02-27 00:31\n * @since JDK 1.8\n */\npublic interface RouteHandle {\n\n    /**\n     * 再一批服务器里进行路由\n     * @param values\n     * @param key\n     * @return\n     */\n    // TODO: 2024/9/13 Use List<RouteInfo> instead of List<String> to make the code more type-safe\n    String routeServer(List<String> values, String key);\n\n    List<String> removeExpireServer(RouteInfo routeInfo);\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/AbstractConsistentHash.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport java.io.UnsupportedEncodingException;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Function:一致性 hash 算法抽象类\n *\n * @author crossoverJie\n * Date: 2019-02-27 00:35\n * @since JDK 1.8\n */\npublic abstract class AbstractConsistentHash {\n\n    /**\n     * 新增节点\n     * @param key\n     * @param value\n     */\n    protected abstract void add(long key, String value);\n\n    /**\n     * remove node\n     * @param value node\n     * @return current data\n     */\n    protected abstract Map<String, String> remove(String value);\n\n    /**\n     * Clear old data in the structure\n     */\n    protected abstract void clear();\n\n    /**\n     * 排序节点，数据结构自身支持排序可以不用重写\n     */\n    protected void sort() { }\n\n    /**\n     * 根据当前的 key 通过一致性 hash 算法的规则取出一个节点\n     * @param value\n     * @return\n     */\n    protected abstract String getFirstNodeValue(String value);\n\n    /**\n     * 传入节点列表以及客户端信息获取一个服务节点\n     * @param values\n     * @param key\n     * @return\n     */\n    public String process(List<String> values, String key) {\n        // fix https://github.com/crossoverJie/cim/issues/79\n        clear();\n        for (String value : values) {\n            add(hash(value), value);\n        }\n        sort();\n\n        return getFirstNodeValue(key);\n    }\n\n    /**\n     * hash 运算\n     * @param value\n     * @return\n     */\n    public Long hash(String value) {\n        MessageDigest md5;\n        try {\n            md5 = MessageDigest.getInstance(\"MD5\");\n        } catch (NoSuchAlgorithmException e) {\n            throw new RuntimeException(\"MD5 not supported\", e);\n        }\n        md5.reset();\n        byte[] keyBytes = null;\n        try {\n            keyBytes = value.getBytes(\"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            throw new RuntimeException(\"Unknown string :\" + value, e);\n        }\n\n        md5.update(keyBytes);\n        byte[] digest = md5.digest();\n\n        // hash code, Truncate to 32-bits\n        long hashCode = ((long) (digest[3] & 0xFF) << 24)\n                | ((long) (digest[2] & 0xFF) << 16)\n                | ((long) (digest[1] & 0xFF) << 8)\n                | (digest[0] & 0xFF);\n\n        long truncateHashCode = hashCode & 0xffffffffL;\n        return truncateHashCode;\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/ConsistentHashHandle.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverjie.cim.common.route.algorithm.RouteHandle;\n\nimport com.crossoverjie.cim.common.util.RouteInfoParseUtil;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-02-27 00:33\n * @since JDK 1.8\n */\npublic class ConsistentHashHandle implements RouteHandle {\n    private AbstractConsistentHash hash;\n\n    public void setHash(AbstractConsistentHash hash) {\n        this.hash = hash;\n    }\n\n    @Override\n    public String routeServer(List<String> values, String key) {\n        return hash.process(values, key);\n    }\n\n    @Override\n    public List<String> removeExpireServer(RouteInfo routeInfo) {\n        Map<String, String> remove = hash.remove(RouteInfoParseUtil.parse(routeInfo));\n        return new ArrayList<>(remove.keySet());\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/SortArrayMapConsistentHash.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport com.crossoverjie.cim.common.data.construct.SortArrayMap;\nimport com.google.common.annotations.VisibleForTesting;\n\nimport java.util.Map;\n\n/**\n * Function:自定义排序 Map 实现\n *\n * @author crossoverJie\n * Date: 2019-02-27 00:38\n * @since JDK 1.8\n */\npublic class SortArrayMapConsistentHash extends AbstractConsistentHash {\n\n    private SortArrayMap sortArrayMap = new SortArrayMap();\n\n    /**\n     * 虚拟节点数量\n     */\n    private static final int VIRTUAL_NODE_SIZE = 2;\n\n    @Override\n    public void add(long key, String value) {\n        for (int i = 0; i < VIRTUAL_NODE_SIZE; i++) {\n            Long hash = super.hash(\"vir\" + key + i);\n            sortArrayMap.add(hash, value);\n        }\n        sortArrayMap.add(key, value);\n    }\n\n    @Override\n    protected Map<String, String> remove(String value) {\n        sortArrayMap = sortArrayMap.remove(value);\n        return sortArrayMap;\n    }\n\n    @Override\n    public void sort() {\n        sortArrayMap.sort();\n    }\n\n    /**\n     * Used only in test.\n     * @return Return the data structure of the current algorithm.\n     */\n    @VisibleForTesting\n    public SortArrayMap getSortArrayMap() {\n        return sortArrayMap;\n    }\n\n    @Override\n    protected void clear() {\n        sortArrayMap.clear();\n    }\n\n    @Override\n    public String getFirstNodeValue(String value) {\n        long hash = super.hash(value);\n        return sortArrayMap.firstNodeValue(hash);\n    }\n\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/TreeMapConsistentHash.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.google.common.annotations.VisibleForTesting;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.SortedMap;\nimport java.util.TreeMap;\n\n/**\n * Function:TreeMap 实现\n *\n * @author crossoverJie\n * Date: 2019-02-27 01:16\n * @since JDK 1.8\n */\npublic class TreeMapConsistentHash extends AbstractConsistentHash {\n    private final TreeMap<Long, String> treeMap = new TreeMap<Long, String>();\n\n    /**\n     * 虚拟节点数量\n     */\n    private static final int VIRTUAL_NODE_SIZE = 2;\n\n    @Override\n    public void add(long key, String value) {\n        for (int i = 0; i < VIRTUAL_NODE_SIZE; i++) {\n            Long hash = super.hash(\"vir\" + key + i);\n            treeMap.put(hash, value);\n        }\n        treeMap.put(key, value);\n    }\n\n    @Override\n    protected Map<String, String> remove(String value) {\n        treeMap.entrySet().removeIf(next -> next.getValue().equals(value));\n        Map<String, String> result = new HashMap<>(treeMap.entrySet().size());\n        for (Map.Entry<Long, String> longStringEntry : treeMap.entrySet()) {\n            result.put(longStringEntry.getValue(), \"\");\n        }\n        return result;\n    }\n\n    @Override\n    protected void clear() {\n        treeMap.clear();\n    }\n\n    /**\n     * Used only in test.\n     * @return Return the data structure of the current algorithm.\n     */\n    @VisibleForTesting\n    public TreeMap getTreeMap() {\n        return treeMap;\n    }\n\n    @Override\n    public String getFirstNodeValue(String value) {\n        long hash = super.hash(value);\n        SortedMap<Long, String> last = treeMap.tailMap(hash);\n        if (!last.isEmpty()) {\n            return last.get(last.firstKey());\n        }\n        if (treeMap.size() == 0) {\n            throw new CIMException(StatusEnum.SERVER_NOT_AVAILABLE);\n        }\n        return treeMap.firstEntry().getValue();\n    }\n\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/loop/LoopHandle.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.loop;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverjie.cim.common.route.algorithm.RouteHandle;\n\nimport com.crossoverjie.cim.common.util.RouteInfoParseUtil;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicLong;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-02-27 15:13\n * @since JDK 1.8\n */\npublic class LoopHandle implements RouteHandle {\n    private final AtomicLong index = new AtomicLong();\n\n    private List<String> values;\n\n    @Override\n    public String routeServer(List<String> values, String key) {\n        if (values.size() == 0) {\n            throw new CIMException(StatusEnum.SERVER_NOT_AVAILABLE);\n        }\n        this.values = values;\n        Long position = index.incrementAndGet() % values.size();\n        if (position < 0) {\n            position = 0L;\n        }\n\n        return values.get(position.intValue());\n    }\n\n    @Override\n    public List<String> removeExpireServer(RouteInfo routeInfo) {\n        String parse = RouteInfoParseUtil.parse(routeInfo);\n        values.removeIf(next -> next.equals(parse));\n        return values;\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/random/RandomHandle.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.random;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverjie.cim.common.route.algorithm.RouteHandle;\n\nimport com.crossoverjie.cim.common.util.RouteInfoParseUtil;\nimport java.util.List;\nimport java.util.concurrent.ThreadLocalRandom;\n\n/**\n * Function: 路由策略， 随机\n *\n * @Author: jiangyunxiong\n * @Date: 2019/3/7 11:56\n * @since JDK 1.8\n */\npublic class RandomHandle implements RouteHandle {\n\n    private List<String> values;\n    @Override\n    public String routeServer(List<String> values, String key) {\n        int size = values.size();\n        if (size == 0) {\n            throw new CIMException(StatusEnum.SERVER_NOT_AVAILABLE);\n        }\n        this.values = values;\n        int offset = ThreadLocalRandom.current().nextInt(size);\n\n        return values.get(offset);\n    }\n\n    @Override\n    public List<String> removeExpireServer(RouteInfo routeInfo) {\n        String parse = RouteInfoParseUtil.parse(routeInfo);\n        values.removeIf(next -> next.equals(parse));\n        return values;\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/util/HttpClient.java",
    "content": "package com.crossoverjie.cim.common.util;\n\nimport okhttp3.MediaType;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request;\nimport okhttp3.RequestBody;\nimport okhttp3.Response;\n\nimport java.io.IOException;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2020-04-25 00:39\n * @since JDK 1.8\n */\npublic final class HttpClient {\n\n    private static final MediaType MEDIA_TYPE = MediaType.parse(\"application/json\");\n\n    public static Response post(OkHttpClient okHttpClient, String params, String url) throws IOException {\n        RequestBody requestBody = RequestBody.create(MEDIA_TYPE, params);\n\n        Request request = new Request.Builder()\n                .url(url)\n                .post(requestBody)\n                .build();\n\n        Response response = okHttpClient.newCall(request).execute();\n        if (!response.isSuccessful()) {\n            throw new IOException(\"request url [\" + url + \"], params [\" + params + \"] failed, response code: \" + response.code()\n                    + \", message: \" + response.message());\n        }\n\n        return response;\n    }\n\n    public static Response get(OkHttpClient okHttpClient, String url) throws IOException {\n        Request request = new Request.Builder()\n                .url(url)\n                .get()\n                .build();\n\n        Response response = okHttpClient.newCall(request).execute();\n        if (!response.isSuccessful()) {\n            throw new IOException(\"Unexpected code \" + response);\n        }\n\n        return response;\n    }\n\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/util/NettyAttrUtil.java",
    "content": "package com.crossoverjie.cim.common.util;\n\nimport io.netty.channel.Channel;\nimport io.netty.util.Attribute;\nimport io.netty.util.AttributeKey;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2019/1/9 00:57\n * @since JDK 1.8\n */\npublic class NettyAttrUtil {\n\n    private static final AttributeKey<String> ATTR_KEY_READER_TIME = AttributeKey.valueOf(\"readerTime\");\n\n\n    public static void updateReaderTime(Channel channel, Long time) {\n        channel.attr(ATTR_KEY_READER_TIME).set(time.toString());\n    }\n\n    public static Long getReaderTime(Channel channel) {\n        String value = getAttribute(channel, ATTR_KEY_READER_TIME);\n\n        if (value != null) {\n            return Long.valueOf(value);\n        }\n        return null;\n    }\n\n\n    private static String getAttribute(Channel channel, AttributeKey<String> key) {\n        Attribute<String> attr = channel.attr(key);\n        return attr.get();\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/util/RouteInfoParseUtil.java",
    "content": "package com.crossoverjie.cim.common.util;\n\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\n\nimport static com.crossoverjie.cim.common.enums.StatusEnum.VALIDATION_FAIL;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2020-04-12 20:42\n * @since JDK 1.8\n */\npublic class RouteInfoParseUtil {\n\n    public static RouteInfo parse(String info) {\n        try {\n            String[] serverInfo = info.split(\":\");\n            return new RouteInfo(serverInfo[0], Integer.parseInt(serverInfo[1]), Integer.parseInt(serverInfo[2]));\n        } catch (Exception e) {\n            throw new CIMException(VALIDATION_FAIL);\n        }\n    }\n\n    public static String parse(RouteInfo routeInfo) {\n        return routeInfo.getIp() + \":\" + routeInfo.getCimServerPort() + \":\" + routeInfo.getHttpPort();\n    }\n\n    private RouteInfoParseUtil() {\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/util/SnowflakeIdWorker.java",
    "content": "package com.crossoverjie.cim.common.util;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/18\n * @description\n */\npublic class SnowflakeIdWorker {\n    private final long workerId = 1L;\n    private static final long EPOCH = 1622505600000L;\n    private long sequence = 0L;\n    private long lastTimestamp = -1L;\n\n    private static final long WORKER_ID_BITS = 10L;\n    private static final long SEQUENCE_BITS = 12L;\n    private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;\n    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;\n    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;\n\n    private long tilNextMillis(long lastTs) {\n        long ts = System.currentTimeMillis();\n        while (ts <= lastTs) {\n            ts = System.currentTimeMillis();\n        }\n        return ts;\n    }\n\n    public synchronized long nextId() {\n        long ts = System.currentTimeMillis();\n        if (ts < lastTimestamp) {\n            throw new IllegalStateException(\"Clock moved backwards.\");\n        }\n        if (ts == lastTimestamp) {\n            sequence = (sequence + 1) & MAX_SEQUENCE;\n            if (sequence == 0) {\n                ts = tilNextMillis(lastTimestamp);\n            }\n        } else {\n            sequence = 0L;\n        }\n        lastTimestamp = ts;\n        return ((ts - EPOCH) << TIMESTAMP_SHIFT)\n                | (workerId << WORKER_ID_SHIFT)\n                | sequence;\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/util/StringUtil.java",
    "content": "package com.crossoverjie.cim.common.util;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 22/05/2018 15:16\n * @since JDK 1.8\n */\npublic class StringUtil {\n    public StringUtil() {\n    }\n\n    public static boolean isNullOrEmpty(String str) {\n        return null == str || 0 == str.trim().length();\n    }\n\n    public static boolean isEmpty(String str) {\n        return str == null || \"\".equals(str.trim());\n    }\n\n    public static boolean isNotEmpty(String str) {\n        return str != null && !\"\".equals(str.trim());\n    }\n\n    public static String formatLike(String str) {\n        return isNotEmpty(str) ? \"%\" + str + \"%\" : null;\n    }\n}\n"
  },
  {
    "path": "cim-common/src/main/proto/cim.proto",
    "content": "syntax = \"proto3\";\npackage com.crossoverjie.cim.common.protocol;\noption java_package = \"com.crossoverjie.cim.common.protocol\";\noption java_multiple_files = true;\n\nmessage Request{\n  int64 requestId = 2;\n  string reqMsg = 1;\n  BaseCommand cmd = 3;\n  map<string, string> properties = 4;\n  repeated string batchReqMsg = 5;\n}\n\nmessage Response{\n  int64 responseId = 2;\n  string resMsg = 1;\n  BaseCommand cmd = 3;\n  map<string, string> properties = 4;\n  repeated string batchResMsg = 5;\n}\n\nenum BaseCommand{\n  LOGIN_REQUEST = 0;\n  MESSAGE = 1;\n  PING = 2;\n  OFFLINE = 3;\n}"
  },
  {
    "path": "cim-common/src/main/resources/log4j.properties",
    "content": "# Global logging configuration\r\nlog4j.rootLogger=DEBUG,CONSOLE,LOGFILE\r\n\r\nlog4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender\r\n\r\nlog4j.appender.CONSOLE.Target = System.out\r\nlog4j.appender.CONSOLE.Threshold = INFO\r\nlog4j.appender.CONSOLE.Encoding = UTF-8\r\n\r\nlog4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout\r\nlog4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %5p (%c:%L) - %m%n\r\n\r\n\r\n\r\nlog4j.appender.LOGFILE=org.apache.log4j.FileAppender\r\nlog4j.appender.LOGFILE.File =./logs/error.log\r\n\r\nlog4j.appender.LOGFILE.Threshold = ERROR\r\nlog4j.appender.LOGFILE.Encoding = UTF-8\r\nlog4j.appender.LOGFILE.layout = org.apache.log4j.PatternLayout\r\nlog4j.appender.LOGFILE.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n\r\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/CommonTest.java",
    "content": "package com.crossoverjie.cim.common;\n\nimport java.time.LocalDate;\nimport java.time.LocalTime;\nimport java.util.concurrent.TimeUnit;\nimport org.junit.Test;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-09-23 14:21\n * @since JDK 1.8\n */\npublic class CommonTest {\n\n\n    @Test\n    public void test2() {\n        System.out.println(LocalDate.now().toString());\n        System.out.println(LocalTime.now().withNano(0).toString());\n    }\n\n    @Test\n    public void test() throws InterruptedException {\n\n\n        System.out.println(is2(9));\n\n        System.out.println(Integer.bitCount(64 - 1));\n\n        int target = 1569312600;\n        int mod = 64;\n        System.out.println(target % mod);\n\n        System.out.println(mod(target, mod));\n        System.out.println(\"============\");\n\n        System.out.println(cycleNum(256, 64));\n\n    }\n\n\n    private int mod(int target, int mod) {\n        // equals target % mod\n        return target & (mod - 1);\n    }\n\n    private int cycleNum(int target, int mod) {\n        //equals target/mod\n        return target >> Integer.bitCount(mod - 1);\n    }\n\n    private boolean is2(int target) {\n        if (target < 0) {\n            return false;\n        }\n\n        int value = target & (target - 1);\n        if (value != 0) {\n            return false;\n        }\n\n        return true;\n    }\n\n\n    private void cycle() throws InterruptedException {\n        int index = 0;\n        while (true) {\n            System.out.println(\"=======\" + index);\n\n            if (++index > 63) {\n                index = 0;\n            }\n            TimeUnit.MILLISECONDS.sleep(200);\n        }\n    }\n\n\n\n}\n\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/core/proxy/RpcProxyManagerTest.java",
    "content": "package com.crossoverjie.cim.common.core.proxy;\n\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties;\nimport com.fasterxml.jackson.annotation.JsonProperty;\nimport java.io.Serializable;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\nimport okhttp3.OkHttpClient;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\n\nclass RpcProxyManagerTest {\n\n    @Test\n    public void testGet() {\n        OkHttpClient client = new OkHttpClient();\n        String url = \"https://api.github.com/users\";\n        Github github = RpcProxyManager.create(Github.class, url, client);\n        GithubResponse githubResponse = github.crossoverjie();\n        Assertions.assertEquals(githubResponse.getName(), \"crossoverJie\");\n        github.torvalds();\n    }\n\n    @Test\n    public void testPost() {\n        OkHttpClient client = new OkHttpClient();\n        String url = \"http://echo.free.beeceptor.com\";\n        Echo echo = RpcProxyManager.create(Echo.class, url, client);\n        EchoRequest request = new EchoRequest();\n        request.setName(\"crossoverJie\");\n        request.setAge(18);\n        request.setCity(\"shenzhen\");\n        EchoResponse response = echo.echo(request);\n        Assertions.assertEquals(response.getParsedBody().getName(), \"crossoverJie\");\n        Assertions.assertEquals(response.getParsedBody().getAge(), 18);\n        Assertions.assertEquals(response.getParsedBody().getCity(), \"shenzhen\");\n    }\n\n    @Test\n    public void testUrl() {\n        OkHttpClient client = new OkHttpClient();\n        String url = \"http://echo.free.beeceptor.com/sample-request?author=beeceptor\";\n        /**\n         * {\n         *   \"method\": \"POST\",\n         *   \"protocol\": \"http\",\n         *   \"host\": \"echo.free.beeceptor.com\",\n         *   \"path\": \"/sample-request?author=beeceptor\",\n         *   \"ip\": \"111.249.70.48:41382\",\n         *   \"headers\": {\n         *     \"Host\": \"echo.free.beeceptor.com\",\n         *     \"User-Agent\": \"okhttp/3.3.1\",\n         *     \"Content-Length\": \"50\",\n         *     \"Accept-Encoding\": \"gzip\",\n         *     \"Content-Type\": \"application/json; charset=utf-8\",\n         *     \"Via\": \"1.1 Caddy\"\n         *   },\n         *   \"parsedQueryParams\": {\n         *     \"author\": \"beeceptor\"\n         *   },\n         *   \"parsedBody\": {\n         *     \"city\": \"shenzhen\",\n         *     \"name\": \"crossoverJie\",\n         *     \"age\": 18\n         *   }\n         * }\n         */\n        Echo echo = RpcProxyManager.create(Echo.class, client);\n        EchoRequest request = new EchoRequest();\n        request.setName(\"crossoverJie\");\n        request.setAge(18);\n        request.setCity(\"shenzhen\");\n        EchoResponse response = echo.echoTarget(url, request);\n        Assertions.assertEquals(response.getParsedBody().getName(), \"crossoverJie\");\n        Assertions.assertEquals(response.getParsedBody().getAge(), 18);\n        Assertions.assertEquals(response.getParsedBody().getCity(), \"shenzhen\");\n        response = echo.echoTarget(request, url);\n        Assertions.assertEquals(response.getParsedBody().getName(), \"crossoverJie\");\n\n        String req = \"/request\";\n        response = echo.request(\"http://echo.free.beeceptor.com\", request);\n        Assertions.assertEquals(response.getPath(), req);\n        Assertions.assertEquals(response.getParsedBody().getAge(), 18);\n\n        Assertions.assertThrows(CIMException.class, () -> echo.echoTarget(request));\n    }\n\n    @Test\n    public void testFail() {\n        OkHttpClient client = new OkHttpClient();\n        String url = \"http://echo.free.beeceptor.com\";\n        Echo echo = RpcProxyManager.create(Echo.class, url, client);\n        EchoRequest request = new EchoRequest();\n        request.setName(\"crossoverJie\");\n        request.setAge(18);\n        request.setCity(\"shenzhen\");\n        Assertions.assertThrows(IllegalArgumentException.class, () -> echo.fail(request, \"test\", \"\"));\n    }\n\n\n    @Test\n    public void testGeneric() {\n        OkHttpClient client = new OkHttpClient();\n        String url = \"http://echo.free.beeceptor.com\";\n        Echo echo = RpcProxyManager.create(Echo.class, url, client);\n        EchoRequest request = new EchoRequest();\n        request.setName(\"crossoverJie\");\n        request.setAge(18);\n        request.setCity(\"shenzhen\");\n        EchoGeneric<EchoResponse.HeadersDTO> response = echo.echoGeneric(request);\n        Assertions.assertEquals(response.getHeaders().getHost(), \"echo.free.beeceptor.com\");\n    }\n\n    interface Echo {\n        @Request(url = \"sample-request?author=beeceptor\")\n        EchoResponse echo(EchoRequest message);\n\n        @Request(url = \"sample-request?author=beeceptor\")\n        EchoResponse echoTarget(@DynamicUrl(useMethodEndpoint = false) String url, EchoRequest message);\n        EchoResponse echoTarget(EchoRequest message, @DynamicUrl(useMethodEndpoint = false) String url);\n        @Request(url = \"sample-request?author=beeceptor\")\n        EchoResponse echoTarget(@DynamicUrl EchoRequest message);\n        EchoResponse request(@DynamicUrl() String url, EchoRequest message);\n        @Request(url = \"sample-request?author=beeceptor\")\n        EchoResponse fail(EchoRequest message, String s, String s1);\n\n        @Request(url = \"sample-request?author=beeceptor\")\n        EchoGeneric<EchoResponse.HeadersDTO> echoGeneric(EchoRequest message);\n    }\n\n    @Data\n    public static class EchoRequest {\n        private String name;\n        private int age;\n        private String city;\n    }\n\n    @Data\n    @AllArgsConstructor\n    @NoArgsConstructor\n    public static class CIMServerResVO implements Serializable {\n\n        private String ip;\n        private Integer cimServerPort;\n        private Integer httpPort;\n\n    }\n\n    @Data\n    @JsonIgnoreProperties(ignoreUnknown = true)\n    @AllArgsConstructor\n    @NoArgsConstructor\n    public static class EchoGeneric<T> {\n        private String method;\n        private String protocol;\n        private String host;\n\n        private T headers;\n    }\n\n    @NoArgsConstructor\n    @Data\n    public static class EchoResponse {\n\n        @JsonProperty(\"method\")\n        private String method;\n        @JsonProperty(\"protocol\")\n        private String protocol;\n        @JsonProperty(\"host\")\n        private String host;\n        @JsonProperty(\"path\")\n        private String path;\n        @JsonProperty(\"ip\")\n        private String ip;\n        @JsonProperty(\"headers\")\n        private HeadersDTO headers;\n        @JsonProperty(\"parsedQueryParams\")\n        private ParsedQueryParamsDTO parsedQueryParams;\n        @JsonProperty(\"parsedBody\")\n        private ParsedBodyDTO parsedBody;\n\n        @NoArgsConstructor\n        @Data\n        public static class HeadersDTO {\n            @JsonProperty(\"Host\")\n            private String host;\n            @JsonProperty(\"User-Agent\")\n            private String userAgent;\n            @JsonProperty(\"Content-Length\")\n            private String contentLength;\n            @JsonProperty(\"Accept\")\n            private String accept;\n            @JsonProperty(\"Content-Type\")\n            private String contentType;\n            @JsonProperty(\"Accept-Encoding\")\n            private String acceptEncoding;\n            @JsonProperty(\"Via\")\n            private String via;\n        }\n\n        @NoArgsConstructor\n        @Data\n        public static class ParsedQueryParamsDTO {\n            @JsonProperty(\"author\")\n            private String author;\n        }\n\n        @NoArgsConstructor\n        @Data\n        public static class ParsedBodyDTO {\n            @JsonProperty(\"name\")\n            private String name;\n            @JsonProperty(\"age\")\n            private Integer age;\n            @JsonProperty(\"city\")\n            private String city;\n        }\n    }\n\n    interface Github {\n        @Request(method = Request.GET)\n        GithubResponse crossoverjie();\n\n        @Request(method = Request.GET)\n        void torvalds();\n    }\n\n    @Data\n    @JsonIgnoreProperties(ignoreUnknown = true)\n    public static class GithubResponse {\n        @JsonProperty(\"name\")\n        private String name;\n    }\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/RingBufferWheelTest.java",
    "content": "package com.crossoverjie.cim.common.data.construct;\n\nimport com.google.common.util.concurrent.ThreadFactoryBuilder;\nimport io.netty.util.HashedWheelTimer;\nimport io.netty.util.Timeout;\nimport io.netty.util.TimerTask;\nimport java.util.concurrent.BlockingQueue;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport lombok.extern.slf4j.Slf4j;\n\n@Slf4j\npublic class RingBufferWheelTest {\n\n    public static void main(String[] args) throws Exception {\n\n        test8();\n\n    }\n\n    private static void test8() throws Exception {\n        ExecutorService executorService = Executors.newFixedThreadPool(2);\n        RingBufferWheel wheel = new RingBufferWheel(executorService);\n        while (true) {\n            log.info(\"task size={}, task map size={}\", wheel.taskSize(), wheel.taskMapSize());\n            TimeUnit.SECONDS.sleep(1);\n\n            for (int i = 0; i < 1000; i++) {\n                RingBufferWheel.Task task = new ByteTask(1024 * 1024);\n                task.setKey(1);\n                wheel.addTask(task);\n            }\n        }\n\n    }\n\n\n    private static class ByteTask extends RingBufferWheel.Task {\n\n        private byte[] b;\n\n        public ByteTask(int size) {\n            this.b = new byte[size];\n        }\n\n        @Override\n        public void run() {\n            // empty task\n        }\n    }\n\n    private static void test1() throws InterruptedException {\n        ExecutorService executorService = Executors.newFixedThreadPool(2);\n\n        RingBufferWheel.Task task = new Task();\n        task.setKey(10);\n        RingBufferWheel wheel = new RingBufferWheel(executorService);\n        wheel.addTask(task);\n\n        task = new Task();\n        task.setKey(74);\n        wheel.addTask(task);\n\n        while (true) {\n            log.info(\"task size={}\", wheel.taskSize());\n            TimeUnit.SECONDS.sleep(1);\n        }\n    }\n\n    private static void test2() throws InterruptedException {\n        ExecutorService executorService = Executors.newFixedThreadPool(2);\n\n        RingBufferWheel.Task task = new Task();\n        task.setKey(10);\n        RingBufferWheel wheel = new RingBufferWheel(executorService);\n        wheel.addTask(task);\n\n        task = new Task();\n        task.setKey(74);\n        wheel.addTask(task);\n\n        wheel.start();\n\n//        new Thread(() -> {\n//            while (true){\n//                logger.info(\"task size={}\" , wheel.taskSize());\n//                try {\n//                    TimeUnit.SECONDS.sleep(1);\n//                } catch (InterruptedException e) {\n//                    e.printStackTrace();\n//                }\n//            }\n//        }).start();\n\n        TimeUnit.SECONDS.sleep(12);\n        wheel.stop(true);\n\n\n    }\n\n    private static void test3() throws InterruptedException {\n        ExecutorService executorService = Executors.newFixedThreadPool(2);\n\n        RingBufferWheel.Task task = new Task();\n        task.setKey(10);\n        RingBufferWheel wheel = new RingBufferWheel(executorService);\n        wheel.addTask(task);\n\n        task = new Task();\n        task.setKey(60);\n        wheel.addTask(task);\n\n\n        TimeUnit.SECONDS.sleep(2);\n        wheel.stop(false);\n\n\n    }\n\n    private static void test4() throws InterruptedException {\n        ExecutorService executorService = Executors.newFixedThreadPool(2);\n\n        RingBufferWheel wheel = new RingBufferWheel(executorService);\n\n        for (int i = 0; i < 65; i++) {\n            RingBufferWheel.Task task = new Job(i);\n            task.setKey(i);\n            wheel.addTask(task);\n        }\n\n        wheel.start();\n\n        log.info(\"task size={}\", wheel.taskSize());\n\n        wheel.stop(false);\n\n\n    }\n\n    private static void test5() throws InterruptedException {\n        ExecutorService executorService = Executors.newFixedThreadPool(2);\n\n        RingBufferWheel wheel = new RingBufferWheel(executorService, 512);\n\n        for (int i = 0; i < 65; i++) {\n            RingBufferWheel.Task task = new Job(i);\n            task.setKey(i);\n            wheel.addTask(task);\n        }\n\n        log.info(\"task size={}\", wheel.taskSize());\n\n        wheel.stop(false);\n\n\n    }\n\n    private static void test6() throws InterruptedException {\n        ExecutorService executorService = Executors.newFixedThreadPool(2);\n\n        RingBufferWheel wheel = new RingBufferWheel(executorService, 512);\n\n        for (int i = 0; i < 10; i++) {\n            RingBufferWheel.Task task = new Job(i);\n            task.setKey(i);\n            wheel.addTask(task);\n        }\n\n        TimeUnit.SECONDS.sleep(5);\n        RingBufferWheel.Task task = new Job(15);\n        task.setKey(15);\n        wheel.addTask(task);\n\n        log.info(\"task size={}\", wheel.taskSize());\n\n        wheel.stop(false);\n    }\n\n    private static void test7() throws InterruptedException {\n        ExecutorService executorService = Executors.newFixedThreadPool(2);\n\n        RingBufferWheel wheel = new RingBufferWheel(executorService, 512);\n\n        for (int i = 0; i < 10; i++) {\n            RingBufferWheel.Task task = new Job(i);\n            task.setKey(i);\n            wheel.addTask(task);\n        }\n\n        RingBufferWheel.Task task = new Job(15);\n        task.setKey(15);\n        int cancel = wheel.addTask(task);\n\n        new Thread(() -> {\n            boolean flag = wheel.cancel(cancel);\n            log.info(\"cancel id={},key={} result={}\", cancel, task.getKey(), flag);\n        }).start();\n\n        RingBufferWheel.Task task1 = new Job(20);\n        task1.setKey(20);\n        wheel.addTask(task1);\n\n        log.info(\"task size={}\", wheel.taskSize());\n\n        wheel.stop(false);\n    }\n\n\n    private static void concurrentTest() throws Exception {\n        BlockingQueue<Runnable> queue = new LinkedBlockingQueue(10);\n        ThreadFactory product = new ThreadFactoryBuilder()\n                .setNameFormat(\"msg-callback-%d\")\n                .setDaemon(true)\n                .build();\n        ThreadPoolExecutor business = new ThreadPoolExecutor(4, 4, 1, TimeUnit.MILLISECONDS, queue, product);\n\n        ExecutorService executorService = Executors.newFixedThreadPool(10);\n        RingBufferWheel wheel = new RingBufferWheel(executorService);\n\n        for (int i = 0; i < 10; i++) {\n            business.execute(new Runnable() {\n                @Override\n                public void run() {\n                    for (int i1 = 0; i1 < 30; i1++) {\n                        RingBufferWheel.Task task = new Job(i1);\n                        task.setKey(i1);\n                        wheel.addTask(task);\n                    }\n                }\n            });\n        }\n\n        log.info(\"task size={}\", wheel.taskSize());\n\n        wheel.stop(false);\n\n\n    }\n\n    private static class Job extends RingBufferWheel.Task {\n\n        private int num;\n\n        public Job(int num) {\n            this.num = num;\n        }\n\n        @Override\n        public void run() {\n            log.info(\"number={}\", num);\n        }\n    }\n\n    private static class Task extends RingBufferWheel.Task {\n\n        @Override\n        public void run() {\n            log.info(\"================\");\n        }\n\n    }\n\n\n    public static void hashTimerTest() {\n\n        BlockingQueue<Runnable> queue = new LinkedBlockingQueue(10);\n        ThreadFactory product = new ThreadFactoryBuilder()\n                .setNameFormat(\"msg-callback-%d\")\n                .setDaemon(true)\n                .build();\n        ThreadPoolExecutor business = new ThreadPoolExecutor(4, 4, 1, TimeUnit.MILLISECONDS, queue, product);\n        HashedWheelTimer hashedWheelTimer = new HashedWheelTimer();\n\n        for (int i = 0; i < 10; i++) {\n            int finalI = i;\n            business.execute(new Runnable() {\n                @Override\n                public void run() {\n\n                    for (int i1 = 0; i1 < 10; i1++) {\n                        hashedWheelTimer.newTimeout(new TimerTask() {\n                            @Override\n                            public void run(Timeout timeout) throws Exception {\n                                log.info(\"====\" + finalI);\n                            }\n                        }, finalI, TimeUnit.SECONDS);\n                    }\n                }\n            });\n        }\n\n\n\n    }\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/ScheduledTest.java",
    "content": "package com.crossoverjie.cim.common.data.construct;\n\nimport com.google.common.util.concurrent.ThreadFactoryBuilder;\nimport java.util.concurrent.ScheduledThreadPoolExecutor;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.TimeUnit;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-10-11 10:41\n * @since JDK 1.8\n */\n@Slf4j\npublic class ScheduledTest {\n\n    public static void main(String[] args) {\n        log.info(\"start.....\");\n        ThreadFactory scheduled = new ThreadFactoryBuilder()\n                .setNameFormat(\"scheduled-%d\")\n                .build();\n        ScheduledThreadPoolExecutor scheduledExecutorService = new ScheduledThreadPoolExecutor(2, scheduled);\n        scheduledExecutorService.schedule(() -> log.info(\"scheduled.........\"), 3, TimeUnit.SECONDS);\n    }\n}\n\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/SortArrayMapTest.java",
    "content": "package com.crossoverjie.cim.common.data.construct;\n\nimport java.util.SortedMap;\nimport java.util.TreeMap;\nimport org.junit.Test;\n\npublic class SortArrayMapTest {\n\n    int count = 1000000;\n\n    @Test\n    public void ad() {\n        SortArrayMap map = new SortArrayMap();\n        for (int i = 0; i < 9; i++) {\n            map.add(Long.valueOf(i), \"127.0.0.\" + i);\n        }\n        map.print();\n        System.out.println(map.size());\n    }\n\n    @Test\n    public void add() {\n        SortArrayMap map = new SortArrayMap();\n        for (int i = 0; i < 10; i++) {\n            map.add(Long.valueOf(i), \"127.0.0.\" + i);\n        }\n        map.print();\n        System.out.println(map.size());\n    }\n\n    @Test\n    public void add2() {\n        SortArrayMap map = new SortArrayMap();\n        for (int i = 0; i < 20; i++) {\n            map.add(Long.valueOf(i), \"127.0.0.\" + i);\n        }\n        map.sort();\n        map.print();\n        System.out.println(map.size());\n    }\n\n    @Test\n    public void add3() {\n        SortArrayMap map = new SortArrayMap();\n\n        map.add(100L, \"127.0.0.100\");\n        map.add(10L, \"127.0.0.10\");\n        map.add(8L, \"127.0.0.8\");\n        map.add(1000L, \"127.0.0.1000\");\n\n        map.print();\n        System.out.println(map.size());\n    }\n\n    @Test\n    public void firstNode() {\n        SortArrayMap map = new SortArrayMap();\n\n        map.add(100L, \"127.0.0.100\");\n        map.add(10L, \"127.0.0.10\");\n        map.add(8L, \"127.0.0.8\");\n        map.add(1000L, \"127.0.0.1000\");\n\n        map.sort();\n        map.print();\n        String value = map.firstNodeValue(101);\n        System.out.println(value);\n    }\n\n    @Test\n    public void firstNode2() {\n        SortArrayMap map = new SortArrayMap();\n\n        map.add(100L, \"127.0.0.100\");\n        map.add(10L, \"127.0.0.10\");\n        map.add(8L, \"127.0.0.8\");\n        map.add(1000L, \"127.0.0.1000\");\n\n        map.sort();\n        map.print();\n        String value = map.firstNodeValue(1);\n        System.out.println(value);\n    }\n\n    @Test\n    public void firstNode3() {\n        SortArrayMap map = new SortArrayMap();\n\n        map.add(100L, \"127.0.0.100\");\n        map.add(10L, \"127.0.0.10\");\n        map.add(8L, \"127.0.0.8\");\n        map.add(1000L, \"127.0.0.1000\");\n\n        map.sort();\n        map.print();\n        String value = map.firstNodeValue(1001);\n        System.out.println(value);\n    }\n\n    @Test\n    public void firstNode4() {\n        SortArrayMap map = new SortArrayMap();\n\n        map.add(100L, \"127.0.0.100\");\n        map.add(10L, \"127.0.0.10\");\n        map.add(8L, \"127.0.0.8\");\n        map.add(1000L, \"127.0.0.1000\");\n\n        map.sort();\n        map.print();\n        String value = map.firstNodeValue(9);\n        System.out.println(value);\n    }\n\n    @Test\n    public void add4() {\n        SortArrayMap map = new SortArrayMap();\n\n        map.add(100L, \"127.0.0.100\");\n        map.add(10L, \"127.0.0.10\");\n        map.add(8L, \"127.0.0.8\");\n        map.add(1000L, \"127.0.0.1000\");\n\n        map.sort();\n        map.print();\n        System.out.println(map.size());\n    }\n\n    @Test\n    public void add5() {\n        SortArrayMap map = new SortArrayMap();\n\n\n        long star = System.currentTimeMillis();\n        for (int i = 0; i < count; i++) {\n            double d = Math.random();\n            int ran = (int) (d * 100);\n            map.add(Long.valueOf(i + ran), \"127.0.0.\" + i);\n        }\n        map.sort();\n        long end = System.currentTimeMillis();\n        System.out.println(\"排序耗时 \" + (end - star));\n        System.out.println(map.size());\n    }\n\n    @Test\n    public void add6() {\n\n        SortArrayMap map = new SortArrayMap();\n        long star = System.currentTimeMillis();\n        for (int i = 0; i < count; i++) {\n            double d = Math.random();\n            int ran = (int) (d * 100);\n            map.add(Long.valueOf(i + ran), \"127.0.0.\" + i);\n        }\n        long end = System.currentTimeMillis();\n        System.out.println(\"不排耗时 \" + (end - star));\n        System.out.println(map.size());\n    }\n\n    @Test\n    public void add7() {\n\n        TreeMap<Long, String> treeMap = new TreeMap<Long, String>();\n        long star = System.currentTimeMillis();\n        for (int i = 0; i < count; i++) {\n            double d = Math.random();\n            int ran = (int) (d * 100);\n            treeMap.put(Long.valueOf(i + ran), \"127.0.0.\" + i);\n        }\n        long end = System.currentTimeMillis();\n        System.out.println(\"耗时 \" + (end - star));\n        System.out.println(treeMap.size());\n    }\n\n    @Test\n    public void add8() {\n\n        TreeMap<Long, String> map = new TreeMap<Long, String>();\n        map.put(100L, \"127.0.0.100\");\n        map.put(10L, \"127.0.0.10\");\n        map.put(8L, \"127.0.0.8\");\n        map.put(1000L, \"127.0.0.1000\");\n\n        SortedMap<Long, String> last = map.tailMap(101L);\n        if (!last.isEmpty()) {\n            System.out.println(last.get(last.firstKey()));\n        } else {\n            System.out.println(map.firstEntry().getValue());\n        }\n    }\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/TimerTest.java",
    "content": "package com.crossoverjie.cim.common.data.construct;\n\nimport java.util.Timer;\nimport java.util.TimerTask;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-10-09 22:48\n * @since JDK 1.8\n */\n@Slf4j\npublic class TimerTest {\n\n    public static void main(String[] args) {\n        log.info(\"start\");\n        Timer timer = new Timer();\n        timer.schedule(new TimerTask() {\n            @Override\n            public void run() {\n                log.info(\"test\");\n            }\n        }, 50000);\n        timer.schedule(new TimerTask() {\n            @Override\n            public void run() {\n                log.info(\"test\");\n            }\n        }, 30000);\n\n    }\n}\n\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/TrieTreeTest.java",
    "content": "package com.crossoverjie.cim.common.data.construct;\n\nimport java.util.List;\nimport org.junit.Assert;\nimport org.junit.Test;\n\npublic class TrieTreeTest {\n\n    @Test\n    public void insert() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"abc\");\n        trieTree.insert(\"abcd\");\n    }\n\n\n    @Test\n    public void all() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"ABC\");\n        trieTree.insert(\"abC\");\n        List<String> all = trieTree.all();\n        String result = \"\";\n        for (String s : all) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n\n        Assert.assertTrue(\"ABC,abC,\".equals(result));\n\n    }\n\n    @Test\n    public void all2() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"abc\");\n        trieTree.insert(\"abC\");\n        List<String> all = trieTree.all();\n        String result = \"\";\n        for (String s : all) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n\n        //Assert.assertTrue(\"ABC,abC,\".equals(result));\n\n    }\n\n    @Test\n    public void prefixSea() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"java\");\n        trieTree.insert(\"jsf\");\n        trieTree.insert(\"jsp\");\n        trieTree.insert(\"javascript\");\n        trieTree.insert(\"php\");\n\n        String result = \"\";\n        List<String> ab = trieTree.prefixSearch(\"jav\");\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n\n        Assert.assertTrue(result.equals(\"java,javascript,\"));\n\n    }\n\n    @Test\n    public void prefixSea2() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"java\");\n        trieTree.insert(\"jsf\");\n        trieTree.insert(\"jsp\");\n        trieTree.insert(\"javascript\");\n        trieTree.insert(\"php\");\n\n        String result = \"\";\n        List<String> ab = trieTree.prefixSearch(\"j\");\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n\n        Assert.assertTrue(result.equals(\"java,javascript,jsf,jsp,\"));\n\n    }\n\n    @Test\n    public void prefixSea3() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"java\");\n        trieTree.insert(\"jsf\");\n        trieTree.insert(\"jsp\");\n        trieTree.insert(\"javascript\");\n        trieTree.insert(\"php\");\n\n        String result = \"\";\n        List<String> ab = trieTree.prefixSearch(\"js\");\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n\n        Assert.assertTrue(result.equals(\"jsf,jsp,\"));\n\n    }\n\n    @Test\n    public void prefixSea4() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"java\");\n        trieTree.insert(\"jsf\");\n        trieTree.insert(\"jsp\");\n        trieTree.insert(\"javascript\");\n        trieTree.insert(\"php\");\n\n        String result = \"\";\n        List<String> ab = trieTree.prefixSearch(\"jav\");\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n\n        Assert.assertTrue(result.equals(\"java,javascript,\"));\n\n    }\n\n    @Test\n    public void prefixSea5() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"java\");\n        trieTree.insert(\"jsf\");\n        trieTree.insert(\"jsp\");\n        trieTree.insert(\"javascript\");\n        trieTree.insert(\"php\");\n\n        String result = \"\";\n        List<String> ab = trieTree.prefixSearch(\"js\");\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n\n        Assert.assertTrue(result.equals(\"jsf,jsp,\"));\n\n    }\n\n    @Test\n    public void prefixSearch() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"abc\");\n        trieTree.insert(\"abd\");\n        trieTree.insert(\"ABe\");\n\n        List<String> ab = trieTree.prefixSearch(\"AB\");\n        for (String s : ab) {\n            System.out.println(s);\n        }\n\n        System.out.println(\"========\");\n\n        //char[] chars = new char[3] ;\n        //for (int i = 0; i < 3; i++) {\n        //    int a = 97 + i ;\n        //    chars[i] = (char) a ;\n        //}\n        //\n        //String s = String.valueOf(chars);\n        //System.out.println(s);\n    }\n\n    @Test\n    public void prefixSearch2() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"Cde\");\n        trieTree.insert(\"CDa\");\n        trieTree.insert(\"ABe\");\n\n        List<String> ab = trieTree.prefixSearch(\"AC\");\n        for (String s : ab) {\n            System.out.println(s);\n        }\n        Assert.assertTrue(ab.size() == 0);\n    }\n\n    @Test\n    public void prefixSearch3() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"Cde\");\n        trieTree.insert(\"CDa\");\n        trieTree.insert(\"ABe\");\n\n        List<String> ab = trieTree.prefixSearch(\"CD\");\n        for (String s : ab) {\n            System.out.println(s);\n        }\n        Assert.assertTrue(ab.size() == 1);\n    }\n\n    @Test\n    public void prefixSearch4() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"Cde\");\n        trieTree.insert(\"CDa\");\n        trieTree.insert(\"ABe\");\n\n        List<String> ab = trieTree.prefixSearch(\"Cd\");\n        String result = \"\";\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n        Assert.assertTrue(result.equals(\"Cde,\"));\n    }\n\n    @Test\n    public void prefixSearch44() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"a\");\n        trieTree.insert(\"b\");\n        trieTree.insert(\"c\");\n        trieTree.insert(\"d\");\n        trieTree.insert(\"e\");\n        trieTree.insert(\"f\");\n        trieTree.insert(\"g\");\n        trieTree.insert(\"h\");\n\n    }\n\n    @Test\n    public void prefixSearch5() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"Cde\");\n        trieTree.insert(\"CDa\");\n        trieTree.insert(\"ABe\");\n        trieTree.insert(\"CDfff\");\n        trieTree.insert(\"Cdfff\");\n\n        List<String> ab = trieTree.prefixSearch(\"Cd\");\n        String result = \"\";\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n        Assert.assertTrue(result.equals(\"Cde,Cdfff,\"));\n    }\n\n    @Test\n    public void prefixSearch6() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"Cde\");\n        trieTree.insert(\"CDa\");\n        trieTree.insert(\"ABe\");\n        trieTree.insert(\"CDfff\");\n        trieTree.insert(\"Cdfff\");\n\n        List<String> ab = trieTree.prefixSearch(\"CD\");\n        String result = \"\";\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n        Assert.assertTrue(result.equals(\"CDa,CDfff,\"));\n    }\n\n    @Test\n    public void prefixSearch7() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"Cde\");\n        trieTree.insert(\"CDa\");\n        trieTree.insert(\"ABe\");\n        trieTree.insert(\"CDfff\");\n        trieTree.insert(\"Cdfff\");\n\n        List<String> ab = trieTree.prefixSearch(\"\");\n        String result = \"\";\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n        Assert.assertTrue(result.equals(\"\"));\n    }\n\n    @Test\n    public void prefixSearch8() throws Exception {\n        TrieTree trieTree = new TrieTree();\n\n        List<String> ab = trieTree.prefixSearch(\"\");\n        String result = \"\";\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n        Assert.assertTrue(result.equals(\"\"));\n    }\n\n\n    @Test\n    public void prefixSearch9() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"Cde\");\n        trieTree.insert(\"CDa\");\n        trieTree.insert(\"ABe\");\n        trieTree.insert(\"CDfff\");\n        trieTree.insert(\"Cdfff\");\n\n        List<String> ab = trieTree.prefixSearch(\"CDFD\");\n        String result = \"\";\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n        Assert.assertTrue(result.equals(\"\"));\n    }\n\n\n    @Test\n    public void prefixSearch10() throws Exception {\n        TrieTree trieTree = new TrieTree();\n        trieTree.insert(\"crossoverJie\");\n        trieTree.insert(\"zhangsan\");\n\n        List<String> ab = trieTree.prefixSearch(\"c\");\n        String result = \"\";\n        for (String s : ab) {\n            result += s + \",\";\n            System.out.println(s);\n        }\n        Assert.assertTrue(result.equals(\"crossoverJie,\"));\n    }\n\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/enums/SystemCommandEnumTypeTest.java",
    "content": "package com.crossoverjie.cim.common.enums;\n\nimport java.util.Map;\nimport org.junit.Test;\n\npublic class SystemCommandEnumTypeTest {\n\n\n    @Test\n    public void getAllStatusCode() throws Exception {\n        Map<String, String> allStatusCode = SystemCommandEnum.getAllStatusCode();\n        for (Map.Entry<String, String> stringStringEntry : allStatusCode.entrySet()) {\n            String key = stringStringEntry.getKey();\n            String value = stringStringEntry.getValue();\n            System.out.println(key + \"----->\" + value);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/metastore/MetaStoreTest.java",
    "content": "package com.crossoverjie.cim.common.metastore;\n\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport lombok.SneakyThrows;\nimport org.I0Itec.zkclient.ZkClient;\nimport org.apache.curator.framework.CuratorFramework;\nimport org.apache.curator.framework.CuratorFrameworkFactory;\nimport org.apache.curator.framework.api.CuratorWatcher;\nimport org.apache.curator.retry.ExponentialBackoffRetry;\nimport org.apache.zookeeper.CreateMode;\nimport org.apache.zookeeper.Watcher;\nimport org.apache.zookeeper.data.Stat;\n\npublic class MetaStoreTest {\n\n    private static final String CONNECTION_STRING = \"127.0.0.1:2181\";\n\n    // TODO: 2024/8/30 integration test\n    @SneakyThrows\n//    @Test\n    public void testZk() {\n        ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);\n        CuratorFramework client = CuratorFrameworkFactory.builder()\n                .connectString(CONNECTION_STRING)\n                .retryPolicy(retryPolicy)\n                .connectionTimeoutMs(5000)\n                .sessionTimeoutMs(5000)\n                .build();\n        client.start();\n\n        Stat stat = client.checkExists().forPath(\"/cim\");\n        if (stat == null) {\n            create(client, \"/cim\", null);\n        }\n\n\n        List<String> list = null;\n        list = curatorWatcherGetChildren(client, \"/cim\", watchedEvent -> {\n            String name = Thread.currentThread().getName();\n            System.out.println(\"watchedEvent = \" + watchedEvent + \" name = \" + name);\n//            try {\n//                List<String> children = watchedGetChildren(client, \"/cim\");\n//                System.out.println(\"children = \" + children);\n//            } catch (Exception e) {\n//                throw new RuntimeException(e);\n//            }\n        });\n\n        System.out.println(list);\n\n//        createEphemeral(client, \"/cim/route1\", null);\n//        createEphemeral(client, \"/cim/route2\", null);\n        TimeUnit.SECONDS.sleep(1000);\n    }\n\n    public static void createEphemeral(CuratorFramework client, String path, byte[] payload) throws Exception {\n        // this will create the given EPHEMERAL ZNode with the given data\n        client.create().withMode(CreateMode.EPHEMERAL).forPath(path, payload);\n    }\n\n    public static void create(CuratorFramework client, String path, byte[] payload) throws Exception {\n        // this will create the given ZNode with the given data\n        client.create().forPath(path, payload);\n    }\n\n\n    public static List<String> watchedGetChildren(CuratorFramework client, String path) throws Exception {\n        /**\n         * Get children and set a watcher on the node. The watcher notification will come through the\n         * CuratorListener (see setDataAsync() above).\n         */\n        return client.getChildren().watched().forPath(path);\n    }\n\n    public static List<String> watchedGetChildren(CuratorFramework client, String path, Watcher watcher)\n            throws Exception {\n        /**\n         * Get children and set the given watcher on the node.\n         */\n        return client.getChildren().usingWatcher(watcher).forPath(path);\n    }\n\n    public static List<String> curatorWatcherGetChildren(CuratorFramework client, String path, CuratorWatcher watcher)\n            throws Exception {\n        /**\n         * Get children and set the given watcher on the node.\n         */\n        return client.getChildren().usingWatcher(watcher).forPath(path);\n    }\n\n\n    @SneakyThrows\n//    @Test\n    public void zkClientTest() {\n        ZkClient zkClient = new ZkClient(CONNECTION_STRING, 5000);\n        zkClient.subscribeChildChanges(\"/cim\", (parentPath, currentChildren) -> {\n            System.out.println(\"parentPath = \" + parentPath);\n            System.out.println(\"currentChildren = \" + currentChildren);\n        });\n        TimeUnit.SECONDS.sleep(1000);\n    }\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/ConsistentHashHandleTest.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter.api.Assertions.assertFalse;\n\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverjie.cim.common.util.RouteInfoParseUtil;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nclass ConsistentHashHandleTest {\n\n    @Test\n    void removeSortMapExpireServer() {\n        ConsistentHashHandle routeHandle = new ConsistentHashHandle();\n        routeHandle.setHash(new SortArrayMapConsistentHash());\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            var routeInfo = new RouteInfo(\"127.0.0.\" + i, 1000, 2000);\n            strings.add(RouteInfoParseUtil.parse(routeInfo));\n        }\n        RouteInfo routeInfo = new RouteInfo(\"127.0.0.9\", 1000, 2000);\n        String parse = RouteInfoParseUtil.parse(routeInfo);\n        String r1 = routeHandle.routeServer(strings, parse);\n        String r2 = routeHandle.routeServer(strings, parse);\n        assertEquals(r1, r2);\n\n        List<String> list = routeHandle.removeExpireServer(routeInfo);\n        boolean contains = list.contains(parse);\n        assertFalse(contains);\n    }\n\n    @Test\n    void removeTreeMapExpireServer() {\n        ConsistentHashHandle routeHandle = new ConsistentHashHandle();\n        routeHandle.setHash(new TreeMapConsistentHash());\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            var routeInfo = new RouteInfo(\"127.0.0.\" + i, 1000, 2000);\n            strings.add(RouteInfoParseUtil.parse(routeInfo));\n        }\n        RouteInfo routeInfo = new RouteInfo(\"127.0.0.9\", 1000, 2000);\n        String parse = RouteInfoParseUtil.parse(routeInfo);\n        String r1 = routeHandle.routeServer(strings, parse);\n        String r2 = routeHandle.routeServer(strings, parse);\n        assertEquals(r1, r2);\n\n        List<String> list = routeHandle.removeExpireServer(routeInfo);\n        boolean contains = list.contains(parse);\n        assertFalse(contains);\n    }\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/RangeCheckTestUtil.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport org.junit.Assert;\n\n/**\n * @description: TODO\n * @author: zhangguoa\n * @date: 2024/9/12 9:58\n * @project: cim\n */\npublic class RangeCheckTestUtil {\n\n    public static void assertInRange(int value, int l, int r) {\n        Assert.assertTrue(value >= l && value <= r);\n    }\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/SortArrayMapConsistentHashTest.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport com.crossoverjie.cim.common.data.construct.SortArrayMap;\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport org.junit.Assert;\nimport org.junit.Test;\n\npublic class SortArrayMapConsistentHashTest {\n\n    @Test\n    public void getFirstNodeValue() {\n        AbstractConsistentHash map = new SortArrayMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            strings.add(\"127.0.0.\" + i);\n        }\n        String process1 = map.process(strings, \"zhangsan\");\n        for (int i = 0; i < 100; i++) {\n            String process = map.process(strings, \"zhangsan\");\n            Assert.assertEquals(process1, process);\n        }\n    }\n\n    @Test\n    public void getFirstNodeValue2() {\n        AbstractConsistentHash map = new SortArrayMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            strings.add(\"127.0.0.\" + i);\n        }\n\n        String process1 = map.process(strings, \"zhangsan2\");\n        for (int i = 0; i < 100; i++) {\n            String process = map.process(strings, \"zhangsan2\");\n            Assert.assertEquals(process1, process);\n        }\n    }\n\n    @Test\n    public void getFirstNodeValue3() {\n        AbstractConsistentHash map = new SortArrayMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            strings.add(\"127.0.0.\" + i);\n        }\n        String process1 = map.process(strings, \"1551253899106\");\n        for (int i = 0; i < 100; i++) {\n            String process = map.process(strings, \"1551253899106\");\n            Assert.assertEquals(process1, process);\n        }\n    }\n\n\n    @Test\n    public void getFirstNodeValue4() {\n        AbstractConsistentHash map = new SortArrayMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        strings.add(\"45.78.28.220:9000:8081\");\n        strings.add(\"45.78.28.220:9100:9081\");\n\n\n        String process1 = map.process(strings, \"1551253899106\");\n        for (int i = 0; i < 100; i++) {\n            String process = map.process(strings, \"1551253899106\");\n            Assert.assertEquals(process1, process);\n        }\n    }\n\n    @Test\n    public void getFirstNodeValue5() {\n        AbstractConsistentHash map = new SortArrayMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        strings.add(\"45.78.28.220:9000:8081\");\n        strings.add(\"45.78.28.220:9100:9081\");\n        strings.add(\"45.78.28.220:9100:10081\");\n\n        String process1 = map.process(strings, \"1551253899106\");\n        for (int i = 0; i < 100; i++) {\n            String process = map.process(strings, \"1551253899106\");\n            Assert.assertEquals(process1, process);\n        }\n    }\n\n    @Test\n    public void getFirstNodeValue6() {\n        AbstractConsistentHash map = new SortArrayMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        strings.add(\"45.78.28.220:9000:8081\");\n        strings.add(\"45.78.28.220:9100:9081\");\n        strings.add(\"45.78.28.220:9100:10081\");\n\n        String process1 = map.process(strings, \"1551253899106\");\n        for (int i = 0; i < 100; i++) {\n            String process = map.process(strings, \"1551253899106\");\n            Assert.assertEquals(process1, process);\n        }\n    }\n\n    @Test\n    public void getFirstNodeValue7() {\n        AbstractConsistentHash map = new SortArrayMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        strings.add(\"45.78.28.220:9000:8081\");\n        strings.add(\"45.78.28.220:9100:9081\");\n        strings.add(\"45.78.28.220:9100:10081\");\n        strings.add(\"45.78.28.220:9100:00081\");\n\n        String process1 = map.process(strings, \"1551253899106\");\n        for (int i = 0; i < 100; i++) {\n            String process = map.process(strings, \"1551253899106\");\n            Assert.assertEquals(process1, process);\n        }\n    }\n\n    @Test\n    public void getFirstNodeValue8() {\n        AbstractConsistentHash map = new SortArrayMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            strings.add(\"127.0.0.\" + i);\n        }\n        Set<String> processes = new HashSet<>();\n        for (int i = 0; i < 10; i++) {\n            String process = map.process(strings, \"zhangsan\" + i);\n            processes.add(process);\n        }\n        RangeCheckTestUtil.assertInRange(processes.size(), 2, 10);\n    }\n\n    @Test\n    public void testVirtualNode() throws NoSuchFieldException, IllegalAccessException {\n        SortArrayMapConsistentHash map = new SortArrayMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            strings.add(\"127.0.0.\" + i);\n        }\n\n        String process = map.process(strings, \"zhangsan\");\n\n        SortArrayMap sortArrayMap = map.getSortArrayMap();\n        int virtualNodeSize = 2;\n\n        System.out.println(\"sortArrayMapSize = \" + sortArrayMap.size() + \"\\n\" + \"virtualNodeSize = \" + virtualNodeSize);\n        Assert.assertEquals(sortArrayMap.size(), (virtualNodeSize + 1) * 10);\n    }\n\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/TreeMapConsistentHashTest.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport java.util.ArrayList;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Set;\nimport java.util.TreeMap;\nimport org.junit.Assert;\nimport org.junit.Test;\n\npublic class TreeMapConsistentHashTest {\n\n\n\n    @Test\n    public void getFirstNodeValue() {\n        AbstractConsistentHash map = new TreeMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            strings.add(\"127.0.0.\" + i);\n        }\n        String process1 = map.process(strings, \"zhangsan\");\n        for (int i = 0; i < 100; i++) {\n            String process = map.process(strings, \"zhangsan\");\n            Assert.assertEquals(process1, process);\n        }\n    }\n\n\n\n    @Test\n    public void getFirstNodeValue2() {\n        AbstractConsistentHash map = new TreeMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            strings.add(\"127.0.0.\" + i);\n        }\n        String process1 = map.process(strings, \"zhangsan2\");\n        for (int i = 0; i < 100; i++) {\n            String process = map.process(strings, \"zhangsan2\");\n            Assert.assertEquals(process1, process);\n        }\n    }\n\n\n    @Test\n    public void getFirstNodeValue3() {\n        AbstractConsistentHash map = new TreeMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            strings.add(\"127.0.0.\" + i);\n        }\n        String process1 = map.process(strings, \"1551253899106\");\n        for (int i = 0; i < 100; i++) {\n            String process = map.process(strings, \"1551253899106\");\n            Assert.assertEquals(process1, process);\n        }\n    }\n\n    @Test\n    public void getFirstNodeValue4() {\n        AbstractConsistentHash map = new TreeMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            strings.add(\"127.0.0.\" + i);\n        }\n        Set<String> processes = new HashSet<>();\n        for (int i = 0; i < 10; i++) {\n            String process = map.process(strings, \"zhangsan\" + i);\n            processes.add(process);\n        }\n        RangeCheckTestUtil.assertInRange(processes.size(), 2, 10);\n    }\n\n    @Test\n    public void testVirtualNode() throws NoSuchFieldException, IllegalAccessException {\n        TreeMapConsistentHash map = new TreeMapConsistentHash();\n\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            strings.add(\"127.0.0.\" + i);\n        }\n\n        String process = map.process(strings, \"zhangsan\");\n\n        TreeMap treeMap = map.getTreeMap();\n        int virtualNodeSize = 2;\n\n        System.out.println(\"treeMapSize = \" + treeMap.size() + \"\\n\" + \"virtualNodeSize = \" + virtualNodeSize);\n        Assert.assertEquals(treeMap.size(), (virtualNodeSize + 1) * 10);\n    }\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/loop/LoopHandleTest.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.loop;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverjie.cim.common.route.algorithm.RouteHandle;\nimport com.crossoverjie.cim.common.util.RouteInfoParseUtil;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nclass LoopHandleTest {\n\n    @Test\n    void removeExpireServer() {\n        RouteHandle routeHandle = new LoopHandle();\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            var routeInfo = new RouteInfo(\"127.0.0.\" + i, 1000, 2000);\n            strings.add(RouteInfoParseUtil.parse(routeInfo));\n        }\n        String zs = routeHandle.routeServer(strings, \"zs\");\n        String zs2 = routeHandle.routeServer(strings, \"zs\");\n        assertNotEquals(zs, zs2);\n\n        RouteInfo remove = new RouteInfo(\"127.0.0.0\", 1000, 2000);\n        List<String> list = routeHandle.removeExpireServer(remove);\n        boolean contains = list.contains(RouteInfoParseUtil.parse(remove));\n        assertFalse(contains);\n    }\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/random/RandomHandleTest.java",
    "content": "package com.crossoverjie.cim.common.route.algorithm.random;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverjie.cim.common.route.algorithm.RouteHandle;\nimport com.crossoverjie.cim.common.util.RouteInfoParseUtil;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.junit.jupiter.api.Test;\n\nclass RandomHandleTest {\n\n    @Test\n    void removeExpireServer() {\n        RouteHandle routeHandle = new RandomHandle();\n        List<String> strings = new ArrayList<>();\n        for (int i = 0; i < 10; i++) {\n            var routeInfo = new RouteInfo(\"127.0.0.\" + i, 1000, 2000);\n            strings.add(RouteInfoParseUtil.parse(routeInfo));\n        }\n        routeHandle.routeServer(strings, \"zs\");\n\n        RouteInfo remove = new RouteInfo(\"127.0.0.0\", 1000, 2000);\n        List<String> list = routeHandle.removeExpireServer(remove);\n        boolean contains = list.contains(RouteInfoParseUtil.parse(remove));\n        assertFalse(contains);\n    }\n\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/util/HttpClientTest.java",
    "content": "package com.crossoverjie.cim.common.util;\n\nimport com.alibaba.fastjson.JSONObject;\nimport java.io.IOException;\nimport java.util.concurrent.TimeUnit;\nimport okhttp3.OkHttpClient;\nimport org.junit.Before;\nimport org.junit.Test;\n\npublic class HttpClientTest {\n\n    private OkHttpClient okHttpClient;\n\n    @Before\n    public void before() {\n        OkHttpClient.Builder builder = new OkHttpClient.Builder();\n        builder.connectTimeout(30, TimeUnit.SECONDS)\n                .readTimeout(10, TimeUnit.SECONDS)\n                .writeTimeout(10, TimeUnit.SECONDS)\n                .retryOnConnectionFailure(true);\n        okHttpClient = builder.build();\n    }\n\n    @Test\n    public void call() throws IOException {\n        JSONObject jsonObject = new JSONObject();\n        jsonObject.put(\"msg\", \"hello\");\n        jsonObject.put(\"userId\", 1586617710861L);\n\n        // TODO: 2024/8/30 Integration test\n//        HttpClient.call(okHttpClient,jsonObject.toString(),\"http://127.0.0.1:8081/sendMsg\") ;\n    }\n}\n"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/util/ProtocolTest.java",
    "content": "package com.crossoverjie.cim.common.util;\n\nimport com.crossoverjie.cim.common.protocol.BaseCommand;\nimport com.crossoverjie.cim.common.protocol.Request;\nimport com.google.protobuf.InvalidProtocolBufferException;\nimport org.junit.Test;\n\npublic class ProtocolTest {\n\n    @Test\n    public void testProtocol() throws InvalidProtocolBufferException {\n        Request protocol = Request.newBuilder()\n                .setRequestId(123L)\n                .setReqMsg(\"你好啊\")\n                .setCmd(BaseCommand.LOGIN_REQUEST)\n                .build();\n\n        byte[] encode = encode(protocol);\n\n        Request parseFrom = decode(encode);\n\n        System.out.println(protocol);\n        System.out.println(protocol.toString().equals(parseFrom.toString()));\n    }\n\n    /**\n     * 编码\n     * @param protocol protocol\n     * @return byte array\n     */\n    public static byte[] encode(Request protocol) {\n        return protocol.toByteArray();\n    }\n\n    /**\n     * 解码\n     * @param bytes bytes\n     * @return Request\n     * @throws InvalidProtocolBufferException exception\n     */\n    public static Request decode(byte[] bytes) throws InvalidProtocolBufferException {\n        return Request.parseFrom(bytes);\n    }\n}\n\n"
  },
  {
    "path": "cim-forward-route/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>cim</artifactId>\n        <groupId>com.crossoverjie.netty</groupId>\n        <version>1.0.0-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>cim-forward-route</artifactId>\n    <packaging>jar</packaging>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <java.version>17</java.version>\n        <swagger.version>2.5.0</swagger.version>\n    </properties>\n\n\n\n    <dependencies>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-persistence-api</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-persistence-mysql</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-persistence-redis</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-common</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-rout-api</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-server-api</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.vintage</groupId>\n            <artifactId>junit-vintage-engine</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.junit.jupiter</groupId>\n            <artifactId>junit-jupiter</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>com.clever-cloud</groupId>\n            <artifactId>testcontainers-zookeeper</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.redis</groupId>\n            <artifactId>testcontainers-redis</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>mysql</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>testcontainers</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.testcontainers</groupId>\n            <artifactId>junit-jupiter</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-configuration-processor</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>io.netty</groupId>\n            <artifactId>netty-all</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.alibaba</groupId>\n            <artifactId>fastjson</artifactId>\n        </dependency>\n\n\n    </dependencies>\n\n\n</project>"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/RouteApplication.java",
    "content": "package com.crossoverjie.cim.route;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.CommandLineRunner;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;\nimport org.springframework.context.annotation.ComponentScan;\n\n/**\n * @author crossoverJie\n */\n@Slf4j\n@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)\n@ComponentScan(basePackages = {\n\t\t\"com.crossoverjie.cim.route\",\n\t\t\"com.crossoverjie.cim.persistence\"\n})\npublic class RouteApplication implements CommandLineRunner {\n\n\tpublic static void main(String[] args) {\n        SpringApplication.run(RouteApplication.class, args);\n\t\tlog.info(\"Start cim route success!!!\");\n\t}\n\n\t@Override\n\tpublic void run(String... args) throws Exception {\n\t}\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/AppConfiguration.java",
    "content": "package com.crossoverjie.cim.route.config;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Component;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/8/24 01:43\n * @since JDK 1.8\n */\n@Component\npublic class AppConfiguration {\n\n    @Value(\"${app.zk.root}\")\n    private String zkRoot;\n\n    @Value(\"${app.zk.addr}\")\n    private String zkAddr;\n\n\n    @Value(\"${server.port}\")\n    private int port;\n\n    @Value(\"${app.zk.connect.timeout}\")\n    private int zkConnectTimeout;\n\n    @Value(\"${app.route.way.handler}\")\n    private String routeWay;\n\n    @Value(\"${app.route.way.consitenthash}\")\n    private String consistentHashWay;\n\n    public int getZkConnectTimeout() {\n\t\treturn zkConnectTimeout;\n\t}\n\n    public int getPort() {\n        return port;\n    }\n\n    public void setPort(int port) {\n        this.port = port;\n    }\n\n    public String getZkRoot() {\n        return zkRoot;\n    }\n\n    public void setZkRoot(String zkRoot) {\n        this.zkRoot = zkRoot;\n    }\n\n    public String getZkAddr() {\n        return zkAddr;\n    }\n\n    public void setZkAddr(String zkAddr) {\n        this.zkAddr = zkAddr;\n    }\n\n    public String getRouteWay() {\n        return routeWay;\n    }\n\n    public void setRouteWay(String routeWay) {\n        this.routeWay = routeWay;\n    }\n\n    public String getConsistentHashWay() {\n        return consistentHashWay;\n    }\n\n    public void setConsistentHashWay(String consistentHashWay) {\n        this.consistentHashWay = consistentHashWay;\n    }\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/BeanConfig.java",
    "content": "package com.crossoverjie.cim.route.config;\n\nimport com.crossoverjie.cim.common.core.proxy.RpcProxyManager;\nimport com.crossoverjie.cim.common.metastore.MetaStore;\nimport com.crossoverjie.cim.common.metastore.ZkConfiguration;\nimport com.crossoverjie.cim.common.metastore.ZkMetaStoreImpl;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.common.route.algorithm.RouteHandle;\nimport com.crossoverjie.cim.common.route.algorithm.consistenthash.AbstractConsistentHash;\nimport com.crossoverjie.cim.common.util.SnowflakeIdWorker;\nimport com.crossoverjie.cim.server.api.ServerApi;\nimport com.github.benmanes.caffeine.cache.CacheLoader;\nimport com.github.benmanes.caffeine.cache.Caffeine;\nimport com.github.benmanes.caffeine.cache.LoadingCache;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport okhttp3.OkHttpClient;\nimport org.apache.curator.retry.ExponentialBackoffRetry;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.redis.connection.RedisConnectionFactory;\nimport org.springframework.data.redis.core.RedisTemplate;\nimport org.springframework.data.redis.core.StringRedisTemplate;\nimport org.springframework.data.redis.serializer.StringRedisSerializer;\n\nimport java.lang.reflect.Method;\nimport java.util.Optional;\nimport java.util.concurrent.TimeUnit;\n\nimport static com.crossoverjie.cim.route.constant.Constant.ACCOUNT_PREFIX;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2018/12/23 00:25\n * @since JDK 1.8\n */\n@Configuration\n@Slf4j\npublic class BeanConfig {\n\n\n    @Resource\n    private AppConfiguration appConfiguration;\n\n    @Bean\n    public MetaStore metaStore() throws Exception {\n        MetaStore metaStore = new ZkMetaStoreImpl();\n        ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);\n        metaStore.initialize(ZkConfiguration.builder()\n                .metaServiceUri(appConfiguration.getZkAddr())\n                .timeoutMs(appConfiguration.getZkConnectTimeout())\n                .retryPolicy(retryPolicy)\n                .build());\n        metaStore.listenServerList((root, currentChildren) -> {\n            log.info(\"Server list change, root=[{}], current server list=[{}]\", root, currentChildren);\n        });\n        return metaStore;\n    }\n\n\n    /**\n     * Redis bean\n     *\n     * @param factory\n     * @return\n     */\n    @Bean\n    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {\n        StringRedisTemplate redisTemplate = new StringRedisTemplate(factory);\n        redisTemplate.setKeySerializer(new StringRedisSerializer());\n        redisTemplate.setValueSerializer(new StringRedisSerializer());\n        redisTemplate.afterPropertiesSet();\n        return redisTemplate;\n    }\n\n\n    /**\n     * http client\n     *\n     * @return okHttp\n     */\n    @Bean\n    public OkHttpClient okHttpClient() {\n        OkHttpClient.Builder builder = new OkHttpClient.Builder();\n        builder.connectTimeout(30, TimeUnit.SECONDS)\n                .readTimeout(10, TimeUnit.SECONDS)\n                .writeTimeout(10, TimeUnit.SECONDS)\n                .retryOnConnectionFailure(true);\n        return builder.build();\n    }\n\n    @Bean\n    public RouteHandle buildRouteHandle() throws Exception {\n        String routeWay = appConfiguration.getRouteWay();\n        RouteHandle routeHandle = (RouteHandle) Class.forName(routeWay).newInstance();\n        log.info(\"Current route algorithm is [{}]\", routeHandle.getClass().getSimpleName());\n        if (routeWay.contains(\"ConsistentHash\")) {\n            //一致性 hash 算法\n            Method method = Class.forName(routeWay).getMethod(\"setHash\", AbstractConsistentHash.class);\n            AbstractConsistentHash consistentHash = (AbstractConsistentHash)\n                    Class.forName(appConfiguration.getConsistentHashWay()).newInstance();\n            method.invoke(routeHandle, consistentHash);\n            return routeHandle;\n        } else {\n\n            return routeHandle;\n\n        }\n\n    }\n\n    @Bean(\"userInfoCache\")\n    public LoadingCache<Long, Optional<CIMUserInfo>> userInfoCache(RedisTemplate<String, String> redisTemplate) {\n        return Caffeine.newBuilder()\n                .initialCapacity(64)\n                .maximumSize(1024)\n                .expireAfterWrite(10, TimeUnit.MINUTES)\n                .build(new CacheLoader<>() {\n                    @Override\n                    public Optional<CIMUserInfo> load(Long userId) throws Exception {\n                        String sendUserName = redisTemplate.opsForValue().get(ACCOUNT_PREFIX + userId);\n                        if (sendUserName == null) {\n                            return Optional.empty();\n                        }\n                        CIMUserInfo cimUserInfo = new CIMUserInfo(userId, sendUserName);\n                        return Optional.of(cimUserInfo);\n                    }\n                });\n    }\n\n    @Bean\n    public ServerApi serverApi(OkHttpClient okHttpClient) {\n        return RpcProxyManager.create(ServerApi.class, okHttpClient);\n    }\n\n    @Bean\n    public SnowflakeIdWorker snowflakeIdWorker() {\n        return new SnowflakeIdWorker();\n    }\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/MySqlPersistenceConfig.java",
    "content": "package com.crossoverjie.cim.route.config;\n\nimport org.mybatis.spring.annotation.MapperScan;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.Import;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/31\n * @description\n */\n@Configuration\n@ConditionalOnProperty(prefix = \"offline.store\", name = \"mode\", havingValue = \"mysql\")\n@MapperScan(basePackages = \"com.crossoverjie.cim.persistence.mysql.offlinemsg.mapper\")\n@Import(DataSourceAutoConfiguration.class)\npublic class MySqlPersistenceConfig {\n\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/OfflineMsgStoreConfig.java",
    "content": "package com.crossoverjie.cim.route.config;\n\nimport com.crossoverjie.cim.persistence.mysql.offlinemsg.OfflineMsgDb;\nimport com.crossoverjie.cim.persistence.mysql.offlinemsg.mapper.OfflineMsgLastSendRecordMapper;\nimport com.crossoverjie.cim.persistence.mysql.offlinemsg.mapper.OfflineMsgMapper;\nimport com.crossoverjie.cim.persistence.redis.OfflineMsgBuffer;\nimport com.crossoverjie.cim.persistence.redis.kit.OfflineMsgScriptExecutor;\nimport com.crossoverjie.cim.route.constant.Constant;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/18\n * @description\n */\n@Configuration\npublic class OfflineMsgStoreConfig {\n\n    @Bean\n    @ConditionalOnProperty(name = \"offline.store.mode\", havingValue = Constant.OfflineStoreMode.MYSQL)\n    public OfflineMsgDb offlineMsgDbStore(OfflineMsgMapper offlineMsgMapper, OfflineMsgLastSendRecordMapper offlineMsgLastSendRecordMapper) {\n        return new OfflineMsgDb(offlineMsgMapper, offlineMsgLastSendRecordMapper);\n    }\n\n    @Bean\n    @ConditionalOnProperty(name = \"offline.store.mode\", havingValue = Constant.OfflineStoreMode.REDIS)\n    public OfflineMsgBuffer offlineMsgBufferStore(OfflineMsgScriptExecutor scriptExecutor,\n            @Value(\"${offline.store.redis.expire.message-ttl-days}\") Integer configuredDays,\n            ObjectMapper objectMapper) {\n        return new OfflineMsgBuffer(scriptExecutor, configuredDays, objectMapper);\n    }\n}\n\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/SwaggerConfig.java",
    "content": "package com.crossoverjie.cim.route.config;\n\nimport io.swagger.v3.oas.models.OpenAPI;\nimport io.swagger.v3.oas.models.info.Contact;\nimport io.swagger.v3.oas.models.info.Info;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\npublic class SwaggerConfig {\n\n    @Bean\n    public OpenAPI createRestApi() {\n        return new OpenAPI()\n                .info(apiInfo());\n    }\n\n    private Info apiInfo() {\n        return new Info()\n                .title(\"cim-forward-route\")\n                .description(\"cim-forward-route api\")\n                .termsOfService(\"http://crossoverJie.top\")\n                .contact(contact())\n                .version(\"1.0.0\");\n    }\n\n    private Contact contact() {\n        Contact contact = new Contact();\n        contact.setName(\"crossoverJie\");\n        return contact;\n    }\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/constant/Constant.java",
    "content": "package com.crossoverjie.cim.route.constant;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/9/10 14:07\n * @since JDK 1.8\n */\npublic final class Constant {\n\n\n    /**\n     * 账号前缀\n     */\n    public static final String ACCOUNT_PREFIX = \"cim-account:\";\n\n    /**\n     * 路由信息前缀\n     */\n    public static final String ROUTE_PREFIX = \"cim-route:\";\n\n    /**\n     * 登录状态前缀\n     */\n    public static final String LOGIN_STATUS_PREFIX = \"login-status\";\n\n\n    public static final class OfflineStoreMode {\n        /**\n         * redis\n         */\n        public static final String REDIS = \"redis\";\n\n        /**\n         * mysql\n         */\n        public static final String MYSQL = \"mysql\";\n    }\n\n\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/controller/RouteController.java",
    "content": "package com.crossoverjie.cim.route.controller;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossoverjie.cim.common.metastore.MetaStore;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverjie.cim.common.res.BaseResponse;\nimport com.crossoverjie.cim.common.res.NULLBody;\nimport com.crossoverjie.cim.common.route.algorithm.RouteHandle;\nimport com.crossoverjie.cim.common.util.RouteInfoParseUtil;\nimport com.crossoverjie.cim.route.api.RouteApi;\nimport com.crossoverjie.cim.route.api.vo.req.ChatReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.LoginReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.OfflineMsgReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.RegisterInfoReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\nimport com.crossoverjie.cim.route.api.vo.res.RegisterInfoResVO;\nimport com.crossoverjie.cim.route.service.AccountService;\nimport com.crossoverjie.cim.route.service.CommonBizService;\nimport com.crossoverjie.cim.route.service.OfflineMsgService;\nimport com.crossoverjie.cim.route.service.UserInfoCacheService;\nimport com.crossoverjie.cim.server.api.ServerApi;\nimport io.swagger.v3.oas.annotations.Operation;\nimport jakarta.annotation.Resource;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.ResponseBody;\n\nimport static com.crossoverjie.cim.common.enums.StatusEnum.OFF_LINE;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 22/05/2018 14:46\n * @since JDK 1.8\n */\n@Slf4j\n@Controller\n@RequestMapping(\"/\")\npublic class RouteController implements RouteApi {\n\n    @Resource\n    private MetaStore metaStore;\n\n    @Resource\n    private AccountService accountService;\n\n    @Resource\n    private UserInfoCacheService userInfoCacheService;\n\n    @Resource\n    private CommonBizService commonBizService;\n\n    @Resource\n    private RouteHandle routeHandle;\n\n    @Resource\n    private ServerApi serverApi;\n\n    @Resource\n    private OfflineMsgService offlineMsgService;\n\n\n    @Operation(summary = \"群聊 API\")\n    @RequestMapping(value = \"groupRoute\", method = RequestMethod.POST)\n    @ResponseBody()\n    @Override\n    public BaseResponse<NULLBody> groupRoute(@RequestBody ChatReqVO groupReqVO) {\n        BaseResponse<NULLBody> res = new BaseResponse();\n\n        log.info(\"msg=[{}]\", groupReqVO.toString());\n\n        Map<Long, CIMServerResVO> serverResVoMap = accountService.loadRouteRelated();\n        for (Map.Entry<Long, CIMServerResVO> cimServerResVoEntry : serverResVoMap.entrySet()) {\n            Long userId = cimServerResVoEntry.getKey();\n            CIMServerResVO cimServerResVO = cimServerResVoEntry.getValue();\n            if (userId.equals(groupReqVO.getUserId())) {\n                // Skip the sender\n                Optional<CIMUserInfo> cimUserInfo = userInfoCacheService.loadUserInfoByUserId(groupReqVO.getUserId());\n                cimUserInfo.ifPresent(userInfo -> log.warn(\"skip send user userId={}\", userInfo));\n                continue;\n            }\n\n            // Push message\n            ChatReqVO chatVO = new ChatReqVO(userId, groupReqVO.getMsg(), null);\n            accountService.pushMsg(cimServerResVO, groupReqVO.getUserId(), chatVO);\n\n        }\n\n        res.setCode(StatusEnum.SUCCESS.getCode());\n        res.setMessage(StatusEnum.SUCCESS.getMessage());\n        return res;\n    }\n\n\n    /**\n     * 私聊路由\n     *\n     * @param p2pRequest\n     * @return\n     */\n    @Operation(summary = \"私聊 API\")\n    @RequestMapping(value = \"p2pRoute\", method = RequestMethod.POST)\n    @ResponseBody()\n    @Override\n    public BaseResponse<NULLBody> p2pRoute(@RequestBody P2PReqVO p2pRequest) {\n        BaseResponse<NULLBody> res = new BaseResponse();\n\n        try {\n            //获取接收消息用户的路由信息\n            Optional<CIMServerResVO> cimServerResVO = accountService.loadRouteRelatedByUserId(p2pRequest.getReceiveUserId());\n            if (cimServerResVO.isEmpty()) {\n                log.warn(\"userId={} not online, save offline msg\", p2pRequest.getReceiveUserId());\n                offlineMsgService.saveOfflineMsg(p2pRequest);\n                throw new CIMException(OFF_LINE);\n            }\n\n            //p2pRequest.getReceiveUserId()==>消息接收者的 userID\n            ChatReqVO chatVO = new ChatReqVO(p2pRequest.getReceiveUserId(), p2pRequest.getMsg(), p2pRequest.getBatchMsg());\n            accountService.pushMsg(cimServerResVO.get(), p2pRequest.getUserId(), chatVO);\n\n            res.setCode(StatusEnum.SUCCESS.getCode());\n            res.setMessage(StatusEnum.SUCCESS.getMessage());\n\n        } catch (CIMException e) {\n            res.setCode(e.getErrorCode());\n            res.setMessage(e.getErrorMessage());\n        }\n        return res;\n    }\n\n\n    @Operation(summary = \"客户端下线\")\n    @RequestMapping(value = \"offLine\", method = RequestMethod.POST)\n    @ResponseBody()\n    @Override\n    public BaseResponse<NULLBody> offLine(@RequestBody ChatReqVO chatReqVO) {\n        BaseResponse<NULLBody> res = new BaseResponse();\n\n        Optional<CIMUserInfo> cimUserInfo = userInfoCacheService.loadUserInfoByUserId(chatReqVO.getUserId());\n\n        cimUserInfo.ifPresent(userInfo -> {\n            log.info(\"user [{}] offline!\", userInfo);\n            accountService.offLine(chatReqVO.getUserId());\n        });\n\n        res.setCode(StatusEnum.SUCCESS.getCode());\n        res.setMessage(StatusEnum.SUCCESS.getMessage());\n        return res;\n    }\n\n    /**\n     * 获取一台 CIM server\n     *\n     * @return\n     */\n    @Operation(summary = \"登录并获取服务器\")\n    @RequestMapping(value = \"login\", method = RequestMethod.POST)\n    @ResponseBody()\n    @Override\n    public BaseResponse<CIMServerResVO> login(@RequestBody LoginReqVO loginReqVO) throws Exception {\n        BaseResponse<CIMServerResVO> res = new BaseResponse();\n        //登录校验\n        StatusEnum status = accountService.login(loginReqVO);\n        res.setCode(status.getCode());\n        res.setMessage(status.getMessage());\n        if (status != StatusEnum.SUCCESS) {\n            return res;\n        }\n\n        // check server available\n        Set<String> availableServerList = metaStore.getAvailableServerList();\n        String key = String.valueOf(loginReqVO.getUserId());\n        String server =\n                routeHandle.routeServer(List.copyOf(availableServerList), key);\n        log.info(\"userInfo=[{}] route server info=[{}]\", loginReqVO, server);\n\n        RouteInfo routeInfo = RouteInfoParseUtil.parse(server);\n        routeInfo = commonBizService.checkServerAvailable(routeInfo, key);\n\n        //保存路由信息\n        accountService.saveRouteInfo(loginReqVO, server);\n\n        CIMServerResVO vo =\n                new CIMServerResVO(routeInfo.getIp(), routeInfo.getCimServerPort(), routeInfo.getHttpPort());\n        res.setDataBody(vo);\n        return res;\n    }\n\n    /**\n     * 注册账号\n     *\n     * @return\n     */\n    @Operation(summary = \"注册账号\")\n    @RequestMapping(value = \"registerAccount\", method = RequestMethod.POST)\n    @ResponseBody()\n    @Override\n    public BaseResponse<RegisterInfoResVO> registerAccount(@RequestBody RegisterInfoReqVO registerInfoReqVO)\n            throws Exception {\n        BaseResponse<RegisterInfoResVO> res = new BaseResponse();\n\n        long userId = System.currentTimeMillis();\n        RegisterInfoResVO info = new RegisterInfoResVO(userId, registerInfoReqVO.getUserName());\n        info = accountService.register(info);\n\n        res.setDataBody(info);\n        res.setCode(StatusEnum.SUCCESS.getCode());\n        res.setMessage(StatusEnum.SUCCESS.getMessage());\n        return res;\n    }\n\n    /**\n     * 获取所有在线用户\n     *\n     * @return\n     */\n    @Operation(summary = \"获取所有在线用户\")\n    @RequestMapping(value = \"onlineUser\", method = RequestMethod.GET)\n    @ResponseBody()\n    @Override\n    public BaseResponse<Set<CIMUserInfo>> onlineUser() throws Exception {\n        BaseResponse<Set<CIMUserInfo>> res = new BaseResponse();\n\n        Set<CIMUserInfo> cimUserInfos = userInfoCacheService.onlineUser();\n        res.setDataBody(cimUserInfos);\n        res.setCode(StatusEnum.SUCCESS.getCode());\n        res.setMessage(StatusEnum.SUCCESS.getMessage());\n        return res;\n    }\n\n    @Operation(summary = \"Client fetch offline messages\")\n    @RequestMapping(value = \"fetchOfflineMsgs\", method = RequestMethod.POST)\n    @ResponseBody()\n    @Override\n    public BaseResponse<NULLBody> fetchOfflineMsgs(@RequestBody OfflineMsgReqVO offlineMsgReqVO) {\n        BaseResponse<NULLBody> res = new BaseResponse();\n\n        try {\n            // Get the routing information of the user receiving the message\n            Optional<CIMServerResVO> cimServerResVO = accountService.loadRouteRelatedByUserId(offlineMsgReqVO.getReceiveUserId());\n\n            cimServerResVO.ifPresent(cimServerRes -> {\n                offlineMsgService.fetchOfflineMsgs(cimServerRes, offlineMsgReqVO.getReceiveUserId());\n            });\n\n            res.setCode(StatusEnum.SUCCESS.getCode());\n            res.setMessage(StatusEnum.SUCCESS.getMessage());\n\n        } catch (CIMException e) {\n            res.setCode(e.getErrorCode());\n            res.setMessage(e.getErrorMessage());\n        }\n        return res;\n    }\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/exception/ExceptionHandlingController.java",
    "content": "package com.crossoverjie.cim.route.exception;\n\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossoverjie.cim.common.res.BaseResponse;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.web.bind.annotation.ControllerAdvice;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.ResponseBody;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2020-04-12 22:13\n * @since JDK 1.8\n */\n@Slf4j\n@ControllerAdvice\npublic class ExceptionHandlingController {\n    @ExceptionHandler(CIMException.class)\n    @ResponseBody()\n    public BaseResponse handleAllExceptions(CIMException ex) {\n        log.error(\"exception\", ex);\n        BaseResponse baseResponse = new BaseResponse();\n        baseResponse.setCode(ex.getErrorCode());\n        baseResponse.setMessage(ex.getMessage());\n        return baseResponse;\n    }\n\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/factory/OfflineMsgFactory.java",
    "content": "package com.crossoverjie.cim.route.factory;\n\nimport com.crossoverjie.cim.common.constant.Constants;\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossoverjie.cim.persistence.api.pojo.OfflineMsg;\nimport com.crossoverjie.cim.common.util.SnowflakeIdWorker;\nimport com.crossoverjie.cim.persistence.api.vo.req.SaveOfflineMsgReqVO;\nimport org.springframework.stereotype.Service;\n\nimport java.time.LocalDateTime;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static com.crossoverjie.cim.common.constant.Constants.MSG_TYPE_TEXT;\nimport static com.crossoverjie.cim.common.constant.Constants.OFFLINE_MSG_PENDING;\n\n@Service\npublic class OfflineMsgFactory {\n\n    private final SnowflakeIdWorker idWorker;\n\n    public OfflineMsgFactory(SnowflakeIdWorker idWorker) {\n        this.idWorker = idWorker;\n    }\n\n    public OfflineMsg createFromVo(SaveOfflineMsgReqVO vo) {\n        try {\n            Long msgId = idWorker.nextId();\n            return OfflineMsg.builder()\n                    .messageId(msgId)\n                    .receiveUserId(vo.getReceiveUserId())\n                    .content(vo.getMsg())\n                    .messageType(MSG_TYPE_TEXT)\n                    .status(OFFLINE_MSG_PENDING)\n                    .createdAt(LocalDateTime.now())\n                    .properties(createPropertiesMap(vo))\n                    .build();\n        } catch (Exception e) {\n            throw new CIMException(\"Failed to create OfflineMsg from SaveOfflineMsgReqVO\", e);\n        }\n    }\n\n    private Map<String, String> createPropertiesMap(SaveOfflineMsgReqVO vo) {\n        Map<String, String> sourceProps = vo.getProperties();\n        if (sourceProps == null) {\n            return Map.of();\n        }\n\n        Map<String, String> properties = new HashMap<>();\n        properties.put(Constants.MetaKey.SEND_USER_ID, sourceProps.get(Constants.MetaKey.SEND_USER_ID));\n        properties.put(Constants.MetaKey.SEND_USER_NAME, sourceProps.get(Constants.MetaKey.SEND_USER_NAME));\n        properties.put(Constants.MetaKey.RECEIVE_USER_ID, sourceProps.get(Constants.MetaKey.RECEIVE_USER_ID));\n        return properties;\n    }\n\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/kit/NetAddressIsReachable.java",
    "content": "package com.crossoverjie.cim.route.kit;\n\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.net.Socket;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2020-04-12 20:32\n * @since JDK 1.8\n */\n@Slf4j\npublic class NetAddressIsReachable {\n\n    /**\n     * check ip and port\n     *\n     * @param address\n     * @param port\n     * @param timeout\n     * @return True if connection successful\n     */\n    public static boolean checkAddressReachable(String address, int port, int timeout) {\n        Socket socket = new Socket();\n        try {\n            socket.connect(new InetSocketAddress(address, port), timeout);\n            return true;\n        } catch (IOException exception) {\n            return false;\n        } finally {\n            try {\n                socket.close();\n            } catch (IOException e) {\n                log.warn(\"close socket error\", e);\n            }\n        }\n    }\n\n    private NetAddressIsReachable() {\n    }\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/AccountService.java",
    "content": "package com.crossoverjie.cim.route.service;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverjie.cim.route.api.vo.req.ChatReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.LoginReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\nimport com.crossoverjie.cim.route.api.vo.res.RegisterInfoResVO;\n\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * Function: 账户服务\n *\n * @author crossoverJie\n *         Date: 2018/12/23 21:57\n * @since JDK 1.8\n */\npublic interface AccountService {\n\n    /**\n     * 注册用户\n     * @param info 用户信息\n     * @return\n     * @throws Exception\n     */\n    RegisterInfoResVO register(RegisterInfoResVO info) throws Exception;\n\n    /**\n     * 登录服务\n     * @param loginReqVO 登录信息\n     * @return true 成功 false 失败\n     * @throws Exception\n     */\n    StatusEnum login(LoginReqVO loginReqVO) throws Exception;\n\n    /**\n     * 保存路由信息\n     * @param msg 服务器信息\n     * @param loginReqVO 用户信息\n     * @throws Exception\n     */\n    void saveRouteInfo(LoginReqVO loginReqVO, String msg) throws Exception;\n\n    /**\n     * 加载所有用户的路有关系\n     * @return 所有的路由关系\n     */\n    Map<Long, CIMServerResVO> loadRouteRelated();\n\n    /**\n     * Get user route info\n     * @param userId\n     * @return route info\n     */\n    Optional<CIMServerResVO> loadRouteRelatedByUserId(Long userId);\n\n\n    /**\n     * 推送消息\n     * @param cimServerResVO\n     * @param groupReqVO 消息\n     * @param sendUserId 发送者的ID\n     * @throws Exception\n     */\n    void pushMsg(CIMServerResVO cimServerResVO, long sendUserId, ChatReqVO groupReqVO);\n\n    /**\n     * 用户下线\n     * @param userId 下线用户ID\n     * @throws Exception\n     */\n    void offLine(Long userId);\n\n\n\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/CommonBizService.java",
    "content": "package com.crossoverjie.cim.route.service;\n\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverjie.cim.common.route.algorithm.RouteHandle;\nimport com.crossoverjie.cim.common.util.RouteInfoParseUtil;\nimport com.crossoverjie.cim.route.kit.NetAddressIsReachable;\nimport jakarta.annotation.Resource;\nimport java.util.List;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2020-04-12 21:40\n * @since JDK 1.8\n */\n@Component\n@Slf4j\npublic class CommonBizService {\n\n\n    @Resource\n    private RouteHandle routeHandle;\n\n    /**\n     * check ip and port, and return a new server if the current server is not available\n     * @param routeInfo\n     */\n    @SneakyThrows\n    public RouteInfo checkServerAvailable(RouteInfo routeInfo, String userId) {\n        boolean reachable = NetAddressIsReachable.checkAddressReachable(routeInfo.getIp(), routeInfo.getCimServerPort(), 1000);\n        if (!reachable) {\n            log.error(\"ip={}, port={} are not available, remove it.\", routeInfo.getIp(), routeInfo.getCimServerPort());\n            List<String> list = routeHandle.removeExpireServer(routeInfo);\n            String routeServer = routeHandle.routeServer(list, userId);\n            log.info(\"Reselect new server:[{}]\", routeServer);\n            return RouteInfoParseUtil.parse(routeServer);\n        }\n        return routeInfo;\n    }\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/OfflineMsgService.java",
    "content": "package com.crossoverjie.cim.route.service;\n\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\n\n/**\n * Offline message push service\n */\npublic interface OfflineMsgService {\n\n    /**\n     * fetch offline messages\n     * @param cimServerResVO\n     * @param receiveUserId\n     */\n    void fetchOfflineMsgs(CIMServerResVO cimServerResVO, Long receiveUserId);\n\n    /**\n     * save offline message\n     * @param p2pRequest\n     */\n    void saveOfflineMsg(P2PReqVO p2pRequest);\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/UserInfoCacheService.java",
    "content": "package com.crossoverjie.cim.route.service;\n\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\n\nimport java.util.Optional;\nimport java.util.Set;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/12/24 11:06\n * @since JDK 1.8\n */\npublic interface UserInfoCacheService {\n\n    /**\n     * 通过 userID 获取用户信息\n     * @param userId 用户唯一 ID\n     * @return\n     * @throws Exception\n     */\n    Optional<CIMUserInfo> loadUserInfoByUserId(Long userId);\n\n    /**\n     * 保存和检查用户登录情况\n     * @param userId userId 用户唯一 ID\n     * @return true 为可以登录 false 为已经登录\n     * @throws Exception\n     */\n    boolean saveAndCheckUserLoginStatus(Long userId) throws Exception;\n\n    /**\n     * query all online user\n     * @return online user\n     */\n    Set<CIMUserInfo> onlineUser();\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/impl/AccountServiceRedisImpl.java",
    "content": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.crossoverjie.cim.common.constant.Constants;\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverjie.cim.common.protocol.BaseCommand;\nimport com.crossoverjie.cim.common.util.RouteInfoParseUtil;\nimport com.crossoverjie.cim.route.api.vo.req.ChatReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.LoginReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\nimport com.crossoverjie.cim.route.api.vo.res.RegisterInfoResVO;\nimport com.crossoverjie.cim.route.service.AccountService;\nimport com.crossoverjie.cim.route.service.UserInfoCacheService;\nimport com.crossoverjie.cim.server.api.ServerApi;\nimport com.crossoverjie.cim.server.api.vo.req.SendMsgReqVO;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.core.io.ClassPathResource;\nimport org.springframework.data.redis.connection.RedisConnection;\nimport org.springframework.data.redis.core.Cursor;\nimport org.springframework.data.redis.core.RedisTemplate;\nimport org.springframework.data.redis.core.ScanOptions;\nimport org.springframework.data.redis.core.script.DefaultRedisScript;\nimport org.springframework.scripting.support.ResourceScriptSource;\nimport org.springframework.stereotype.Service;\n\nimport java.nio.charset.StandardCharsets;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\n\nimport static com.crossoverjie.cim.route.constant.Constant.ACCOUNT_PREFIX;\nimport static com.crossoverjie.cim.route.constant.Constant.LOGIN_STATUS_PREFIX;\nimport static com.crossoverjie.cim.route.constant.Constant.ROUTE_PREFIX;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2018/12/23 21:58\n * @since JDK 1.8\n */\n@Slf4j\n@Service\npublic class AccountServiceRedisImpl implements AccountService {\n\n    @Resource\n    private RedisTemplate<String, String> redisTemplate;\n\n    @Resource\n    private UserInfoCacheService userInfoCacheService;\n\n    @Resource\n    private ServerApi serverApi;\n\n    @Override\n    public RegisterInfoResVO register(RegisterInfoResVO info) {\n        String key = ACCOUNT_PREFIX + info.getUserId();\n\n        String name = redisTemplate.opsForValue().get(info.getUserName());\n        if (null == name) {\n            //为了方便查询，冗余一份\n            redisTemplate.opsForValue().set(key, info.getUserName());\n            redisTemplate.opsForValue().set(info.getUserName(), key);\n        } else {\n            long userId = Long.parseLong(name.split(\":\")[1]);\n            info.setUserId(userId);\n            info.setUserName(info.getUserName());\n        }\n\n        return info;\n    }\n\n    @Override\n    public StatusEnum login(LoginReqVO loginReqVO) throws Exception {\n        //再去Redis里查询\n        String key = ACCOUNT_PREFIX + loginReqVO.getUserId();\n        String userName = redisTemplate.opsForValue().get(key);\n        if (null == userName) {\n            return StatusEnum.ACCOUNT_NOT_MATCH;\n        }\n\n        if (!userName.equals(loginReqVO.getUserName())) {\n            return StatusEnum.ACCOUNT_NOT_MATCH;\n        }\n\n        //登录成功，保存登录状态\n        boolean status = userInfoCacheService.saveAndCheckUserLoginStatus(loginReqVO.getUserId());\n        if (!status) {\n            //重复登录\n            return StatusEnum.REPEAT_LOGIN;\n        }\n\n        return StatusEnum.SUCCESS;\n    }\n\n    @Override\n    public void saveRouteInfo(LoginReqVO loginReqVO, String msg) throws Exception {\n        String key = ROUTE_PREFIX + loginReqVO.getUserId();\n        redisTemplate.opsForValue().set(key, msg);\n    }\n\n    @Override\n    public Map<Long, CIMServerResVO> loadRouteRelated() {\n\n        Map<Long, CIMServerResVO> routes = new HashMap<>(64);\n\n\n        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();\n        ScanOptions options = ScanOptions.scanOptions()\n                .match(ROUTE_PREFIX + \"*\")\n                .build();\n        Cursor<byte[]> scan = connection.scan(options);\n\n        while (scan.hasNext()) {\n            byte[] next = scan.next();\n            String key = new String(next, StandardCharsets.UTF_8);\n            log.info(\"key={}\", key);\n            parseServerInfo(routes, key);\n\n        }\n        scan.close();\n\n        return routes;\n    }\n\n    @Override\n    public Optional<CIMServerResVO> loadRouteRelatedByUserId(Long userId) {\n        String value = redisTemplate.opsForValue().get(ROUTE_PREFIX + userId);\n\n        if (value == null) {\n            return Optional.empty();\n        }\n\n        RouteInfo parse = RouteInfoParseUtil.parse(value);\n        CIMServerResVO cimServerResVO =\n                new CIMServerResVO(parse.getIp(), parse.getCimServerPort(), parse.getHttpPort());\n        return Optional.of(cimServerResVO);\n    }\n\n    private void parseServerInfo(Map<Long, CIMServerResVO> routes, String key) {\n        long userId = Long.valueOf(key.split(\":\")[1]);\n        String value = redisTemplate.opsForValue().get(key);\n        RouteInfo parse = RouteInfoParseUtil.parse(value);\n        CIMServerResVO cimServerResVO =\n                new CIMServerResVO(parse.getIp(), parse.getCimServerPort(), parse.getHttpPort());\n        routes.put(userId, cimServerResVO);\n    }\n\n\n    @Override\n    public void pushMsg(CIMServerResVO cimServerResVO, long sendUserId, ChatReqVO chatReqVO) {\n        Optional<CIMUserInfo> cimUserInfo = userInfoCacheService.loadUserInfoByUserId(sendUserId);\n\n        cimUserInfo.ifPresent(sendUserInfo -> {\n            String url = \"http://\" + cimServerResVO.getIp() + \":\" + cimServerResVO.getHttpPort();\n            SendMsgReqVO vo =\n                    new SendMsgReqVO(chatReqVO.getMsg(), chatReqVO.getUserId(), chatReqVO.getBatchMsg(), BaseCommand.MESSAGE);\n            vo.setProperties(Map.of(\n                    Constants.MetaKey.SEND_USER_ID, String.valueOf(sendUserId),\n                    Constants.MetaKey.SEND_USER_NAME, sendUserInfo.getUserName(),\n                    Constants.MetaKey.RECEIVE_USER_ID, String.valueOf(chatReqVO.getUserId()))\n            );\n            serverApi.sendMsg(vo, url);\n\n        });\n    }\n\n    @Override\n    public void offLine(Long userId) {\n\n        DefaultRedisScript redisScript = new DefaultRedisScript();\n        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(\"lua/offLine.lua\")));\n\n        redisTemplate.execute(redisScript,\n                Collections.singletonList(ROUTE_PREFIX + userId),\n                LOGIN_STATUS_PREFIX,\n                userId.toString());\n    }\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/impl/OfflineMsgServiceImpl.java",
    "content": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.crossoverjie.cim.common.constant.Constants;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.common.protocol.BaseCommand;\nimport com.crossoverjie.cim.persistence.api.pojo.OfflineMsg;\nimport com.crossoverjie.cim.persistence.api.service.OfflineMsgStore;\nimport com.crossoverjie.cim.persistence.api.vo.req.SaveOfflineMsgReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\nimport com.crossoverjie.cim.route.factory.OfflineMsgFactory;\nimport com.crossoverjie.cim.route.service.OfflineMsgService;\nimport com.crossoverjie.cim.route.service.UserInfoCacheService;\nimport com.crossoverjie.cim.server.api.ServerApi;\nimport com.crossoverjie.cim.server.api.vo.req.SendMsgReqVO;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Service;\n\nimport java.util.Comparator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Optional;\n\n@Slf4j\n@Service\npublic class OfflineMsgServiceImpl implements OfflineMsgService {\n\n    @Resource\n    private OfflineMsgStore offlineMsgStore;\n\n    @Resource\n    private OfflineMsgFactory offlineMsgFactory;\n\n    @Resource\n    private UserInfoCacheService userInfoCacheService;\n\n    @Resource\n    private ServerApi serverApi;\n\n    @Override\n    public void fetchOfflineMsgs(CIMServerResVO cimServerResVO, Long receiveUserId) {\n\n        String url = \"http://\" + cimServerResVO.getIp() + \":\" + cimServerResVO.getHttpPort();\n\n        while (true) {\n            List<OfflineMsg> offlineMsgs = offlineMsgStore.fetch(receiveUserId);\n            if (offlineMsgs == null || offlineMsgs.isEmpty()) {\n                break;\n            }\n            offlineMsgs.sort(Comparator.comparing(OfflineMsg::getCreatedAt));\n\n            SendMsgReqVO msgReqVO = SendMsgReqVO\n                    .builder()\n                    .userId(receiveUserId)\n                    .cmd(BaseCommand.OFFLINE).batchMsg(offlineMsgs.stream().map(OfflineMsg::getContent).toList())\n                    .properties(offlineMsgs.get(0).getProperties())\n                    .build();\n\n            serverApi.sendMsg(msgReqVO, url);\n\n            offlineMsgStore.markDelivered(receiveUserId, offlineMsgs.stream().map(OfflineMsg::getMessageId).toList());\n        }\n\n\n    }\n\n\n    @Override\n    public void saveOfflineMsg(P2PReqVO p2pRequest) {\n\n        Optional<CIMUserInfo> cimUserInfo = userInfoCacheService.loadUserInfoByUserId(p2pRequest.getUserId());\n\n        cimUserInfo.ifPresent(userInfo -> {\n            SaveOfflineMsgReqVO saveOfflineMsgReqVO = SaveOfflineMsgReqVO.builder()\n                    .msg(p2pRequest.getMsg())\n                    .receiveUserId(p2pRequest.getReceiveUserId())\n                    .properties(Map.of(\n                            Constants.MetaKey.SEND_USER_ID, userInfo.getUserId().toString(),\n                            Constants.MetaKey.SEND_USER_NAME, userInfo.getUserName(),\n                            Constants.MetaKey.RECEIVE_USER_ID, p2pRequest.getReceiveUserId().toString()\n                    )).build();\n            OfflineMsg offlineMsg = offlineMsgFactory.createFromVo(saveOfflineMsgReqVO);\n            offlineMsgStore.save(offlineMsg);\n        });\n    }\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/impl/UserInfoCacheServiceImpl.java",
    "content": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.route.service.UserInfoCacheService;\nimport com.github.benmanes.caffeine.cache.LoadingCache;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.data.redis.core.RedisTemplate;\nimport org.springframework.stereotype.Service;\n\nimport java.util.HashSet;\nimport java.util.Optional;\nimport java.util.Set;\n\nimport static com.crossoverjie.cim.route.constant.Constant.LOGIN_STATUS_PREFIX;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/12/24 11:06\n * @since JDK 1.8\n */\n\n@Slf4j\n@Service\npublic class UserInfoCacheServiceImpl implements UserInfoCacheService {\n\n    @Autowired\n    private RedisTemplate<String, String> redisTemplate;\n\n    @Resource(name = \"userInfoCache\")\n    private LoadingCache<Long, Optional<CIMUserInfo>> userInfoMap;\n\n    @Override\n    public Optional<CIMUserInfo> loadUserInfoByUserId(Long userId) {\n        //Retrieve user information using a second-level cache.\n        return userInfoMap.get(userId);\n    }\n\n    @Override\n    public boolean saveAndCheckUserLoginStatus(Long userId) throws Exception {\n\n        Long add = redisTemplate.opsForSet().add(LOGIN_STATUS_PREFIX, userId.toString());\n        return add != 0;\n    }\n\n    @Override\n    public Set<CIMUserInfo> onlineUser() {\n        Set<CIMUserInfo> set = null;\n        Set<String> members = redisTemplate.opsForSet().members(LOGIN_STATUS_PREFIX);\n        for (String member : members) {\n            if (set == null) {\n                set = new HashSet<>(64);\n            }\n            try {\n                Optional<CIMUserInfo> cimUserInfo = loadUserInfoByUserId(Long.valueOf(member));\n                cimUserInfo.ifPresent(set::add);\n            } catch (NumberFormatException e) {\n                log.warn(\"Skipping invalid user ID format in Redis set: {}\", member);\n            }\n        }\n\n        return set;\n    }\n\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/util/SpringBeanFactory.java",
    "content": "package com.crossoverjie.cim.route.util;\n\nimport org.springframework.beans.BeansException;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic final class SpringBeanFactory implements ApplicationContextAware {\n    private static ApplicationContext context;\n\n    public static <T> T getBean(Class<T> c) {\n        return context.getBean(c);\n    }\n\n\n    public static <T> T getBean(String name, Class<T> clazz) {\n        return context.getBean(name, clazz);\n    }\n\n    @Override\n    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\n        context = applicationContext;\n    }\n\n\n}\n"
  },
  {
    "path": "cim-forward-route/src/main/resources/application.yaml",
    "content": "spring:\r\n  application:\r\n    name: cim-forward-route\r\n  data:\r\n    redis:\r\n      host: 127.0.0.1\r\n      port: 6379\r\n      jedis:\r\n        pool:\r\n          max-active: 100\r\n          max-idle: 100\r\n          max-wait: 1000\r\n          min-idle: 10\r\n\r\n#  datasource:\r\n#    driver-class-name: com.mysql.cj.jdbc.Driver\r\n#    url: jdbc:mysql://localhost:3306/cim-test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8\r\n#    username: root\r\n#    password: zcyzcy159162\r\n\r\n\r\n# web port\r\nserver:\r\n  port: 8083\r\n\r\nlogging:\r\n  level:\r\n    root: info\r\n\r\n  # enable swagger\r\nspringdoc:\r\n  swagger-ui:\r\n    enabled: true\r\n\r\napp:\r\n  zk:\r\n    addr: 127.0.0.1:2181\r\n    connect:\r\n      timeout: 30000\r\n    root: /route\r\n\r\n  # route strategy\r\n  #app.route.way=com.crossoverjie.cim.common.route.algorithm.loop.LoopHandle\r\n\r\n  # route strategy\r\n  #app.route.way=com.crossoverjie.cim.common.route.algorithm.random.RandomHandle\r\n\r\n  # route strategy\r\n  route:\r\n    way:\r\n      handler: com.crossoverjie.cim.common.route.algorithm.consistenthash.ConsistentHashHandle\r\n\r\n  #app.route.way.consitenthash=com.crossoverjie.cim.common.route.algorithm.consistenthash.SortArrayMapConsistentHash\r\n\r\n      consitenthash: com.crossoverjie.cim.common.route.algorithm.consistenthash.TreeMapConsistentHash\r\n\r\nmybatis:\r\n  configuration:\r\n    map-underscore-to-camel-case: true # 自动驼峰转换\r\n  type-aliases-package: com.crossoverjie.cim.persistence.api.pojo # 实体类包路径\r\n  mapper-locations: classpath*:mapper/**/*.xml\r\n  global-config:\r\n    db-config:\r\n      id-type: auto\r\n\r\noffline:\r\n  store:\r\n    mode: redis\r\n    redis:\r\n      expire:\r\n        message-ttl-days: 3\r\n\r\n"
  },
  {
    "path": "cim-forward-route/src/main/resources/banner.txt",
    "content": "      _                         __\n ____(_)_ _      _______  __ __/ /____\n/ __/ /  ' \\    / __/ _ \\/ // / __/ -_)\n\\__/_/_/_/_/   /_/  \\___/\\_,_/\\__/\\__/\n Power by @crossoverJie\n\n\n\n"
  },
  {
    "path": "cim-forward-route/src/main/resources/lua/offLine.lua",
    "content": "\nredis.call('DEL', KEYS[1])\n\nredis.call('SREM', ARGV[1], ARGV[2])\n\n"
  },
  {
    "path": "cim-forward-route/src/test/java/CommonTest.java",
    "content": "import com.crossoverjie.cim.route.kit.NetAddressIsReachable;\nimport org.junit.Test;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2020-04-12 18:38\n * @since JDK 1.8\n */\npublic class CommonTest {\n\n    @Test\n    public void test() {\n        boolean reachable = NetAddressIsReachable.checkAddressReachable(\"127.0.0.1\", 11211, 1000);\n        System.out.println(reachable);\n    }\n\n\n\n}\n"
  },
  {
    "path": "cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/AbstractBaseTest.java",
    "content": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.clevercloud.testcontainers.zookeeper.ZooKeeperContainer;\nimport com.redis.testcontainers.RedisContainer;\nimport java.time.Duration;\nimport java.util.List;\nimport org.junit.jupiter.api.AfterAll;\nimport org.junit.jupiter.api.BeforeAll;\nimport org.testcontainers.containers.MySQLContainer;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.utility.DockerImageName;\nimport org.testcontainers.utility.MountableFile;\n\npublic class AbstractBaseTest {\n\n    @Container\n    static RedisContainer redis = new RedisContainer(DockerImageName.parse(\"redis:7.4.0\"));\n\n    private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName\n            .parse(\"zookeeper\")\n            .withTag(\"3.9.2\");\n\n    private static final Duration DEFAULT_STARTUP_TIMEOUT = Duration.ofSeconds(60);\n\n    @Container\n    static final ZooKeeperContainer\n            ZOOKEEPER_CONTAINER = new ZooKeeperContainer(DEFAULT_IMAGE_NAME, DEFAULT_STARTUP_TIMEOUT);\n\n    @Container\n    static final MySQLContainer<?> MYSQL = new MySQLContainer<>(\"mysql:8.0.33\")\n            .withDatabaseName(\"cim-test\")\n            .withUsername(\"cimUserName\")\n            .withPassword(\"cimPassWord\")\n            .withCopyFileToContainer(\n                    MountableFile.forClasspathResource(\"init.sql\"),\n                    \"/docker-entrypoint-initdb.d/init.sql\"\n            )\n            .withExposedPorts(3306)\n            .withReuse(true);\n\n    @BeforeAll\n    public static void before() {\n        redis.setExposedPorts(List.of(6379));\n        redis.setPortBindings(List.of(\"6379:6379\"));\n        redis.start();\n\n        ZOOKEEPER_CONTAINER.setExposedPorts(List.of(2181));\n        ZOOKEEPER_CONTAINER.setPortBindings(List.of(\"2181:2181\"));\n        ZOOKEEPER_CONTAINER.start();\n\n\n        // 启动 MySQL\n        MYSQL.start();\n\n        // 动态设置 Spring 数据源配置（如果使用 Spring Boot）\n        System.setProperty(\"spring.datasource.url\", MYSQL.getJdbcUrl());\n        System.setProperty(\"spring.datasource.username\", MYSQL.getUsername());\n        System.setProperty(\"spring.datasource.password\", MYSQL.getPassword());\n    }\n\n    @AfterAll\n    public static void after() {\n        redis.stop();\n        ZOOKEEPER_CONTAINER.stop();\n        MYSQL.stop();\n    }\n\n}\n"
  },
  {
    "path": "cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/AccountServiceRedisImplTest.java",
    "content": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.alibaba.fastjson.JSON;\nimport com.crossoverjie.cim.route.RouteApplication;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\nimport com.crossoverjie.cim.route.service.AccountService;\nimport java.util.Map;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@Slf4j\n@SpringBootTest(classes = RouteApplication.class)\npublic class AccountServiceRedisImplTest extends AbstractBaseTest {\n\n    @Autowired\n    private AccountService accountService;\n\n    @Test\n    public void loadRouteRelated() throws Exception {\n        for (int i = 0; i < 100; i++) {\n\n            Map<Long, CIMServerResVO> longCIMServerResVOMap = accountService.loadRouteRelated();\n            log.info(\"longCIMServerResVOMap={},cun={}\", JSON.toJSONString(longCIMServerResVOMap),i);\n        }\n    }\n\n}\n"
  },
  {
    "path": "cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/RedisTest.java",
    "content": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.crossoverjie.cim.route.RouteApplication;\nimport org.junit.jupiter.api.Assertions;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\nimport org.springframework.data.redis.core.RedisTemplate;\n\n\n@SpringBootTest(classes = RouteApplication.class)\npublic class RedisTest extends AbstractBaseTest {\n\n    @Autowired\n    private RedisTemplate<String,String> redisTemplate;\n\n    @Test\n    public void test() {\n        redisTemplate.opsForValue().set(\"test\",\"test\");\n        String test = redisTemplate.opsForValue().get(\"test\");\n        Assertions.assertEquals(\"test\",test);\n    }\n}\n"
  },
  {
    "path": "cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/UserInfoCacheServiceImplTest.java",
    "content": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.alibaba.fastjson.JSON;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.route.RouteApplication;\nimport com.crossoverjie.cim.route.service.UserInfoCacheService;\nimport java.util.Set;\nimport lombok.extern.slf4j.Slf4j;\nimport org.junit.jupiter.api.Test;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.boot.test.context.SpringBootTest;\n\n@Slf4j\n@SpringBootTest(classes = RouteApplication.class)\npublic class UserInfoCacheServiceImplTest extends AbstractBaseTest {\n\n    @Autowired\n    private UserInfoCacheService userInfoCacheService;\n\n    @Test\n    public void checkUserLoginStatus() throws Exception {\n        boolean status = userInfoCacheService.saveAndCheckUserLoginStatus(2000L);\n        log.info(\"status={}\", status);\n    }\n\n    @Test\n    public void onlineUser() {\n        Set<CIMUserInfo> cimUserInfos = userInfoCacheService.onlineUser();\n        log.info(\"cimUserInfos={}\", JSON.toJSONString(cimUserInfos));\n    }\n\n}\n"
  },
  {
    "path": "cim-forward-route/src/test/resources/application.yaml",
    "content": "spring:\n  application:\n    name: cim-forward-route\n  data:\n    redis:\n      host: 127.0.0.1\n      port: 6379\n      jedis:\n        pool:\n          max-active: 100\n          max-idle: 100\n          max-wait: 1000\n          min-idle: 10\n\n  datasource:\n    driver-class-name: com.mysql.cj.jdbc.Driver\n    url: jdbc:mysql://localhost:3306/cim-test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8\n    username: root\n    password: ${DB_PASSWORD}\n\n# web port\nserver:\n  port: 8083\n\nlogging:\n  level:\n    root: info\n\n  # enable swagger\nspringdoc:\n  swagger-ui:\n    enabled: true\n\napp:\n  zk:\n    addr: 127.0.0.1:2181\n    connect:\n      timeout: 30000\n    root: /route\n\n  # route strategy\n  #app.route.way=com.crossoverjie.cim.common.route.algorithm.loop.LoopHandle\n\n  # route strategy\n  #app.route.way=com.crossoverjie.cim.common.route.algorithm.random.RandomHandle\n\n  # route strategy\n  route:\n    way:\n      handler: com.crossoverjie.cim.common.route.algorithm.consistenthash.ConsistentHashHandle\n\n  #app.route.way.consitenthash=com.crossoverjie.cim.common.route.algorithm.consistenthash.SortArrayMapConsistentHash\n\n      consitenthash: com.crossoverjie.cim.common.route.algorithm.consistenthash.TreeMapConsistentHash\n\nmybatis:\n  configuration:\n    map-underscore-to-camel-case: true # 自动驼峰转换\n  type-aliases-package: com.crossoverjie.cim.persistence.api.pojo # 实体类包路径\n  mapper-locations: classpath*:mapper/**/*.xml\n  global-config:\n    db-config:\n      id-type: auto\n\noffline:\n  store:\n    mode: mysql"
  },
  {
    "path": "cim-forward-route/src/test/resources/init.sql",
    "content": "-- 创建表\nCREATE TABLE IF NOT EXISTS `offline_msg`\n(\n    `id`\n                   BIGINT\n        PRIMARY\n            KEY\n        AUTO_INCREMENT,\n    `message_id`\n                   BIGINT\n        NOT\n            NULL,\n    `receive_user_id`\n                   BIGINT\n        NOT\n            NULL,\n    `content`\n                   VARCHAR(2000),\n    `message_type` INT,\n    `status`       TINYINT COMMENT '0: Pending, 1: Acked',\n    `created_at`   DATETIME,\n    `properties`   VARCHAR(2000),\n    INDEX `idx_receive_user_id`\n        (\n         `receive_user_id`\n            )\n);\nCREATE TABLE offline_msg_last_send_record\n(\n    receive_user_id BIGINT NOT NULL PRIMARY KEY,\n    last_message_id BIGINT,\n    updated_at      DATETIME\n) ENGINE = InnoDB\n  DEFAULT CHARSET = utf8mb4;"
  },
  {
    "path": "cim-integration-test/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n  <modelVersion>4.0.0</modelVersion>\n  <parent>\n    <groupId>com.crossoverjie.netty</groupId>\n    <artifactId>cim</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n  </parent>\n\n  <artifactId>cim-integration-test</artifactId>\n\n  <properties>\n    <maven.compiler.source>17</maven.compiler.source>\n    <maven.compiler.target>17</maven.compiler.target>\n    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n  </properties>\n\n  <dependencies>\n    <dependency>\n      <groupId>com.crossoverjie.netty</groupId>\n      <artifactId>cim-server</artifactId>\n      <version>${project.version}</version>\n      <scope>compile</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>com.crossoverjie.netty</groupId>\n      <artifactId>cim-forward-route</artifactId>\n      <version>${project.version}</version>\n      <scope>compile</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.junit.vintage</groupId>\n      <artifactId>junit-vintage-engine</artifactId>\n      <scope>test</scope>\n    </dependency>\n\n\n    <dependency>\n      <groupId>org.junit.jupiter</groupId>\n      <artifactId>junit-jupiter</artifactId>\n      <scope>test</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>testcontainers</artifactId>\n\t\t\t<scope>compile</scope>\n    </dependency>\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>junit-jupiter</artifactId>\n    </dependency>\n\n    <dependency>\n      <groupId>org.springframework.boot</groupId>\n      <artifactId>spring-boot-starter-test</artifactId>\n      <scope>compile</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>com.clever-cloud</groupId>\n      <artifactId>testcontainers-zookeeper</artifactId>\n      <scope>compile</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>com.redis</groupId>\n      <artifactId>testcontainers-redis</artifactId>\n      <scope>compile</scope>\n    </dependency>\n\n    <dependency>\n      <groupId>org.testcontainers</groupId>\n      <artifactId>mysql</artifactId>\n      <scope>compile</scope>\n    </dependency>\n\n  </dependencies>\n</project>"
  },
  {
    "path": "cim-integration-test/src/main/java/com/crossoverjie/cim/client/sdk/route/AbstractRouteBaseTest.java",
    "content": "package com.crossoverjie.cim.client.sdk.route;\n\nimport com.crossoverjie.cim.common.res.BaseResponse;\nimport com.crossoverjie.cim.client.sdk.server.AbstractServerBaseTest;\nimport com.crossoverjie.cim.route.RouteApplication;\nimport com.crossoverjie.cim.route.api.RouteApi;\nimport com.crossoverjie.cim.route.api.vo.req.RegisterInfoReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.RegisterInfoResVO;\nimport com.redis.testcontainers.RedisContainer;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.utility.DockerImageName;\n\npublic abstract class AbstractRouteBaseTest extends AbstractServerBaseTest {\n\n    @Container\n    RedisContainer redis = new RedisContainer(DockerImageName.parse(\"redis:7.4.0\"));\n\n    protected ConfigurableApplicationContext run;\n    public void startRoute(String offlineModel) {\n        redis.start();\n        SpringApplication route = new SpringApplication(RouteApplication.class);\n        String[] args = new String[]{\n                \"--spring.data.redis.host=\" + redis.getHost(),\n                \"--spring.data.redis.port=\" + redis.getMappedPort(6379),\n                \"--app.zk.addr=\" + super.getZookeeperAddr(),\n                \"--offline.store.model=\" + offlineModel,\n        };\n        route.setAdditionalProfiles(\"route\");\n        run = route.run(args);\n    }\n\n    public void close() {\n        super.close();\n        redis.close();\n        run.close();\n    }\n\n    public Long registerAccount(String userName) throws Exception {\n        // register user\n        RouteApi routeApi = com.crossoverjie.cim.route.util.SpringBeanFactory.getBean(RouteApi.class);\n        RegisterInfoReqVO reqVO = new RegisterInfoReqVO();\n        reqVO.setUserName(userName);\n        BaseResponse<RegisterInfoResVO> account =\n                routeApi.registerAccount(reqVO);\n        return account.getDataBody().getUserId();\n    }\n}\n"
  },
  {
    "path": "cim-integration-test/src/main/java/com/crossoverjie/cim/client/sdk/route/OfflineMsgStoreRouteBaseTest.java",
    "content": "package com.crossoverjie.cim.client.sdk.route;\n\nimport com.crossoverjie.cim.route.RouteApplication;\nimport com.crossoverjie.cim.route.constant.Constant;\nimport org.springframework.boot.SpringApplication;\nimport org.testcontainers.containers.MySQLContainer;\nimport org.testcontainers.utility.DockerImageName;\nimport org.testcontainers.utility.MountableFile;\n\n/**\n * @author zhongcanyu\n * @date 2025/6/5\n * @description\n */\npublic class OfflineMsgStoreRouteBaseTest extends AbstractRouteBaseTest {\n\n    private MySQLContainer<?> mysql;\n\n    @Override\n    public void startRoute(String offlineModel) {\n        redis.start();\n        SpringApplication route = new SpringApplication(RouteApplication.class);\n        String[] args;\n        if (Constant.OfflineStoreMode.MYSQL.equals(offlineModel)) {\n            mysql = new MySQLContainer<>(DockerImageName.parse(\"mysql:8.0.33\"))\n                    .withDatabaseName(\"cim-test\")\n                    .withUsername(\"cimUserName\")\n                    .withPassword(\"cimPassWord\")\n                    .withCopyFileToContainer(\n                            MountableFile.forClasspathResource(\"init.sql\"),\n                            \"/docker-entrypoint-initdb.d/init.sql\"\n                    )\n                    .withExposedPorts(3306)\n                    .withReuse(true);\n            mysql.start();\n\n            args = new String[]{\n                    \"--spring.data.redis.host=\" + redis.getHost(),\n                    \"--spring.data.redis.port=\" + redis.getMappedPort(6379),\n                    \"--app.zk.addr=\" + super.getZookeeperAddr(),\n                    \"--offline.store.model=\" + offlineModel,\n                    \"--spring.datasource.url=\" + mysql.getJdbcUrl(),\n                    \"--spring.datasource.username=\" + mysql.getUsername(),\n                    \"--spring.datasource.password=\" + mysql.getPassword()\n            };\n        } else {\n            args = new String[]{\n                    \"--spring.data.redis.host=\" + redis.getHost(),\n                    \"--spring.data.redis.port=\" + redis.getMappedPort(6379),\n                    \"--app.zk.addr=\" + super.getZookeeperAddr(),\n                    \"--offline.store.model=\" + offlineModel,\n            };\n        }\n\n        route.setAdditionalProfiles(\"route\");\n        run = route.run(args);\n    }\n\n    @Override\n    public void close() {\n        if (mysql != null) {\n            mysql.stop();\n        }\n        super.close();\n    }\n}\n"
  },
  {
    "path": "cim-integration-test/src/main/java/com/crossoverjie/cim/client/sdk/server/AbstractServerBaseTest.java",
    "content": "package com.crossoverjie.cim.client.sdk.server;\n\nimport com.clevercloud.testcontainers.zookeeper.ZooKeeperContainer;\nimport com.crossoverjie.cim.server.CIMServerApplication;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.Map;\nimport lombok.Getter;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.testcontainers.junit.jupiter.Container;\nimport org.testcontainers.utility.DockerImageName;\n\npublic abstract class AbstractServerBaseTest {\n    private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName\n            .parse(\"zookeeper\")\n            .withTag(\"3.9.2\");\n\n    private static final Duration DEFAULT_STARTUP_TIMEOUT = Duration.ofSeconds(60);\n\n    @Container\n    public final ZooKeeperContainer\n            zooKeeperContainer = new ZooKeeperContainer(DEFAULT_IMAGE_NAME, DEFAULT_STARTUP_TIMEOUT);\n\n    @Getter\n    private String zookeeperAddr;\n\n    private ConfigurableApplicationContext singleRun;\n    public void starSingleServer() {\n        zooKeeperContainer.start();\n        zookeeperAddr = String.format(\"%s:%d\", zooKeeperContainer.getHost(), zooKeeperContainer.getMappedPort(ZooKeeperContainer.DEFAULT_CLIENT_PORT));\n        SpringApplication server = new SpringApplication(CIMServerApplication.class);\n        String[] args = new String[]{\n                \"--app.zk.addr=\" + zookeeperAddr,\n                \"--spring.autoconfigure.exclude=\" + DataSourceAutoConfiguration.class.getName()\n        };\n        singleRun = server.run(args);\n    }\n    public void stopSingle() {\n        singleRun.close();\n    }\n\n    private final Map<Integer, ConfigurableApplicationContext> runMap = new HashMap<>(2);\n    public void startTwoServer() {\n        if (!zooKeeperContainer.isRunning()) {\n            zooKeeperContainer.start();\n        }\n        zookeeperAddr = String.format(\"%s:%d\", zooKeeperContainer.getHost(), zooKeeperContainer.getMappedPort(ZooKeeperContainer.DEFAULT_CLIENT_PORT));\n        SpringApplication server = new SpringApplication(CIMServerApplication.class);\n        String[] args1 = new String[]{\n                \"--cim.server.port=11211\",\n                \"--server.port=8081\",\n                \"--app.zk.addr=\" + zookeeperAddr,\n                \"--spring.autoconfigure.exclude=\" + DataSourceAutoConfiguration.class.getName()\n        };\n        ConfigurableApplicationContext run1 = server.run(args1);\n        runMap.put(Integer.parseInt(\"11211\"), run1);\n\n\n        SpringApplication server2 = new SpringApplication(CIMServerApplication.class);\n        String[] args2 = new String[]{\n                \"--cim.server.port=11212\",\n                \"--server.port=8082\",\n                \"--app.zk.addr=\" + zookeeperAddr,\n                \"--spring.autoconfigure.exclude=\" + DataSourceAutoConfiguration.class.getName()\n        };\n        ConfigurableApplicationContext run2 = server2.run(args2);\n        runMap.put(Integer.parseInt(\"11212\"), run2);\n    }\n\n    public void stopServer(Integer port) {\n        runMap.get(port).close();\n        runMap.remove(port);\n    }\n    public void stopTwoServer() {\n        runMap.forEach((k, v) -> v.close());\n    }\n\n    public void close() {\n        zooKeeperContainer.close();\n    }\n\n}\n"
  },
  {
    "path": "cim-integration-test/src/test/resources/application-client.yaml",
    "content": "spring:\n  application:\n    name: cim-client\n\n# web port\nserver:\n  port: 8082\n\nlogging:\n  level:\n    root: error\n\n# enable swagger\nspringdoc:\n  swagger-ui:\n    enabled: true\n\n# log path\ncim:\n  msg:\n    logger:\n      path: /opt/logs/cim/\n  route:\n    url: http://localhost:8083 # route url suggested that this is Nginx address\n  user: # cim userId and userName\n    id: 1722343979085\n    userName: zhangsan\n  callback:\n    thread:\n      queue:\n        size: 2\n      pool:\n        size: 2\n  heartbeat:\n    time: 60 # cim heartbeat time (seconds)\n  reconnect:\n    count: 3"
  },
  {
    "path": "cim-integration-test/src/test/resources/application-route.yaml",
    "content": "spring:\n  application:\n    name:\n      cim-forward-route\n  data:\n    redis:\n      host: 127.0.0.1\n      port: 6379\n      jedis:\n        pool:\n          max-active: 100\n          max-idle: 100\n          max-wait: 1000\n          min-idle: 10\n\n  datasource:\n    driver-class-name: com.mysql.cj.jdbc.Driver\n    url: jdbc:mysql://localhost:3306/cim-test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8\n    username: root\n    password: ${DB_PASSWORD}\n# web port\nserver:\n  port: 8083\n\nlogging:\n  level:\n    root: info\n\n  # enable swagger\nspringdoc:\n  swagger-ui:\n    enabled: true\n\napp:\n  zk:\n    connect:\n      timeout: 30000\n    root: /route\n\n  # route strategy\n  #app.route.way=com.crossoverjie.cim.common.route.algorithm.loop.LoopHandle\n\n  # route strategy\n  #app.route.way=com.crossoverjie.cim.common.route.algorithm.random.RandomHandle\n\n  # route strategy\n  route:\n    way:\n      handler: com.crossoverjie.cim.common.route.algorithm.consistenthash.ConsistentHashHandle\n\n  #app.route.way.consitenthash=com.crossoverjie.cim.common.route.algorithm.consistenthash.SortArrayMapConsistentHash\n\n      consitenthash: com.crossoverjie.cim.common.route.algorithm.consistenthash.TreeMapConsistentHash\n\n\n"
  },
  {
    "path": "cim-persistence/cim-persistence-api/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.crossoverjie.netty</groupId>\n        <artifactId>cim-persistence</artifactId>\n        <version>1.0.0-SNAPSHOT</version>\n    </parent>\n    <groupId>com.crossoverjie.netty</groupId>\n    <artifactId>cim-persistence-api</artifactId>\n\n    <properties>\n        <java.version>17</java.version>\n    </properties>\n    <dependencies>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-data-redis</artifactId>\n        </dependency>\n\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <skip>true</skip>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/config/BeanConfig.java",
    "content": "package com.crossoverjie.cim.persistence.api.config;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.data.redis.connection.RedisConnectionFactory;\nimport org.springframework.data.redis.core.RedisTemplate;\nimport org.springframework.data.redis.hash.Jackson2HashMapper;\nimport org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;\nimport org.springframework.data.redis.serializer.StringRedisSerializer;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/19\n * @description\n */\n@Configuration(\"persistenceBeanConfig\")\n@Slf4j\npublic class BeanConfig {\n\n    /**\n     * Redis bean\n     *\n     * @param factory\n     * @return\n     */\n    @Bean\n    public RedisTemplate<String, Object> stringObjectRedisTemplate(RedisConnectionFactory factory) {\n        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();\n        redisTemplate.setConnectionFactory(factory);\n        redisTemplate.setKeySerializer(new StringRedisSerializer());\n        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());\n        redisTemplate.setHashKeySerializer(new StringRedisSerializer());\n        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());\n        redisTemplate.afterPropertiesSet();\n        return redisTemplate;\n    }\n\n    @Bean\n    public Jackson2HashMapper hashMapper(ObjectMapper objectMapper) {\n        return new Jackson2HashMapper(objectMapper, false);\n    }\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/pojo/OfflineMsg.java",
    "content": "package com.crossoverjie.cim.persistence.api.pojo;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport java.time.LocalDateTime;\nimport java.util.Map;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/18\n * @description\n */\n@Data\n@AllArgsConstructor\n@NoArgsConstructor\n@Builder\npublic class OfflineMsg {\n    private Long messageId;\n    private Long receiveUserId;\n    private String content;\n    private Integer messageType;   // 0: Text, 1: Image\n    private Integer status;        // 0: Pending, 1: Acked\n    private LocalDateTime createdAt;\n    /**\n     * 消息来源存储在这里\n     */\n    private Map<String, String> properties;\n\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/pojo/OfflineMsgLastSendRecord.java",
    "content": "package com.crossoverjie.cim.persistence.api.pojo;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\nimport java.time.LocalDateTime;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/18\n * @description\n */\n@Data\n@AllArgsConstructor\n@NoArgsConstructor\npublic class OfflineMsgLastSendRecord {\n    private Long userId;\n    private String lastMessageId;\n    private LocalDateTime updatedAt;\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/service/OfflineMsgStore.java",
    "content": "package com.crossoverjie.cim.persistence.api.service;\n\n\nimport com.crossoverjie.cim.persistence.api.pojo.OfflineMsg;\n\nimport java.util.List;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/18\n * @description\n */\npublic interface OfflineMsgStore {\n\n    /**\n     * Save offline message\n     *\n     * @param offlineMsg\n     */\n    void save(OfflineMsg offlineMsg);\n\n    /**\n     * Fetch offline messages for a user\n     *\n     * @param userId\n     * @return\n     */\n    List<OfflineMsg> fetch(Long userId);\n\n    /**\n     * Mark messages as delivered\n     *\n     * @param userId\n     * @param messageIds\n     */\n    void markDelivered(Long userId, List<Long> messageIds);\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/vo/req/SaveOfflineMsgReqVO.java",
    "content": "package com.crossoverjie.cim.persistence.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport jakarta.validation.constraints.NotNull;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport java.util.Map;\n\n@Data\n@EqualsAndHashCode(callSuper = true)\n@Builder\n@AllArgsConstructor\npublic class SaveOfflineMsgReqVO extends BaseRequest {\n\n    @NotNull(message = \"msg 不能为空\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"msg\", example = \"hello\")\n    private String msg;\n\n    @NotNull(message = \"userId 不能为空\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"userId\", example = \"11\")\n    private Long receiveUserId;\n\n    @Setter\n    @Getter\n    private Map<String, String> properties;\n\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.crossoverjie.netty</groupId>\n        <artifactId>cim-persistence</artifactId>\n        <version>1.0.0-SNAPSHOT</version>\n    </parent>\n    <groupId>com.crossoverjie.netty</groupId>\n    <artifactId>cim-persistence-mysql</artifactId>\n\n    <properties>\n        <java.version>17</java.version>\n    </properties>\n    <dependencies>\n\n        <dependency>\n            <groupId>org.mybatis.spring.boot</groupId>\n            <artifactId>mybatis-spring-boot-starter</artifactId>\n            <version>3.0.3</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.mysql</groupId>\n            <artifactId>mysql-connector-j</artifactId>\n            <version>8.2.0</version>\n        </dependency>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-persistence-api</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <skip>true</skip>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/config/MyBatisConfig.java",
    "content": "package com.crossoverjie.cim.persistence.mysql.config;\n\nimport com.crossoverjie.cim.persistence.mysql.util.MapToJsonTypeHandler;\nimport org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/18\n * @description\n */\n@Configuration\npublic class MyBatisConfig {\n    @Bean\n    public ConfigurationCustomizer configurationCustomizer() {\n        return configuration -> {\n            configuration.getTypeHandlerRegistry().register(MapToJsonTypeHandler.class);\n        };\n    }\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/offlinemsg/OfflineMsgDb.java",
    "content": "package com.crossoverjie.cim.persistence.mysql.offlinemsg;\n\nimport com.crossoverjie.cim.persistence.api.pojo.OfflineMsg;\nimport com.crossoverjie.cim.persistence.api.service.OfflineMsgStore;\nimport com.crossoverjie.cim.persistence.mysql.offlinemsg.mapper.OfflineMsgLastSendRecordMapper;\nimport com.crossoverjie.cim.persistence.mysql.offlinemsg.mapper.OfflineMsgMapper;\n\nimport java.util.List;\n\nimport static com.crossoverjie.cim.common.constant.Constants.FETCH_OFFLINE_MSG_LIMIT;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/18\n * @description\n */\npublic class OfflineMsgDb implements OfflineMsgStore {\n\n    private final OfflineMsgMapper offlineMsgMapper;\n    private final OfflineMsgLastSendRecordMapper offlineMsgLastSendRecordMapper;\n\n    public OfflineMsgDb(OfflineMsgMapper offlineMsgMapper, OfflineMsgLastSendRecordMapper offlineMsgLastSendRecordMapper) {\n        this.offlineMsgMapper = offlineMsgMapper;\n        this.offlineMsgLastSendRecordMapper = offlineMsgLastSendRecordMapper;\n    }\n\n    @Override\n    public void save(OfflineMsg offlineMsg) {\n        offlineMsgMapper.insert(offlineMsg);\n    }\n\n    @Override\n    public List<OfflineMsg> fetch(Long receiveUserId) {\n        return offlineMsgMapper.fetchOfflineMsgsWithCursor(receiveUserId, FETCH_OFFLINE_MSG_LIMIT);\n    }\n\n    @Override\n    public void markDelivered(Long receiveUserId, List<Long> messageIds) {\n        offlineMsgMapper.updateStatus(receiveUserId, messageIds);\n        offlineMsgLastSendRecordMapper.saveLatestMessageId(receiveUserId, messageIds.get(messageIds.size() - 1));\n    }\n}\n\n"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/offlinemsg/mapper/OfflineMsgLastSendRecordMapper.java",
    "content": "package com.crossoverjie.cim.persistence.mysql.offlinemsg.mapper;\n\nimport org.apache.ibatis.annotations.Mapper;\nimport org.apache.ibatis.annotations.Param;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/10\n * @description\n */\n@Mapper\npublic interface OfflineMsgLastSendRecordMapper {\n\n    void saveLatestMessageId(@Param(\"receiveUserId\") Long receiveUserId, @Param(\"lastMessageId\") Long lastMessageId);\n\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/offlinemsg/mapper/OfflineMsgMapper.java",
    "content": "package com.crossoverjie.cim.persistence.mysql.offlinemsg.mapper;\n\n\n\nimport com.crossoverjie.cim.persistence.api.pojo.OfflineMsg;\nimport org.apache.ibatis.annotations.Mapper;\nimport org.apache.ibatis.annotations.Param;\n\nimport java.util.List;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/9\n * @description\n */\n@Mapper\npublic interface OfflineMsgMapper {\n\n    int insert(OfflineMsg msg);\n\n    int insertBatch(@Param(\"offlineMsgs\") List<OfflineMsg> offlineMsgs);\n\n    List<OfflineMsg> fetchOfflineMsgsWithCursor(@Param(\"receiveUserId\") Long receiveUserId,  @Param(\"limit\") Integer limit);\n\n    int updateStatus(\n            @Param(\"receiveUserId\") Long receiveUserId,\n            @Param(\"messageIds\") List<Long> messageIds);\n\n    List<Long> fetchOfflineMsgIdsWithCursor(@Param(\"receiveUserId\") Long receiveUserId);\n\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/util/MapToJsonTypeHandler.java",
    "content": "package com.crossoverjie.cim.persistence.mysql.util;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.apache.ibatis.type.BaseTypeHandler;\nimport org.apache.ibatis.type.JdbcType;\n\nimport java.sql.CallableStatement;\nimport java.sql.PreparedStatement;\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.Map;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/18\n * @description\n */\npublic class MapToJsonTypeHandler extends BaseTypeHandler<Map<String, String>> {\n    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();\n\n    @Override\n    public void setNonNullParameter(PreparedStatement ps, int i, Map<String, String> parameter, JdbcType jdbcType) throws SQLException {\n        try {\n            String json = OBJECT_MAPPER.writeValueAsString(parameter);\n            ps.setString(i, json);\n        } catch (Exception e) {\n            throw new SQLException(\"Failed to convert Map to JSON\", e);\n        }\n    }\n\n    @Override\n    public Map<String, String> getNullableResult(ResultSet rs, String columnName) throws SQLException {\n        return parseJson(rs.getString(columnName));\n    }\n\n    @Override\n    public Map<String, String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {\n        return parseJson(rs.getString(columnIndex));\n    }\n\n    @Override\n    public Map<String, String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {\n        return parseJson(cs.getString(columnIndex));\n    }\n\n    private Map<String, String> parseJson(String json) {\n        try {\n            return OBJECT_MAPPER.readValue(json, new TypeReference<Map<String, String>>() { });\n        } catch (Exception e) {\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/resources/mapper/OfflineMsgLastSendRecordMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\" >\n<mapper namespace=\"com.crossoverjie.cim.persistence.mysql.offlinemsg.mapper.OfflineMsgLastSendRecordMapper\">\n\n    <insert id=\"saveLatestMessageId\">\n        INSERT INTO offline_msg_last_send_record\n            (receive_user_id, last_message_id, updated_at)\n        VALUES\n            (#{receiveUserId}, #{lastMessageId}, NOW())\n            ON DUPLICATE KEY UPDATE\n                                 last_message_id = #{lastMessageId},\n                                 updated_at = NOW()\n    </insert>\n</mapper>\n\n"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/resources/mapper/OfflineMsgMapper.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\" >\n<mapper namespace=\"com.crossoverjie.cim.persistence.mysql.offlinemsg.mapper.OfflineMsgMapper\">\n\n    <insert id=\"insert\" parameterType=\"com.crossoverjie.cim.persistence.api.pojo.OfflineMsg\">\n        INSERT INTO offline_msg (message_id,\n                                 receive_user_id,\n                                 content,\n                                 message_type,\n                                 status,\n                                 created_at,\n                                 properties)\n        VALUES (#{messageId},\n                #{receiveUserId},\n                #{content},\n                #{messageType},\n                #{status},\n                #{createdAt},\n                #{properties, typeHandler=com.crossoverjie.cim.persistence.mysql.util.MapToJsonTypeHandler})\n    </insert>\n\n    <insert id=\"insertBatch\" parameterType=\"java.util.List\">\n        INSERT INTO offline_msg (\n        message_id,\n        receive_user_id,\n        content,\n        message_type,\n        status,\n        created_at,\n        properties\n        ) VALUES\n        <foreach collection=\"offlineMsgs\" item=\"item\" separator=\",\">\n            (\n            #{item.messageId},\n            #{item.receiveUserId},\n            #{item.content},\n            #{item.messageType},\n            #{item.status},\n            #{item.createdAt},\n            #{item.properties, typeHandler=com.crossoverjie.cim.persistence.mysql.util.MapToJsonTypeHandler}\n            )\n        </foreach>\n    </insert>\n\n    <select id=\"fetchOfflineMsgsWithCursor\" resultType=\"com.crossoverjie.cim.persistence.api.pojo.OfflineMsg\">\n        SELECT\n        message_id,\n        receive_user_id,\n        content,\n        message_type,\n        status,\n        created_at,\n        properties\n        FROM offline_msg\n        WHERE receive_user_id = #{receiveUserId} and status=0\n        AND message_id &gt; COALESCE((select last_message_id from offline_msg_last_send_record where receive_user_id = #{receiveUserId}),0)\n        ORDER BY message_id ASC\n        LIMIT #{limit}\n    </select>\n\n    <select id=\"fetchOfflineMsgIdsWithCursor\" resultType=\"java.lang.Long\" parameterType=\"java.lang.Long\">\n        SELECT\n        message_id\n        FROM offline_msg\n        WHERE receive_user_id = #{receiveUserId}\n        AND message_id &gt; (select last_message_id from offline_msg_last_send_record where receive_user_id = #{receiveUserId})\n        ORDER BY message_id ASC\n    </select>\n\n    <update id=\"updateStatus\" parameterType=\"map\">\n        UPDATE offline_msg\n        SET status = 1\n        WHERE receive_user_id = #{receiveUserId}\n        <if test=\"messageIds != null and !messageIds.isEmpty()\">\n            AND message_id IN\n            <foreach item=\"id\" collection=\"messageIds\" open=\"(\" separator=\",\" close=\")\">\n                #{id}\n            </foreach>\n        </if>\n    </update>\n</mapper>\n\n"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.crossoverjie.netty</groupId>\n        <artifactId>cim-persistence</artifactId>\n        <version>1.0.0-SNAPSHOT</version>\n    </parent>\n    <groupId>com.crossoverjie.netty</groupId>\n    <artifactId>cim-persistence-redis</artifactId>\n\n    <properties>\n        <java.version>17</java.version>\n    </properties>\n    <dependencies>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-persistence-api</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n    </dependencies>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.springframework.boot</groupId>\n                <artifactId>spring-boot-maven-plugin</artifactId>\n                <configuration>\n                    <skip>true</skip>\n                </configuration>\n            </plugin>\n        </plugins>\n    </build>\n\n</project>\n"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/java/com/crossoverjie/cim/persistence/redis/OfflineMsgBuffer.java",
    "content": "package com.crossoverjie.cim.persistence.redis;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossoverjie.cim.persistence.api.pojo.OfflineMsg;\nimport com.crossoverjie.cim.persistence.api.service.OfflineMsgStore;\nimport com.crossoverjie.cim.persistence.redis.kit.OfflineMsgScriptExecutor;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport io.lettuce.core.RedisException;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.data.redis.serializer.SerializationException;\nimport org.springframework.util.CollectionUtils;\n\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport static com.crossoverjie.cim.persistence.redis.constant.Constant.FETCH_OFFLINE_MSG_SIZE;\nimport static com.crossoverjie.cim.persistence.redis.constant.Constant.OFFLINE_MSG_TTL_DAYS;\n\n\n/**\n * @author zhongcanyu\n * @date 2025/5/18\n * @description\n */\n@Slf4j\npublic class OfflineMsgBuffer implements OfflineMsgStore {\n\n    private final int messageTtlDays;\n    private final OfflineMsgScriptExecutor scriptExecutor;\n    private final ObjectMapper objectMapper;\n\n    public OfflineMsgBuffer(OfflineMsgScriptExecutor scriptExecutor, Integer configuredDays, ObjectMapper objectMapper) {\n        this.messageTtlDays = ensureValidTtlOrDefault(configuredDays);\n        this.scriptExecutor = scriptExecutor;\n        this.objectMapper = objectMapper;\n    }\n\n    private int ensureValidTtlOrDefault(Integer configuredDays) {\n        return (configuredDays != null && configuredDays > 0) ? configuredDays : OFFLINE_MSG_TTL_DAYS;\n    }\n\n    @Override\n    public void save(OfflineMsg msg) {\n        try {\n            scriptExecutor.saveOfflineMsg(msg, messageTtlDays);\n        } catch (SerializationException | RedisException e) {\n            log.error(\"Failed to save offline message\", e);\n            throw new CIMException(StatusEnum.OFFLINE_MESSAGE_STORAGE_ERROR);\n        }\n    }\n\n    @Override\n    public List<OfflineMsg> fetch(Long userId) {\n        try {\n            List<String> jsonResult = scriptExecutor.fetchOfflineMsgs(userId, FETCH_OFFLINE_MSG_SIZE);\n            List<OfflineMsg> offlineMsgs = jsonResult.stream()\n                    .map(json -> {\n                        try {\n                            return objectMapper.readValue(json, OfflineMsg.class);\n                        } catch (IOException e) {\n                            throw new UncheckedIOException(e);\n                        }\n                    })\n                    .collect(Collectors.toList());\n            return offlineMsgs;\n        } catch (UncheckedIOException | SerializationException | RedisException e) {\n            log.error(\"Failed to fetch offline messages for userId: {}\", userId, e);\n            throw new CIMException(StatusEnum.OFFLINE_MESSAGE_FETCH_ERROR);\n        }\n    }\n\n    @Override\n    public void markDelivered(Long userId, List<Long> messageIds) {\n        if (CollectionUtils.isEmpty(messageIds)) {\n            return;\n        }\n        try {\n            scriptExecutor.deleteOfflineMsg(userId, messageIds);\n        } catch (RedisException e) {\n            log.error(\"Failed to delete offline messages for userId: {}\", userId, e);\n            throw new CIMException(StatusEnum.OFFLINE_MESSAGE_DELETE_ERROR);\n        }\n    }\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/java/com/crossoverjie/cim/persistence/redis/constant/Constant.java",
    "content": "package com.crossoverjie.cim.persistence.redis.constant;\n\n/**\n * @author zhongcanyu\n * @date 2025/6/14\n * @description\n */\npublic final class Constant {\n\n    /**\n     * The number of messages captured from redis each time\n     */\n    public static final Integer FETCH_OFFLINE_MSG_SIZE = 100;\n\n    /**\n     * Default expire time for offline message\n     */\n    public static final Integer OFFLINE_MSG_TTL_DAYS = 7;\n\n    /**\n     * Redis key prefix for offline message\n     */\n    public static final String MSG_KEY = \"offline:msg:\";\n\n    /**\n     * Redis key prefix for offline message index\n     */\n    public static final String USER_IDX = \"offline:msg:user:\";\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/java/com/crossoverjie/cim/persistence/redis/kit/OfflineMsgScriptExecutor.java",
    "content": "package com.crossoverjie.cim.persistence.redis.kit;\n\nimport com.crossoverjie.cim.persistence.api.pojo.OfflineMsg;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.springframework.core.io.ClassPathResource;\nimport org.springframework.data.redis.core.RedisTemplate;\nimport org.springframework.data.redis.core.script.DefaultRedisScript;\nimport org.springframework.data.redis.serializer.SerializationException;\nimport org.springframework.stereotype.Component;\n\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static com.crossoverjie.cim.persistence.redis.constant.Constant.MSG_KEY;\nimport static com.crossoverjie.cim.persistence.redis.constant.Constant.USER_IDX;\n\n/**\n * @author zhongcanyu\n * @date 2025/6/13\n * @description\n */\n@Component\npublic class OfflineMsgScriptExecutor {\n\n    private final RedisTemplate<String, Object> redisTemplate;\n    private final ObjectMapper objectMapper;\n\n    public OfflineMsgScriptExecutor(RedisTemplate<String, Object> redisTemplate,\n                                    ObjectMapper objectMapper) {\n        this.redisTemplate = redisTemplate;\n        this.objectMapper = objectMapper;\n    }\n\n    private static final DefaultRedisScript<Long> SAVE_OFFLINE_MSG_SCRIPT;\n    private static final DefaultRedisScript<List> FETCH_OFFLINE_MSG_SCRIPT;\n    private static final DefaultRedisScript<Long> DELETE_OFFLINE_MSG_SCRIPT;\n\n    static {\n        SAVE_OFFLINE_MSG_SCRIPT = new DefaultRedisScript<>();\n        SAVE_OFFLINE_MSG_SCRIPT.setLocation(new ClassPathResource(\"lua/saveOfflineMsg.lua\"));\n        SAVE_OFFLINE_MSG_SCRIPT.setResultType(Long.class);\n\n        FETCH_OFFLINE_MSG_SCRIPT = new DefaultRedisScript<>();\n        FETCH_OFFLINE_MSG_SCRIPT.setLocation(new ClassPathResource(\"lua/fetchOfflineMsg.lua\"));\n        FETCH_OFFLINE_MSG_SCRIPT.setResultType(List.class);\n\n        DELETE_OFFLINE_MSG_SCRIPT = new DefaultRedisScript<>();\n        DELETE_OFFLINE_MSG_SCRIPT.setLocation(new ClassPathResource(\"lua/deleteOfflineMsg.lua\"));\n        DELETE_OFFLINE_MSG_SCRIPT.setResultType(Long.class);\n    }\n\n    public Long saveOfflineMsg(OfflineMsg msg, Integer messageTtlDays) {\n        List<String> keys = Arrays.asList(MSG_KEY, USER_IDX);\n        List<Object> allArgs = new ArrayList<>();\n        allArgs.add(msg.getMessageId());\n        allArgs.add(msg.getReceiveUserId());\n        allArgs.add(serialize(msg));\n        allArgs.add(Duration.ofDays(messageTtlDays).getSeconds());\n\n        return redisTemplate.execute(SAVE_OFFLINE_MSG_SCRIPT, keys, allArgs.toArray());\n    }\n\n    private String serialize(OfflineMsg msg) {\n        try {\n            return objectMapper.writeValueAsString(msg);\n        } catch (JsonProcessingException e) {\n            throw new SerializationException(\"OfflineMsg serialization failed\", e);\n        }\n    }\n\n\n    public List<String> fetchOfflineMsgs(Long userId, Integer size) {\n        List<String> keys = Arrays.asList(MSG_KEY, USER_IDX);\n        List<Object> allArgs = new ArrayList<>();\n        allArgs.add(userId);\n        allArgs.add(size);\n\n        return (List<String>) redisTemplate.execute(FETCH_OFFLINE_MSG_SCRIPT, keys, allArgs.toArray());\n    }\n\n    public Long deleteOfflineMsg(Long userId, List<Long> msgIds) {\n        List<String> keys = Arrays.asList(MSG_KEY, USER_IDX);\n        List<Object> allArgs = new ArrayList<>();\n        allArgs.add(userId);\n        allArgs.addAll(msgIds);\n\n        return redisTemplate.execute(DELETE_OFFLINE_MSG_SCRIPT, keys, allArgs.toArray());\n    }\n}\n"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/resources/lua/deleteOfflineMsg.lua",
    "content": "local msgPrefix = KEYS[1]\nlocal userIdxPrefix = KEYS[2]\nlocal userId = ARGV[1]\nlocal userListKey = userIdxPrefix .. userId\n\nfor i = 2, #ARGV do\n    local msgKey = msgPrefix .. ARGV[i]\n    redis.call(\"DEL\", msgKey)\n    redis.call(\"LREM\", userListKey, 0, ARGV[i])\nend"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/resources/lua/fetchOfflineMsg.lua",
    "content": "local userId = ARGV[1]\nlocal rangeSize = tonumber(ARGV[2])\nlocal msgPrefix = KEYS[1]\nlocal userIdxPrefix = KEYS[2]\nlocal userListKey = userIdxPrefix .. userId\nlocal ids = redis.call('LRANGE', userListKey, 0, rangeSize - 1)\nlocal result = {}\n\nfor i, id in ipairs(ids) do\n  local msgKey = msgPrefix .. id\n  local serializedMsg = redis.call('GET', msgKey)\n\n  if serializedMsg then\n    table.insert(result, serializedMsg)\n  end\nend\nreturn result"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/resources/lua/saveOfflineMsg.lua",
    "content": "local msgPrefix = KEYS[1]\nlocal userIdxPrefix = KEYS[2]\nlocal msgId = ARGV[1]\nlocal receiveUserId = ARGV[2]\nlocal msgValue = ARGV[3]\nlocal ttlSeconds = tonumber(ARGV[4])\nlocal msgKey = msgPrefix .. msgId\nlocal userListKey = userIdxPrefix .. receiveUserId\n\nredis.call(\"SET\", msgKey, msgValue)\nif ttlSeconds and ttlSeconds > 0 then\n    redis.call(\"EXPIRE\", msgKey, ttlSeconds)\nend\nredis.call(\"RPUSH\", userListKey, msgId)\nif ttlSeconds and ttlSeconds > 0 then\n    redis.call(\"EXPIRE\", userListKey, ttlSeconds)\nend\n\nreturn 1\n"
  },
  {
    "path": "cim-persistence/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\">\n\t<modelVersion>4.0.0</modelVersion>\n\t<parent>\n\t\t<artifactId>cim</artifactId>\n\t\t<groupId>com.crossoverjie.netty</groupId>\n\t\t<version>1.0.0-SNAPSHOT</version>\n\t</parent>\n\t<groupId>com.crossoverjie.netty</groupId>\n\t<artifactId>cim-persistence</artifactId>\n\n\t<packaging>pom</packaging>\n\n\t<modules>\n\t\t<module>cim-persistence-api</module>\n\t\t<module>cim-persistence-mysql</module>\n\t\t<module>cim-persistence-redis</module>\n\t</modules>\n\n\t<properties>\n\t\t<java.version>17</java.version>\n\t</properties>\n\t<dependencies>\n\n\t\t<dependency>\n\t\t\t<groupId>com.crossoverjie.netty</groupId>\n\t\t\t<artifactId>cim-common</artifactId>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter</artifactId>\n\t\t</dependency>\n\n\t\t<dependency>\n\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t<artifactId>spring-boot-starter-test</artifactId>\n\t\t\t<scope>test</scope>\n\t\t</dependency>\n\t</dependencies>\n\n\t<build>\n\t\t<plugins>\n\t\t\t<plugin>\n\t\t\t\t<groupId>org.springframework.boot</groupId>\n\t\t\t\t<artifactId>spring-boot-maven-plugin</artifactId>\n\t\t\t</plugin>\n\t\t</plugins>\n\t</build>\n\n</project>\n"
  },
  {
    "path": "cim-rout-api/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>cim</artifactId>\n        <groupId>com.crossoverjie.netty</groupId>\n        <version>1.0.0-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>cim-rout-api</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n\n\n    <dependencies>\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-common</artifactId>\n            <exclusions>\n                <exclusion>\n                    <artifactId>log4j</artifactId>\n                    <groupId>log4j</groupId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>jakarta.validation</groupId>\n            <artifactId>jakarta.validation-api</artifactId>\n        </dependency>\n    </dependencies>\n</project>"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/RouteApi.java",
    "content": "package com.crossoverjie.cim.route.api;\n\nimport com.crossoverjie.cim.common.core.proxy.Request;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.common.res.BaseResponse;\nimport com.crossoverjie.cim.common.res.NULLBody;\nimport com.crossoverjie.cim.route.api.vo.req.ChatReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.LoginReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.OfflineMsgReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport com.crossoverjie.cim.route.api.vo.req.RegisterInfoReqVO;\nimport com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;\nimport com.crossoverjie.cim.route.api.vo.res.RegisterInfoResVO;\n\nimport java.util.Set;\n\n/**\n * Function: Route Api\n *\n * @author crossoverJie\n * Date: 2020-04-24 23:43\n * @since JDK 1.8\n */\npublic interface RouteApi {\n\n    /**\n     * group chat\n     *\n     * @param groupReqVO\n     * @return\n     * @throws Exception\n     */\n    BaseResponse<NULLBody> groupRoute(ChatReqVO groupReqVO);\n\n    /**\n     * Point to point chat\n     * @param p2pRequest\n     * @return\n     * @throws Exception\n     */\n    BaseResponse<NULLBody> p2pRoute(P2PReqVO p2pRequest);\n\n\n    /**\n     * Offline account\n     *\n     * @param groupReqVO\n     * @return\n     * @throws Exception\n     */\n    BaseResponse<NULLBody> offLine(ChatReqVO groupReqVO);\n\n    /**\n     * Login account\n     * @param loginReqVO\n     * @return\n     * @throws Exception\n     */\n    BaseResponse<CIMServerResVO> login(LoginReqVO loginReqVO) throws Exception;\n\n    /**\n     * Register account\n     *\n     * @param registerInfoReqVO\n     * @return\n     * @throws Exception\n     */\n    BaseResponse<RegisterInfoResVO> registerAccount(RegisterInfoReqVO registerInfoReqVO) throws Exception;\n\n    /**\n     * Get all online users\n     *\n     * @return\n     * @throws Exception\n     */\n    @Request(method = Request.GET)\n    BaseResponse<Set<CIMUserInfo>> onlineUser() throws Exception;\n\n\n    BaseResponse<NULLBody> fetchOfflineMsgs(OfflineMsgReqVO offlineMsgReqVO);\n    // TODO: 2024/8/19  Get cache server & metastore server\n}\n"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/ChatReqVO.java",
    "content": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport jakarta.validation.constraints.NotNull;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.EqualsAndHashCode;\nimport lombok.NoArgsConstructor;\n\nimport java.util.List;\n\n/**\n * Function: Google Protocol 编解码发送\n *\n * @author crossoverJie\n *         Date: 2018/05/21 15:56\n * @since JDK 1.8\n */\n@EqualsAndHashCode(callSuper = true)\n@Data\n@AllArgsConstructor\n@NoArgsConstructor\npublic class ChatReqVO extends BaseRequest {\n\n    @NotNull(message = \"userId 不能为空\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"userId\", example = \"1545574049323\")\n    private Long userId;\n\n\n    @NotNull(message = \"msg 不能为空\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"msg\", example = \"hello\")\n    private String msg;\n\n    private List<String> batchMsg;\n\n\n    @Override\n    public String toString() {\n        return \"GroupReqVO{\" +\n                \"userId=\" + userId +\n                \", msg='\" + msg + '\\'' +\n                \"} \" + super.toString();\n    }\n}\n"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/LoginReqVO.java",
    "content": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\nimport lombok.AllArgsConstructor;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/12/23 22:30\n * @since JDK 1.8\n */\n@AllArgsConstructor\npublic class LoginReqVO extends BaseRequest {\n    private Long userId;\n    private String userName;\n\n    public Long getUserId() {\n        return userId;\n    }\n\n    public void setUserId(Long userId) {\n        this.userId = userId;\n    }\n\n    public String getUserName() {\n        return userName;\n    }\n\n    public void setUserName(String userName) {\n        this.userName = userName;\n    }\n\n    @Override\n    public String toString() {\n        return \"LoginReqVO{\" +\n                \"userId=\" + userId +\n                \", userName='\" + userName + '\\'' +\n                \"} \" + super.toString();\n    }\n}\n"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/OfflineMsgReqVO.java",
    "content": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport jakarta.validation.constraints.NotNull;\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/11\n * @description\n */\n@Builder\n@Data\n@AllArgsConstructor\n@NoArgsConstructor\npublic class OfflineMsgReqVO {\n\n    @NotNull(message = \"userId can't be null\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"message received userId\", example = \"1545574049323\")\n    private Long receiveUserId;\n\n}\n"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/P2PReqVO.java",
    "content": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport jakarta.validation.constraints.NotNull;\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.Setter;\n\nimport java.util.List;\n\n/**\n * Function: P2P request\n *\n * @author crossoverJie\n *         Date: 2018/05/21 15:56\n * @since JDK 1.8\n */\n@Builder\npublic class P2PReqVO extends BaseRequest {\n\n    @NotNull(message = \"userId can't be null\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"current send userId\", example = \"1545574049323\")\n    private Long userId;\n\n\n    @NotNull(message = \"userId can't be null\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"message received userId\", example = \"1545574049323\")\n    private Long receiveUserId;\n\n\n\n\n    @NotNull(message = \"msg can't be null\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"msg\", example = \"hello\")\n    private String msg;\n\n    @Getter\n    @Setter\n    private List<String> batchMsg;\n\n    public P2PReqVO() {\n    }\n\n    public P2PReqVO(Long userId, Long receiveUserId, String msg) {\n        this.userId = userId;\n        this.receiveUserId = receiveUserId;\n        this.msg = msg;\n    }\n    public P2PReqVO(Long userId, Long receiveUserId, String msg, List<String> batchMsg) {\n        this.userId = userId;\n        this.receiveUserId = receiveUserId;\n        this.msg = msg;\n        this.batchMsg = batchMsg;\n    }\n\n    public Long getReceiveUserId() {\n        return receiveUserId;\n    }\n\n    public void setReceiveUserId(Long receiveUserId) {\n        this.receiveUserId = receiveUserId;\n    }\n\n    public String getMsg() {\n        return msg;\n    }\n\n    public void setMsg(String msg) {\n        this.msg = msg;\n    }\n\n    public Long getUserId() {\n        return userId;\n    }\n\n    public void setUserId(Long userId) {\n        this.userId = userId;\n    }\n\n    @Override\n    public String toString() {\n        return \"GroupReqVO{\" +\n                \"userId=\" + userId +\n                \", msg='\" + msg + '\\'' +\n                \"} \" + super.toString();\n    }\n}\n"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/RegisterInfoReqVO.java",
    "content": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport jakarta.validation.constraints.NotNull;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/12/23 22:04\n * @since JDK 1.8\n */\npublic class RegisterInfoReqVO extends BaseRequest {\n\n    @NotNull(message = \"用户名不能为空\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"userName\", example = \"zhangsan\")\n    private String userName;\n\n    public String getUserName() {\n        return userName;\n    }\n\n    public void setUserName(String userName) {\n        this.userName = userName;\n    }\n\n    @Override\n    public String toString() {\n        return \"RegisterInfoReqVO{\" +\n                \"userName='\" + userName + '\\'' +\n                \"} \" + super.toString();\n    }\n}\n"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/SendMsgReqVO.java",
    "content": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport jakarta.validation.constraints.NotNull;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/05/21 15:56\n * @since JDK 1.8\n */\npublic class SendMsgReqVO extends BaseRequest {\n\n    @NotNull(message = \"msg 不能为空\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"msg\", example = \"hello\")\n    private String msg;\n\n    @NotNull(message = \"id 不能为空\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"id\", example = \"11\")\n    private long id;\n\n    public String getMsg() {\n        return msg;\n    }\n\n    public void setMsg(String msg) {\n        this.msg = msg;\n    }\n\n    public long getId() {\n        return id;\n    }\n\n    public void setId(long id) {\n        this.id = id;\n    }\n}\n"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/res/CIMServerResVO.java",
    "content": "package com.crossoverjie.cim.route.api.vo.res;\n\nimport java.io.Serializable;\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsConstructor;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/12/23 00:43\n * @since JDK 1.8\n */\n@Data\n@AllArgsConstructor\n@NoArgsConstructor\npublic class CIMServerResVO implements Serializable {\n\n    private String ip;\n    private Integer cimServerPort;\n    private Integer httpPort;\n\n}\n"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/res/RegisterInfoResVO.java",
    "content": "package com.crossoverjie.cim.route.api.vo.res;\n\nimport java.io.Serializable;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/12/23 21:54\n * @since JDK 1.8\n */\npublic class RegisterInfoResVO implements Serializable {\n    private Long userId;\n    private String userName;\n\n    public RegisterInfoResVO(Long userId, String userName) {\n        this.userId = userId;\n        this.userName = userName;\n    }\n\n    public Long getUserId() {\n        return userId;\n    }\n\n    public void setUserId(Long userId) {\n        this.userId = userId;\n    }\n\n    public String getUserName() {\n        return userName;\n    }\n\n    public void setUserName(String userName) {\n        this.userName = userName;\n    }\n\n    @Override\n    public String toString() {\n        return \"RegisterInfo{\" +\n                \"userId=\" + userId +\n                \", userName='\" + userName + '\\'' +\n                '}';\n    }\n}\n"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/res/SendMsgResVO.java",
    "content": "package com.crossoverjie.cim.route.api.vo.res;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2017/6/26 15:43\n * @since JDK 1.8\n */\npublic class SendMsgResVO {\n    private String msg;\n\n    public String getMsg() {\n        return msg;\n    }\n\n    public void setMsg(String msg) {\n        this.msg = msg;\n    }\n}\n"
  },
  {
    "path": "cim-server/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <parent>\n        <groupId>com.crossoverjie.netty</groupId>\n        <artifactId>cim</artifactId>\n        <version>1.0.0-SNAPSHOT</version>\n    </parent>\n    <artifactId>cim-server</artifactId>\n    <packaging>jar</packaging>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <java.version>17</java.version>\n    </properties>\n\n\n    <dependencies>\n        <dependency>\n            <groupId>com.google.protobuf</groupId>\n            <artifactId>protobuf-java</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-common</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-rout-api</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-server-api</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>javax.servlet</groupId>\n            <artifactId>javax.servlet-api</artifactId>\n            <version>4.0.1</version>\n            <scope>provided</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-test</artifactId>\n            <scope>test</scope>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-configuration-processor</artifactId>\n            <optional>true</optional>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-actuator</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n        </dependency>\n\n        <dependency>\n            <groupId>com.alibaba</groupId>\n            <artifactId>fastjson</artifactId>\n        </dependency>\n\n\n        <dependency>\n            <groupId>jakarta.validation</groupId>\n            <artifactId>jakarta.validation-api</artifactId>\n        </dependency>\n\n    </dependencies>\n\n</project>"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/CIMServerApplication.java",
    "content": "package com.crossoverjie.cim.server;\n\nimport com.crossoverjie.cim.common.metastore.MetaStore;\nimport com.crossoverjie.cim.server.config.AppConfiguration;\nimport com.crossoverjie.cim.server.kit.RegistryMetaStore;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.boot.CommandLineRunner;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\n\nimport java.net.InetAddress;\n\n/**\n * @author crossoverJie\n */\n@SpringBootApplication\n@Slf4j\npublic class CIMServerApplication implements CommandLineRunner {\n\n\n\t@Resource\n\tprivate AppConfiguration appConfiguration;\n\n\t@Resource\n\tprivate MetaStore metaStore;\n\n\t@Value(\"${server.port}\")\n\tprivate int httpPort;\n\n\tpublic static void main(String[] args) {\n        SpringApplication.run(CIMServerApplication.class, args);\n\t\tlog.info(\"Start cim server success!!!\");\n\t}\n\n\t@Override\n\tpublic void run(String... args) throws Exception {\n\t\tString addr = InetAddress.getLocalHost().getHostAddress();\n\t\tThread thread = new Thread(new RegistryMetaStore(metaStore, addr, appConfiguration.getCimServerPort(), httpPort));\n\t\tthread.setName(\"registry-zk\");\n\t\tthread.start();\n\t}\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/config/AppConfiguration.java",
    "content": "package com.crossoverjie.cim.server.config;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Component;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/8/24 01:43\n * @since JDK 1.8\n */\n@Component\npublic class AppConfiguration {\n\n    @Value(\"${app.zk.root}\")\n    private String zkRoot;\n\n    @Value(\"${app.zk.addr}\")\n    private String zkAddr;\n\n    @Value(\"${app.zk.switch}\")\n    private boolean zkSwitch;\n\n    @Value(\"${cim.server.port}\")\n    private int cimServerPort;\n\n    @Value(\"${cim.route.url}\")\n    private String routeUrl;\n\n    public String getRouteUrl() {\n        return routeUrl;\n    }\n\n    public void setRouteUrl(String routeUrl) {\n        this.routeUrl = routeUrl;\n    }\n\n    @Value(\"${cim.heartbeat.time}\")\n    private long heartBeatTime;\n\n    @Value(\"${app.zk.connect.timeout}\")\n    private int zkConnectTimeout;\n\n    public int getZkConnectTimeout() {\n\t\treturn zkConnectTimeout;\n\t}\n\n    public String getZkRoot() {\n        return zkRoot;\n    }\n\n    public void setZkRoot(String zkRoot) {\n        this.zkRoot = zkRoot;\n    }\n\n    public String getZkAddr() {\n        return zkAddr;\n    }\n\n    public void setZkAddr(String zkAddr) {\n        this.zkAddr = zkAddr;\n    }\n\n    public boolean isZkSwitch() {\n        return zkSwitch;\n    }\n\n    public void setZkSwitch(boolean zkSwitch) {\n        this.zkSwitch = zkSwitch;\n    }\n\n    public int getCimServerPort() {\n        return cimServerPort;\n    }\n\n    public void setCimServerPort(int cimServerPort) {\n        this.cimServerPort = cimServerPort;\n    }\n\n    public long getHeartBeatTime() {\n        return heartBeatTime;\n    }\n\n    public void setHeartBeatTime(long heartBeatTime) {\n        this.heartBeatTime = heartBeatTime;\n    }\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/config/BeanConfig.java",
    "content": "package com.crossoverjie.cim.server.config;\n\nimport com.crossoverjie.cim.common.core.proxy.RpcProxyManager;\nimport com.crossoverjie.cim.common.metastore.MetaStore;\nimport com.crossoverjie.cim.common.metastore.ZkMetaStoreImpl;\nimport com.crossoverjie.cim.common.protocol.BaseCommand;\nimport com.crossoverjie.cim.common.protocol.Request;\nimport com.crossoverjie.cim.route.api.RouteApi;\nimport jakarta.annotation.Resource;\nimport java.util.concurrent.TimeUnit;\nimport okhttp3.OkHttpClient;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/12/23 00:25\n * @since JDK 1.8\n */\n@Configuration\npublic class BeanConfig {\n\n    @Resource\n    private AppConfiguration appConfiguration;\n\n    /**\n     * http client\n     * @return okHttp\n     */\n    @Bean\n    public OkHttpClient okHttpClient() {\n        OkHttpClient.Builder builder = new OkHttpClient.Builder();\n        builder.connectTimeout(30, TimeUnit.SECONDS)\n                .readTimeout(10, TimeUnit.SECONDS)\n                .writeTimeout(10, TimeUnit.SECONDS)\n                .retryOnConnectionFailure(true);\n        return builder.build();\n    }\n\n    @Bean\n    public MetaStore metaStore() {\n        return new ZkMetaStoreImpl();\n    }\n\n    /**\n     * 创建心跳单例\n     * @return\n     */\n    @Bean(value = \"heartBeat\")\n    public Request heartBeat() {\n        return Request.newBuilder()\n                .setRequestId(0L)\n                .setReqMsg(\"pong\")\n                .setCmd(BaseCommand.PING)\n                .build();\n    }\n\n    @Bean\n    public RouteApi routeApi(OkHttpClient okHttpClient) {\n        return RpcProxyManager.create(RouteApi.class, appConfiguration.getRouteUrl(), okHttpClient);\n    }\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/config/SwaggerConfig.java",
    "content": "package com.crossoverjie.cim.server.config;\n\nimport io.swagger.v3.oas.models.OpenAPI;\nimport io.swagger.v3.oas.models.info.Contact;\nimport io.swagger.v3.oas.models.info.Info;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration\npublic class SwaggerConfig {\n    @Bean\n    public OpenAPI createRestApi() {\n        return new OpenAPI()\n                .info(apiInfo());\n    }\n\n    private Info apiInfo() {\n        return new Info()\n                .title(\"cim server\")\n                .description(\"cim server api\")\n                .termsOfService(\"http://crossoverJie.top\")\n                .contact(contact())\n                .version(\"1.0.0\");\n    }\n\n    private Contact contact() {\n        Contact contact = new Contact();\n        contact.setName(\"crossoverJie\");\n        return contact;\n    }\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/controller/IndexController.java",
    "content": "package com.crossoverjie.cim.server.controller;\n\nimport com.crossoverjie.cim.common.core.proxy.DynamicUrl;\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverjie.cim.common.res.BaseResponse;\nimport com.crossoverjie.cim.server.api.ServerApi;\nimport com.crossoverjie.cim.server.api.vo.req.SendMsgReqVO;\nimport com.crossoverjie.cim.server.api.vo.res.SendMsgResVO;\nimport com.crossoverjie.cim.server.server.CIMServer;\nimport io.swagger.v3.oas.annotations.Operation;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RequestMethod;\nimport org.springframework.web.bind.annotation.ResponseBody;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 22/05/2018 14:46\n * @since JDK 1.8\n */\n@Controller\n@RequestMapping(\"/\")\npublic class IndexController implements ServerApi {\n\n    @Autowired\n    private CIMServer cimServer;\n\n\n    /**\n     *\n     * @param sendMsgReqVO\n     * @return\n     */\n    @Override\n    @Operation(summary = \"Push msg to client\")\n    @RequestMapping(value = \"sendMsg\", method = RequestMethod.POST)\n    @ResponseBody\n    public BaseResponse<SendMsgResVO> sendMsg(@RequestBody SendMsgReqVO sendMsgReqVO, @DynamicUrl String url) {\n        BaseResponse<SendMsgResVO> res = new BaseResponse();\n        cimServer.sendMsg(sendMsgReqVO);\n\n        // TODO: 2024/5/30 metrics\n\n        SendMsgResVO sendMsgResVO = new SendMsgResVO();\n        sendMsgResVO.setMsg(\"OK\");\n        res.setCode(StatusEnum.SUCCESS.getCode());\n        res.setMessage(StatusEnum.SUCCESS.getMessage());\n        res.setDataBody(sendMsgResVO);\n        return res;\n    }\n\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/handle/CIMServerHandle.java",
    "content": "package com.crossoverjie.cim.server.handle;\n\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossoverjie.cim.common.kit.HeartBeatHandler;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.common.protocol.BaseCommand;\nimport com.crossoverjie.cim.common.protocol.Request;\nimport com.crossoverjie.cim.common.util.NettyAttrUtil;\nimport com.crossoverjie.cim.server.kit.RouteHandler;\nimport com.crossoverjie.cim.server.kit.ServerHeartBeatHandlerImpl;\nimport com.crossoverjie.cim.server.util.SessionSocketHolder;\nimport com.crossoverjie.cim.server.util.SpringBeanFactory;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.ChannelHandler;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.SimpleChannelInboundHandler;\nimport io.netty.channel.socket.nio.NioSocketChannel;\nimport io.netty.handler.timeout.IdleState;\nimport io.netty.handler.timeout.IdleStateEvent;\nimport lombok.extern.slf4j.Slf4j;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 17/05/2018 18:52\n * @since JDK 1.8\n */\n@ChannelHandler.Sharable\n@Slf4j\npublic class CIMServerHandle extends SimpleChannelInboundHandler<Request> {\n\n\n\n    /**\n     * 取消绑定\n     *\n     * @param ctx\n     * @throws Exception\n     */\n    @Override\n    public void channelInactive(ChannelHandlerContext ctx) throws Exception {\n        //可能出现业务判断离线后再次触发 channelInactive\n        CIMUserInfo userInfo = SessionSocketHolder.getUserId((NioSocketChannel) ctx.channel());\n        if (userInfo != null) {\n            log.warn(\"[{}] trigger channelInactive offline!\", userInfo.getUserName());\n\n            //Clear route info and offline.\n            RouteHandler routeHandler = SpringBeanFactory.getBean(RouteHandler.class);\n            routeHandler.userOffLine(userInfo, (NioSocketChannel) ctx.channel());\n\n            ctx.channel().close();\n        }\n    }\n\n    @Override\n    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {\n        if (evt instanceof IdleStateEvent) {\n            IdleStateEvent idleStateEvent = (IdleStateEvent) evt;\n            if (idleStateEvent.state() == IdleState.READER_IDLE) {\n\n                log.info(\"!!READER_IDLE!!\");\n\n                HeartBeatHandler heartBeatHandler = SpringBeanFactory.getBean(ServerHeartBeatHandlerImpl.class);\n                heartBeatHandler.process(ctx);\n            }\n        }\n        super.userEventTriggered(ctx, evt);\n    }\n\n\n\n    @Override\n    protected void channelRead0(ChannelHandlerContext ctx, Request msg) throws Exception {\n        log.info(\"received msg=[{}]\", msg.toString());\n\n        if (msg.getCmd() == BaseCommand.LOGIN_REQUEST) {\n            //保存客户端与 Channel 之间的关系\n            SessionSocketHolder.put(msg.getRequestId(), (NioSocketChannel) ctx.channel());\n            SessionSocketHolder.saveSession(msg.getRequestId(), msg.getReqMsg());\n            log.info(\"client [{}] online success!!\", msg.getReqMsg());\n        }\n\n        //心跳更新时间\n        if (msg.getCmd() == BaseCommand.PING) {\n            NettyAttrUtil.updateReaderTime(ctx.channel(), System.currentTimeMillis());\n            //向客户端响应 pong 消息\n            Request heartBeat = SpringBeanFactory.getBean(\"heartBeat\", Request.class);\n            ctx.writeAndFlush(heartBeat).addListeners((ChannelFutureListener) future -> {\n                if (!future.isSuccess()) {\n                    log.error(\"IO error,close Channel\");\n                    future.channel().close();\n                }\n            });\n        }\n\n    }\n\n\n    @Override\n    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n        if (CIMException.isResetByPeer(cause.getMessage())) {\n            return;\n        }\n\n        log.error(cause.getMessage(), cause);\n\n    }\n\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/init/CIMServerInitializer.java",
    "content": "package com.crossoverjie.cim.server.init;\n\nimport com.crossoverjie.cim.common.protocol.Request;\nimport com.crossoverjie.cim.server.handle.CIMServerHandle;\nimport io.netty.channel.Channel;\nimport io.netty.channel.ChannelInitializer;\nimport io.netty.handler.codec.protobuf.ProtobufDecoder;\nimport io.netty.handler.codec.protobuf.ProtobufEncoder;\nimport io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;\nimport io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;\nimport io.netty.handler.timeout.IdleStateHandler;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 17/05/2018 18:51\n * @since JDK 1.8\n */\npublic class CIMServerInitializer extends ChannelInitializer<Channel> {\n\n    private final CIMServerHandle cimServerHandle = new CIMServerHandle();\n\n    @Override\n    protected void initChannel(Channel ch) throws Exception {\n\n        ch.pipeline()\n                //11 秒没有向客户端发送消息就发生心跳\n                .addLast(new IdleStateHandler(11, 0, 0))\n                // google Protobuf 编解码\n                .addLast(new ProtobufVarint32FrameDecoder())\n                .addLast(new ProtobufDecoder(Request.getDefaultInstance()))\n                .addLast(new ProtobufVarint32LengthFieldPrepender())\n                .addLast(new ProtobufEncoder())\n                .addLast(cimServerHandle);\n    }\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/kit/RegistryMetaStore.java",
    "content": "package com.crossoverjie.cim.server.kit;\n\nimport com.crossoverjie.cim.common.metastore.MetaStore;\nimport com.crossoverjie.cim.common.metastore.ZkConfiguration;\nimport com.crossoverjie.cim.server.config.AppConfiguration;\nimport com.crossoverjie.cim.server.util.SpringBeanFactory;\nimport lombok.SneakyThrows;\nimport lombok.extern.slf4j.Slf4j;\nimport org.apache.curator.retry.ExponentialBackoffRetry;\n\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/8/24 01:37\n * @since JDK 1.8\n */\n@Slf4j\npublic class RegistryMetaStore implements Runnable {\n\n\n    private MetaStore metaStore;\n\n    private AppConfiguration appConfiguration;\n\n    private String ip;\n    private int cimServerPort;\n    private int httpPort;\n    public RegistryMetaStore(MetaStore metaStore, String ip, int cimServerPort, int httpPort) {\n        this.ip = ip;\n        this.cimServerPort = cimServerPort;\n        this.httpPort = httpPort;\n        this.metaStore = metaStore;\n        appConfiguration = SpringBeanFactory.getBean(AppConfiguration.class);\n    }\n\n    @SneakyThrows\n    @Override\n    public void run() {\n\n        if (!appConfiguration.isZkSwitch()) {\n            log.info(\"Skip registry to metaStore\");\n            return;\n        }\n\n        ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);\n        metaStore.initialize(ZkConfiguration.builder()\n                .metaServiceUri(appConfiguration.getZkAddr())\n                .timeoutMs(appConfiguration.getZkConnectTimeout())\n                .retryPolicy(retryPolicy)\n                .build());\n        metaStore.addServer(ip, cimServerPort, httpPort);\n    }\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/kit/RouteHandler.java",
    "content": "package com.crossoverjie.cim.server.kit;\n\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.route.api.RouteApi;\nimport com.crossoverjie.cim.route.api.vo.req.ChatReqVO;\nimport com.crossoverjie.cim.server.util.SessionSocketHolder;\nimport io.netty.channel.socket.nio.NioSocketChannel;\nimport jakarta.annotation.Resource;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.stereotype.Component;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-20 17:20\n * @since JDK 1.8\n */\n@Component\n@Slf4j\npublic class RouteHandler {\n\n    @Resource\n    private RouteApi routeApi;\n\n    /**\n     * 用户下线\n     *\n     * @param userInfo\n     * @param channel\n     */\n    public void userOffLine(CIMUserInfo userInfo, NioSocketChannel channel) {\n        if (userInfo != null) {\n            log.info(\"Account [{}] offline\", userInfo.getUserName());\n            SessionSocketHolder.removeSession(userInfo.getUserId());\n            //清除路由关系\n            clearRouteInfo(userInfo);\n        }\n        SessionSocketHolder.remove(channel);\n\n    }\n\n\n    /**\n     * 清除路由关系\n     *\n     * @param userInfo\n     * @throws IOException\n     */\n    public void clearRouteInfo(CIMUserInfo userInfo) {\n        ChatReqVO vo = new ChatReqVO(userInfo.getUserId(), userInfo.getUserName(), null);\n        routeApi.offLine(vo);\n    }\n\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/kit/ServerHeartBeatHandlerImpl.java",
    "content": "package com.crossoverjie.cim.server.kit;\n\nimport com.crossoverjie.cim.common.kit.HeartBeatHandler;\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.cim.common.util.NettyAttrUtil;\nimport com.crossoverjie.cim.server.config.AppConfiguration;\nimport com.crossoverjie.cim.server.util.SessionSocketHolder;\nimport io.netty.channel.ChannelHandlerContext;\nimport io.netty.channel.socket.nio.NioSocketChannel;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-20 17:16\n * @since JDK 1.8\n */\n@Service\n@Slf4j\npublic class ServerHeartBeatHandlerImpl implements HeartBeatHandler {\n\n\n    @Autowired\n    private RouteHandler routeHandler;\n\n    @Autowired\n    private AppConfiguration appConfiguration;\n\n    @Override\n    public void process(ChannelHandlerContext ctx) throws Exception {\n\n        long heartBeatTime = appConfiguration.getHeartBeatTime() * 1000;\n\n        Long lastReadTime = NettyAttrUtil.getReaderTime(ctx.channel());\n        long now = System.currentTimeMillis();\n        if (lastReadTime != null && now - lastReadTime > heartBeatTime) {\n            CIMUserInfo userInfo = SessionSocketHolder.getUserId((NioSocketChannel) ctx.channel());\n            if (userInfo != null) {\n                log.warn(\"客户端[{}]心跳超时[{}]ms，需要关闭连接!\", userInfo.getUserName(), now - lastReadTime);\n            }\n            routeHandler.userOffLine(userInfo, (NioSocketChannel) ctx.channel());\n            ctx.channel().close();\n        }\n    }\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/server/CIMServer.java",
    "content": "package com.crossoverjie.cim.server.server;\n\nimport com.crossoverjie.cim.common.protocol.Request;\nimport com.crossoverjie.cim.server.api.vo.req.SendMsgReqVO;\nimport com.crossoverjie.cim.server.init.CIMServerInitializer;\nimport com.crossoverjie.cim.server.util.SessionSocketHolder;\nimport io.netty.bootstrap.ServerBootstrap;\nimport io.netty.channel.ChannelFuture;\nimport io.netty.channel.ChannelFutureListener;\nimport io.netty.channel.ChannelOption;\nimport io.netty.channel.EventLoopGroup;\nimport io.netty.channel.nio.NioEventLoopGroup;\nimport io.netty.channel.socket.nio.NioServerSocketChannel;\nimport io.netty.channel.socket.nio.NioSocketChannel;\nimport jakarta.annotation.PostConstruct;\nimport jakarta.annotation.PreDestroy;\nimport java.net.InetSocketAddress;\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.stereotype.Component;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 21/05/2018 00:30\n * @since JDK 1.8\n */\n@Component\n@Slf4j\npublic class CIMServer {\n\n\n    private EventLoopGroup boss = new NioEventLoopGroup();\n    private EventLoopGroup work = new NioEventLoopGroup();\n\n\n    @Value(\"${cim.server.port}\")\n    private int nettyPort;\n\n\n    /**\n     * 启动 cim server\n     *\n     * @return\n     * @throws InterruptedException\n     */\n    @PostConstruct\n    public void start() throws InterruptedException {\n\n        ServerBootstrap bootstrap = new ServerBootstrap()\n                .group(boss, work)\n                .channel(NioServerSocketChannel.class)\n                .localAddress(new InetSocketAddress(nettyPort))\n                //保持长连接\n                .childOption(ChannelOption.SO_KEEPALIVE, true)\n                .childHandler(new CIMServerInitializer());\n\n        ChannelFuture future = bootstrap.bind().sync();\n        if (future.isSuccess()) {\n            log.info(\"Start cim server success!!!\");\n        }\n    }\n\n\n    /**\n     * 销毁\n     */\n    @PreDestroy\n    public void destroy() {\n        boss.shutdownGracefully().syncUninterruptibly();\n        work.shutdownGracefully().syncUninterruptibly();\n        log.info(\"Close cim server success!!!\");\n    }\n\n\n    /**\n     * Push msg to client.\n     * @param sendMsgReqVO message body\n     */\n    public void sendMsg(SendMsgReqVO sendMsgReqVO) {\n        NioSocketChannel socketChannel = SessionSocketHolder.get(sendMsgReqVO.getUserId());\n\n        if (null == socketChannel) {\n            log.error(\"client {} offline!\", sendMsgReqVO.getUserId());\n            return;\n        }\n\n        Request.Builder requestBuilder = Request.newBuilder()\n                .setRequestId(sendMsgReqVO.getUserId())\n                .putAllProperties(sendMsgReqVO.getProperties())\n                .setCmd(sendMsgReqVO.getCmd());\n\n        boolean isBatch = sendMsgReqVO.getBatchMsg() != null && sendMsgReqVO.getBatchMsg().size() > 0;\n        if (isBatch) {\n            requestBuilder.addAllBatchReqMsg(sendMsgReqVO.getBatchMsg());\n        } else {\n            requestBuilder.setReqMsg(sendMsgReqVO.getMsg());\n        }\n\n        Request protocol = requestBuilder.build();\n\n        ChannelFuture future = socketChannel.writeAndFlush(protocol);\n        future.addListener((ChannelFutureListener) channelFuture ->\n                log.info(\"server push {} msg:[{}], socketChannel:{}\", isBatch ? \"batch\" : \"single\", sendMsgReqVO, socketChannel));\n    }\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/util/SessionSocketHolder.java",
    "content": "package com.crossoverjie.cim.server.util;\n\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport io.netty.channel.socket.nio.NioSocketChannel;\n\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 22/05/2018 18:33\n * @since JDK 1.8\n */\npublic class SessionSocketHolder {\n    private static final Map<Long, NioSocketChannel> CHANNEL_MAP = new ConcurrentHashMap<>(16);\n    private static final Map<Long, String> SESSION_MAP = new ConcurrentHashMap<>(16);\n\n    public static void saveSession(Long userId, String userName) {\n        SESSION_MAP.put(userId, userName);\n    }\n\n    public static void removeSession(Long userId) {\n        SESSION_MAP.remove(userId);\n    }\n\n    /**\n     * Save the relationship between the userId and the channel.\n     * @param id\n     * @param socketChannel\n     */\n    public static void put(Long id, NioSocketChannel socketChannel) {\n        CHANNEL_MAP.put(id, socketChannel);\n    }\n\n    public static NioSocketChannel get(Long id) {\n        return CHANNEL_MAP.get(id);\n    }\n\n    public static Map<Long, NioSocketChannel> getRelationShip() {\n        return CHANNEL_MAP;\n    }\n\n    public static void remove(NioSocketChannel nioSocketChannel) {\n        CHANNEL_MAP.entrySet().stream().filter(entry -> entry.getValue() == nioSocketChannel).forEach(entry -> CHANNEL_MAP.remove(entry.getKey()));\n    }\n\n    /**\n     * 获取注册用户信息\n     * @param nioSocketChannel\n     * @return\n     */\n    public static CIMUserInfo getUserId(NioSocketChannel nioSocketChannel) {\n        for (Map.Entry<Long, NioSocketChannel> entry : CHANNEL_MAP.entrySet()) {\n            NioSocketChannel value = entry.getValue();\n            if (nioSocketChannel == value) {\n                Long key = entry.getKey();\n                String userName = SESSION_MAP.get(key);\n                CIMUserInfo info = new CIMUserInfo(key, userName);\n                return info;\n            }\n        }\n\n        return null;\n    }\n\n\n\n}\n"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/util/SpringBeanFactory.java",
    "content": "package com.crossoverjie.cim.server.util;\n\nimport org.springframework.beans.BeansException;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.context.ApplicationContextAware;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic final class SpringBeanFactory implements ApplicationContextAware {\n    private static ApplicationContext context;\n\n    public static <T> T getBean(Class<T> c) {\n        return context.getBean(c);\n    }\n\n\n    public static <T> T getBean(String name, Class<T> clazz) {\n        return context.getBean(name, clazz);\n    }\n\n    @Override\n    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {\n        context = applicationContext;\n    }\n\n\n}\n"
  },
  {
    "path": "cim-server/src/main/resources/application.yaml",
    "content": "spring:\r\n  application:\r\n    name:\r\n      cim-server\r\n  \r\n# web port\r\nserver:\r\n  port: 8081\r\n  \r\n# enable swagger\r\nspringdoc:\r\n  swagger-ui:\r\n    enabled: true\r\n\r\nlogging:\r\n  level:\r\n    root: info\r\n\r\n# enable zk\r\napp:\r\n  zk:\r\n    switch: true\r\n    addr: 127.0.0.1:2181\r\n    connect:\r\n      timeout: 30000\r\n    root: /route # zk root path\r\n# cim server config\r\ncim:\r\n  server:\r\n    port: 11211\r\n  route:\r\n    url: http://localhost:8083/ # route url suggested that this is Nginx address\r\n  heartbeat:\r\n    time: 30 # cim heartbeat time(seconds)\r\n\r\n\r\n"
  },
  {
    "path": "cim-server/src/main/resources/banner.txt",
    "content": "\n      _\n ____(_)_ _      ___ ___ _____  _____ ____\n/ __/ /  ' \\    (_-</ -_) __/ |/ / -_) __/\n\\__/_/_/_/_/   /___/\\__/_/  |___/\\__/_/\n Power by @crossoverJie\n\n\n\n"
  },
  {
    "path": "cim-server/src/test/com/crossoverjie/cim/server/util/NettyAttrUtilTest.java",
    "content": "package com.crossoverjie.cim.server.util;\n\n\n\nimport java.util.concurrent.TimeUnit;\nimport org.junit.jupiter.api.Test;\n\npublic class NettyAttrUtilTest {\n\n    @Test\n    public void test() throws InterruptedException {\n        long heartbeat = 2 * 1000 ;\n\n        long now = System.currentTimeMillis();\n        TimeUnit.SECONDS.sleep(1);\n\n        long end = System.currentTimeMillis();\n\n        if ((end - now) > heartbeat){\n            System.out.println(\"超时\");\n        }else {\n            System.out.println(\"没有超时\");\n        }\n    }\n\n}"
  },
  {
    "path": "cim-server-api/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <artifactId>cim</artifactId>\n        <groupId>com.crossoverjie.netty</groupId>\n        <version>1.0.0-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>cim-server-api</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n\n    <dependencies>\n        <dependency>\n            <groupId>com.crossoverjie.netty</groupId>\n            <artifactId>cim-common</artifactId>\n            <exclusions>\n                <exclusion>\n                    <artifactId>log4j</artifactId>\n                    <groupId>log4j</groupId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n\n        <dependency>\n            <groupId>org.springframework.boot</groupId>\n            <artifactId>spring-boot-starter-web</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>jakarta.validation</groupId>\n            <artifactId>jakarta.validation-api</artifactId>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "cim-server-api/src/main/java/com/crossoverjie/cim/server/api/ServerApi.java",
    "content": "package com.crossoverjie.cim.server.api;\n\nimport com.crossoverjie.cim.common.core.proxy.DynamicUrl;\nimport com.crossoverjie.cim.common.res.BaseResponse;\nimport com.crossoverjie.cim.server.api.vo.req.SendMsgReqVO;\nimport com.crossoverjie.cim.server.api.vo.res.SendMsgResVO;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2020-04-25 14:23\n * @since JDK 1.8\n */\npublic interface ServerApi {\n\n    /**\n     * Push msg to client\n     * @param sendMsgReqVO\n     * @return\n     * @throws Exception\n     */\n    BaseResponse<SendMsgResVO> sendMsg(SendMsgReqVO sendMsgReqVO, @DynamicUrl String url);\n}\n"
  },
  {
    "path": "cim-server-api/src/main/java/com/crossoverjie/cim/server/api/vo/req/SendMsgReqVO.java",
    "content": "package com.crossoverjie.cim.server.api.vo.req;\n\nimport com.crossoverjie.cim.common.protocol.BaseCommand;\nimport com.crossoverjie.cim.common.req.BaseRequest;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport jakarta.validation.constraints.NotNull;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Getter;\nimport lombok.Setter;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/05/21 15:56\n * @since JDK 1.8\n */\n@Builder\n@AllArgsConstructor\npublic class SendMsgReqVO extends BaseRequest {\n\n    @NotNull(message = \"msg 不能为空\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"msg\", example = \"hello\")\n    private String msg;\n\n    @Getter\n    @Setter\n    private List<String> batchMsg;\n\n    @NotNull(message = \"userId 不能为空\")\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"userId\", example = \"11\")\n    @Getter\n    private Long userId;\n\n    @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = \"cmd\", example = \"message\")\n    @Getter\n    private BaseCommand cmd;\n\n    @Setter\n    @Getter\n    private Map<String, String> properties;\n\n    public SendMsgReqVO() {\n    }\n\n    public SendMsgReqVO(String msg, Long userId, List<String> batchMsg, BaseCommand cmd) {\n        this.msg = msg;\n        this.batchMsg = batchMsg;\n        this.userId = userId;\n        this.cmd = cmd;\n    }\n\n    public String getMsg() {\n        return msg;\n    }\n\n    public void setMsg(String msg) {\n        this.msg = msg;\n    }\n\n\n    @Override\n    public String toString() {\n        return \"SendMsgReqVO{\" +\n                \"msg='\" + msg + '\\'' +\n                \", batchMsg=\" + batchMsg +\n                \", userId=\" + userId +\n                \", cmd=\" + cmd +\n                \", properties=\" + properties +\n                '}';\n    }\n}\n"
  },
  {
    "path": "cim-server-api/src/main/java/com/crossoverjie/cim/server/api/vo/res/OfflineMsgResVO.java",
    "content": "package com.crossoverjie.cim.server.api.vo.res;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/11\n * @description\n */\npublic class OfflineMsgResVO {\n\n    private String msg;\n\n    public String getMsg() {\n        return msg;\n    }\n\n    public void setMsg(String msg) {\n        this.msg = msg;\n    }\n}\n"
  },
  {
    "path": "cim-server-api/src/main/java/com/crossoverjie/cim/server/api/vo/res/SaveOfflineMsgResVO.java",
    "content": "package com.crossoverjie.cim.server.api.vo.res;\n\npublic class SaveOfflineMsgResVO {\n\n    private String msg;\n\n    public String getMsg() {\n        return msg;\n    }\n\n    public void setMsg(String msg) {\n        this.msg = msg;\n    }\n}\n"
  },
  {
    "path": "cim-server-api/src/main/java/com/crossoverjie/cim/server/api/vo/res/SendMsgResVO.java",
    "content": "package com.crossoverjie.cim.server.api.vo.res;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2017/6/26 15:43\n * @since JDK 1.8\n */\npublic class SendMsgResVO {\n    private String msg;\n\n    public String getMsg() {\n        return msg;\n    }\n\n    public void setMsg(String msg) {\n        this.msg = msg;\n    }\n}\n"
  },
  {
    "path": "doc/QA.md",
    "content": "# 以下问题由网友问答整理而来\n\n\n## 部署 server 要不要加端口号？\n\n![](https://ws2.sinaimg.cn/large/006tNbRwly1fymb41bob6j31g90c9dk6.jpg)\n\n`server` 端口号通过 `cim-server.port` 设置，同一台服务器启动多个 `server` 只要保证端口号唯一即可。\n\n## 部署路由服务器 `zk` 和 `redis` 地址加不加端口？\n\n![](https://ws2.sinaimg.cn/large/006tNbRwly1fymb9wgo5hj31g909jjv6.jpg)\n\n\n```\nspring.redis.host=xx \n spring.redis.port=6379\n```\n\n其实所有的配置都是通过 `SpringBoot` 来加载的，看这个配置就知道了。\n如果不加会默认使用 jar 包里的配置。\n\n\n## 本地启动 路由服务器写 `127.0.0.1` 吗？\n\n![](https://ws4.sinaimg.cn/large/006tNbRwly1fymbc9lzidj31g908g0xb.jpg)\n\n本地的路由服务器是多少就是多少，本机肯定就是 `127.0.0.1`.\n\n\n## 本地启动如何注册账号？\n\n`cim-forward-route` 服务启动之后有一个 `registerAccount` 接口可以注册账号。\n\n![](https://ws2.sinaimg.cn/large/006tNbRwly1fymbjn98f6j31bn0u0aff.jpg)\n\n账号信息会存放在 `Redis`。\n\n\n## 本地如何模拟调试？\n\n至少需要启动以下服务:\n\n1. 服务端\n2. 路由\n3. 至少两个客户端\n4. `redis`、`zk` 基础组件"
  },
  {
    "path": "docker/README.md",
    "content": "\n\n# Build in local\n```shell\ndocker build -t cim-allin1:latest -f allin1-ubuntu.Dockerfile .\ndocker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1  cim-allin1\n```\n\n# Client\n\n```shell\njava -jar target/cim-client-1.0.0-SNAPSHOT.jar --server.port=8084 --cim.user.id={userId} --cim.user.userName={userName} --cim.route.url=http://127.0.0.1:8083\n```"
  },
  {
    "path": "docker/allin1-ubuntu.Dockerfile",
    "content": "\nFROM ubuntu:22.04\n\n# install basic dependencies\nRUN apt-get update && apt-get install -y \\\n    openjdk-17-jdk \\\n    redis-server \\\n    wget \\\n    supervisor \\\n    netcat-openbsd \\\n && rm -rf /var/lib/apt/lists/*\n\n# install zookeeper\nRUN wget https://dlcdn.apache.org/zookeeper/zookeeper-3.9.3/apache-zookeeper-3.9.3-bin.tar.gz \\\n    && tar -xzf apache-zookeeper-3.9.3-bin.tar.gz -C /opt \\\n    && mv /opt/apache-zookeeper-3.9.3-bin /opt/zookeeper \\\n    && rm apache-zookeeper-3.9.3-bin.tar.gz\n\n# configure zookeeper\nRUN mkdir -p /var/lib/zookeeper && \\\n    echo \"tickTime=2000\\n\\\ndataDir=/var/lib/zookeeper\\n\\\nclientPort=2181\\n\\\ninitLimit=5\\n\\\nsyncLimit=2\\n\" > /opt/zookeeper/conf/zoo.cfg\n\n# wait-for-it.sh\nADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /wait-for-it.sh\nRUN chmod +x /wait-for-it.sh\n\n# copy java app\nADD https://github.com/crossoverJie/cim/releases/download/v2.1.0/cim-server-1.0.0-SNAPSHOT.jar /cim-server.jar\nADD https://github.com/crossoverJie/cim/releases/download/v2.1.0/cim-forward-route-1.0.0-SNAPSHOT.jar /cim-route.jar\n\nRUN mkdir -p /var/log/supervisor\nADD https://raw.githubusercontent.com/crossoverJie/cim/refs/heads/master/docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf\n\nCMD [\"supervisord\", \"-n\"]\n"
  },
  {
    "path": "docker/client-ubuntu.Dockerfile",
    "content": "# This Dockerfile is reserved for configuring the Ubuntu-based client environment.\n# Implementation will be added in the future as part of the client setup process."
  },
  {
    "path": "docker/supervisord.conf",
    "content": "[supervisord]\nnodaemon=true\nlogfile=/var/log/supervisor/supervisord.log\npidfile=/var/run/supervisord.pid\n\n[program:redis]\ncommand=redis-server --bind 0.0.0.0\nautostart=true\nautorestart=unexpected\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:zookeeper]\ncommand=/opt/zookeeper/bin/zkServer.sh start-foreground\nautostart=true\nautorestart=unexpected\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n[program:server]\ncommand=bash -c \"/wait-for-it.sh -t 60 localhost:6379 -- \\\n                /wait-for-it.sh -t 60 localhost:2181 -- \\\n                java -jar /cim-server.jar\"\nautostart=true\nautorestart=false\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\n\n\n[program:route]\ncommand=bash -c \"\\\n    /wait-for-it.sh -t 60 localhost:11211 -- \\\n    java -jar /cim-route.jar\"\nautorestart=false\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0"
  },
  {
    "path": "docker/wait-for-it.sh",
    "content": "#!/usr/bin/env bash\n# Use this script to test if a given TCP host/port are available\n\nWAITFORIT_cmdname=${0##*/}\n\nechoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo \"$@\" 1>&2; fi }\n\nusage()\n{\n    cat << USAGE >&2\nUsage:\n    $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args]\n    -h HOST | --host=HOST       Host or IP under test\n    -p PORT | --port=PORT       TCP port under test\n                                Alternatively, you specify the host and port as host:port\n    -s | --strict               Only execute subcommand if the test succeeds\n    -q | --quiet                Don't output any status messages\n    -t TIMEOUT | --timeout=TIMEOUT\n                                Timeout in seconds, zero for no timeout\n    -- COMMAND ARGS             Execute command with args after the test finishes\nUSAGE\n    exit 1\n}\n\nwait_for()\n{\n    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then\n        echoerr \"$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT\"\n    else\n        echoerr \"$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout\"\n    fi\n    WAITFORIT_start_ts=$(date +%s)\n    while :\n    do\n        if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then\n            nc -z $WAITFORIT_HOST $WAITFORIT_PORT\n            WAITFORIT_result=$?\n        else\n            (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1\n            WAITFORIT_result=$?\n        fi\n        if [[ $WAITFORIT_result -eq 0 ]]; then\n            WAITFORIT_end_ts=$(date +%s)\n            echoerr \"$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds\"\n            break\n        fi\n        sleep 1\n    done\n    return $WAITFORIT_result\n}\n\nwait_for_wrapper()\n{\n    # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692\n    if [[ $WAITFORIT_QUIET -eq 1 ]]; then\n        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &\n    else\n        timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT &\n    fi\n    WAITFORIT_PID=$!\n    trap \"kill -INT -$WAITFORIT_PID\" INT\n    wait $WAITFORIT_PID\n    WAITFORIT_RESULT=$?\n    if [[ $WAITFORIT_RESULT -ne 0 ]]; then\n        echoerr \"$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT\"\n    fi\n    return $WAITFORIT_RESULT\n}\n\n# process arguments\nwhile [[ $# -gt 0 ]]\ndo\n    case \"$1\" in\n        *:* )\n        WAITFORIT_hostport=(${1//:/ })\n        WAITFORIT_HOST=${WAITFORIT_hostport[0]}\n        WAITFORIT_PORT=${WAITFORIT_hostport[1]}\n        shift 1\n        ;;\n        --child)\n        WAITFORIT_CHILD=1\n        shift 1\n        ;;\n        -q | --quiet)\n        WAITFORIT_QUIET=1\n        shift 1\n        ;;\n        -s | --strict)\n        WAITFORIT_STRICT=1\n        shift 1\n        ;;\n        -h)\n        WAITFORIT_HOST=\"$2\"\n        if [[ $WAITFORIT_HOST == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --host=*)\n        WAITFORIT_HOST=\"${1#*=}\"\n        shift 1\n        ;;\n        -p)\n        WAITFORIT_PORT=\"$2\"\n        if [[ $WAITFORIT_PORT == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --port=*)\n        WAITFORIT_PORT=\"${1#*=}\"\n        shift 1\n        ;;\n        -t)\n        WAITFORIT_TIMEOUT=\"$2\"\n        if [[ $WAITFORIT_TIMEOUT == \"\" ]]; then break; fi\n        shift 2\n        ;;\n        --timeout=*)\n        WAITFORIT_TIMEOUT=\"${1#*=}\"\n        shift 1\n        ;;\n        --)\n        shift\n        WAITFORIT_CLI=(\"$@\")\n        break\n        ;;\n        --help)\n        usage\n        ;;\n        *)\n        echoerr \"Unknown argument: $1\"\n        usage\n        ;;\n    esac\ndone\n\nif [[ \"$WAITFORIT_HOST\" == \"\" || \"$WAITFORIT_PORT\" == \"\" ]]; then\n    echoerr \"Error: you need to provide a host and port to test.\"\n    usage\nfi\n\nWAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15}\nWAITFORIT_STRICT=${WAITFORIT_STRICT:-0}\nWAITFORIT_CHILD=${WAITFORIT_CHILD:-0}\nWAITFORIT_QUIET=${WAITFORIT_QUIET:-0}\n\n# Check to see if timeout is from busybox?\nWAITFORIT_TIMEOUT_PATH=$(type -p timeout)\nWAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH)\n\nWAITFORIT_BUSYTIMEFLAG=\"\"\nif [[ $WAITFORIT_TIMEOUT_PATH =~ \"busybox\" ]]; then\n    WAITFORIT_ISBUSY=1\n    # Check if busybox timeout uses -t flag\n    # (recent Alpine versions don't support -t anymore)\n    if timeout &>/dev/stdout | grep -q -e '-t '; then\n        WAITFORIT_BUSYTIMEFLAG=\"-t\"\n    fi\nelse\n    WAITFORIT_ISBUSY=0\nfi\n\nif [[ $WAITFORIT_CHILD -gt 0 ]]; then\n    wait_for\n    WAITFORIT_RESULT=$?\n    exit $WAITFORIT_RESULT\nelse\n    if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then\n        wait_for_wrapper\n        WAITFORIT_RESULT=$?\n    else\n        wait_for\n        WAITFORIT_RESULT=$?\n    fi\nfi\n\nif [[ $WAITFORIT_CLI != \"\" ]]; then\n    if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then\n        echoerr \"$WAITFORIT_cmdname: strict mode, refusing to execute subprocess\"\n        exit $WAITFORIT_RESULT\n    fi\n    exec \"${WAITFORIT_CLI[@]}\"\nelse\n    exit $WAITFORIT_RESULT\nfi"
  },
  {
    "path": "pom.xml",
    "content": "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n    <groupId>com.crossoverjie.netty</groupId>\n    <artifactId>cim</artifactId>\n    <version>1.0.0-SNAPSHOT</version>\n    <packaging>pom</packaging>\n\n    <name>cim</name>\n    <description>Spring Boot</description>\n\n\n    <properties>\n        <junit.version>4.12</junit.version>\n        <netty.version>4.1.68.Final</netty.version>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>\n        <swagger.version>2.5.0</swagger.version>\n        <curator.version>5.1.0</curator.version>\n        <zookeeper.version>3.8.6</zookeeper.version>\n        <jacoco-maven-plugin.version>0.8.11</jacoco-maven-plugin.version>\n        <protobuf-java.version>4.28.3</protobuf-java.version>\n        <protoc-gen-grpc-java.version>1.19.0</protoc-gen-grpc-java.version>\n        <checkstyle.version>10.14.2</checkstyle.version>\n        <maven-checkstyle-plugin.version>3.3.1</maven-checkstyle-plugin.version>\n    </properties>\n\n    <parent>\n        <groupId>org.springframework.boot</groupId>\n        <artifactId>spring-boot-starter-parent</artifactId>\n        <version>3.3.0</version>\n        <relativePath/> <!-- lookup parent from repository -->\n    </parent>\n\n    <modules>\n        <module>cim-server</module>\n        <module>cim-client</module>\n        <module>cim-common</module>\n        <module>cim-forward-route</module>\n        <module>cim-rout-api</module>\n        <module>cim-server-api</module>\n        <module>cim-integration-test</module>\n        <module>cim-client-sdk</module>\n        <module>cim-persistence</module>\n    </modules>\n\n\n    <dependencyManagement>\n        <dependencies>\n\n            <dependency>\n                <groupId>com.github.xiaoymin</groupId>\n                <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>\n                <version>4.4.0</version>\n            </dependency>\n\n            <dependency>\n                <groupId>com.squareup.okhttp3</groupId>\n                <artifactId>okhttp</artifactId>\n                <version>4.9.2</version>\n            </dependency>\n            <!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->\n            <dependency>\n                <groupId>com.101tec</groupId>\n                <artifactId>zkclient</artifactId>\n                <version>0.11</version>\n            </dependency>\n\n\n            <dependency>\n                <groupId>org.apache.curator</groupId>\n                <artifactId>curator-recipes</artifactId>\n                <version>${curator.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>org.apache.curator</groupId>\n                <artifactId>curator-x-discovery</artifactId>\n                <version>${curator.version}</version>\n            </dependency>\n\n            <!-- zookeeper dependencies -->\n            <dependency>\n                <groupId>org.apache.zookeeper</groupId>\n                <artifactId>zookeeper</artifactId>\n                <version>${zookeeper.version}</version>\n                <exclusions>\n                    <exclusion>\n                        <groupId>net.java.dev.javacc</groupId>\n                        <artifactId>javacc</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>ch.qos.logback</groupId>\n                        <artifactId>*</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>io.netty</groupId>\n                        <artifactId>*</artifactId>\n                    </exclusion>\n                </exclusions>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.zookeeper</groupId>\n                <artifactId>zookeeper</artifactId>\n                <version>${zookeeper.version}</version>\n                <type>test-jar</type>\n                <exclusions>\n                    <exclusion>\n                        <groupId>org.slf4j</groupId>\n                        <artifactId>slf4j-api</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>ch.qos.logback</groupId>\n                        <artifactId>*</artifactId>\n                    </exclusion>\n                    <exclusion>\n                        <groupId>io.netty</groupId>\n                        <artifactId>*</artifactId>\n                    </exclusion>\n                </exclusions>\n            </dependency>\n\n            <dependency>\n                <groupId>com.crossoverjie.netty</groupId>\n                <artifactId>cim-common</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n            <dependency>\n                <groupId>com.crossoverjie.netty</groupId>\n                <artifactId>cim-client-sdk</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>com.crossoverjie.netty</groupId>\n                <artifactId>cim-rout-api</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>com.crossoverjie.netty</groupId>\n                <artifactId>cim-server-api</artifactId>\n                <version>${project.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>com.google.protobuf</groupId>\n                <artifactId>protobuf-java</artifactId>\n                <version>${protobuf-java.version}</version>\n            </dependency>\n\n\n            <dependency>\n                <groupId>io.netty</groupId>\n                <artifactId>netty-all</artifactId>\n                <version>${netty.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>junit</groupId>\n                <artifactId>junit</artifactId>\n                <version>${junit.version}</version>\n            </dependency>\n\n            <dependency>\n                <groupId>com.alibaba</groupId>\n                <artifactId>fastjson</artifactId>\n                <version>1.2.83</version>\n            </dependency>\n\n            <dependency>\n                <groupId>com.github.ben-manes.caffeine</groupId>\n                <artifactId>caffeine</artifactId>\n                <version>3.2.3</version>\n            </dependency>\n            <dependency>\n                <groupId>de.codecentric</groupId>\n                <artifactId>spring-boot-admin-starter-client</artifactId>\n                <version>1.5.7</version>\n            </dependency>\n\n            <dependency>\n                <groupId>jakarta.validation</groupId>\n                <artifactId>jakarta.validation-api</artifactId>\n                <version>3.0.0</version>\n            </dependency>\n\n            <dependency>\n                <groupId>com.clever-cloud</groupId>\n                <artifactId>testcontainers-zookeeper</artifactId>\n                <version>0.1.3</version>\n                <scope>test</scope>\n            </dependency>\n\n            <dependency>\n                <groupId>com.redis</groupId>\n                <artifactId>testcontainers-redis</artifactId>\n                <version>2.2.2</version>\n                <scope>test</scope>\n            </dependency>\n\n            <dependency>\n                <groupId>org.testcontainers</groupId>\n                <artifactId>mysql</artifactId>\n                <version>1.19.7</version>\n                <scope>test</scope>\n            </dependency>\n\n\n        </dependencies>\n    </dependencyManagement>\n\n    <profiles>\n        <profile>\n            <id>x86</id>\n            <activation>\n                <activeByDefault>true</activeByDefault>\n            </activation>\n            <properties>\n            </properties>\n        </profile>\n\n        <profile>\n            <id>aarch</id>\n            <activation>\n                <os>\n                    <arch>aarch64</arch>\n                </os>\n            </activation>\n            <properties>\n                <os.detected.classifier>osx-x86_64</os.detected.classifier>\n            </properties>\n        </profile>\n    </profiles>\n\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <configuration>\n                    <source>17</source>\n                    <target>17</target>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.jacoco</groupId>\n                <artifactId>jacoco-maven-plugin</artifactId>\n                <version>${jacoco-maven-plugin.version}</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>prepare-agent</goal>\n                        </goals>\n                    </execution>\n                    <execution>\n                        <id>report</id>\n                        <phase>test</phase>\n                        <goals>\n                            <goal>report</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-checkstyle-plugin</artifactId>\n                <version>${maven-checkstyle-plugin.version}</version>\n                <dependencies>\n                    <dependency>\n                        <groupId>com.puppycrawl.tools</groupId>\n                        <artifactId>checkstyle</artifactId>\n                        <version>${checkstyle.version}</version>\n                    </dependency>\n                </dependencies>\n                <configuration>\n                    <configLocation>checkstyle/checkstyle.xml</configLocation>\n                    <suppressionsLocation>checkstyle/suppressions.xml</suppressionsLocation>\n                    <consoleOutput>true</consoleOutput>\n                    <failsOnError>true</failsOnError>\n                    <failOnViolation>true</failOnViolation>\n                    <includeTestSourceDirectory>true</includeTestSourceDirectory>\n                </configuration>\n                <executions>\n                    <execution>\n                        <id>validate</id>\n                        <phase>validate</phase>\n                        <goals>\n                            <goal>check</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n        </plugins>\n    </build>\n</project>"
  },
  {
    "path": "script/build.sh",
    "content": "# todo build"
  },
  {
    "path": "script/deploy.sh",
    "content": "#!/usr/bin/env bash\n\ngit pull\n\ncd ..\n\nmvn -Dmaven.test.skip=true clean package\n\n# 分发路由\ncp /root/work/netty-action/cim-forward-route/target/cim-forward-route-1.0.0-SNAPSHOT.jar /root/work/route/\n\nappname=\"route\" ;\nPID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}')\n\n# 遍历杀掉 pid\nfor var in ${PID[@]};\ndo\n    echo \"loop pid= $var\"\n    kill -9 $var\ndone\n\necho \"开始部署路由。。。。\"\n\nsh /root/work/route/route-startup.sh\n\necho \"部署路由成功！\"\n\n#=======================================\n# 部署server\ncp /root/work/netty-action/cim-server/target/cim-server-1.0.0-SNAPSHOT.jar /root/work/server/\n\nappname=\"cim-server\" ;\nPID=$(ps -ef | grep $appname | grep -v grep | awk '{print $2}')\n\n# 遍历杀掉 pid\nfor var in ${PID[@]};\ndo\n    echo \"loop pid= $var\"\n    kill -9 $var\ndone\n\necho \"开始部署服务1。。。。\"\nsh /root/work/server/server-startup.sh\necho \"部署服务1成功！\"\n\n\necho \"开始部署服务2。。。。\"\ncp /root/work/netty-action/cim-server/target/cim-server-1.0.0-SNAPSHOT.jar /root/work/server2/\nsh /root/work/server2/server-startup.sh\necho \"部署服务2成功！\"\n\n\n"
  },
  {
    "path": "script/route-startup.sh",
    "content": "#!/usr/bin/env bash\nnohup java -jar  /root/work/route/cim-forward-route-1.0.0-SNAPSHOT.jar  > /root/work/route/log.file 2>&1 &"
  },
  {
    "path": "script/server-startup.sh",
    "content": "#!/usr/bin/env bash\nnohup java -jar  /root/work/server/cim-server-1.0.0-SNAPSHOT.jar --cim.server.port=9000  > /root/work/server/log.file 2>&1 &"
  },
  {
    "path": "sql/01schema.sql",
    "content": "\ncreate database `cim-test` default character set utf8mb4 collate utf8mb4_general_ci;\n"
  },
  {
    "path": "sql/offline_msg.sql",
    "content": "CREATE TABLE offline_msg (\n                             id BIGINT PRIMARY KEY AUTO_INCREMENT,\n                             message_id BIGINT NOT NULL,\n                             receive_user_id BIGINT NOT NULL,\n                             content VARCHAR(2000),\n                             message_type INT,\n                             status TINYINT,  -- 0: Pending, 1: Acked\n                             created_at DATETIME,\n                             properties VARCHAR(2000),\n                             INDEX idx_receive_user_id (receive_user_id)\n);"
  },
  {
    "path": "sql/offline_msg_last_send_record.sql",
    "content": "CREATE TABLE offline_msg_last_send_record\n(\n    receive_user_id         BIGINT NOT NULL PRIMARY KEY,\n    last_message_id BIGINT,\n    updated_at      DATETIME\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"
  }
]