Full Code of crossoverJie/cim for AI

master f1f3bae6a3d5 cached
226 files
423.0 KB
113.6k tokens
872 symbols
1 requests
Download .txt
Showing preview only (495K chars total). Download the full file or copy to clipboard to get everything.
Repository: crossoverJie/cim
Branch: master
Commit: f1f3bae6a3d5
Files: 226
Total size: 423.0 KB

Directory structure:
gitextract_k26pvt4s/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   └── workflows/
│       ├── docker.yml
│       ├── maven.yml
│       └── reusable_run_tests.yml
├── .gitignore
├── CLAUDE.md
├── LICENSE
├── Makefile
├── README-zh.md
├── README.md
├── checkstyle/
│   ├── checkstyle.xml
│   └── suppressions.xml
├── cim-client/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── crossoverjie/
│       │   │           └── cim/
│       │   │               └── client/
│       │   │                   ├── CIMClientApplication.java
│       │   │                   ├── config/
│       │   │                   │   ├── AppConfiguration.java
│       │   │                   │   ├── BeanConfig.java
│       │   │                   │   └── SwaggerConfig.java
│       │   │                   ├── scanner/
│       │   │                   │   └── Scan.java
│       │   │                   ├── service/
│       │   │                   │   ├── InnerCommand.java
│       │   │                   │   ├── InnerCommandContext.java
│       │   │                   │   ├── MsgHandle.java
│       │   │                   │   ├── MsgLogger.java
│       │   │                   │   ├── ShutDownSign.java
│       │   │                   │   └── impl/
│       │   │                   │       ├── AsyncMsgLogger.java
│       │   │                   │       ├── EchoServiceImpl.java
│       │   │                   │       ├── MsgCallBackListener.java
│       │   │                   │       ├── MsgHandler.java
│       │   │                   │       └── command/
│       │   │                   │           ├── CloseAIModelCommand.java
│       │   │                   │           ├── DelayMsgCommand.java
│       │   │                   │           ├── EchoInfoCommand.java
│       │   │                   │           ├── EmojiCommand.java
│       │   │                   │           ├── OpenAIModelCommand.java
│       │   │                   │           ├── PrefixSearchCommand.java
│       │   │                   │           ├── PrintAllCommand.java
│       │   │                   │           ├── PrintOnlineUsersCommand.java
│       │   │                   │           ├── QueryHistoryCommand.java
│       │   │                   │           └── ShutDownCommand.java
│       │   │                   └── util/
│       │   │                       └── SpringBeanFactory.java
│       │   └── resources/
│       │       ├── application.yaml
│       │       └── banner.txt
│       └── test/
│           ├── java/
│           │   └── com/
│           │       └── crossoverjie/
│           │           └── cim/
│           │               ├── client/
│           │               │   └── service/
│           │               │       ├── InnerCommandContextTest.java
│           │               │       └── impl/
│           │               │           └── AsyncMsgLoggerTest.java
│           │               └── server/
│           │                   └── test/
│           │                       ├── CommonTest.java
│           │                       └── EchoTest.java
│           └── resources/
│               └── application.yaml
├── cim-client-sdk/
│   ├── README.md
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   └── java/
│       │       └── com/
│       │           └── crossoverjie/
│       │               └── cim/
│       │                   └── client/
│       │                       └── sdk/
│       │                           ├── Client.java
│       │                           ├── ClientBuilder.java
│       │                           ├── ClientState.java
│       │                           ├── Event.java
│       │                           ├── FetchOfflineMsgJob.java
│       │                           ├── ReConnectManager.java
│       │                           ├── RouteManager.java
│       │                           ├── impl/
│       │                           │   ├── ClientBuilderImpl.java
│       │                           │   ├── ClientConfigurationData.java
│       │                           │   └── ClientImpl.java
│       │                           └── io/
│       │                               ├── CIMClientHandle.java
│       │                               ├── CIMClientHandleInitializer.java
│       │                               ├── MessageListener.java
│       │                               ├── ReconnectCheck.java
│       │                               └── backoff/
│       │                                   ├── BackoffStrategy.java
│       │                                   └── RandomBackoff.java
│       └── test/
│           ├── java/
│           │   └── com/
│           │       └── crossoverjie/
│           │           └── cim/
│           │               └── client/
│           │                   └── sdk/
│           │                       ├── ClientTest.java
│           │                       └── OfflineMsgTest.java
│           └── resources/
│               ├── application-route.yaml
│               └── init.sql
├── cim-common/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── crossoverjie/
│       │   │           └── cim/
│       │   │               └── common/
│       │   │                   ├── constant/
│       │   │                   │   └── Constants.java
│       │   │                   ├── core/
│       │   │                   │   └── proxy/
│       │   │                   │       ├── DynamicUrl.java
│       │   │                   │       ├── Request.java
│       │   │                   │       └── RpcProxyManager.java
│       │   │                   ├── data/
│       │   │                   │   └── construct/
│       │   │                   │       ├── RingBufferWheel.java
│       │   │                   │       ├── SortArrayMap.java
│       │   │                   │       └── TrieTree.java
│       │   │                   ├── enums/
│       │   │                   │   ├── StatusEnum.java
│       │   │                   │   └── SystemCommandEnum.java
│       │   │                   ├── exception/
│       │   │                   │   ├── CIMException.java
│       │   │                   │   └── GenericException.java
│       │   │                   ├── kit/
│       │   │                   │   └── HeartBeatHandler.java
│       │   │                   ├── metastore/
│       │   │                   │   ├── AbstractConfiguration.java
│       │   │                   │   ├── MetaStore.java
│       │   │                   │   ├── ZkConfiguration.java
│       │   │                   │   └── ZkMetaStoreImpl.java
│       │   │                   ├── pojo/
│       │   │                   │   ├── CIMUserInfo.java
│       │   │                   │   └── RouteInfo.java
│       │   │                   ├── req/
│       │   │                   │   └── BaseRequest.java
│       │   │                   ├── res/
│       │   │                   │   ├── BaseResponse.java
│       │   │                   │   └── NULLBody.java
│       │   │                   ├── route/
│       │   │                   │   └── algorithm/
│       │   │                   │       ├── RouteHandle.java
│       │   │                   │       ├── consistenthash/
│       │   │                   │       │   ├── AbstractConsistentHash.java
│       │   │                   │       │   ├── ConsistentHashHandle.java
│       │   │                   │       │   ├── SortArrayMapConsistentHash.java
│       │   │                   │       │   └── TreeMapConsistentHash.java
│       │   │                   │       ├── loop/
│       │   │                   │       │   └── LoopHandle.java
│       │   │                   │       └── random/
│       │   │                   │           └── RandomHandle.java
│       │   │                   └── util/
│       │   │                       ├── HttpClient.java
│       │   │                       ├── NettyAttrUtil.java
│       │   │                       ├── RouteInfoParseUtil.java
│       │   │                       ├── SnowflakeIdWorker.java
│       │   │                       └── StringUtil.java
│       │   ├── proto/
│       │   │   └── cim.proto
│       │   └── resources/
│       │       └── log4j.properties
│       └── test/
│           └── java/
│               └── com/
│                   └── crossoverjie/
│                       └── cim/
│                           └── common/
│                               ├── CommonTest.java
│                               ├── core/
│                               │   └── proxy/
│                               │       └── RpcProxyManagerTest.java
│                               ├── data/
│                               │   └── construct/
│                               │       ├── RingBufferWheelTest.java
│                               │       ├── ScheduledTest.java
│                               │       ├── SortArrayMapTest.java
│                               │       ├── TimerTest.java
│                               │       └── TrieTreeTest.java
│                               ├── enums/
│                               │   └── SystemCommandEnumTypeTest.java
│                               ├── metastore/
│                               │   └── MetaStoreTest.java
│                               ├── route/
│                               │   └── algorithm/
│                               │       ├── consistenthash/
│                               │       │   ├── ConsistentHashHandleTest.java
│                               │       │   ├── RangeCheckTestUtil.java
│                               │       │   ├── SortArrayMapConsistentHashTest.java
│                               │       │   └── TreeMapConsistentHashTest.java
│                               │       ├── loop/
│                               │       │   └── LoopHandleTest.java
│                               │       └── random/
│                               │           └── RandomHandleTest.java
│                               └── util/
│                                   ├── HttpClientTest.java
│                                   └── ProtocolTest.java
├── cim-forward-route/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── crossoverjie/
│       │   │           └── cim/
│       │   │               └── route/
│       │   │                   ├── RouteApplication.java
│       │   │                   ├── config/
│       │   │                   │   ├── AppConfiguration.java
│       │   │                   │   ├── BeanConfig.java
│       │   │                   │   ├── MySqlPersistenceConfig.java
│       │   │                   │   ├── OfflineMsgStoreConfig.java
│       │   │                   │   └── SwaggerConfig.java
│       │   │                   ├── constant/
│       │   │                   │   └── Constant.java
│       │   │                   ├── controller/
│       │   │                   │   └── RouteController.java
│       │   │                   ├── exception/
│       │   │                   │   └── ExceptionHandlingController.java
│       │   │                   ├── factory/
│       │   │                   │   └── OfflineMsgFactory.java
│       │   │                   ├── kit/
│       │   │                   │   └── NetAddressIsReachable.java
│       │   │                   ├── service/
│       │   │                   │   ├── AccountService.java
│       │   │                   │   ├── CommonBizService.java
│       │   │                   │   ├── OfflineMsgService.java
│       │   │                   │   ├── UserInfoCacheService.java
│       │   │                   │   └── impl/
│       │   │                   │       ├── AccountServiceRedisImpl.java
│       │   │                   │       ├── OfflineMsgServiceImpl.java
│       │   │                   │       └── UserInfoCacheServiceImpl.java
│       │   │                   └── util/
│       │   │                       └── SpringBeanFactory.java
│       │   └── resources/
│       │       ├── application.yaml
│       │       ├── banner.txt
│       │       └── lua/
│       │           └── offLine.lua
│       └── test/
│           ├── java/
│           │   ├── CommonTest.java
│           │   └── com/
│           │       └── crossoverjie/
│           │           └── cim/
│           │               └── route/
│           │                   └── service/
│           │                       └── impl/
│           │                           ├── AbstractBaseTest.java
│           │                           ├── AccountServiceRedisImplTest.java
│           │                           ├── RedisTest.java
│           │                           └── UserInfoCacheServiceImplTest.java
│           └── resources/
│               ├── application.yaml
│               └── init.sql
├── cim-integration-test/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   └── java/
│       │       └── com/
│       │           └── crossoverjie/
│       │               └── cim/
│       │                   └── client/
│       │                       └── sdk/
│       │                           ├── route/
│       │                           │   ├── AbstractRouteBaseTest.java
│       │                           │   └── OfflineMsgStoreRouteBaseTest.java
│       │                           └── server/
│       │                               └── AbstractServerBaseTest.java
│       └── test/
│           └── resources/
│               ├── application-client.yaml
│               └── application-route.yaml
├── cim-persistence/
│   ├── cim-persistence-api/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               └── com/
│   │                   └── crossoverjie/
│   │                       └── cim/
│   │                           └── persistence/
│   │                               └── api/
│   │                                   ├── config/
│   │                                   │   └── BeanConfig.java
│   │                                   ├── pojo/
│   │                                   │   ├── OfflineMsg.java
│   │                                   │   └── OfflineMsgLastSendRecord.java
│   │                                   ├── service/
│   │                                   │   └── OfflineMsgStore.java
│   │                                   └── vo/
│   │                                       └── req/
│   │                                           └── SaveOfflineMsgReqVO.java
│   ├── cim-persistence-mysql/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           ├── java/
│   │           │   └── com/
│   │           │       └── crossoverjie/
│   │           │           └── cim/
│   │           │               └── persistence/
│   │           │                   └── mysql/
│   │           │                       ├── config/
│   │           │                       │   └── MyBatisConfig.java
│   │           │                       ├── offlinemsg/
│   │           │                       │   ├── OfflineMsgDb.java
│   │           │                       │   └── mapper/
│   │           │                       │       ├── OfflineMsgLastSendRecordMapper.java
│   │           │                       │       └── OfflineMsgMapper.java
│   │           │                       └── util/
│   │           │                           └── MapToJsonTypeHandler.java
│   │           └── resources/
│   │               └── mapper/
│   │                   ├── OfflineMsgLastSendRecordMapper.xml
│   │                   └── OfflineMsgMapper.xml
│   ├── cim-persistence-redis/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           ├── java/
│   │           │   └── com/
│   │           │       └── crossoverjie/
│   │           │           └── cim/
│   │           │               └── persistence/
│   │           │                   └── redis/
│   │           │                       ├── OfflineMsgBuffer.java
│   │           │                       ├── constant/
│   │           │                       │   └── Constant.java
│   │           │                       └── kit/
│   │           │                           └── OfflineMsgScriptExecutor.java
│   │           └── resources/
│   │               └── lua/
│   │                   ├── deleteOfflineMsg.lua
│   │                   ├── fetchOfflineMsg.lua
│   │                   └── saveOfflineMsg.lua
│   └── pom.xml
├── cim-rout-api/
│   ├── pom.xml
│   └── src/
│       └── main/
│           └── java/
│               └── com/
│                   └── crossoverjie/
│                       └── cim/
│                           └── route/
│                               └── api/
│                                   ├── RouteApi.java
│                                   └── vo/
│                                       ├── req/
│                                       │   ├── ChatReqVO.java
│                                       │   ├── LoginReqVO.java
│                                       │   ├── OfflineMsgReqVO.java
│                                       │   ├── P2PReqVO.java
│                                       │   ├── RegisterInfoReqVO.java
│                                       │   └── SendMsgReqVO.java
│                                       └── res/
│                                           ├── CIMServerResVO.java
│                                           ├── RegisterInfoResVO.java
│                                           └── SendMsgResVO.java
├── cim-server/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── crossoverjie/
│       │   │           └── cim/
│       │   │               └── server/
│       │   │                   ├── CIMServerApplication.java
│       │   │                   ├── config/
│       │   │                   │   ├── AppConfiguration.java
│       │   │                   │   ├── BeanConfig.java
│       │   │                   │   └── SwaggerConfig.java
│       │   │                   ├── controller/
│       │   │                   │   └── IndexController.java
│       │   │                   ├── handle/
│       │   │                   │   └── CIMServerHandle.java
│       │   │                   ├── init/
│       │   │                   │   └── CIMServerInitializer.java
│       │   │                   ├── kit/
│       │   │                   │   ├── RegistryMetaStore.java
│       │   │                   │   ├── RouteHandler.java
│       │   │                   │   └── ServerHeartBeatHandlerImpl.java
│       │   │                   ├── server/
│       │   │                   │   └── CIMServer.java
│       │   │                   └── util/
│       │   │                       ├── SessionSocketHolder.java
│       │   │                       └── SpringBeanFactory.java
│       │   └── resources/
│       │       ├── application.yaml
│       │       └── banner.txt
│       └── test/
│           └── com/
│               └── crossoverjie/
│                   └── cim/
│                       └── server/
│                           └── util/
│                               └── NettyAttrUtilTest.java
├── cim-server-api/
│   ├── pom.xml
│   └── src/
│       └── main/
│           └── java/
│               └── com/
│                   └── crossoverjie/
│                       └── cim/
│                           └── server/
│                               └── api/
│                                   ├── ServerApi.java
│                                   └── vo/
│                                       ├── req/
│                                       │   └── SendMsgReqVO.java
│                                       └── res/
│                                           ├── OfflineMsgResVO.java
│                                           ├── SaveOfflineMsgResVO.java
│                                           └── SendMsgResVO.java
├── doc/
│   └── QA.md
├── docker/
│   ├── README.md
│   ├── allin1-ubuntu.Dockerfile
│   ├── client-ubuntu.Dockerfile
│   ├── supervisord.conf
│   └── wait-for-it.sh
├── pom.xml
├── script/
│   ├── build.sh
│   ├── deploy.sh
│   ├── route-startup.sh
│   └── server-startup.sh
└── sql/
    ├── 01schema.sql
    ├── offline_msg.sql
    └── offline_msg_last_send_record.sql

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: 'Please check if there are similar issues before submitting'
labels: ''
assignees: ''

---

**Bug Description**
A clear and concise description of what the bug is.

**Reproduce**
step:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected results**
A clear and concise description of what you expected to happen.

**Screenshot**
If applicable, add screenshots to help explain your problem.


**Additional Information**
Add any other context about the problem here.


================================================
FILE: .github/workflows/docker.yml
================================================
name: Multi-Platform Docker Build

on:
  push:
    tags: ['image-*']
  workflow_dispatch:    # keep this to allow manual triggering of the workflow

jobs:
  build-multi-arch:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write  # multipass needs this permission to authenticate with the registry

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          driver: docker-container
          platforms: linux/amd64,linux/arm64,linux/arm/v7  # set the platforms you want to build for

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and Push Multi-Arch Image
        uses: docker/build-push-action@v5
        with:
          context: .
          file: docker/allin1-ubuntu.Dockerfile
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          push: true
          tags: |
            ghcr.io/crossoverjie/allin1-ubuntu:latest
            ghcr.io/crossoverjie/allin1-ubuntu:${{ github.run_id }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

================================================
FILE: .github/workflows/maven.yml
================================================
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: Java CI with Maven

on:
  push:
    branches: [ "master" ]
  pull_request:
    # Run on all pull requests regardless of target branch to support stacked PRs
    branches: [ "**" ]

jobs:
  build:
    uses: ./.github/workflows/reusable_run_tests.yml
    secrets:
      codecov_token: ${{ secrets.CODECOV_TOKEN }}


================================================
FILE: .github/workflows/reusable_run_tests.yml
================================================
name: A reusable workflow to build and run the unit test suite

on:
  workflow_call:
    secrets:
      codecov_token:
        required: true
  workflow_dispatch:

jobs:
  build_and_test:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: maven
      - name: Run Checkstyle
        run: mvn checkstyle:check --file pom.xml
        continue-on-error: true
      - name: Build with Maven
        run: mvn -B package --file pom.xml
      - name: Upload coverage reports to Codecov
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          verbose: true

================================================
FILE: .gitignore
================================================
# Created by .ignore support plugin (hsz.mobi)
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Maven template
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties

# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
!/.mvn/wrapper/maven-wrapper.jar
### Java template
# Compiled class file
*.class

# Log file
*.log

# BlueJ files
*.ctxt

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839

# User-specific stuff
.idea/**/tasks.xml
.idea/**/dictionaries
.idea/**/shelf

# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml

# Gradle
.idea/**/gradle.xml

# CMake
cmake-build-debug/
cmake-build-release/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests
.idea/
*.iml

# Eclipse Project 
bin
*.project
*.settings/
*.classpath
*.factorypath
.vscode/
/.metadata/




================================================
FILE: CLAUDE.md
================================================
# CIM Project Guide

CIM (Cross-platform Instant Messaging) is a Java-based instant messaging framework.

## Requirements

- **Minimum JDK Version**: JDK 17
- **Build Tool**: Maven
- **Spring Boot Version**: 3.3.0

## Environment Setup

Before running compile or test commands, set the correct JDK version:

```bash
export JAVA_HOME=$JAVA_17_HOME
```

`JAVA_17_HOME` is defined in `~/.zshrc`.

## Common Commands

### Compile the project
```bash
mvn clean compile
```

### Run tests
```bash
mvn test
```

### Package the project
```bash
mvn clean package -DskipTests
```

### Full build (with tests)
```bash
mvn clean install
```

## Notes

- Checkstyle code style checks run automatically during Maven's `validate` phase
- Ensure code passes Checkstyle checks before committing


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 crossoverJie

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
IMAGE_NAME = allin1-ubuntu
REGISTRY = ghcr.io
OWNER = $(shell git config --get remote.origin.url | sed -e 's/.*github.com[:/]\([^/]*\)\/.*/\1/')
TAG_PREFIX = image

# make tag VERSION=v1.0
tag:
	$(if $(VERSION),,$(error VERSION variable not set. Usage: make tag VERSION=x.y.z))
	git tag -f $(TAG_PREFIX)-$(VERSION)
	git push origin $(TAG_PREFIX)-$(VERSION)

# list all tags with the prefix
list-tags:
	git tag -l "$(TAG_PREFIX)-*" | sort -V

# get the latest tag
get-latest:
	@git describe --abbrev=0 --tags --match="$(TAG_PREFIX)-*" 2>/dev/null | sed 's/$(TAG_PREFIX)-//' || echo "No tags found"

# make version TYPE=major|minor|patch
version:
	$(if $(TYPE),,$(error TYPE variable not set. Options: major, minor, patch))
	$(eval CURRENT := $(shell make get-latest))
	$(if $(CURRENT),,$(error No existing tags found. Create first tag with: make tag VERSION=1.0.0))

	$(eval MAJOR := $(shell echo $(CURRENT) | cut -d. -f1))
	$(eval MINOR := $(shell echo $(CURRENT) | cut -d. -f2))
	$(eval PATCH := $(shell echo $(CURRENT) | cut -d. -f3))

	$(if $(filter $(TYPE),major),$(eval NEW_VERSION := $(($(MAJOR)+1)).0.0))
	$(if $(filter $(TYPE),minor),$(eval NEW_VERSION := $(MAJOR).$(($(MINOR)+1)).0))
	$(if $(filter $(TYPE),patch),$(eval NEW_VERSION := $(MAJOR).$(MINOR).$(($(PATCH)+1))))

	make tag VERSION=$(NEW_VERSION)

.PHONY: tag list-tags get-latest version

================================================
FILE: README-zh.md
================================================



<div align="center">

<img src="https://i.loli.net/2020/02/21/rfOGvKlTcHCmM92.png"  />
<br/>

[![codecov](https://codecov.io/gh/crossoverJie/cim/graph/badge.svg?token=oW5Gd1oKmf)](https://codecov.io/gh/crossoverJie/cim)
[![Build Status](https://img.shields.io/badge/cim-cross--im-brightgreen.svg)](https://github.com/crossoverJie/cim)
[![](https://badge.juejin.im/entry/5c2c000e6fb9a049f5713e26/likes.svg?style=flat-square)](https://juejin.im/post/5c2bffdc51882509181395d7)

📘[介绍](#介绍) |📽[视频演示](#视频演示) | 🏖[TODO LIST](#todo-list) | 🌈[系统架构](#系统架构) |💡[流程图](#流程图)|🌁[快速启动](#快速启动)|👨🏻‍✈️[内置命令](#客户端内置命令)|🎤[通信](#群聊私聊)|❓[QA](https://github.com/crossoverJie/cim/blob/master/doc/QA.md)|💌[联系作者](#联系作者)

[English](README.md)

</div>
<br/>

# V2.0
- [x] 升级至 JDK17 & springboot3.0
- [x] Client SDK
- [ ] 客户端使用 [picocli](https://picocli.info/) 替代 springboot
- [x] 支持集成测试
- [ ] 集成 OpenTelemetry
- [ ] 支持单节点启动(不依赖外部组件)
- [ ] 第三方组件支持替换(Redis/Zookeeper 等)
- [ ] 支持 Web 客户端(websocket)
- [x] 支持 Docker 容器
- [ ] 支持 Kubernetes 部署
- [ ] 支持二进制客户端(使用 golang 构建)

## 介绍

`CIM(CROSS-IM)` 是面向开发者的 `IM(即时通讯)` 系统;同时提供了一些组件帮助开发者构建自己可扩展的 `IM`。
借助 `CIM` 你可以实现以下需求:
- `IM` 即时通讯系统。
- `APP` 消息推送中间件。
- `IOT` 海量连接场景中的消息中间件。

> 如果在使用或开发过程中有任何问题,可以[联系作者](#联系作者)。

## 视频演示

> 点击下方链接可以查看视频版 Demo。

| YouTube | Bilibili|
| :------:| :------: |
| [群聊](https://youtu.be/_9a4lIkQ5_o) [私聊](https://youtu.be/kfEfQFPLBTQ) | [群聊](https://www.bilibili.com/video/av39405501) [私聊](https://www.bilibili.com/video/av39405821) |
| <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" />

![demo.gif](pic/demo.gif)

## TODO LIST

* [x] [群聊](#群聊)
* [x] [私聊](#私聊)
* [x] [内置命令](#客户端内置命令)
* [x] [聊天记录查询](#聊天记录查询)
* [x] [一键开启 AI 模式](#ai-模式)
* [x] 使用 `Google Protocol Buffer` 高效编解码
* [x] 根据实际情况灵活的水平扩容、缩容
* [x] 服务端自动剔除离线客户端
* [x] 客户端自动重连
* [x] [延时消息](#延时消息)
* [x] SDK 开发包
* [ ] 分组群聊
* [ ] 离线消息
* [ ] 消息加密



## 系统架构

![](pic/architecture.png)

- `CIM` 中的各个组件均采用 `SpringBoot` 构建
  - 客户端基于 [cim-client-sdk](https://github.com/crossoverJie/cim/tree/master/cim-client-sdk) 构建
- 采用 `Netty` 构建底层通信
- `MetaStore` 用于 `IM-server` 服务的注册与发现


### cim-server
IM 服务端,用于接收客户端连接、消息转发、消息推送等功能。
支持集群部署。

### cim-route

路由服务器;用于处理消息路由、消息转发、用户登录、用户下线以及一些运维工具(获取在线用户数等)。

### cim-client
IM 客户端终端,一个命令即可启动并与其他人进行通信(群聊、私聊)。

## 流程图

![](https://s2.loli.net/2024/10/13/8teMn7BSa5VWuvi.png)

- Server 注册到 `MetaStore`
- Route 订阅 `MetaStore`
- Client 登录到 Route
  - Route 从 `MetaStore` 获取 Server 信息
- Client 与 Server 建立连接
- Client1 发送消息到 Route
- Route 选择 Server 并将消息转发给 Server
- Server 将消息推送给 Client2


## 快速启动

### Docker

`allin1` 镜像内置了 Zookeeper、Redis、cim-server、cim-forward-route 四个服务,使用 [Supervisor](http://supervisord.org/) 统一管理,开箱即用。

**支持平台:** linux/amd64, linux/arm64, linux/arm/v7

**端口说明:**

| 端口 | 服务 | 说明 |
|------|---------|-------------|
| 2181 | Zookeeper | 服务注册与发现 |
| 6379 | Redis | 数据缓存 |
| 8083 | Route Server | HTTP API 路由服务 |

拉取镜像并启动:

```shell
docker pull ghcr.io/crossoverjie/allin1-ubuntu:latest
docker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 ghcr.io/crossoverjie/allin1-ubuntu:latest
```

容器启动后,可参考下方 [注册账号](#注册账号) 和 [启动客户端](#启动客户端) 章节快速体验完整的 IM 流程。

### 本地构建 Docker 镜像

如果需要从源码构建镜像:

```shell
# 在项目根目录执行
docker build -t cim-allin1:latest -f docker/allin1-ubuntu.Dockerfile .
docker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 cim-allin1:latest
```

### 本地编译

首先需要安装 `Zookeeper`、`Redis` 并保证网络通畅。

```shell
docker run --rm --name zookeeper -d -p 2181:2181 zookeeper:3.9.2
docker run --rm --name redis -d -p 6379:6379 redis:7.4.0
```

```shell
git clone https://github.com/crossoverJie/cim.git
cd cim
mvn clean install -DskipTests=true
cd cim-server && cim-client && cim-forward-route
mvn clean package spring-boot:repackage -DskipTests=true
```

### 部署 IM-server(cim-server)

```shell
cp /cim/cim-server/target/cim-server-1.0.0-SNAPSHOT.jar /xx/work/server0/
cd /xx/work/server0/
nohup 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 &
```

> cim-server 集群部署同理,只要保证 Zookeeper 地址相同即可。

### 部署路由服务器(cim-forward-route)

```shell
cp /cim/cim-server/cim-forward-route/target/cim-forward-route-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
nohup 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 &
```

> cim-forward-route 本身就是无状态,可以部署多台;使用 Nginx 代理即可。


### 启动客户端

```shell
cp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
java -jar cim-client-1.0.0-SNAPSHOT.jar --server.port=8084 --cim.user.id=唯一客户端ID --cim.user.userName=用户名 --cim.route.url=http://路由服务器:8083/
```

![](https://ws2.sinaimg.cn/large/006tNbRwly1fylgxjgshfj31vo04m7p9.jpg)
![](https://ws1.sinaimg.cn/large/006tNbRwly1fylgxu0x4uj31hy04q75z.jpg)

如上图,启动两个客户端可以互相通信即可。

### 本地启动客户端

#### 注册账号
```shell
curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{
  "reqNo": "1234567890",
  "timeStamp": 0,
  "userName": "zhangsan"
}' 'http://路由服务器:8083/registerAccount'
```

从返回结果中获取 `userId`

```json
{
    "code":"9000",
    "message":"成功",
    "reqNo":null,
    "dataBody":{
        "userId":1547028929407,
        "userName":"test"
    }
}
```

#### 启动本地客户端
```shell
# 启动本地客户端
cp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
java -jar cim-client-1.0.0-SNAPSHOT.jar --server.port=8084 --cim.user.id=上方返回的userId --cim.user.userName=用户名 --cim.route.url=http://路由服务器:8083/
```

## 客户端内置命令

| 命令 | 描述|
| ------ | ------ |
| `:q!` | 退出客户端|
| `:olu` | 获取所有在线用户信息 |
| `:all` | 获取所有命令 |
| `:q [option]` | 【:q 关键字】查询聊天记录 |
| `:ai` | 开启 AI 模式 |
| `:qai` | 关闭 AI 模式 |
| `:pu` | 模糊匹配用户 |
| `:info` | 获取客户端信息 |
| `:emoji [option]` | 查询表情包 [option:页码] |
| `:delay [msg] [delayTime]` | 发送延时消息 |
| `:` | 更多命令正在开发中。。 |

![](https://ws3.sinaimg.cn/large/006tNbRwly1fylh7bdlo6g30go01shdt.gif)

### 聊天记录查询

![](https://i.loli.net/2019/05/08/5cd1c310cb796.jpg)

使用命令 `:q 关键字` 即可查询与个人相关的聊天记录。

> 客户端聊天记录默认存放在 `/opt/logs/cim/`,所以需要这个目录的写入权限。也可在启动命令中加入 `--cim.msg.logger.path = /自定义` 参数自定义目录。



### AI 模式

![](https://i.loli.net/2019/05/08/5cd1c30e47d95.jpg)

使用命令 `:ai` 开启 AI 模式,之后所有的消息都会由 `AI` 响应。

`:qai` 退出 AI 模式。

### 前缀匹配用户名

![](https://i.loli.net/2019/05/08/5cd1c32ac3397.jpg)

使用命令 `:qu prefix` 可以按照前缀的方式搜索用户信息。

> 该功能主要用于在移动端中的输入框中搜索用户。

### 群聊/私聊

#### 群聊

![](https://ws1.sinaimg.cn/large/006tNbRwly1fyli54e8e1j31t0056x11.jpg)
![](https://ws3.sinaimg.cn/large/006tNbRwly1fyli5yyspmj31im06atb8.jpg)
![](https://ws3.sinaimg.cn/large/006tNbRwly1fyli6sn3c8j31ss06qmzq.jpg)

群聊只需要在控制台里输入消息回车后即可发送,同时所有在线客户端都可收到消息。

#### 私聊

私聊首先需要知道对方的 `userID` 才能进行。

输入命令 `:olu` 可列出所有在线用户。

![](https://ws4.sinaimg.cn/large/006tNbRwly1fyli98mlf3j31ta06mwhv.jpg)

接着使用 `userId;;消息内容` 的格式即可发送私聊消息。

![](https://ws4.sinaimg.cn/large/006tNbRwly1fylib08qlnj31sk082zo6.jpg)
![](https://ws1.sinaimg.cn/large/006tNbRwly1fylibc13etj31wa0564lp.jpg)
![](https://ws3.sinaimg.cn/large/006tNbRwly1fylicmjj6cj31wg07c4qp.jpg)
![](https://ws1.sinaimg.cn/large/006tNbRwly1fylicwhe04j31ua03ejsv.jpg)

同时另一个账号收不到消息。
![](https://ws3.sinaimg.cn/large/006tNbRwly1fylie727jaj31t20dq1ky.jpg)



### emoji 表情支持

使用命令 `:emoji 1` 查询出所有表情列表,使用表情别名即可发送表情。

![](https://tva1.sinaimg.cn/large/006y8mN6ly1g6j910cqrzj30dn05qjw9.jpg)
![](https://tva1.sinaimg.cn/large/006y8mN6ly1g6j99hazg6j30ax03hq35.jpg)

### 延时消息

发送 10s 的延时消息:

```shell
:delay delayMsg 10
```

![](pic/delay.gif)

## 联系作者

## 贡献指南

欢迎贡献代码!提交 PR 前,请确保代码通过 Checkstyle 检查。

### 代码风格

本项目使用 [Checkstyle](https://checkstyle.org/) 来规范代码风格,规则定义在 `checkstyle/checkstyle.xml` 中。

**提交前在本地运行 Checkstyle:**

```shell
mvn checkstyle:check
```

**主要规则:**
- `{`、`}` 和运算符前后使用空格
- 行尾不能有空格
- 文件必须以换行符结尾
- 删除未使用的 import
- 常量(`static final`)必须使用 `UPPER_SNAKE_CASE` 命名
- 使用 Java 风格的数组声明:`String[] args`(而非 `String args[]`)

**快速构建时跳过 Checkstyle:**

```shell
mvn package -Dcheckstyle.skip=true
```

<div align="center">

<a href="https://t.zsxq.com/odQDJ" target="_blank"><img src="https://s2.loli.net/2024/05/17/zRkabDu2SKfChLX.png" alt="202405171520366.png"></a>
</div>

最近开通了知识星球,感谢大家对 CIM 的支持,为大家提供 100 份 10 元优惠券,也就是 69-10=59 元,具体福利大家可以扫码参考再决定是否加入。

> PS: 后续会在星球开始 V2.0 版本的重构,感兴趣的可以加入星球当面催更(当然代码依然会开源)。

- [crossoverJie@gmail.com](mailto:crossoverJie@gmail.com)
- 微信公众号

![index.jpg](https://i.loli.net/2021/10/12/ckQW9LYXSxFogJZ.jpg)


================================================
FILE: README.md
================================================



<div align="center">

<img src="https://i.loli.net/2020/02/21/rfOGvKlTcHCmM92.png"  />
<br/>

[![codecov](https://codecov.io/gh/crossoverJie/cim/graph/badge.svg?token=oW5Gd1oKmf)](https://codecov.io/gh/crossoverJie/cim)
[![Build Status](https://img.shields.io/badge/cim-cross--im-brightgreen.svg)](https://github.com/crossoverJie/cim)
[![](https://badge.juejin.im/entry/5c2c000e6fb9a049f5713e26/likes.svg?style=flat-square)](https://juejin.im/post/5c2bffdc51882509181395d7)

📘[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)

[中文文档](README-zh.md)

</div>
<br/>

# V2.0
- [x] Upgrade to JDK17 & springboot3.0
- [x] Client SDK
- [ ] Client use [picocli](https://picocli.info/) instead of springboot.
- [x] Support integration testing.
- [ ] Integrate OpenTelemetry .
- [ ] Support single node startup(Contains no components).
- [ ] Third-party components support replacement(Redis/Zookeeper, etc.).
- [ ] Support web client(websocket).
- [x] Support docker container.
- [ ] Support kubernetes operation.
- [ ] Supports binary client(build with golang).

## Introduction

`CIM(CROSS-IM)` is an `IM (instant messaging)` system for developers; it also provides some components to help developers build their own scalable `IM`.
Using `CIM`, you can achieve the following requirements:
- `IM` instant messaging system.
- Message push middleware for `APP`.
- Message middleware for `IOT` massive connection scenarios.

> If you have any questions during use or development, you can [contact the author](#contact).

## Video Demo

> Click the links below to watch the video demo.

| YouTube | Bilibili|
| :------:| :------: |
| [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) |
| <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" />

![demo.gif](pic/demo.gif)

## TODO LIST

* [x] [Group Chat](#group-chat)
* [x] [Private Chat](#private-chat)
* [x] [Built-in Commands](#built-in-commands)
* [x] [Chat History Query](#chat-history-query)
* [x] [AI Mode](#ai-mode)
* [x] Efficient encoding/decoding with `Google Protocol Buffer`
* [x] Flexible horizontal scaling based on actual needs
* [x] Server-side automatic removal of offline clients
* [x] Client automatic reconnection
* [x] [Delayed Messages](#delayed-messages)
* [x] SDK development package
* [ ] Group categorization
* [ ] Offline messages
* [ ] Message encryption



## Architecture

![](pic/architecture.png)

- Each component in `CIM` is built using `SpringBoot`
  - Client build with [cim-client-sdk](https://github.com/crossoverJie/cim/tree/master/cim-client-sdk)
- Use `Netty` to build the underlying communication.
- `MetaStore` is used for registration and discovery of `IM-server` services.


### cim-server
IM server is used to receive client connections, message forwarding, message push, etc.
Support cluster deployment.

### cim-route

Route server; used to process message routing, message forwarding, user login, user offline, and some operation tools (get the number of online users, etc.).

### cim-client
IM client terminal, a command can be started and initiated to communicate with others (group chat, private chat).

## Flow Chart

![](https://s2.loli.net/2024/10/13/8teMn7BSa5VWuvi.png)

- Server register to `MetaStore`
- Route subscribe `MetaStore`
- Client login to Route
  - Route get Server info from `MetaStore`
- Client open connection to Server
- Client1 send message to Route
- Route select Server and forward message to Server
- Server push message to Client2


## Quick Start

### Docker

The `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.

**Supported platforms:** linux/amd64, linux/arm64, linux/arm/v7

**Port mapping:**

| Port | Service | Description |
|------|---------|-------------|
| 2181 | Zookeeper | Service registration & discovery |
| 6379 | Redis | Data caching |
| 8083 | Route Server | HTTP API routing service |

Pull the image and start the container:

```shell
docker pull ghcr.io/crossoverjie/allin1-ubuntu:latest
docker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 ghcr.io/crossoverjie/allin1-ubuntu:latest
```

After the container starts, refer to the [Register Account](#register-account) and [Start Client](#start-client) sections below to experience the full IM workflow.

### Build Docker Image Locally

To build the Docker image from source:

```shell
# Run from the project root directory
docker build -t cim-allin1:latest -f docker/allin1-ubuntu.Dockerfile .
docker run -p 2181:2181 -p 6379:6379 -p 8083:8083 --rm --name cim-allin1 cim-allin1:latest
```

### Build from Source

First, install `Zookeeper` and `Redis` and ensure the network is accessible.

```shell
docker run --rm --name zookeeper -d -p 2181:2181 zookeeper:3.9.2
docker run --rm --name redis -d -p 6379:6379 redis:7.4.0
```

```shell
git clone https://github.com/crossoverJie/cim.git
cd cim
mvn clean install -DskipTests=true
cd cim-server && cim-client && cim-forward-route
mvn clean package spring-boot:repackage -DskipTests=true
```

### Deploy IM-server (cim-server)

```shell
cp /cim/cim-server/target/cim-server-1.0.0-SNAPSHOT.jar /xx/work/server0/
cd /xx/work/server0/
nohup 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 &
```

> For cim-server cluster deployment, just ensure all instances point to the same Zookeeper address.

### Deploy Route Server (cim-forward-route)

```shell
cp /cim/cim-server/cim-forward-route/target/cim-forward-route-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
nohup 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 &
```

> cim-forward-route is stateless and can be deployed on multiple nodes; use Nginx as a reverse proxy.


### Start Client

```shell
cp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
java -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/
```

![](https://ws2.sinaimg.cn/large/006tNbRwly1fylgxjgshfj31vo04m7p9.jpg)
![](https://ws1.sinaimg.cn/large/006tNbRwly1fylgxu0x4uj31hy04q75z.jpg)

As shown above, two clients can communicate with each other.

### Local Client Startup

#### Register Account
```shell
curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{
  "reqNo": "1234567890",
  "timeStamp": 0,
  "userName": "zhangsan"
}' 'http://<route-server>:8083/registerAccount'
```

Get the `userId` from the response:

```json
{
    "code":"9000",
    "message":"success",
    "reqNo":null,
    "dataBody":{
        "userId":1547028929407,
        "userName":"test"
    }
}
```

#### Start Local Client
```shell
# Start local client
cp /cim/cim-client/target/cim-client-1.0.0-SNAPSHOT.jar /xx/work/route0/
cd /xx/work/route0/
java -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/
```

## Built-in Commands

| Command | Description |
| ------ | ------ |
| `:q!` | Quit the client |
| `:olu` | List all online users |
| `:all` | Show all available commands |
| `:q [keyword]` | Search chat history by keyword |
| `:ai` | Enable AI mode |
| `:qai` | Disable AI mode |
| `:pu` | Fuzzy search users |
| `:info` | Show client information |
| `:emoji [option]` | Browse emoji list [option: page number] |
| `:delay [msg] [delayTime]` | Send a delayed message |
| `:` | More commands are under development... |

![](https://ws3.sinaimg.cn/large/006tNbRwly1fylh7bdlo6g30go01shdt.gif)

### Chat History Query

![](https://i.loli.net/2019/05/08/5cd1c310cb796.jpg)

Use the command `:q keyword` to search chat history related to you.

> 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.



### AI Mode

![](https://i.loli.net/2019/05/08/5cd1c30e47d95.jpg)

Use the command `:ai` to enable AI mode. After that, all messages will be responded to by `AI`.

Use `:qai` to exit AI mode.

### Prefix Match Username

![](https://i.loli.net/2019/05/08/5cd1c32ac3397.jpg)

Use the command `:qu prefix` to search user information by prefix.

> This feature is primarily designed for searching users in input fields on mobile clients.

### Group Chat/Private Chat

#### Group Chat

![](https://ws1.sinaimg.cn/large/006tNbRwly1fyli54e8e1j31t0056x11.jpg)
![](https://ws3.sinaimg.cn/large/006tNbRwly1fyli5yyspmj31im06atb8.jpg)
![](https://ws3.sinaimg.cn/large/006tNbRwly1fyli6sn3c8j31ss06qmzq.jpg)

For group chat, simply type a message in the console and press Enter to send. All online clients will receive the message.

#### Private Chat

To send a private message, you need to know the recipient's `userID`.

Use the command `:olu` to list all online users.

![](https://ws4.sinaimg.cn/large/006tNbRwly1fyli98mlf3j31ta06mwhv.jpg)

Then use the format `userId;;message content` to send a private message.

![](https://ws4.sinaimg.cn/large/006tNbRwly1fylib08qlnj31sk082zo6.jpg)
![](https://ws1.sinaimg.cn/large/006tNbRwly1fylibc13etj31wa0564lp.jpg)
![](https://ws3.sinaimg.cn/large/006tNbRwly1fylicmjj6cj31wg07c4qp.jpg)
![](https://ws1.sinaimg.cn/large/006tNbRwly1fylicwhe04j31ua03ejsv.jpg)

Meanwhile, the other account will not receive the message.
![](https://ws3.sinaimg.cn/large/006tNbRwly1fylie727jaj31t20dq1ky.jpg)



### Emoji Support

Use the command `:emoji 1` to list all available emojis. Use the emoji alias to send an emoji.

![](https://tva1.sinaimg.cn/large/006y8mN6ly1g6j910cqrzj30dn05qjw9.jpg)
![](https://tva1.sinaimg.cn/large/006y8mN6ly1g6j99hazg6j30ax03hq35.jpg)

### Delayed Messages

Send a message with a 10-second delay:

```shell
:delay delayMsg 10
```

![](pic/delay.gif)

## Contact

## Contributing

We welcome contributions! Before submitting a PR, please ensure your code passes the Checkstyle check.

### Code Style

This project uses [Checkstyle](https://checkstyle.org/) to enforce code style. The rules are defined in `checkstyle/checkstyle.xml`.

**Run Checkstyle locally before committing:**

```shell
mvn checkstyle:check
```

**Key rules:**
- Use spaces around `{`, `}`, and operators
- No trailing whitespace
- Files must end with a newline
- Remove unused imports
- Constants (`static final`) must be `UPPER_SNAKE_CASE`
- Use Java-style array declarations: `String[] args` (not `String args[]`)

**Skip Checkstyle for quick builds:**

```shell
mvn package -Dcheckstyle.skip=true
```

- [crossoverJie@gmail.com](mailto:crossoverJie@gmail.com)


================================================
FILE: checkstyle/checkstyle.xml
================================================
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
        "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
        "https://checkstyle.org/dtds/configuration_1_3.dtd">

<!--
    CIM Project Checkstyle Configuration
    Based on Google Java Style with customizations
-->
<module name="Checker">
    <property name="charset" value="UTF-8"/>
    <property name="severity" value="error"/>
    <property name="fileExtensions" value="java"/>

    <!-- Suppressions -->
    <module name="SuppressionFilter">
        <property name="file" value="${org.checkstyle.google.suppressionfilter.config}"
                  default="checkstyle/suppressions.xml"/>
        <property name="optional" value="true"/>
    </module>

    <!-- File Length -->
    <module name="FileLength">
        <property name="max" value="2000"/>
    </module>

    <!-- No Trailing Whitespace -->
    <module name="RegexpSingleline">
        <property name="format" value="\s+$"/>
        <property name="minimum" value="0"/>
        <property name="maximum" value="0"/>
        <property name="message" value="Line has trailing spaces."/>
    </module>

    <!-- Newline at end of file -->
    <module name="NewlineAtEndOfFile">
        <property name="lineSeparator" value="lf"/>
    </module>

    <!-- Line Length (must be at Checker level in Checkstyle 10.x) -->
    <module name="LineLength">
        <property name="max" value="150"/>
        <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
    </module>

    <module name="TreeWalker">
        <!-- Naming Conventions -->
        <module name="TypeName">
            <property name="format" value="^[A-Z][a-zA-Z0-9]*$"/>
            <message key="name.invalidPattern"
                     value="Type name ''{0}'' must match pattern ''{1}'' (UpperCamelCase)."/>
        </module>
        <module name="MemberName">
            <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
            <message key="name.invalidPattern"
                     value="Member name ''{0}'' must match pattern ''{1}'' (lowerCamelCase)."/>
        </module>
        <module name="MethodName">
            <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
            <message key="name.invalidPattern"
                     value="Method name ''{0}'' must match pattern ''{1}'' (lowerCamelCase)."/>
        </module>
        <module name="ParameterName">
            <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
            <message key="name.invalidPattern"
                     value="Parameter name ''{0}'' must match pattern ''{1}'' (lowerCamelCase)."/>
        </module>
        <module name="LocalVariableName">
            <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
            <message key="name.invalidPattern"
                     value="Local variable name ''{0}'' must match pattern ''{1}'' (lowerCamelCase)."/>
        </module>
        <module name="ConstantName">
            <property name="format" value="^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$"/>
            <message key="name.invalidPattern"
                     value="Constant name ''{0}'' must match pattern ''{1}'' (UPPER_SNAKE_CASE)."/>
        </module>
        <module name="PackageName">
            <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
            <message key="name.invalidPattern"
                     value="Package name ''{0}'' must match pattern ''{1}'' (all lowercase)."/>
        </module>

        <!-- Import Checks -->
        <module name="AvoidStarImport">
            <property name="excludes" value="java.io,java.net,java.lang.Math"/>
            <property name="allowClassImports" value="false"/>
            <property name="allowStaticMemberImports" value="false"/>
        </module>
        <module name="RedundantImport"/>
        <module name="UnusedImports"/>
        <module name="IllegalImport">
            <property name="illegalPkgs" value="sun"/>
        </module>

        <!-- Braces -->
        <module name="LeftCurly">
            <property name="option" value="eol"/>
        </module>
        <module name="RightCurly">
            <property name="option" value="same"/>
            <property name="tokens" value="LITERAL_TRY,LITERAL_CATCH,LITERAL_FINALLY,LITERAL_IF,LITERAL_ELSE"/>
        </module>
        <module name="NeedBraces">
            <property name="tokens" value="LITERAL_DO,LITERAL_ELSE,LITERAL_FOR,LITERAL_IF,LITERAL_WHILE"/>
        </module>

        <!-- Whitespace -->
        <module name="GenericWhitespace"/>
        <module name="WhitespaceAround">
            <property name="allowEmptyConstructors" value="true"/>
            <property name="allowEmptyMethods" value="true"/>
            <property name="allowEmptyTypes" value="true"/>
            <property name="allowEmptyLoops" value="true"/>
        </module>
        <module name="NoWhitespaceBefore"/>
        <module name="NoWhitespaceAfter">
            <property name="tokens" value="AT,INC,DEC,UNARY_MINUS,UNARY_PLUS,BNOT,LNOT,DOT,ARRAY_DECLARATOR,INDEX_OP"/>
        </module>

        <!-- Coding -->
        <module name="ArrayTypeStyle"/>
        <module name="MissingSwitchDefault"/>
        <module name="ModifierOrder"/>
        <module name="EmptyBlock">
            <property name="option" value="TEXT"/>
            <property name="tokens" value="LITERAL_TRY,LITERAL_FINALLY,LITERAL_IF,LITERAL_ELSE,LITERAL_SWITCH"/>
        </module>
        <module name="EmptyStatement"/>
        <module name="EqualsHashCode"/>
        <module name="SimplifyBooleanExpression"/>
        <module name="SimplifyBooleanReturn"/>
        <module name="StringLiteralEquality"/>
        <module name="UpperEll"/>
        <module name="OneStatementPerLine"/>
        <module name="MultipleVariableDeclarations"/>

        <!-- Javadoc (warnings only for now) -->
        <module name="JavadocMethod">
            <property name="severity" value="ignore"/>
        </module>
        <module name="JavadocType">
            <property name="severity" value="ignore"/>
        </module>

        <!-- Suppressions via annotation -->
        <module name="SuppressWarningsHolder"/>
    </module>

    <module name="SuppressWarningsFilter"/>
</module>


================================================
FILE: checkstyle/suppressions.xml
================================================
<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
        "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
        "https://checkstyle.org/dtds/suppressions_1_2.dtd">

<!--
    CIM Project Checkstyle Suppressions
    Exemptions for generated code and specific patterns
-->
<suppressions>
    <!-- Suppress all checks for generated protobuf files -->
    <suppress files=".*[/\\]generated[/\\].*" checks=".*"/>
    <suppress files=".*[/\\]target[/\\].*" checks=".*"/>

    <!-- Suppress naming checks for protobuf generated classes -->
    <suppress files=".*Proto\.java$" checks=".*"/>
    <suppress files=".*Grpc\.java$" checks=".*"/>

    <!-- Suppress line length for long string literals in tests -->
    <suppress files=".*Test\.java$" checks="LineLength"/>
    <suppress files=".*Tests\.java$" checks="LineLength"/>

    <!-- Suppress star import check for test files (allow static imports for assertions) -->
    <suppress files=".*Test\.java$" checks="AvoidStarImport"/>
    <suppress files=".*Tests\.java$" checks="AvoidStarImport"/>
</suppressions>


================================================
FILE: cim-client/pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.crossoverjie.netty</groupId>
        <artifactId>cim</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <artifactId>cim-client</artifactId>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>17</java.version>
        <swagger.version>2.5.0</swagger.version>
    </properties>


    <dependencies>


        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.crossoverjie.netty</groupId>
            <artifactId>cim-common</artifactId>
        </dependency>

        <dependency>
            <groupId>com.crossoverjie.netty</groupId>
            <artifactId>cim-client-sdk</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>


        <dependency>
            <groupId>com.vdurmont</groupId>
            <artifactId>emoji-java</artifactId>
            <version>5.0.0</version>
        </dependency>

        <dependency>
            <groupId>com.crossoverjie.netty</groupId>
            <artifactId>cim-rout-api</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <!-- spring-boot-maven-plugin (提供了直接运行项目的插件:如果是通过parent方式继承spring-boot-starter-parent则不用此插件) -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>


</project>

================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/CIMClientApplication.java
================================================
package com.crossoverjie.cim.client;

import com.crossoverjie.cim.client.scanner.Scan;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author crossoverJie
 */
@Slf4j
@SpringBootApplication
public class CIMClientApplication implements CommandLineRunner {


	public static void main(String[] args) {
        SpringApplication.run(CIMClientApplication.class, args);
		log.info("Client start success");
	}

	@Override
	public void run(String... args) {
		Scan scan = new Scan();
		Thread thread = new Thread(scan);
		thread.setName("scan-thread");
		thread.start();
	}
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/config/AppConfiguration.java
================================================
package com.crossoverjie.cim.client.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * Function:
 *
 * @author crossoverJie
 *         Date: 2018/8/24 01:43
 * @since JDK 1.8
 */
@Component
@Data
public class AppConfiguration {

    @Value("${cim.user.id}")
    private Long userId;

    @Value("${cim.user.userName}")
    private String userName;

    @Value("${cim.msg.logger.path}")
    private String msgLoggerPath;

    @Value("${cim.heartbeat.time}")
    private long heartBeatTime;

    @Value("${cim.reconnect.count}")
    private int reconnectCount;

    @Value("${cim.route.url}")
    private String routeUrl;
    @Value("${cim.callback.thread.queue.size}")
    private int queueSize;
    @Value("${cim.callback.thread.pool.size}")
    private int poolSize;
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/config/BeanConfig.java
================================================
package com.crossoverjie.cim.client.config;

import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.client.sdk.io.backoff.RandomBackoff;
import com.crossoverjie.cim.client.service.MsgLogger;
import com.crossoverjie.cim.client.service.ShutDownSign;
import com.crossoverjie.cim.client.service.impl.MsgCallBackListener;
import com.crossoverjie.cim.common.data.construct.RingBufferWheel;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import jakarta.annotation.Resource;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Function:bean 配置
 *
 * @author crossoverJie
 * Date: 24/05/2018 15:55
 * @since JDK 1.8
 */
@Configuration
public class BeanConfig {

    @Resource
    private AppConfiguration appConfiguration;

    @Resource
    private ShutDownSign shutDownSign;

    @Resource
    private MsgLogger msgLogger;


    @Bean
    public Client buildClient(@Qualifier("callBackThreadPool") ThreadPoolExecutor callbackThreadPool,
                              Event event) {
        OkHttpClient okHttpClient = new OkHttpClient.Builder().connectTimeout(3, TimeUnit.SECONDS)
                .readTimeout(3, TimeUnit.SECONDS)
                .writeTimeout(3, TimeUnit.SECONDS)
                .retryOnConnectionFailure(true).build();

        return Client.builder()
                .auth(ClientConfigurationData.Auth.builder()
                        .userName(appConfiguration.getUserName())
                        .userId(appConfiguration.getUserId())
                        .build())
                .routeUrl(appConfiguration.getRouteUrl())
                .loginRetryCount(appConfiguration.getReconnectCount())
                .event(event)
                .reconnectCheck(client -> !shutDownSign.checkStatus())
                .okHttpClient(okHttpClient)
                .messageListener(new MsgCallBackListener(msgLogger, event))
                .callbackThreadPool(callbackThreadPool)
                .backoffStrategy(new RandomBackoff())
                .build();
    }

    /**
     * http client
     *
     * @return okHttp
     */
    @Bean
    public OkHttpClient okHttpClient() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.connectTimeout(3, TimeUnit.SECONDS)
                .readTimeout(3, TimeUnit.SECONDS)
                .writeTimeout(3, TimeUnit.SECONDS)
                .retryOnConnectionFailure(true);
        return builder.build();
    }


    /**
     * Create callback thread pool
     *
     * @return
     */
    @Bean("callBackThreadPool")
    public ThreadPoolExecutor buildCallerThread() {
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(appConfiguration.getQueueSize());
        ThreadFactory executor = new ThreadFactoryBuilder()
                .setNameFormat("msg-callback-%d")
                .setDaemon(true)
                .build();
        return new ThreadPoolExecutor(appConfiguration.getPoolSize(), appConfiguration.getPoolSize(), 1,
                TimeUnit.MILLISECONDS, queue, executor);
    }


    @Bean
    public RingBufferWheel bufferWheel() {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        return new RingBufferWheel(executorService);
    }

}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/config/SwaggerConfig.java
================================================
package com.crossoverjie.cim.client.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class SwaggerConfig {

    @Bean
    public OpenAPI createRestApi() {
        return new OpenAPI()
                .info(apiInfo());
    }

    private Info apiInfo() {
        return new Info()
                .title("cim client")
                .description("cim client api")
                .termsOfService("http://crossoverJie.top")
                .contact(contact())
                .version("1.0.0");
    }

    private Contact contact() {
        Contact contact = new Contact();
        contact.setName("crossoverJie");
        return contact;
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/scanner/Scan.java
================================================
package com.crossoverjie.cim.client.scanner;

import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.MsgHandle;
import com.crossoverjie.cim.client.service.MsgLogger;
import com.crossoverjie.cim.client.util.SpringBeanFactory;
import java.util.Scanner;
import lombok.SneakyThrows;

/**
 * Function:
 *
 * @author crossoverJie
 *         Date: 2018/12/21 16:44
 * @since JDK 1.8
 */
public class Scan implements Runnable {


    private final MsgHandle msgHandle;

    private final MsgLogger msgLogger;
    private final Event event;

    public Scan() {
        this.msgHandle = SpringBeanFactory.getBean(MsgHandle.class);
        this.msgLogger = SpringBeanFactory.getBean(MsgLogger.class);
        this.event = SpringBeanFactory.getBean(Event.class);
    }

    @SneakyThrows
    @Override
    public void run() {
        Scanner sc = new Scanner(System.in);
        while (true) {
            String msg = sc.nextLine();

            if (msgHandle.checkMsg(msg)) {
                continue;
            }

            // internal cmd
            if (msgHandle.innerCommand(msg)) {
                continue;
            }

            msgHandle.sendMsg(msg);

            // write to log
            msgLogger.log(msg);

            event.info(msg);
        }
    }

}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/InnerCommand.java
================================================
package com.crossoverjie.cim.client.service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-01-27 19:26
 * @since JDK 1.8
 */
public interface InnerCommand {

    /**
     * 执行
     * @param msg
     */
    void process(String msg) throws Exception;
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/InnerCommandContext.java
================================================
package com.crossoverjie.cim.client.service;

import com.crossoverjie.cim.client.service.impl.command.PrintAllCommand;
import com.crossoverjie.cim.client.util.SpringBeanFactory;
import com.crossoverjie.cim.common.enums.SystemCommandEnum;
import com.crossoverjie.cim.common.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-01-27 19:39
 * @since JDK 1.8
 */
@Slf4j
@Component
public class InnerCommandContext {

    /**
     * 获取执行器实例
     * @param command 执行器实例
     * @return
     */
    public InnerCommand getInstance(String command) {

        Map<String, String> allClazz = SystemCommandEnum.getAllClazz();

        //兼容需要命令后接参数的数据 :q cross
        String[] trim = command.trim().split(" ");
        String clazz = allClazz.get(trim[0]);
        InnerCommand innerCommand = null;
        try {
            if (StringUtil.isEmpty(clazz)) {
                clazz = PrintAllCommand.class.getName();
            }
            innerCommand = (InnerCommand) SpringBeanFactory.getBean(Class.forName(clazz));
        } catch (Exception e) {
            log.error("Exception", e);
        }

        return innerCommand;
    }

}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/MsgHandle.java
================================================
package com.crossoverjie.cim.client.service;

/**
 * Function:消息处理器
 *
 * @author crossoverJie
 * Date: 2018/12/26 11:11
 * @since JDK 1.8
 */
public interface MsgHandle {

    /**
     * 统一的发送接口,包含了 groupChat p2pChat
     *
     * @param msg
     */
    void sendMsg(String msg) throws Exception;


    /**
     * 校验消息
     *
     * @param msg
     * @return 不能为空,后续可以加上一些敏感词
     * @throws Exception
     */
    boolean checkMsg(String msg);

    /**
     * 执行内部命令
     *
     * @param msg
     * @return 是否应当跳过当前消息(包含了":" 就需要跳过)
     */
    boolean innerCommand(String msg) throws Exception;


    /**
     * 开启 AI 模式
     */
    void openAIModel();

    /**
     * 关闭 AI 模式
     */
    void closeAIModel();
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/MsgLogger.java
================================================
package com.crossoverjie.cim.client.service;

/**
 * Function:
 *
 * @author crossoverJie
 *         Date: 2019/1/6 15:23
 * @since JDK 1.8
 */
public interface MsgLogger {

    /**
     * write log
     * @param msg
     */
    void log(String msg);


    /**
     * 停止写入
     */
    void stop();

    /**
     * 查询聊天记录
     * @param key 关键字
     * @return
     */
    String query(String key);
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/ShutDownSign.java
================================================
package com.crossoverjie.cim.client.service;

import org.springframework.stereotype.Component;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-02-27 16:17
 * @since JDK 1.8
 */
@Component
public class ShutDownSign {
    private boolean isCommand;

    /**
     * Set user exit sign.
     */
    public void shutdown() {
        isCommand = true;
    }

    public boolean checkStatus() {
        return isCommand;
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/AsyncMsgLogger.java
================================================
package com.crossoverjie.cim.client.service.impl;

import com.crossoverjie.cim.client.config.AppConfiguration;
import com.crossoverjie.cim.client.service.MsgLogger;
import jakarta.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.stream.Stream;

/**
 * Function:
 *
 * @author crossoverJie
 *         Date: 2019/1/6 15:26
 * @since JDK 1.8
 */
@Slf4j
@Service
public class AsyncMsgLogger implements MsgLogger {


    /**
     * The default buffer size.
     */
    private static final int DEFAULT_QUEUE_SIZE = 16;
    private final BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<String>(DEFAULT_QUEUE_SIZE);

    private volatile boolean started = false;
    private final Worker worker = new Worker();

    @Resource
    private AppConfiguration appConfiguration;

    @Override
    public void log(String msg) {
        // start worker
        startMsgLogger();
        try {
            // TODO: 2019/1/6 消息堆满是否阻塞线程?
            blockingQueue.put(msg);
        } catch (InterruptedException e) {
            log.error("InterruptedException", e);
        }
    }

    private class Worker extends Thread {


        @Override
        public void run() {
            while (started) {
                try {
                    String msg = blockingQueue.take();
                    writeLog(msg);
                } catch (InterruptedException e) {
                    break;
                }
            }
        }

    }


    private void writeLog(String msg) {

        LocalDate today = LocalDate.now();
        int year = today.getYear();
        int month = today.getMonthValue();
        int day = today.getDayOfMonth();

        String dir = appConfiguration.getMsgLoggerPath() + appConfiguration.getUserName() + "/";
        String fileName = dir + year + month + day + ".log";

        Path file = Paths.get(fileName);
        boolean exists = Files.exists(Paths.get(dir), LinkOption.NOFOLLOW_LINKS);
        try {
            if (!exists) {
                Files.createDirectories(Paths.get(dir));
            }

            List<String> lines = Collections.singletonList(msg);

            Files.write(file, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        } catch (IOException e) {
            log.info("IOException", e);
        }

    }

    /**
     * Begin worker
     */
    private void startMsgLogger() {
        if (started) {
            return;
        }

        worker.setDaemon(true);
        worker.setName("AsyncMsgLogger-Worker");
        started = true;
        worker.start();
    }


    @Override
    public void stop() {
        started = false;
        worker.interrupt();
    }

    @Override
    public String query(String key) {
        StringBuilder sb = new StringBuilder();

        Path path = Paths.get(appConfiguration.getMsgLoggerPath() + appConfiguration.getUserName() + "/");

        try {
            @Cleanup
            Stream<Path> list = Files.list(path);
            List<Path> collect = list.toList();
            for (Path file : collect) {
                List<String> strings = Files.readAllLines(file);
                for (String msg : strings) {
                    if (msg.trim().contains(key)) {
                        sb.append(msg).append("\n");
                    }
                }

            }
        } catch (IOException e) {
            log.info("IOException", e);
        }

        return sb.toString().replace(key, "\033[31;4m" + key + "\033[0m");
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/EchoServiceImpl.java
================================================
package com.crossoverjie.cim.client.service.impl;

import com.crossoverjie.cim.client.config.AppConfiguration;
import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.MsgLogger;
import com.vdurmont.emoji.EmojiParser;
import jakarta.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalTime;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-08-27 22:37
 * @since JDK 1.8
 */
@Service
public class EchoServiceImpl implements Event {

    private static final String PREFIX = "$";

    @Resource
    private AppConfiguration appConfiguration;

    @Resource
    private MsgLogger msgLogger;

    @Override
    public void debug(String msg, Object... replace) {
        msgLogger.log(String.format("Debug[%s]", msg));
    }

    @Override
    public void info(String msg, Object... replace) {
        // Make terminal can display the emoji
        msg = EmojiParser.parseToUnicode(msg);
        String date = LocalDate.now() + " " + LocalTime.now().withNano(0).toString();

        msg = "[" + date + "] \033[31;4m" + appConfiguration.getUserName() + PREFIX + "\033[0m" + " " + msg;

        String log = print(msg, replace);

        System.out.println(log);
    }

    @Override
    public void warn(String msg, Object... replace) {
        info(String.format("Warn##%s##", msg), replace);
    }

    @Override
    public void error(String msg, Object... replace) {
        info(String.format("Error!!%s!!", msg), replace);
    }

    @Override
    public void fatal(Client client) {
        info("{} fatal error, shutdown client", client.getAuth());
    }


    /**
     * print msg
     *
     * @param msg
     * @param place
     * @return
     */
    private String print(String msg, Object... place) {
        StringBuilder sb = new StringBuilder();
        int k = 0;
        for (int i = 0; i < place.length; i++) {
            int index = msg.indexOf("{}", k);

            if (index == -1) {
                return msg;
            }

            if (index != 0) {
                sb.append(msg, k, index);
                sb.append(place[i]);

                if (place.length == 1) {
                    sb.append(msg, index + 2, msg.length());
                }

            } else {
                sb.append(place[i]);
                if (place.length == 1) {
                    sb.append(msg, index + 2, msg.length());
                }
            }

            k = index + 2;
        }
        if (sb.toString().equals("")) {
            return msg;
        } else {
            return sb.toString();
        }
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/MsgCallBackListener.java
================================================
package com.crossoverjie.cim.client.service.impl;

import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.sdk.io.MessageListener;
import com.crossoverjie.cim.client.service.MsgLogger;
import com.crossoverjie.cim.common.constant.Constants;
import java.util.Map;

/**
 * Function:自定义收到消息回调
 *
 * @author crossoverJie
 * Date: 2019/1/6 17:49
 * @since JDK 1.8
 */
public class MsgCallBackListener implements MessageListener {


    private final MsgLogger msgLogger;
    private final Event event;

    public MsgCallBackListener(MsgLogger msgLogger, Event event) {
        this.msgLogger = msgLogger;
        this.event = event;
    }


    @Override
    public void received(Client client, Map<String, String> properties, String msg) {
        String sendUserName = properties.getOrDefault(Constants.MetaKey.SEND_USER_NAME, "nobody");
        this.msgLogger.log(sendUserName + ":" + msg);
        this.event.info(sendUserName + ":" + msg);
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/MsgHandler.java
================================================
package com.crossoverjie.cim.client.service.impl;

import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.InnerCommandContext;
import com.crossoverjie.cim.client.service.MsgHandle;
import com.crossoverjie.cim.common.util.StringUtil;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2018/12/26 11:15
 * @since JDK 1.8
 */
@Slf4j
@Service
public class MsgHandler implements MsgHandle {


    @Resource
    private InnerCommandContext innerCommandContext;

    @Resource
    private Client client;

    private boolean aiModel = false;

    @Override
    public void sendMsg(String msg) throws Exception {
        if (aiModel) {
            aiChat(msg);
        } else {
            normalChat(msg);
        }
    }

    private void normalChat(String msg) throws Exception {
        String[] totalMsg = msg.split(";;");
        if (totalMsg.length > 1) {
            P2PReqVO p2PReqVO = new P2PReqVO();
            p2PReqVO.setReceiveUserId(Long.parseLong(totalMsg[0]));
            p2PReqVO.setMsg(totalMsg[1]);
            client.sendP2P(p2PReqVO);

        } else {
            client.sendGroup(msg);
        }
    }

    /**
     * AI model
     *
     * @param msg
     */
    private void aiChat(String msg) {
        msg = msg.replace("吗", "");
        msg = msg.replace("嘛", "");
        msg = msg.replace("?", "!");
        msg = msg.replace("?", "!");
        msg = msg.replace("你", "我");
        System.out.println("AI:\033[31;4m" + msg + "\033[0m");
    }

    @Override
    public boolean checkMsg(String msg) {
        if (StringUtil.isEmpty(msg)) {
            log.warn("不能发送空消息!");
            return true;
        }
        return false;
    }

    @Override
    public boolean innerCommand(String msg) throws Exception {

        if (msg.startsWith(":")) {

            InnerCommand instance = innerCommandContext.getInstance(msg);
            instance.process(msg);

            return true;

        } else {
            return false;
        }
    }

    @Override
    public void openAIModel() {
        aiModel = true;
    }

    @Override
    public void closeAIModel() {
        aiModel = false;
    }

}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/CloseAIModelCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;

import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.MsgHandle;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-01-27 19:37
 * @since JDK 1.8
 */
@Service
public class CloseAIModelCommand implements InnerCommand {


    @Resource
    private MsgHandle msgHandle;

    @Resource
    private Event event;

    @Override
    public void process(String msg) {
        msgHandle.closeAIModel();

        event.info("\033[31;4m" + "。゚(゚´ω`゚)゚。  AI 下线了!" + "\033[0m");
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/DelayMsgCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;

import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.MsgHandle;
import com.crossoverjie.cim.common.data.construct.RingBufferWheel;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-09-25 00:37
 * @since JDK 1.8
 */
@Service
@Slf4j
public class DelayMsgCommand implements InnerCommand {

    @Resource
    private Event event;

    @Resource
    private MsgHandle msgHandle;

    @Resource
    private RingBufferWheel ringBufferWheel;

    @Override
    public void process(String msg) {
        if (msg.split(" ").length <= 2) {
            event.info("incorrect commond, :delay [msg] [delayTime]");
            return;
        }

        String message = msg.split(" ")[1];
        int delayTime = Integer.parseInt(msg.split(" ")[2]);

        RingBufferWheel.Task task = new DelayMsgJob(message);
        task.setKey(delayTime);
        ringBufferWheel.addTask(task);
        event.info(msg);
    }



    private class DelayMsgJob extends RingBufferWheel.Task {

        private String msg;

        public DelayMsgJob(String msg) {
            this.msg = msg;
        }

        @Override
        public void run() {
            try {
                msgHandle.sendMsg(msg);
            } catch (Exception e) {
                log.error("Delay message send error", e);
            }
        }
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/EchoInfoCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;

import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-01-27 19:37
 * @since JDK 1.8
 */
@Service
public class EchoInfoCommand implements InnerCommand {

    @Resource
    private Client client;

    @Resource
    private Event event;

    @Override
    public void process(String msg) {
        event.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
        event.info("client info={}", client.getAuth());
        event.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/EmojiCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;

import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.vdurmont.emoji.Emoji;
import com.vdurmont.emoji.EmojiManager;
import com.vdurmont.emoji.EmojiParser;
import jakarta.annotation.Resource;
import java.util.List;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-01-27 19:37
 * @since JDK 1.8
 */
@Service
public class EmojiCommand implements InnerCommand {

    @Resource
    private Event event;


    @Override
    public void process(String msg) {
        if (msg.split(" ").length <= 1) {
            event.info("incorrect commond, :emoji [option]");
            return;
        }
        String value = msg.split(" ")[1];
        if (value != null) {
            int index = Integer.parseInt(value);
            List<Emoji> all = (List<Emoji>) EmojiManager.getAll();
            all = all.subList(5 * index, 5 * index + 5);

            for (Emoji emoji : all) {
                event.info(EmojiParser.parseToAliases(emoji.getUnicode()) + "--->" + emoji.getUnicode());
            }
        }

    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/OpenAIModelCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;

import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.MsgHandle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-01-27 19:37
 * @since JDK 1.8
 */
@Service
public class OpenAIModelCommand implements InnerCommand {


    @Autowired
    private MsgHandle msgHandle;

    @Override
    public void process(String msg) {
        msgHandle.openAIModel();
        System.out.println("\033[31;4m" + "Hello,我是估值两亿的 AI 机器人!" + "\033[0m");
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrefixSearchCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;

import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.common.data.construct.TrieTree;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import jakarta.annotation.Resource;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-01-27 19:37
 * @since JDK 1.8
 */
@Slf4j
@Service
public class PrefixSearchCommand implements InnerCommand {


    @Resource
    private Client client;
    @Resource
    private Event event;

    @Override
    public void process(String msg) {
        try {
            Set<CIMUserInfo> onlineUsers = client.getOnlineUser();
            TrieTree trieTree = new TrieTree();
            for (CIMUserInfo onlineUser : onlineUsers) {
                trieTree.insert(onlineUser.getUserName());
            }

            String[] split = msg.split(" ");
            String key = split[1];
            List<String> list = trieTree.prefixSearch(key);

            for (String res : list) {
                res = res.replace(key, "\033[31;4m" + key + "\033[0m");
                event.info(res);
            }

        } catch (Exception e) {
            log.error("Exception", e);
        }
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrintAllCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;

import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.common.enums.SystemCommandEnum;
import jakarta.annotation.Resource;
import java.util.Map;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-01-27 19:37
 * @since JDK 1.8
 */
@Service
public class PrintAllCommand implements InnerCommand {


    @Resource
    private Event event;

    @Override
    public void process(String msg) {
        Map<String, String> allStatusCode = SystemCommandEnum.getAllStatusCode();
        event.info("====================================");
        for (Map.Entry<String, String> stringStringEntry : allStatusCode.entrySet()) {
            String key = stringStringEntry.getKey();
            String value = stringStringEntry.getValue();
            event.info(key + "----->" + value);
        }
        event.info("====================================");
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrintOnlineUsersCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;

import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import jakarta.annotation.Resource;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-01-27 19:37
 * @since JDK 1.8
 */
@Slf4j
@Service
public class PrintOnlineUsersCommand implements InnerCommand {

    @Resource
    private Client client;

    @Resource
    private Event event;

    @Override
    public void process(String msg) {
        try {
            Set<CIMUserInfo> onlineUsers = client.getOnlineUser();

            event.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
            for (CIMUserInfo onlineUser : onlineUsers) {
                event.info("userId={}=====userName={}", onlineUser.getUserId(), onlineUser.getUserName());
            }
            event.info("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");

        } catch (Exception e) {
            log.error("Exception", e);
        }
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/QueryHistoryCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;

import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.MsgLogger;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-01-27 19:37
 * @since JDK 1.8
 */
@Slf4j
@Service
public class QueryHistoryCommand implements InnerCommand {

    @Resource
    private MsgLogger msgLogger;

    @Resource
    private Event event;

    @Override
    public void process(String msg) {
        String[] split = msg.split(" ");
        if (split.length < 2) {
            return;
        }
        String res = msgLogger.query(split[1]);
        event.info(res);
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/ShutDownCommand.java
================================================
package com.crossoverjie.cim.client.service.impl.command;

import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.service.InnerCommand;
import com.crossoverjie.cim.client.service.MsgLogger;
import com.crossoverjie.cim.client.service.ShutDownSign;
import com.crossoverjie.cim.common.data.construct.RingBufferWheel;
import jakarta.annotation.Resource;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.stereotype.Service;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-01-27 19:28
 * @since JDK 1.8
 */
@Slf4j
@Service
@ConditionalOnWebApplication
public class ShutDownCommand implements InnerCommand {

    @Resource
    private Client cimClient;

    @Resource
    private MsgLogger msgLogger;

    @Resource(name = "callBackThreadPool")
    private ThreadPoolExecutor callBackExecutor;

    @Resource
    private Event event;

    @Resource
    private ShutDownSign shutDownSign;

    @Resource
    private RingBufferWheel ringBufferWheel;

    @Override
    public void process(String msg) throws Exception {
        event.info("cim client closing...");
        cimClient.close();
        shutDownSign.shutdown();
        msgLogger.stop();
        callBackExecutor.shutdown();
        ringBufferWheel.stop(false);
        try {
            while (!callBackExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
                event.info("thread pool closing");
            }
        } catch (Exception e) {
            log.error("exception", e);
        }
        event.info("cim close success!");
        System.exit(0);
    }
}


================================================
FILE: cim-client/src/main/java/com/crossoverjie/cim/client/util/SpringBeanFactory.java
================================================
package com.crossoverjie.cim.client.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public final class SpringBeanFactory implements ApplicationContextAware {
    private static ApplicationContext context;

    public static <T> T getBean(Class<T> c) {
        return context.getBean(c);
    }


    public static <T> T getBean(String name, Class<T> clazz) {
        return context.getBean(name, clazz);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }


}


================================================
FILE: cim-client/src/main/resources/application.yaml
================================================
spring:
  application:
    name: cim-client

# web port
server:
  port: 8082

logging:
  level:
    root: error

# enable swagger
springdoc:
  swagger-ui:
    enabled: true

# log path
cim:
  msg:
    logger:
      path: /opt/logs/cim/
  route:
    url: http://localhost:8083 # route url suggested that this is Nginx address
  user: # cim userId and userName
    id: 1747309836375
    userName: yuge
  callback:
    thread:
      queue:
        size: 10
      pool:
        size: 1
  heartbeat:
    time: 60 # cim heartbeat time (seconds)
  reconnect:
    count: 3


================================================
FILE: cim-client/src/main/resources/banner.txt
================================================
      _              ___          __
 ____(_)_ _     ____/ (_)__ ___  / /_
/ __/ /  ' \   / __/ / / -_) _ \/ __/
\__/_/_/_/_/   \__/_/_/\__/_//_/\__/
 Power by @crossoverJie





================================================
FILE: cim-client/src/test/java/com/crossoverjie/cim/client/service/InnerCommandContextTest.java
================================================
package com.crossoverjie.cim.client.service;

import com.crossoverjie.cim.client.CIMClientApplication;
import com.crossoverjie.cim.common.enums.SystemCommandEnum;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest(classes = CIMClientApplication.class)
@RunWith(SpringRunner.class)
public class InnerCommandContextTest {

    @Autowired
    private InnerCommandContext context;

    @Test
    public void execute() throws Exception {
        String msg = ":all";
        InnerCommand execute = context.getInstance(msg);
        execute.process(msg);
    }

//    @Test
    public void execute3() throws Exception {
        // TODO: 2024/8/31 Integration test
        String msg = SystemCommandEnum.ONLINE_USER.getCommandType();
        InnerCommand execute = context.getInstance(msg);
        execute.process(msg);
    }

    @Test
    public void execute4() throws Exception {
        String msg = ":q 天气";
        InnerCommand execute = context.getInstance(msg);
        execute.process(msg);
    }

    @Test
    public void execute5() throws Exception {
        String msg = ":q crossoverJie";
        InnerCommand execute = context.getInstance(msg);
        execute.process(msg);
    }

    @Test
    public void execute6() throws Exception {
        String msg = SystemCommandEnum.AI.getCommandType();
        InnerCommand execute = context.getInstance(msg);
        execute.process(msg);
    }

    @Test
    public void execute7() throws Exception {
        String msg = SystemCommandEnum.QAI.getCommandType();
        InnerCommand execute = context.getInstance(msg);
        execute.process(msg);
    }

//    @Test
    public void execute8() throws Exception {
        // TODO: 2024/8/31 Integration test
        String msg = ":pu cross";
        InnerCommand execute = context.getInstance(msg);
        execute.process(msg);
    }

    @Test
    public void execute9() throws Exception {
        String msg = SystemCommandEnum.INFO.getCommandType();
        InnerCommand execute = context.getInstance(msg);
        execute.process(msg);
    }

    @Test
    public void execute10() throws Exception {
        String msg = "dsds";
        InnerCommand execute = context.getInstance(msg);
        execute.process(msg);
    }



   // @Test
    public void quit() throws Exception {
        String msg = ":q!";
        InnerCommand execute = context.getInstance(msg);
        execute.process(msg);
    }
}


================================================
FILE: cim-client/src/test/java/com/crossoverjie/cim/client/service/impl/AsyncMsgLoggerTest.java
================================================
package com.crossoverjie.cim.client.service.impl;

import com.crossoverjie.cim.client.CIMClientApplication;
import com.crossoverjie.cim.client.service.MsgLogger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.TimeUnit;

@SpringBootTest(classes = CIMClientApplication.class)
@RunWith(SpringRunner.class)
public class AsyncMsgLoggerTest {



    @Autowired
    private MsgLogger msgLogger;

    @Test
    public void writeLog() throws Exception {
        for (int i = 0; i < 10; i++) {
            msgLogger.log("zhangsan:【asdsd】" + i);
        }

        TimeUnit.SECONDS.sleep(2);
    }



    @Test
    public void query() {
        String crossoverJie = msgLogger.query("crossoverJie");
        System.out.println(crossoverJie);
    }

}


================================================
FILE: cim-client/src/test/java/com/crossoverjie/cim/server/test/CommonTest.java
================================================
package com.crossoverjie.cim.server.test;


import com.crossoverjie.cim.common.core.proxy.RpcProxyManager;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.common.res.BaseResponse;
import com.crossoverjie.cim.route.api.RouteApi;
import com.crossoverjie.cim.route.api.vo.req.LoginReqVO;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.vdurmont.emoji.EmojiParser;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import org.junit.Test;

/**
 * Function:
 *
 * @author crossoverJie
 *         Date: 22/05/2018 18:44
 * @since JDK 1.8
 */
@Slf4j
public class CommonTest {




    @Test
    public void searchMsg2() {
        StringBuilder sb = new StringBuilder();
        String allMsg = "于是在之前的基础上我完善了一些内容,先来看看这个项目的介绍吧:\n" +
                "\n" +
                "CIM(CROSS-IM) 一款面向开发者的 IM(即时通讯)系统;同时提供了一些组件帮助开发者构建一款属于自己可水平扩展的 IM 。\n" +
                "\n" +
                "借助 CIM 你可以实现以下需求:";

        String key = "CIM";

        String[] split = allMsg.split("\n");
        for (String msg : split) {
            if (msg.trim().contains(key)) {
                sb.append(msg).append("\n");
            }
        }
        int pos = 0;

        String result = sb.toString();

        int count = 1;
        int multiple = 2;
        while ((pos = result.indexOf(key, pos)) >= 0) {

            log.info("{},{}",pos, pos + key.length());

            pos += key.length();


            count++;
        }

        System.out.println(sb.toString());
        System.out.println(sb.toString().replace(key, "\033[31;4m" + key + "\033[0m"));
    }

    @Test
    public void log() {
        String msg = "hahahdsadsd";
        LocalDate today = LocalDate.now();
        int year = today.getYear();
        int month = today.getMonthValue();
        int day = today.getDayOfMonth();

        String dir = "/opt/logs/cim/zhangsan" + "/";
        String fileName = dir + year + month + day + ".log";
        log.info("fileName={}", fileName);

        Path file = Paths.get(fileName);
        boolean exists = Files.exists(Paths.get(dir), LinkOption.NOFOLLOW_LINKS);
        try {
            if (!exists) {
                Files.createDirectories(Paths.get(dir));
            }

            List<String> lines = Arrays.asList(msg);

            Files.write(file, lines, Charset.forName("UTF-8"), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        } catch (IOException e) {
            log.info("IOException", e);
        }

    }

    @Test
    public void emoji() throws Exception {
        String str = "An :grinning:awesome :smiley:string &#128516;with a few :wink:emojis!";
        String result = EmojiParser.parseToUnicode(str);
        System.out.println(result);


        result = EmojiParser.parseToAliases(str);
        System.out.println(result);
//
//        Collection<Emoji> all = EmojiManager.getAll();
//        for (Emoji emoji : all) {
//            System.out.println(EmojiParser.parseToAliases(emoji.getUnicode())  + "--->" + emoji.getUnicode() );
//        }

    }

    @Test
    public void emoji2() {
        String emostring = "😂";

        String faceWithTearsOfJoy = emostring.replaceAll("\uD83D\uDE02", "face with tears of joy");
        System.out.println(faceWithTearsOfJoy);

        System.out.println("======" + faceWithTearsOfJoy.replaceAll("face with tears of joy","\uD83D\uDE02"));
    }

//    @Test
    public void deSerialize() throws Exception {
        RouteApi routeApi = RpcProxyManager.create(RouteApi.class, "http://localhost:8083", new OkHttpClient());

        BaseResponse<com.crossoverjie.cim.route.api.vo.res.CIMServerResVO> login =
                routeApi.login(new LoginReqVO(1725722966520L, "cj"));
        System.out.println(login.getDataBody());

        BaseResponse<Set<CIMUserInfo>> setBaseResponse = routeApi.onlineUser();
        log.info("setBaseResponse={}",setBaseResponse.getDataBody());
    }

    @Test
    public void json() throws JsonProcessingException, ClassNotFoundException {
        String json = "{\"code\":\"9000\",\"message\":\"成功\",\"reqNo\":null,\"dataBody\":{\"ip\":\"127.0.0.1\",\"cimServerPort\":11211,\"httpPort\":8081}}";

        ObjectMapper objectMapper = new ObjectMapper();
        Class<?> generic = null;
        for (Method declaredMethod : RouteApi.class.getDeclaredMethods()) {
            if (declaredMethod.getName().equals("login")) {
                Type returnType = declaredMethod.getGenericReturnType();

                // check if the return type is a parameterized type
                if (returnType instanceof ParameterizedType) {
                    ParameterizedType parameterizedType = (ParameterizedType) returnType;

                    Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();

                    for (Type typeArgument : actualTypeArguments) {
                        System.out.println("generic: " + typeArgument.getTypeName());
                        generic = Class.forName(typeArgument.getTypeName());
                        break;
                    }
                } else {
                    System.out.println("not a generic type");
                }
            }
        }
        BaseResponse<com.crossoverjie.cim.route.api.vo.res.CIMServerResVO> response = objectMapper.readValue(json,
                objectMapper.getTypeFactory().constructParametricType(BaseResponse.class, generic));
        System.out.println(response.getDataBody().getIp());
    }


    private static class Gen<T, R> {
        private T t;
        private R r;
    }

    interface TestInterface {
        Gen<String, P2PReqVO> login();
    }


    @Test
    public void test1() throws JsonProcessingException {
        String json = "{\"code\":\"200\",\"message\":\"Success\",\"reqNo\":null,\"dataBody\":[{\"userId\":\"123\",\"userName\":\"Alice\"}, {\"userId\":\"456\",\"userName\":\"Bob\"}]}";

        ObjectMapper objectMapper = new ObjectMapper();

        // 获取 BaseResponse<Set<CIMUserInfo>> 的泛型参数
        Type setType = getGenericTypeOfBaseResponse();

        // 将泛型类型传递给 ObjectMapper 进行反序列化
        BaseResponse<Set<CIMUserInfo>> response = objectMapper.readValue(json,
                objectMapper.getTypeFactory().constructParametricType(BaseResponse.class, objectMapper.getTypeFactory().constructType(setType)));

        System.out.println("Response Code: " + response.getCode());
        System.out.println("Online Users: ");
        for (CIMUserInfo user : response.getDataBody()) {
            System.out.println("User ID: " + user.getUserId() + ", User Name: " + user.getUserName());
        }
    }

    // 通过反射获取 BaseResponse<Set<CIMUserInfo>> 中的泛型类型
    public static Type getGenericTypeOfBaseResponse() {
        // 这里模拟你需要处理的 BaseResponse<Set<CIMUserInfo>>
        ParameterizedType baseResponseType = (ParameterizedType) new TypeReference<BaseResponse<Set<CIMUserInfo>>>() {}.getType();

        // 获取 BaseResponse 的泛型参数,即 Set<CIMUserInfo>
        Type[] actualTypeArguments = baseResponseType.getActualTypeArguments();

        // 返回第一个泛型参数 (Set<CIMUserInfo>)
        return actualTypeArguments[0];
    }
}


================================================
FILE: cim-client/src/test/java/com/crossoverjie/cim/server/test/EchoTest.java
================================================
package com.crossoverjie.cim.server.test;

import org.junit.Assert;
import org.junit.Test;

/**
 * Function:
 *
 * @author crossoverJie
 * Date: 2019-08-28 01:47
 * @since JDK 1.8
 */
public class EchoTest {
    @Test
    public void echo() {
        String msg = "{} say,you {}";
        String[] place = {"zhangsan", "haha"};

        String log = log(msg, place);
        System.out.println(log);
        Assert.assertEquals(log,"zhangsan say,you haha");
    }

    @Test
    public void echo2() {
        String msg = "{} say,you {},zhangsan say {}";
        String[] place = {"zhangsan", "haha", "nihao"};

        String log = log(msg, place);
        System.out.println(log);
        Assert.assertEquals(log,"zhangsan say,you haha,zhangsan say nihao");
    }

    @Test
    public void echo3() {
        String msg = "see you {},zhangsan say";
        String[] place = {"zhangsan"};

        String log = log(msg, place);
        System.out.println(log);
        Assert.assertEquals(log,"see you zhangsan,zhangsan say");
    }
    @Test
    public void echo4() {
        String msg = "{}see you,zhangsan say";
        String[] place = {"!!!"};

        String log = log(msg, place);
        System.out.println(log);
        Assert.assertEquals(log,"!!!see you,zhangsan say");
    }
    @Test
    public void echo5() {
        String msg = "see you,zhangsan say{}";
        String[] place = {"!!!"};

        String log = log(msg, place);
        System.out.println(log);
        Assert.assertEquals(log,"see you,zhangsan say!!!");
    }

    @Test
    public void echo6() {
        String msg = "see you,zhangsan say";
        String[] place = {""};

        String log = log(msg, place);
        System.out.println(log);
        Assert.assertEquals(log,"see you,zhangsan say");
    }

    private String log(String msg, String... place) {
        StringBuilder sb = new StringBuilder();
        int k = 0;
        for (int i = 0; i < place.length; i++) {
            int index = msg.indexOf("{}", k);

            if (index == -1) {
                return msg;
            }

            if (index != 0) {
                sb.append(msg, k, index);
                sb.append(place[i]);

                if (place.length == 1) {
                    sb.append(msg, index + 2, msg.length());
                }

            } else {
                sb.append(place[i]);
                if (place.length == 1) {
                    sb.append(msg, index + 2, msg.length());
                }
            }

            k = index + 2;
        }
        return sb.toString();
    }
}


================================================
FILE: cim-client/src/test/resources/application.yaml
================================================
spring:
  application:
    name: cim-client
  main:
    # this will not be used to create real spring context, because don't need this context in test case.
    web-application-type: none

# web port
server:
  port: 8082

logging:
  level:
    root: error

# enable swagger
springdoc:
  swagger-ui:
    enabled: true

# log path
cim:
  msg:
    logger:
      path: /opt/logs/cim/
  route:
    url: http://localhost:8083 # route url suggested that this is Nginx address
  user: # cim userId and userName
    id: 1722343979085
    userName: zhangsan
  callback:
    thread:
      queue:
        size: 1000
      pool:
        size: 2
  heartbeat:
    time: 60 # cim heartbeat time (seconds)
  reconnect:
    count: 3

================================================
FILE: cim-client-sdk/README.md
================================================

```java
    var auth1 = ClientConfigurationData.Auth.builder()
    .userId(id)
    .userName(cj)
    .build();
    
    Client client1 = Client.builder()
                    .auth(auth1)
                    .routeUrl(routeUrl)
                    .build();
    
    ClientState.State state = client1.getState();
    Awaitility.await().atMost(10, TimeUnit.SECONDS)
    .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
    Optional<CIMServerResVO> serverInfo = client1.getServerInfo();
    Assertions.assertTrue(serverInfo.isPresent());
    
    // send msg
    String msg = "hello";
    client1.sendGroup(msg);
    
    // get oline user
    Set<CIMUserInfo> onlineUser = client1.getOnlineUser();
```

================================================
FILE: cim-client-sdk/pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.crossoverjie.netty</groupId>
    <artifactId>cim</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>

  <artifactId>cim-client-sdk</artifactId>

  <properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>


  <dependencies>
    <dependency>
      <groupId>com.crossoverjie.netty</groupId>
      <artifactId>cim-common</artifactId>
    </dependency>

    <dependency>
      <groupId>com.crossoverjie.netty</groupId>
      <artifactId>cim-rout-api</artifactId>
    </dependency>

    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.junit.vintage</groupId>
      <artifactId>junit-vintage-engine</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.crossoverjie.netty</groupId>
      <artifactId>cim-integration-test</artifactId>
      <version>${project.version}</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

</project>

================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/Client.java
================================================
package com.crossoverjie.cim.client.sdk;

import com.crossoverjie.cim.client.sdk.impl.ClientBuilderImpl;
import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;
import java.io.Closeable;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

public interface Client extends Closeable {

    static ClientBuilder builder() {
        return new ClientBuilderImpl();
    }

    default void sendP2P(P2PReqVO p2PReqVO) throws Exception {
        sendP2PAsync(p2PReqVO).get();
    }

    CompletableFuture<Void> sendP2PAsync(P2PReqVO p2PReqVO);

    default void sendGroup(String msg) throws Exception {
        sendGroupAsync(msg).get();
    }

    CompletableFuture<Void> sendGroupAsync(String msg);

    ClientState.State getState();

    ClientConfigurationData.Auth getAuth();

    Set<CIMUserInfo> getOnlineUser() throws Exception;

    Optional<CIMServerResVO> getServerInfo();

}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ClientBuilder.java
================================================
package com.crossoverjie.cim.client.sdk;

import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.client.sdk.io.MessageListener;
import com.crossoverjie.cim.client.sdk.io.ReconnectCheck;
import java.util.concurrent.ThreadPoolExecutor;

import com.crossoverjie.cim.client.sdk.io.backoff.BackoffStrategy;
import okhttp3.OkHttpClient;

/**
 * @author crossoverJie
 */
public interface ClientBuilder {

    Client build();
    ClientBuilder auth(ClientConfigurationData.Auth auth);
    ClientBuilder routeUrl(String routeUrl);
    ClientBuilder loginRetryCount(int loginRetryCount);
    ClientBuilder event(Event event);
    ClientBuilder reconnectCheck(ReconnectCheck reconnectCheck);
    ClientBuilder okHttpClient(OkHttpClient okHttpClient);
    ClientBuilder messageListener(MessageListener messageListener);
    ClientBuilder callbackThreadPool(ThreadPoolExecutor callbackThreadPool);
    ClientBuilder backoffStrategy(BackoffStrategy backoffStrategy);
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ClientState.java
================================================
package com.crossoverjie.cim.client.sdk;

import java.util.concurrent.atomic.AtomicReference;

public abstract class ClientState {

    private static final AtomicReference<State> STATE = new AtomicReference<>(State.Initialized);

    public enum State {
        /**
         * Client state
         */
        Initialized, Reconnecting, Ready, Closed, Failed
    }

    public void setState(State s) {
        STATE.set(s);
    }

    public State getState() {
        return STATE.get();
    }
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/Event.java
================================================
package com.crossoverjie.cim.client.sdk;

public interface Event {
    void debug(String msg, Object... replace);
    void info(String msg, Object... replace);
    void warn(String msg, Object... replace);
    void error(String msg, Object... replace);
    void fatal(Client client);

    class DefaultEvent implements Event {
        @Override
        public void debug(String msg, Object... replace) {
            System.out.println(msg);
        }

        @Override
        public void info(String msg, Object... replace) {
            System.out.println(msg);
        }

        @Override
        public void warn(String msg, Object... replace) {
            System.out.println(msg);
        }

        @Override
        public void error(String msg, Object... replace) {
            System.err.println(msg);
        }

        @Override
        public void fatal(Client client) {

        }
    }
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/FetchOfflineMsgJob.java
================================================
package com.crossoverjie.cim.client.sdk;

import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.common.data.construct.RingBufferWheel;

public class FetchOfflineMsgJob extends RingBufferWheel.Task {
    private static final int INITIAL_DELAY_SECONDS = 5;

    private RouteManager routeManager;
    private ClientConfigurationData conf;

    public FetchOfflineMsgJob(RouteManager routeManager, ClientConfigurationData conf) {
        this.routeManager = routeManager;
        this.conf = conf;
        setKey(INITIAL_DELAY_SECONDS); //It will be sent with a 5-second delay
    }

    @Override
    public void run() {
        routeManager.fetchOfflineMsgs(conf.getAuth().getUserId());
    }
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ReConnectManager.java
================================================
package com.crossoverjie.cim.client.sdk;

import com.crossoverjie.cim.client.sdk.impl.ClientImpl;
import com.crossoverjie.cim.common.kit.HeartBeatHandler;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.netty.channel.ChannelHandlerContext;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public final class ReConnectManager {

    private ScheduledExecutorService scheduledExecutorService;

    /**
     * Trigger reconnect job
     *
     * @param ctx
     */
    public void reConnect(ChannelHandlerContext ctx) {
        buildExecutor();
        scheduledExecutorService.scheduleAtFixedRate(() -> {
                    try {
                        ClientImpl.getClient().getHeartBeatHandler().process(ctx);
                    } catch (Exception e) {
                        ClientImpl.getClient().getConf().getEvent().error("ReConnectManager reConnect error", e);
                    }
                },
                0, 10, TimeUnit.SECONDS);
    }

    /**
     * Close reconnect job if reconnect success.
     */
    public void reConnectSuccess() {
        scheduledExecutorService.shutdown();
    }


    /***
     * build a thread executor
     */
    private void buildExecutor() {
        if (scheduledExecutorService == null || scheduledExecutorService.isShutdown()) {
            ThreadFactory factory = new ThreadFactoryBuilder()
                    .setNameFormat("reConnect-job-%d")
                    .setDaemon(true)
                    .build();
            scheduledExecutorService = new ScheduledThreadPoolExecutor(1, factory);
        }
    }

    private static class ClientHeartBeatHandle implements HeartBeatHandler {

        @Override
        public void process(ChannelHandlerContext ctx) throws Exception {
            ClientImpl.getClient().reconnect();
        }
    }

    public static ReConnectManager createReConnectManager() {
        return new ReConnectManager();
    }

    public static HeartBeatHandler createHeartBeatHandler() {
        return new ClientHeartBeatHandle();
    }
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/RouteManager.java
================================================
package com.crossoverjie.cim.client.sdk;

import com.crossoverjie.cim.client.sdk.impl.ClientImpl;
import com.crossoverjie.cim.common.core.proxy.RpcProxyManager;
import com.crossoverjie.cim.common.enums.StatusEnum;
import com.crossoverjie.cim.common.exception.CIMException;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.common.res.BaseResponse;
import com.crossoverjie.cim.common.res.NULLBody;
import com.crossoverjie.cim.route.api.RouteApi;
import com.crossoverjie.cim.route.api.vo.req.ChatReqVO;
import com.crossoverjie.cim.route.api.vo.req.LoginReqVO;
import com.crossoverjie.cim.route.api.vo.req.OfflineMsgReqVO;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import okhttp3.OkHttpClient;

public class RouteManager {


    private final RouteApi routeApi;
    private final Event event;

    public RouteManager(String routeUrl, OkHttpClient okHttpClient, Event event) {
        routeApi = RpcProxyManager.create(RouteApi.class, routeUrl, okHttpClient);
        this.event = event;
    }

    public CIMServerResVO getServer(LoginReqVO loginReqVO) throws Exception {
        BaseResponse<CIMServerResVO> cimServerResVO = routeApi.login(loginReqVO);

        // repeat fail
        if (!cimServerResVO.getCode().equals(StatusEnum.SUCCESS.getCode())) {
            event.info(cimServerResVO.getMessage());

            // when client in Reconnecting state, could exit.
            if (ClientImpl.getClient().getState() == ClientState.State.Reconnecting) {
                event.warn("###{}###", StatusEnum.RECONNECT_FAIL.getMessage());
                throw new CIMException(StatusEnum.RECONNECT_FAIL);
            }
        }
        return cimServerResVO.getDataBody();
    }

    public CompletableFuture<Void> sendP2P(CompletableFuture<Void> future, P2PReqVO p2PReqVO) {
        return CompletableFuture.runAsync(() -> {
            try {
                BaseResponse<NULLBody> response = routeApi.p2pRoute(p2PReqVO);
                if (response.getCode().equals(StatusEnum.OFF_LINE.getCode())) {
                    future.completeExceptionally(new CIMException(StatusEnum.OFF_LINE));
                }
                future.complete(null);
            } catch (Exception e) {
                future.completeExceptionally(e);
                event.error("send p2p msg error", e);
            }
        });
    }

    public CompletableFuture<Void> sendGroupMsg(ChatReqVO chatReqVO) {
        return CompletableFuture.runAsync(() -> {
            try {
                routeApi.groupRoute(chatReqVO);
            } catch (Exception e) {
                event.error("send group msg error", e);
            }
        });
    }

    public void offLine(Long userId) {
        ChatReqVO vo = new ChatReqVO(userId, "offLine", null);
        routeApi.offLine(vo);
    }

    public Set<CIMUserInfo> onlineUser() throws Exception {
        BaseResponse<Set<CIMUserInfo>> onlineUsersResVO = routeApi.onlineUser();
        return onlineUsersResVO.getDataBody();
    }

    public void fetchOfflineMsgs(Long userId) {
        OfflineMsgReqVO offlineMsgReqVO = OfflineMsgReqVO.builder().receiveUserId(userId).build();
        routeApi.fetchOfflineMsgs(offlineMsgReqVO);
    }
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientBuilderImpl.java
================================================
package com.crossoverjie.cim.client.sdk.impl;

import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.ClientBuilder;
import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.sdk.io.MessageListener;
import com.crossoverjie.cim.client.sdk.io.ReconnectCheck;
import com.crossoverjie.cim.client.sdk.io.backoff.BackoffStrategy;
import com.crossoverjie.cim.common.util.StringUtil;
import java.util.concurrent.ThreadPoolExecutor;
import okhttp3.OkHttpClient;

public class ClientBuilderImpl implements ClientBuilder {


    private final ClientConfigurationData conf;

    public ClientBuilderImpl() {
        this(new ClientConfigurationData());
    }
    public ClientBuilderImpl(ClientConfigurationData conf) {
        this.conf = conf;
    }

    @Override
    public Client build() {
        return new ClientImpl(conf);
    }

    @Override
    public ClientBuilder auth(ClientConfigurationData.Auth auth) {
        if (auth.getUserId() <= 0 || StringUtil.isEmpty(auth.getUserName())) {
            throw new IllegalArgumentException("userId and userName must be set");
        }
        this.conf.setAuth(auth);
        return this;
    }

    @Override
    public ClientBuilder routeUrl(String routeUrl) {
        if (StringUtil.isEmpty(routeUrl)) {
            throw new IllegalArgumentException("routeUrl must be set");
        }
        this.conf.setRouteUrl(routeUrl);
        return this;
    }

    @Override
    public ClientBuilder loginRetryCount(int loginRetryCount) {
        this.conf.setLoginRetryCount(loginRetryCount);
        return this;
    }

    @Override
    public ClientBuilder event(Event event) {
        this.conf.setEvent(event);
        return this;
    }

    @Override
    public ClientBuilder reconnectCheck(ReconnectCheck reconnectCheck) {
        this.conf.setReconnectCheck(reconnectCheck);
        return this;
    }

    @Override
    public ClientBuilder okHttpClient(OkHttpClient okHttpClient) {
        this.conf.setOkHttpClient(okHttpClient);
        return this;
    }

    @Override
    public ClientBuilder messageListener(MessageListener messageListener) {
        this.conf.setMessageListener(messageListener);
        return this;
    }

    @Override
    public ClientBuilder callbackThreadPool(ThreadPoolExecutor callbackThreadPool) {
        this.conf.setCallbackThreadPool(callbackThreadPool);
        return this;
    }

    @Override
    public ClientBuilder backoffStrategy(BackoffStrategy backoffStrategy) {
        this.conf.setBackoffStrategy(backoffStrategy);
        return this;
    }
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientConfigurationData.java
================================================
package com.crossoverjie.cim.client.sdk.impl;

import com.crossoverjie.cim.client.sdk.Event;
import com.crossoverjie.cim.client.sdk.io.backoff.BackoffStrategy;
import com.crossoverjie.cim.client.sdk.io.MessageListener;
import com.crossoverjie.cim.client.sdk.io.backoff.RandomBackoff;
import com.crossoverjie.cim.client.sdk.io.ReconnectCheck;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.concurrent.ThreadPoolExecutor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import okhttp3.OkHttpClient;

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ClientConfigurationData {

    private Auth auth;

    @Data
    @AllArgsConstructor
    @Builder
    public static class Auth {
        private long userId;
        private String userName;
    }

    private String routeUrl;
    private int loginRetryCount = 5;

    @JsonIgnore
    private Event event = new Event.DefaultEvent();

    @JsonIgnore
    private MessageListener messageListener =
            (client, properties, msg) -> System.out.printf("id:[%s] msg:[%s]%n \n", client.getAuth(), msg);

    @JsonIgnore
    private OkHttpClient okHttpClient = new OkHttpClient();

    @JsonIgnore
    private ThreadPoolExecutor callbackThreadPool;

    @JsonIgnore
    private ReconnectCheck reconnectCheck = (__) -> true;

    @JsonIgnore
    private BackoffStrategy backoffStrategy = new RandomBackoff();
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientImpl.java
================================================
package com.crossoverjie.cim.client.sdk.impl;

import static com.crossoverjie.cim.common.enums.StatusEnum.RECONNECT_FAIL;

import com.crossoverjie.cim.client.sdk.Client;
import com.crossoverjie.cim.client.sdk.ClientState;
import com.crossoverjie.cim.client.sdk.FetchOfflineMsgJob;
import com.crossoverjie.cim.client.sdk.ReConnectManager;
import com.crossoverjie.cim.client.sdk.RouteManager;
import com.crossoverjie.cim.client.sdk.io.CIMClientHandleInitializer;
import com.crossoverjie.cim.common.data.construct.RingBufferWheel;
import com.crossoverjie.cim.common.exception.CIMException;
import com.crossoverjie.cim.common.kit.HeartBeatHandler;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.common.protocol.BaseCommand;
import com.crossoverjie.cim.common.protocol.Request;
import com.crossoverjie.cim.route.api.vo.req.ChatReqVO;
import com.crossoverjie.cim.route.api.vo.req.LoginReqVO;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.DefaultThreadFactory;

import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ClientImpl extends ClientState implements Client {

    @Getter
    private final ClientConfigurationData conf;
    private static final int CALLBACK_QUEUE_SIZE = 1024;
    private static final int CALLBACK_POOL_SIZE = 10;

    // ======= private ========
    private int errorCount;
    private SocketChannel channel;

    private final RouteManager routeManager;

    @Getter
    private final HeartBeatHandler heartBeatHandler = ReConnectManager.createHeartBeatHandler();
    @Getter
    private final ReConnectManager reConnectManager = ReConnectManager.createReConnectManager();

    @Getter
    private static ClientImpl client;
    @Getter
    private static Map<Long, ClientImpl> clientMap = new ConcurrentHashMap<>();
    @Getter
    private final Request heartBeatPacket;

    private RingBufferWheel ringBufferWheel;

    // Client connected server info
    private CIMServerResVO serverInfo;

    public ClientImpl(ClientConfigurationData conf) {
        this.conf = conf;

        if (this.conf.getCallbackThreadPool() == null) {
            BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(CALLBACK_QUEUE_SIZE);
            ThreadFactory factory = new ThreadFactoryBuilder()
                    .setNameFormat("msg-callback-%d")
                    .setDaemon(true)
                    .build();
            this.conf.setCallbackThreadPool(
                    new ThreadPoolExecutor(CALLBACK_POOL_SIZE, CALLBACK_POOL_SIZE, 1, TimeUnit.SECONDS, queue,
                            factory));
        }

        routeManager = new RouteManager(conf.getRouteUrl(), conf.getOkHttpClient(), conf.getEvent());

        heartBeatPacket = Request.newBuilder()
                .setRequestId(this.conf.getAuth().getUserId())
                .setReqMsg("ping")
                .setCmd(BaseCommand.PING)
                .build();
        client = this;
        clientMap.put(conf.getAuth().getUserId(), this);

        connectServer(v -> this.conf.getEvent().info("Login success!"));

        postConnectionSetup();
    }

    /**
     * 1. Pull offline messages from the server
     */
    private void postConnectionSetup() {
        ringBufferWheel = new RingBufferWheel(Executors.newFixedThreadPool(1));
        ringBufferWheel.addTask(new FetchOfflineMsgJob(routeManager, conf));
    }

    private void connectServer(Consumer<Void> success) {
        this.doConnectServer().whenComplete((r, e) -> {
            if (r) {
                success.accept(null);
            }
            if (e != null) {
                if (e instanceof CIMException cimException && cimException.getErrorCode()
                        .equals(RECONNECT_FAIL.getCode())) {
                    this.conf.getEvent().fatal(this);
                } else {
                    if (errorCount++ >= this.conf.getLoginRetryCount()) {
                        this.conf.getEvent()
                                .error("The maximum number of reconnections has been reached[{}]times, exit cim client!",
                                        errorCount);
                        this.conf.getEvent().fatal(this);
                    }
                }
            }

        });
    }

    /**
     * 1. User login and get target server
     * 2. Connect target server
     * 3. send login cmd to server
     */
    private CompletableFuture<Boolean> doConnectServer() {
        CompletableFuture<Boolean> future = new CompletableFuture<>();
        this.userLogin(future).ifPresentOrElse((cimServer) -> {
            this.doConnectServer(cimServer, future);
            this.loginServer();
            this.serverInfo = cimServer;
            future.complete(true);
        }, () -> {
            this.conf.getEvent().error("Login fail!, cannot get server info!");
            this.conf.getEvent().fatal(this);
            future.complete(false);
        });
        return future;
    }

    /**
     * Login and get server info
     *
     * @return Server info
     */
    private Optional<CIMServerResVO> userLogin(CompletableFuture<Boolean> future) {
        LoginReqVO loginReqVO = new LoginReqVO(conf.getAuth().getUserId(),
                conf.getAuth().getUserName());

        CIMServerResVO cimServer = null;
        try {
            cimServer = routeManager.getServer(loginReqVO);
            log.info("cimServer=[{}]", cimServer);
        } catch (Exception e) {
            log.error("login fail", e);
            future.completeExceptionally(e);
        }
        return Optional.ofNullable(cimServer);
    }

    private final EventLoopGroup group = new NioEventLoopGroup(0, new DefaultThreadFactory("cim-work"));

    private void doConnectServer(CIMServerResVO cimServer, CompletableFuture<Boolean> future) {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .handler(new CIMClientHandleInitializer());
        ChannelFuture sync;
        try {
            sync = bootstrap.connect(cimServer.getIp(), cimServer.getCimServerPort()).sync();
            if (sync.isSuccess()) {
                this.conf.getEvent().info("Start cim client success!");
                channel = (SocketChannel) sync.channel();
            }
        } catch (InterruptedException e) {
            future.completeExceptionally(e);
        }
    }

    /**
     * Send login cmd to server
     */
    private void loginServer() {
        Request login = Request.newBuilder()
                .setRequestId(this.conf.getAuth().getUserId())
                .setReqMsg(this.conf.getAuth().getUserName())
                .setCmd(BaseCommand.LOGIN_REQUEST)
                .build();
        channel.writeAndFlush(login)
                .addListener((ChannelFutureListener) channelFuture ->
                        this.conf.getEvent().info("Registry cim server success!")
                );
    }

    /**
     * 1. clear route information.
     * 2. reconnect.
     * 3. shutdown reconnect job.
     * 4. reset reconnect state.
     * @throws Exception
     */
    public void reconnect() throws Exception {
        if (channel != null && channel.isActive()) {
            return;
        }
        this.serverInfo = null;
        // clear route information.
        this.routeManager.offLine(this.getConf().getAuth().getUserId());

        this.conf.getEvent().info("cim trigger reconnecting....");

        this.conf.getBackoffStrategy().runBackoff();

        // don't set State ready, because when connect success, the State will be set to ready automate.
        connectServer(v -> {
            this.reConnectManager.reConnectSuccess();
            this.conf.getEvent().info("Great! reConnect success!!!");
        });
    }

    @Override
    public void close() {
        if (channel != null) {
            channel.close();
            channel = null;
        }
        super.setState(ClientState.State.Closed);
        this.routeManager.offLine(this.getAuth().getUserId());
        this.clientMap.remove(this.getAuth().getUserId());
        ringBufferWheel.stop(true);
    }

    @Override
    public void sendP2P(P2PReqVO p2PReqVO) throws Exception {
        recordSendLog(sendP2PAsync(p2PReqVO), "P2P");
    }

    @Override
    public void sendGroup(String msg) throws Exception {
        recordSendLog(sendGroupAsync(msg), "GROUP");
    }

    private void recordSendLog(CompletableFuture<Void> future, String msgWay) {
        future.orTimeout(10, TimeUnit.SECONDS)
                .whenComplete((result, throwable) -> {
                    if (throwable == null) {
                        log.info("{} message task completed successfully", msgWay);
                    } else if (throwable instanceof TimeoutException) {
                        log.error("{} message processing timeout", msgWay, throwable);
                    } else {
                        log.warn("{} message task completed with exception", msgWay, throwable);
                    }
                });
    }

    @Override
    public CompletableFuture<Void> sendP2PAsync(P2PReqVO p2PReqVO) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        p2PReqVO.setUserId(this.conf.getAuth().getUserId());
        return routeManager.sendP2P(future, p2PReqVO);
    }

    @Override
    public CompletableFuture<Void> sendGroupAsync(String msg) {
        // TODO: 2024/9/12 return messageId
        return this.routeManager.sendGroupMsg(new ChatReqVO(this.conf.getAuth().getUserId(), msg, null));
    }

    @Override
    public ClientConfigurationData.Auth getAuth() {
        return this.conf.getAuth();
    }

    @Override
    public ClientState.State getState() {
        return super.getState();
    }

    @Override
    public Set<CIMUserInfo> getOnlineUser() throws Exception {
        return routeManager.onlineUser();
    }

    @Override
    public Optional<CIMServerResVO> getServerInfo() {
        return Optional.ofNullable(this.serverInfo);
    }
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/CIMClientHandle.java
================================================
package com.crossoverjie.cim.client.sdk.io;

import com.crossoverjie.cim.client.sdk.ClientState;
import com.crossoverjie.cim.client.sdk.impl.ClientImpl;
import com.crossoverjie.cim.common.constant.Constants;
import com.crossoverjie.cim.common.protocol.BaseCommand;
import com.crossoverjie.cim.common.protocol.Response;
import com.crossoverjie.cim.common.util.NettyAttrUtil;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;

@ChannelHandler.Sharable
@Slf4j
public class CIMClientHandle extends SimpleChannelInboundHandler<Response> {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

        if (evt instanceof IdleStateEvent idleStateEvent) {

            if (idleStateEvent.state() == IdleState.WRITER_IDLE) {

                ctx.writeAndFlush(ClientImpl.getClient().getHeartBeatPacket()).addListeners((ChannelFutureListener) future -> {
                    if (!future.isSuccess()) {
                        log.error("heart beat error,close Channel");
                        ClientImpl.getClient().getConf().getEvent().warn("heart beat error,close Channel");
                        future.channel().close();
                    }
                });
            }

        }

        super.userEventTriggered(ctx, evt);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ClientImpl.getClient().getConf().getEvent().debug("ChannelActive");
        ClientImpl.getClient().setState(ClientState.State.Ready);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {

        if (!ClientImpl.getClient().getConf().getReconnectCheck().isNeedReconnect(ClientImpl.getClient())) {
            return;
        }
        ClientImpl.getClient().setState(ClientState.State.Closed);

        ClientImpl.getClient().getConf().getEvent().warn("Client inactive, let's reconnect");
        ClientImpl.getClient().getReConnectManager().reConnect(ctx);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Response msg) {
        if (msg.getCmd() == BaseCommand.PING) {
            ClientImpl.getClient().getConf().getEvent().debug("received ping from server");
            NettyAttrUtil.updateReaderTime(ctx.channel(), System.currentTimeMillis());
        }

        if (msg.getCmd() != BaseCommand.PING) {
            String receiveUserId = msg.getPropertiesMap().get(Constants.MetaKey.RECEIVE_USER_ID);
            ClientImpl client = ClientImpl.getClientMap().get(Long.valueOf(receiveUserId));
            if (client == null) {
                log.error("client not found for userId: {}", receiveUserId);
                return;
            }
            // callback
            client.getConf().getCallbackThreadPool().execute(() -> {
                log.info("client address: {} :{}", ctx.channel().remoteAddress(), client);
                MessageListener messageListener = client.getConf().getMessageListener();
                if (msg.getBatchResMsgCount() > 0) {
                    for (int i = 0; i < msg.getBatchResMsgCount(); i++) {
                        messageListener.received(client, msg.getPropertiesMap(), msg.getBatchResMsg(i));
                    }
                } else {
                    messageListener.received(client, msg.getPropertiesMap(), msg.getResMsg());
                }
            });
        }

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        ClientImpl.getClient().getConf().getEvent().error(cause.getCause().toString());
        ctx.close();
    }
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/CIMClientHandleInitializer.java
================================================
package com.crossoverjie.cim.client.sdk.io;

import com.crossoverjie.cim.common.protocol.Response;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;
import io.netty.handler.timeout.IdleStateHandler;

public class CIMClientHandleInitializer extends ChannelInitializer<Channel> {

    private final CIMClientHandle cimClientHandle = new CIMClientHandle();

    @Override
    protected void initChannel(Channel ch) {
        ch.pipeline()
                .addLast(new IdleStateHandler(0, 10, 0))

                // google Protobuf
                .addLast(new ProtobufVarint32FrameDecoder())
                .addLast(new ProtobufDecoder(Response.getDefaultInstance()))
                .addLast(new ProtobufVarint32LengthFieldPrepender())
                .addLast(new ProtobufEncoder())
                .addLast(cimClientHandle);
    }
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/MessageListener.java
================================================
package com.crossoverjie.cim.client.sdk.io;

import com.crossoverjie.cim.client.sdk.Client;
import java.util.Map;

public interface MessageListener {

    /**
     * @param client     client
     * @param properties meta data
     * @param msg        msgs
     */
    void received(Client client, Map<String, String> properties, String msg);
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/ReconnectCheck.java
================================================
package com.crossoverjie.cim.client.sdk.io;

import com.crossoverjie.cim.client.sdk.Client;

public interface ReconnectCheck {

    /**
     * By the default, the client will reconnect to the server when the connection is close(inactive).
     * @return false if the client should not reconnect to the server.
     */
    boolean isNeedReconnect(Client client);
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/backoff/BackoffStrategy.java
================================================
package com.crossoverjie.cim.client.sdk.io.backoff;

import java.util.concurrent.TimeUnit;

/**
 * @author:qjj
 * @create: 2024-09-21 12:16
 * @Description: backoff strategy interface
 */

public interface BackoffStrategy {
    /**
     * @return the backoff time in milliseconds
     */
    long nextBackoff();

    /**
     * Run the backoff strategy
     * @throws InterruptedException
     */
    default void runBackoff() throws InterruptedException {
        TimeUnit.SECONDS.sleep(nextBackoff());
    }
}


================================================
FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/backoff/RandomBackoff.java
================================================
package com.crossoverjie.cim.client.sdk.io.backoff;

/**
 * @author:qjj
 * @create: 2024-09-21 12:22
 * @Description: random backoff strategy
 */

public class RandomBackoff implements BackoffStrategy {

    @Override
    public long nextBackoff() {
        return (int) (Math.random() * 7 + 3);
    }

}


================================================
FILE: cim-client-sdk/src/test/java/com/crossoverjie/cim/client/sdk/ClientTest.java
================================================
package com.crossoverjie.cim.client.sdk;

import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.client.sdk.impl.ClientImpl;
import com.crossoverjie.cim.client.sdk.io.backoff.RandomBackoff;
import com.crossoverjie.cim.client.sdk.route.AbstractRouteBaseTest;
import com.crossoverjie.cim.common.constant.Constants;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import com.crossoverjie.cim.route.constant.Constant;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

@Slf4j
public class ClientTest extends AbstractRouteBaseTest {


    @AfterEach
    public void tearDown() {
        super.close();
    }

    @Test
    public void groupChat() throws Exception {
        super.starSingleServer();
        super.startRoute(Constant.OfflineStoreMode.REDIS);
        String routeUrl = "http://localhost:8083";
        String cj = "crossoverJie";
        String zs = "zs";
        Long id = super.registerAccount(cj);
        Long zsId = super.registerAccount(zs);
        var auth1 = ClientConfigurationData.Auth.builder()
                .userId(id)
                .userName(cj)
                .build();
        var auth2 = ClientConfigurationData.Auth.builder()
                .userId(zsId)
                .userName(zs)
                .build();

        @Cleanup
        Client client1 = Client.builder()
                .auth(auth1)
                .routeUrl(routeUrl)
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state = client1.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();
        Assertions.assertTrue(serverInfo.isPresent());
        System.out.println("client1 serverInfo = " + serverInfo.get());


        AtomicReference<String> client2Receive = new AtomicReference<>();
        @Cleanup
        Client client2 = Client.builder()
                .auth(auth2)
                .routeUrl(routeUrl)
                .messageListener((client, properties, message) -> {
                    client2Receive.set(message);
                    Assertions.assertEquals(properties.get(Constants.MetaKey.SEND_USER_ID), String.valueOf(auth1.getUserId()));
                    Assertions.assertEquals(properties.get(Constants.MetaKey.SEND_USER_NAME), auth1.getUserName());
                })
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state2 = client2.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));

        Optional<CIMServerResVO> serverInfo2 = client2.getServerInfo();
        Assertions.assertTrue(serverInfo2.isPresent());
        System.out.println("client2 serverInfo = " + serverInfo2.get());

        // send msg
        String msg = "hello";
        client1.sendGroup(msg);

        Set<CIMUserInfo> onlineUser = client1.getOnlineUser();
        Assertions.assertEquals(onlineUser.size(), 2);
        onlineUser.forEach(userInfo -> {
            log.info("online user = {}", userInfo);
            Long userId = userInfo.getUserId();
            if (userId.equals(id)) {
                Assertions.assertEquals(cj, userInfo.getUserName());
            } else if (userId.equals(zsId)) {
                Assertions.assertEquals(zs, userInfo.getUserName());
            }
        });

        Awaitility.await().untilAsserted(
                () -> Assertions.assertEquals(msg, client2Receive.get()));
        super.stopSingle();
        client1.close();
        client2.close();
    }

    @Test
    public void testP2PChat() throws Exception {
        super.starSingleServer();
        super.startRoute(Constant.OfflineStoreMode.REDIS);
        String routeUrl = "http://localhost:8083";
        String cj = "cj";
        String zs = "zs";
        String ls = "ls";
        Long cjId = super.registerAccount(cj);
        Long zsId = super.registerAccount(zs);
        Long lsId = super.registerAccount(ls);
        var auth1 = ClientConfigurationData.Auth.builder()
                .userName(cj)
                .userId(cjId)
                .build();
        var auth2 = ClientConfigurationData.Auth.builder()
                .userName(zs)
                .userId(zsId)
                .build();
        var auth3 = ClientConfigurationData.Auth.builder()
                .userName(ls)
                .userId(lsId)
                .build();

        var client1Receive = new ArrayList<>();
        @Cleanup
        Client client1 = Client.builder()
                .auth(auth1)
                .routeUrl(routeUrl)
                .messageListener((__, properties, message) -> {
                    log.info("client1 receive message = {}", message);
                    client1Receive.add(message);
                })
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state = client1.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();
        Assertions.assertTrue(serverInfo.isPresent());
        System.out.println("client1 serverInfo = " + serverInfo.get());


        // client2
        AtomicReference<String> client2Receive = new AtomicReference<>();
        @Cleanup
        Client client2 = Client.builder()
                .auth(auth2)
                .routeUrl(routeUrl)
                .messageListener((client, properties, message) -> client2Receive.set(message))
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state2 = client2.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));

        Optional<CIMServerResVO> serverInfo2 = client2.getServerInfo();
        Assertions.assertTrue(serverInfo2.isPresent());
        System.out.println("client2 serverInfo = " + serverInfo2.get());

        // client3
        AtomicReference<String> client3Receive = new AtomicReference<>();
        @Cleanup
        Client client3 = Client.builder()
                .auth(auth3)
                .routeUrl(routeUrl)
                .messageListener((client, properties, message) -> {
                    log.info("client3 receive message = {}", message);
                    client3Receive.set(message);
                })
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state3 = client3.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));

        Optional<CIMServerResVO> serverInfo3 = client3.getServerInfo();
        Assertions.assertTrue(serverInfo3.isPresent());
        System.out.println("client3 serverInfo = " + serverInfo3.get());

        // client1 send msg to client3
        String msg = "hello";
        client1.sendP2P(P2PReqVO.builder()
                .receiveUserId(lsId)
                .msg(msg)
                .build());

        Set<CIMUserInfo> onlineUser = client1.getOnlineUser();
        Assertions.assertEquals(onlineUser.size(), 3);
        onlineUser.forEach(userInfo -> {
            log.info("online user = {}", userInfo);
            Long userId = userInfo.getUserId();
            if (userId.equals(cjId)) {
                Assertions.assertEquals(cj, userInfo.getUserName());
            } else if (userId.equals(zsId)) {
                Assertions.assertEquals(zs, userInfo.getUserName());
            } else if (userId.equals(lsId)) {
                Assertions.assertEquals(ls, userInfo.getUserName());
            }
        });

        // client2 send batch msg to client1
        var batchMsg = List.of("a","b","c");
        client2.sendP2P(P2PReqVO.builder()
                .receiveUserId(cjId)
                .batchMsg(batchMsg)
                .build());

        Assertions.assertEquals(ClientImpl.getClientMap().size(), 3);
        Awaitility.await().untilAsserted(
                () -> Assertions.assertEquals(msg, client3Receive.get()));
        Awaitility.await().untilAsserted(
                () -> Assertions.assertNull(client2Receive.get()));
        Awaitility.await().untilAsserted(
                () -> Assertions.assertEquals(batchMsg, client1Receive));
        super.stopSingle();
        client1.close();
        client2.close();
        client3.close();
        Assertions.assertEquals(ClientImpl.getClientMap().size(), 0);
    }

    /**
     * 1. Start two servers.
     * 2. Start two client, and send message.
     * 3. Stop one server which is connected by client1.
     * 4. Wait for client1 reconnect.
     * 5. Send message again.
     *
     * @throws Exception
     */
    @Test
    public void testReconnect() throws Exception {
        super.startTwoServer();
        super.startRoute(Constant.OfflineStoreMode.REDIS);

        String routeUrl = "http://localhost:8083";
        String cj = "cj";
        String zs = "zs";
        Long cjId = super.registerAccount(cj);
        Long zsId = super.registerAccount(zs);
        var auth1 = ClientConfigurationData.Auth.builder()
                .userName(cj)
                .userId(cjId)
                .build();
        var auth2 = ClientConfigurationData.Auth.builder()
                .userName(zs)
                .userId(zsId)
                .build();
        var backoffStrategy = new RandomBackoff();

        @Cleanup
        Client client1 = Client.builder()
                .auth(auth1)
                .routeUrl(routeUrl)
                .backoffStrategy(backoffStrategy)
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state = client1.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));


        AtomicReference<String> client2Receive = new AtomicReference<>();
        @Cleanup
        Client client2 = Client.builder()
                .auth(auth2)
                .routeUrl(routeUrl)
                .messageListener((client, properties, message) -> client2Receive.set(message))
                .backoffStrategy(backoffStrategy)
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state2 = client2.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));

        Optional<CIMServerResVO> serverInfo2 = client2.getServerInfo();
        Assertions.assertTrue(serverInfo2.isPresent());
        System.out.println("client2 serverInfo = " + serverInfo2.get());

        // send msg
        String msg = "hello";
        client1.sendGroup(msg);
        Awaitility.await()
                .untilAsserted(() -> Assertions.assertEquals(msg, client2Receive.get()));
        client2Receive.set("");


        System.out.println("ready to restart server");
        TimeUnit.SECONDS.sleep(3);
        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();
        Assertions.assertTrue(serverInfo.isPresent());
        System.out.println("server info = " + serverInfo.get());

        super.stopServer(serverInfo.get().getCimServerPort());
        System.out.println("stop server success! " + serverInfo.get());


        // Waiting server stopped, and client reconnect.
        TimeUnit.SECONDS.sleep(30);
        System.out.println("reconnect state: " + client1.getState());
        Awaitility.await().atMost(15, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
        serverInfo = client1.getServerInfo();
        Assertions.assertTrue(serverInfo.isPresent());
        System.out.println("client1 reconnect server info = " + serverInfo.get());

        // Send message again.
        log.info("send message again, client2Receive = {}", client2Receive.get());
        client1.sendGroup(msg);
        Awaitility.await()
                .untilAsserted(() -> Assertions.assertEquals(msg, client2Receive.get()));
        super.stopTwoServer();
        client1.close();
        client2.close();
    }

    @Test
    public void offLineAndOnline() throws Exception {
        super.starSingleServer();
        super.startRoute(Constant.OfflineStoreMode.REDIS);
        String routeUrl = "http://localhost:8083";
        String cj = "crossoverJie";
        String zs = "zs";
        Long id = super.registerAccount(cj);
        Long zsId = super.registerAccount(zs);
        var auth1 = ClientConfigurationData.Auth.builder()
                .userId(id)
                .userName(cj)
                .build();
        var auth2 = ClientConfigurationData.Auth.builder()
                .userId(zsId)
                .userName(zs)
                .build();

        @Cleanup
        Client client1 = Client.builder()
                .auth(auth1)
                .routeUrl(routeUrl)
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state = client1.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();
        Assertions.assertTrue(serverInfo.isPresent());
        System.out.println("client1 serverInfo = " + serverInfo.get());


        AtomicReference<String> client2Receive = new AtomicReference<>();
        Client client2 = Client.builder()
                .auth(auth2)
                .routeUrl(routeUrl)
                .messageListener((client, properties, message) -> client2Receive.set(message))
                // Avoid auto reconnect, this test will manually close client.
                .reconnectCheck((client) -> false)
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state2 = client2.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state2));

        Optional<CIMServerResVO> serverInfo2 = client2.getServerInfo();
        Assertions.assertTrue(serverInfo2.isPresent());
        System.out.println("client2 serverInfo = " + serverInfo2.get());

        // send msg
        String msg = "hello";
        client1.sendGroup(msg);
        Awaitility.await().untilAsserted(
                () -> Assertions.assertEquals(msg, client2Receive.get()));
        client2Receive.set("");

        // Manually offline
        client2.close();
        TimeUnit.SECONDS.sleep(10);
        client2 = Client.builder()
                .auth(auth2)
                .routeUrl(routeUrl)
                .messageListener((client, properties, message) -> client2Receive.set(message))
                // Avoid to auto reconnect, this test will manually close client.
                .reconnectCheck((client) -> false)
                .build();
        ClientState.State state3 = client2.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));

        // send msg again
        client1.sendGroup(msg);
        Awaitility.await().untilAsserted(
                () -> Assertions.assertEquals(msg, client2Receive.get()));

        super.stopSingle();
        client1.close();
        client2.close();
    }

    @Test
    public void testClose() throws Exception {
        super.starSingleServer();
        super.startRoute(Constant.OfflineStoreMode.REDIS);
        String routeUrl = "http://localhost:8083";
        String cj = "crossoverJie";
        Long id = super.registerAccount(cj);
        var auth1 = ClientConfigurationData.Auth.builder()
                .userId(id)
                .userName(cj)
                .build();

        Client client1 = Client.builder()
                .auth(auth1)
                .routeUrl(routeUrl)
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state = client1.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();
        Assertions.assertTrue(serverInfo.isPresent());
        System.out.println("client1 serverInfo = " + serverInfo.get());

        client1.close();
        ClientState.State state1 = client1.getState();
        Assertions.assertEquals(ClientState.State.Closed, state1);
        super.stopSingle();
    }

    @Test
    public void testIncorrectUser() throws Exception {
        super.starSingleServer();
        super.startRoute(Constant.OfflineStoreMode.REDIS);
        String routeUrl = "http://localhost:8083";
        String cj = "xx";
        long id = 100L;
        var auth1 = ClientConfigurationData.Auth.builder()
                .userId(id)
                .userName(cj)
                .build();

        Client client1 = Client.builder()
                .auth(auth1)
                .routeUrl(routeUrl)
                .build();
        TimeUnit.SECONDS.sleep(3);

        Assertions.assertDoesNotThrow(client1::close);

        super.stopSingle();
    }
}


================================================
FILE: cim-client-sdk/src/test/java/com/crossoverjie/cim/client/sdk/OfflineMsgTest.java
================================================
package com.crossoverjie.cim.client.sdk;

import com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;
import com.crossoverjie.cim.client.sdk.impl.ClientImpl;
import com.crossoverjie.cim.client.sdk.route.OfflineMsgStoreRouteBaseTest;
import com.crossoverjie.cim.common.pojo.CIMUserInfo;
import com.crossoverjie.cim.route.api.vo.req.P2PReqVO;
import com.crossoverjie.cim.route.api.vo.res.CIMServerResVO;
import com.crossoverjie.cim.route.constant.Constant;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.*;

import java.util.ArrayList;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

@Slf4j
public class OfflineMsgTest extends OfflineMsgStoreRouteBaseTest {

    @Test
    public void testP2POfflineChatRedis() throws Exception {
        super.starSingleServer();
        super.startRoute(Constant.OfflineStoreMode.REDIS);
        String routeUrl = "http://localhost:8083";
        String cj = "cj";
        String ls = "ls";
        Long cjId = super.registerAccount(cj);
        Long lsId = super.registerAccount(ls);
        var auth1 = ClientConfigurationData.Auth.builder()
                .userName(cj)
                .userId(cjId)
                .build();
        var auth3 = ClientConfigurationData.Auth.builder()
                .userName(ls)
                .userId(lsId)
                .build();

        var client1Receive = new ArrayList<>();
        Client client1 = Client.builder()
                .auth(auth1)
                .routeUrl(routeUrl)
                .messageListener((__, properties, message) -> {
                    log.info("client1 receive message = {}", message);
                    client1Receive.add(message);
                })
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state = client1.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();
        Assertions.assertTrue(serverInfo.isPresent());
        System.out.println("client1 serverInfo = " + serverInfo.get());


        // client3
        AtomicReference<String> client3Receive = new AtomicReference<>();
        Client client3 = Client.builder()
                .auth(auth3)
                .routeUrl(routeUrl)
                .messageListener((client, properties, message) -> {
                    log.info("client3 receive message = {}", message);
                    client3Receive.set(message);
                })
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state3 = client3.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));

        Optional<CIMServerResVO> serverInfo3 = client3.getServerInfo();
        Assertions.assertTrue(serverInfo3.isPresent());
        System.out.println("client3 serverInfo = " + serverInfo3.get());

        // client1 send msg to client3
        String msg = "hello";
        client1.sendP2P(P2PReqVO.builder()
                .receiveUserId(lsId)
                .msg(msg)
                .build());

        Set<CIMUserInfo> onlineUser = client1.getOnlineUser();
        Assertions.assertEquals(onlineUser.size(), 2);
        onlineUser.forEach(userInfo -> {
            log.info("online user = {}", userInfo);
            Long userId = userInfo.getUserId();
            if (userId.equals(cjId)) {
                Assertions.assertEquals(cj, userInfo.getUserName());
            } else if (userId.equals(lsId)) {
                Assertions.assertEquals(ls, userInfo.getUserName());
            }
        });


        Awaitility.await().untilAsserted(
                () -> Assertions.assertEquals(msg, client3Receive.get()));

        // Manually offline client3
        client3.close();
        client3Receive.set(null);
        ClientState.State closeState = client3.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Closed, closeState));

        // client1 send client3 an offline message
        String offlineMsg = "offline message";
        client1.sendP2P(P2PReqVO.builder()
                .receiveUserId(lsId)
                .msg(offlineMsg)
                .build());

        // online again
        client3 = Client.builder()
                .auth(auth3)
                .routeUrl(routeUrl)
                .messageListener((client, properties, message) -> {
                    log.info("client3 online again receive message = {}", message);
                    client3Receive.set(message);
                })
                .build();

        ClientState.State client3AgainState = client3.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, client3AgainState));


        // check offline message
        Awaitility.await().untilAsserted(
                () -> Assertions.assertEquals(offlineMsg, client3Receive.get()));

        // close
        client1.close();
        client3.close();
        super.close();
        super.stopSingle();
        Assertions.assertEquals(ClientImpl.getClientMap().size(), 0);
    }

    @Test
    public void testP2POfflineChatMysql() throws Exception {
        super.starSingleServer();
        super.startRoute(Constant.OfflineStoreMode.MYSQL);
        String routeUrl = "http://localhost:8083";
        String cj = "cj";
        String ls = "ls";
        Long cjId = super.registerAccount(cj);
        Long lsId = super.registerAccount(ls);
        var auth1 = ClientConfigurationData.Auth.builder()
                .userName(cj)
                .userId(cjId)
                .build();
        var auth3 = ClientConfigurationData.Auth.builder()
                .userName(ls)
                .userId(lsId)
                .build();

        var client1Receive = new ArrayList<>();
        Client client1 = Client.builder()
                .auth(auth1)
                .routeUrl(routeUrl)
                .messageListener((__, properties, message) -> {
                    log.info("client1 receive message = {}", message);
                    client1Receive.add(message);
                })
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state = client1.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state));
        Optional<CIMServerResVO> serverInfo = client1.getServerInfo();
        Assertions.assertTrue(serverInfo.isPresent());
        System.out.println("client1 serverInfo = " + serverInfo.get());


        // client3
        AtomicReference<String> client3Receive = new AtomicReference<>();
        Client client3 = Client.builder()
                .auth(auth3)
                .routeUrl(routeUrl)
                .messageListener((client, properties, message) -> {
                    log.info("client3 receive message = {}", message);
                    client3Receive.set(message);
                })
                .build();
        TimeUnit.SECONDS.sleep(3);
        ClientState.State state3 = client3.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, state3));

        Optional<CIMServerResVO> serverInfo3 = client3.getServerInfo();
        Assertions.assertTrue(serverInfo3.isPresent());
        System.out.println("client3 serverInfo = " + serverInfo3.get());

        // client1 send msg to client3
        String msg = "hello";
        client1.sendP2P(P2PReqVO.builder()
                .receiveUserId(lsId)
                .msg(msg)
                .build());

        Set<CIMUserInfo> onlineUser = client1.getOnlineUser();
        Assertions.assertEquals(onlineUser.size(), 2);
        onlineUser.forEach(userInfo -> {
            log.info("online user = {}", userInfo);
            Long userId = userInfo.getUserId();
            if (userId.equals(cjId)) {
                Assertions.assertEquals(cj, userInfo.getUserName());
            } else if (userId.equals(lsId)) {
                Assertions.assertEquals(ls, userInfo.getUserName());
            }
        });


        Awaitility.await().untilAsserted(
                () -> Assertions.assertEquals(msg, client3Receive.get()));

        // Manually offline client3
        client3.close();
        client3Receive.set(null);
        ClientState.State closeState = client3.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Closed, closeState));

        // client1 send client3 an offline message
        String offlineMsg = "offline message";
        client1.sendP2P(P2PReqVO.builder()
                .receiveUserId(lsId)
                .msg(offlineMsg)
                .build());

        // online again
        client3 = Client.builder()
                .auth(auth3)
                .routeUrl(routeUrl)
                .messageListener((client, properties, message) -> {
                    log.info("client3 online again receive message = {}", message);
                    client3Receive.set(message);
                })
                .build();

        ClientState.State client3AgainState = client3.getState();
        Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .untilAsserted(() -> Assertions.assertEquals(ClientState.State.Ready, client3AgainState));


        // check offline message
        Awaitility.await().untilAsserted(
                () -> Assertions.assertEquals(offlineMsg, client3Receive.get()));

        // close
        client1.close();
        client3.close();
        super.close();
        super.stopSingle();
        Assertions.assertEquals(ClientImpl.getClientMap().size(), 0);
    }
}


================================================
FILE: cim-client-sdk/src/test/resources/application-route.yaml
================================================
spring:
  application:
    name:
      cim-forward-route
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      jedis:
        pool:
          max-active: 100
          max-idle: 100
          max-wait: 1000
          min-idle: 10


# web port
server:
  port: 8083

logging:
  level:
    root: info

  # enable swagger
springdoc:
  swagger-ui:
    enabled: true

app:
  zk:
    connect:
      timeout: 30000
    root: /route

  # route strategy
  #app.route.way=com.crossoverjie.cim.common.route.algorithm.loop.LoopHandle

  # route strategy
  #app.route.way=com.crossoverjie.cim.common.route.algorithm.random.RandomHandle

  # route strategy
  route:
    way:
      handler: com.crossoverjie.cim.common.route.algorithm.loop.LoopHandle

  #app.route.way.consitenthash=com.crossoverjie.cim.common.route.algorithm.consistenthash.SortArrayMapConsistentHash

      consitenthash: com.crossoverjie.cim.common.route.algorithm.consistenthash.TreeMapConsistentHash

offline:
  store:
    mode: redis
    redis:
      expire:
        message-ttl-days: 3



================================================
FILE: cim-client-sdk/src/test/resources/init.sql
================================================
-- 创建表
CREATE TABLE IF NOT EXISTS `offline_msg`
(
    `id`
                   BIGINT
        PRIMARY
            KEY
        AUTO_INCREMENT,
    `message_id`
                   BIGINT
        NOT
            NULL,
    `receive_user_id`
                   BIGINT
        NOT
            NULL,
    `content`
                   VARCHAR(2000),
    `message_type` INT,
    `status`       TINYINT COMMENT '0: Pending, 1: Acked',
    `created_at`   DATETIME,
    `properties`   VARCHAR(2000),
    INDEX `idx_receive_user_id`
        (
         `receive_user_id`
            )
);
CREATE TABLE offline_msg_last_send_record
(
    receive_user_id BIGINT NOT NULL PRIMARY KEY,
    last_message_id BIGINT,
    updated_at      DATETIME
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

================================================
FILE: cim-common/pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cim</artifactId>
        <groupId>com.crossoverjie.netty</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <version>1.0.0-SNAPSHOT</version>

    <artifactId>cim-common</artifactId>


    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
        <dependency>
            <groupId>com.101tec</groupId>
            <artifactId>zkclient</artifactId>
        </dependency>


        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>

        <dependency>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>${netty.version}</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
        </dependency>
    </dependencies>

    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.5.0.Final</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.5.1</version>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.19.0:exe:${os.detected.classifier}</pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/constant/Constants.java
================================================
package com.crossoverjie.cim.common.constant;

/**
 * Function:常量
 *
 * @author crossoverJie
 *         Date: 28/03/2018 17:41
 * @since JDK 1.8
 */
public class Constants {


    /**
     * 服务端手动 push 次数
     */
    public static final String COUNTER_SERVER_PUSH_COUNT = "counter.server.push.count";


    /**
     * 客户端手动 push 次数
     */
    public static final String COUNTER_CLIENT_PUSH_COUNT = "counter.client.push.count";

    public static class MetaKey {
        public static final String SEND_USER_ID = "sendUserId";
        public static final String SEND_USER_NAME = "sendUserName";
        public static final String RECEIVE_USER_ID = "receiveUserId";
        public static final String RECEIVE_USER_NAME = "receiveUserName";
    }

    //从数据库读取离线消息的每次获取量
    public static final Integer FETCH_OFFLINE_MSG_LIMIT = 100;

    public static final Integer OFFLINE_MSG_PENDING = 0;

    public static final Integer OFFLINE_MSG_DELIVERED = 1;

    public static final Integer MSG_TYPE_TEXT = 0;

    public static final Integer MSG_TYPE_IMAGE = 1;

}


================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/DynamicUrl.java
================================================
package com.crossoverjie.cim.common.core.proxy;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author crossoverJie
 */
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicUrl {
    boolean useMethodEndpoint() default true;
}


================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/Request.java
================================================
package com.crossoverjie.cim.common.core.proxy;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * @author crossoverJie
 */
@Retention(RetentionPolicy.RUNTIME)
public @interface Request {
    String method() default POST;
    String url() default "";

    String GET = "GET";
    String POST = "POST";
}


================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/RpcProxyManager.java
================================================
package com.crossoverjie.cim.common.core.proxy;

import static com.crossoverjie.cim.common.enums.StatusEnum.VALIDATION_FAIL;
import com.alibaba.fastjson.JSONObject;
import com.crossoverjie.cim.common.exception.CIMException;
import com.crossoverjie.cim.common.util.HttpClient;
import com.crossoverjie.cim.common.util.StringUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.net.URI;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Response;

/**
 * RpcProxyManager is a proxy manager for creating dynamic proxy instances of interfaces.
 * It handles HTTP requests and responses using OkHttpClient.
 *
 * @param <T> the type of the proxied interface
 */
@Slf4j
public final class RpcProxyManager<T> {

    private Class<T> clazz;
    private String url;
    private OkHttpClient okHttpClient;
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * Private constructor to initialize RpcProxyManager.
     *
     * @param clazz        Proxied interface
     * @param url          Server provider URL
     * @param okHttpClient HTTP client
     */
    private RpcProxyManager(Class<T> clazz, String url, OkHttpClient okHttpClient) {
        this.clazz = clazz;
        this.url = url;
        this.okHttpClient = okHttpClient;
    }

    private RpcProxyManager(Class<T> clazz, OkHttpClient okHttpClient) {
        this(clazz, "", okHttpClient);
    }

    /**
     * Default private constructor.
     */
    private RpcProxyManager() {
    }

    /**
     * Creates a proxy instance of the specified interface.
     *
     * @param clazz        Proxied interface
     * @param url          Server provider URL
     * @param okHttpClient HTTP client
     * @param <T>          Type of the proxied interface
     * @return Proxy instance of the specified interface
     */
    public static <T> T create(Class<T> clazz, String url, OkHttpClient okHttpClient) {
        return new RpcProxyManager<>(clazz, url, okHttpClient).getInstance();
    }

    public static <T> T create(Class<T> clazz, OkHttpClient okHttpClient) {
        return new RpcProxyManager<>(clazz, okHttpClient).getInstance();
    }

    /**
     * Gets the proxy instance of the API.
     *
     * @return Proxy instance of the API
     */
    @SuppressWarnings("unchecked")
    public T getInstance() {
        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{clazz},
                new ProxyInvocation());
    }

    /**
     * ProxyInvocation is an invocation handler for handling method calls on proxy instances.
     */
    private class ProxyInvocation implements InvocationHandler {

        /**
         * Handles method calls on proxy instances.
         *
         * @param proxy  The proxy instance
         * @param method The method being called
         * @param args   The arguments of the method call
         * @return The result of the method call
         * @throws Throwable if an error occurs during method invocation
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

            Response result = null;
            String serverUrl = url + "/" + method.getName();
            Request annotation = method.getAnnotation(Request.class);
            if (annotation != null && StringUtil.isNotEmpty(annotation.url())) {
                serverUrl = url + "/" + annotation.url();
            }
            URI serverUri = new URI(serverUrl);
            serverUrl = serverUri.normalize().toString();

            Object para = null;
            Class<?> parameterType = null;
            for (int i = 0; i < method.getParameterAnnotations().length; i++) {
                Annotation[] annotations = method.getParameterAnnotations()[i];
                if (annotations.length == 0) {
                    para = args[i];
                    parameterType = method.getParameterTypes()[i];
                }

                for (Annotation ann : annotations) {
                    if (ann instanceof DynamicUrl) {
                        if (args[i] instanceof String) {
                            serverUrl = (String) args[i];
                            if (((DynamicUrl) ann).useMethodEndpoint()) {
                                serverUrl = serverUrl + "/" + method.getName();
                            }
                            break;
                        } else {
                            throw new CIMException("DynamicUrl must be String type");
                        }
                    }
                }
            }

            try {
                if (annotation != null && annotation.method().equals(Request.GET)) {
                    result = HttpClient.get(okHttpClient, serverUrl);
                } else {

                    if (args == null || args.length > 2 || para == null || parameterType == null) {
                        throw new IllegalArgumentException(VALIDATION_FAIL.message());
                    }
                    JSONObject jsonObject = new JSONObject();
                    for (Field field : parameterType.getDeclaredFields()) {
                        field.setAccessible(true);
                        jsonObject.put(field.getName(), field.get(para));
                    }

                    result = HttpClient.post(okHttpClient, jsonObject.toString(), serverUrl);
                }
                if (method.getReturnType() == void.class) {
                    return null;
                }

                String json = result.body().string();
                Type genericTypeOfBaseResponse = getGenericTypeOfBaseResponse(method);
                if (genericTypeOfBaseResponse == null) {
                    return objectMapper.readValue(json, method.getReturnType());
                } else {
                    return objectMapper.readValue(json, objectMapper.getTypeFactory()
                            .constructParametricType(method.getReturnType(),
                                    objectMapper.getTypeFactory().constructType(genericTypeOfBaseResponse)));
                }
            } finally {
                if (result != null) {
                    result.body().close();
                }
            }
        }
    }

    private Type getGenericTypeOfBaseResponse(Method declaredMethod) {
        Type returnType = declaredMethod.getGenericReturnType();

        // check if the return type is a parameterized type
        if (returnType instanceof ParameterizedType parameterizedType) {

            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();

            for (Type typeArgument : actualTypeArguments) {
                return typeArgument;
            }
        }

        return null;

    }

    /**
     * Gets the generic type of the BaseResponse.
     *
     * @param declaredMethod The method whose return type is being checked
     * @return The generic type of the BaseResponse, or null if not found
     * @throws ClassNotFoundException if the class of the generic type is not found
    private Class<?> getBaseResponseGeneric(Method declaredMethod) throws ClassNotFoundException {

    Type returnType = declaredMethod.getGenericReturnType();

    // check if the return type is a parameterized type
    if (returnType instanceof ParameterizedType parameterizedType) {

    Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();

    for (Type typeArgument : actualTypeArguments) {
    // BaseResponse only has one generic type
    return getClass(typeArgument);
    }
    }

    return null;
    }

    public static Class<?> getClass(Type type) throws ClassNotFoundException {
    if (type instanceof Class<?>) {
    // 普通类型,直接返回
    return (Class<?>) type;
    } else if (type instanceof ParameterizedType) {
    // 参数化类型,返回原始类型
    return getClass(((ParameterizedType) type).getRawType());
    } else if (type instanceof TypeVariable<?>) {
    // 类型变量,无法在运行时获取具体类型
    return Object.class;
    } else {
    throw new ClassNotFoundException("无法处理的类型: " + type.toString());
    }
    }*/

}


================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/RingBufferWheel.java
================================================
package com.crossoverjie.cim.common.data.construct;

import lombok.extern.slf4j.Slf4j;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * Function:Ring Queue, it can be used to delay task.
 *
 * @author crossoverJie
 * Date: 2019-09-20 14:46
 * @since JDK 1.8
 */
@Slf4j
public final class RingBufferWheel {


    /**
     * default ring buffer size
     */
    private static final int STATIC_RING_SIZE = 64;

    private Object[] ringBuffer;

    private int bufferSize;

    /**
     * business thread pool
     */
    private ExecutorService executorService;

    private volatile int size = 0;

    /***
     * task stop sign
     */
    private volatile boolean stop = false;

    /**
     * task start sign
     */
    private volatile AtomicBoolean start = new AtomicBoolean(false);

    /**
     * total tick times
     */
    private AtomicInteger tick = new AtomicInteger();

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    private AtomicInteger taskId = new AtomicInteger();
    private Map<Integer, Task> taskMap = new ConcurrentHashMap<>(16);

    /**
     * Create a new delay task ring buffer by default size
     *
     * @param executorService the business thread pool
     */
    public RingBufferWheel(ExecutorService executorService) {
        this.executorService = executorService;
        this.bufferSize = STATIC_RING_SIZE;
        this.ringBuffer = new Object[bufferSize];
    }


    /**
     * Create a new delay task ring buffer by custom buffer size
     *
     * @param executorService the business thread pool
     * @param bufferSize      custom buffer size
     */
    public RingBufferWheel(ExecutorService executorService, int bufferSize) {
        this(executorService);

        if (!powerOf2(bufferSize)) {
            throw new RuntimeException("bufferSize=[" + bufferSize + "] must be a power of 2");
        }
        this.bufferSize = bufferSize;
        this.ringBuffer = new Object[bufferSize];
    }

    /**
     * Add a task into the ring buffer(thread safe)
     *
     * @param task business task extends {@link Task}
     */
    public int addTask(Task task) {
        int key = task.getKey();
        int id;

        try {
            lock.lock();
            int index = mod(key, bufferSize);
            task.setIndex(index);
            Set<Task> tasks = get(index);

            int cycleNum = cycleNum(key, bufferSize);
            if (tasks != null) {
                task.setCycleNum(cycleNum);
                tasks.add(task);
            } else {
                task.setIndex(index);
                task.setCycleNum(cycleNum);
                Set<Task> sets = new HashSet<>();
                sets.add(task);
                put(key, sets);
            }
            id = taskId.incrementAndGet();
            task.setTaskId(id);
            taskMap.put(id, task);
            size++;
        } finally {
            lock.unlock();
        }

        start();

        return id;
    }


    /**
     * Cancel task by taskId
     * @param id unique id through {@link #addTask(Task)}
     * @return
     */
    public boolean cancel(int id) {

        boolean flag = false;
        Set<Task> tempTask = new HashSet<>();

        try {
            lock.lock();
            Task task = taskMap.get(id);
            if (task == null) {
                return false;
            }

            Set<Task> tasks = get(task.getIndex());
            for (Task tk : tasks) {
                if (tk.getKey() == task.getKey() && tk.getCycleNum() == task.getCycleNum()) {
                    size--;
                    flag = true;
                    taskMap.remove(id);
                } else {
                    tempTask.add(tk);
                }

            }
            //update origin data
            ringBuffer[task.getIndex()] = tempTask;
        } finally {
            lock.unlock();
        }

        return flag;
    }

    /**
     * Thread safe
     *
     * @return the size of ring buffer
     */
    public int taskSize() {
        return size;
    }

    /**
     * Same with method {@link #taskSize}
     * @return
     */
    public int taskMapSize() {
        return taskMap.size();
    }

    /**
     * Start background thread to consumer wheel timer, it will always run until you call method {@link #stop}
     */
    public void start() {
        if (!start.get()) {

            if (start.compareAndSet(start.get(), true)) {
                log.info("Delay task is starting");
                Thread job = new Thread(new TriggerJob());
                job.setName("consumer RingBuffer thread");
                job.start();
                start.set(true);
            }

        }
    }

    /**
     * Stop consumer ring buffer thread
     *
     * @param force True will force close consumer thread and discard all pending tasks
     *              otherwise the consumer thread waits for all tasks to completes before closing.
     */
    public void stop(boolean force) {
        if (force) {
            log.info("Delay task is forced stop");
            stop = true;
            executorService.shutdownNow();
        } else {
            log.info("Delay task is stopping");
            if (taskSize() > 0) {
                try {
                    lock.lock();
                    condition.await();
                    stop = true;
                } catch (InterruptedException e) {
                    log.error("InterruptedException", e);
                } finally {
                    lock.unlock();
                }
            }
            executorService.shutdown();
        }


    }


    private Set<Task> get(int index) {
        return (Set<Task>) ringBuffer[index];
    }

    private void put(int key, Set<Task> tasks) {
        int index = mod(key, bufferSize);
        ringBuffer[index] = tasks;
    }

    /**
     * Remove and get task list.
     * @param key
     * @return task list
     */
    private Set<Task> remove(int key) {
        Set<Task> tempTask = new HashSet<>();
        Set<Task> result = new HashSet<>();

        Set<Task> tasks = (Set<Task>) ringBuffer[key];
        if (tasks == null) {
            return result;
        }

        for (Task task : tasks) {
            if (task.getCycleNum() == 0) {
                result.add(task);

                size2Notify();
            } else {
                // decrement 1 cycle number and update origin data
                task.setCycleNum(task.getCycleNum() - 1);
                tempTask.add(task);
            }
            // remove task, and free the memory.
            taskMap.remove(task.getTaskId());
        }

        //update origin data
        ringBuffer[key] = tempTask;

        return result;
    }

    private void size2Notify() {
        try {
            lock.lock();
            size--;
            if (size == 0) {
                condition.signal();
            }
        } finally {
            lock.unlock();
        }
    }

    private boolean powerOf2(int target) {
        if (target < 0) {
            return false;
        }
        int value = target & (target - 1);
        if (value != 0) {
            return false;
        }

        return true;
    }

    private int mod(int target, int mod) {
        // equals target % mod
        target = target + tick.get();
        return target & (mod - 1);
    }

    private int cycleNum(int target, int mod) {
        //equals target/mod
        return target >> Integer.bitCount(mod - 1);
    }

    /**
     * An abstract class used to implement business.
     */
    public abstract static class Task extends Thread {

        private int index;

        private int cycleNum;

        private int key;

        /**
         * The unique ID of the task
         */
        private int taskId;

        @Override
        public void run() {
        }

        public int getKey() {
            return key;
        }

        /**
         *
         * @param key Delay time(seconds)
         */
        public void setKey(int key) {
            this.key = key;
        }

        public int getCycleNum() {
            return cycleNum;
        }

        private void setCycleNum(int cycleNum) {
            this.cycleNum = cycleNum;
        }

        public int getIndex() {
            return index;
        }

        private void setIndex(int index) {
            this.index = index;
        }

        public int getTaskId() {
            return taskId;
        }

        public void setTaskId(int taskId) {
            this.taskId = taskId;
        }
    }


    private class TriggerJob implements Runnable {

        @Override
        public void run() {
            int index = 0;
            while (!stop) {
                try {
                    Set<Task> tasks = remove(index);
                    for (Task task : tasks) {
                        executorService.submit(task);
                    }

                    if (++index > bufferSize - 1) {
                        index = 0;
                    }

                    //Total tick number of records
                    tick.incrementAndGet();
                    TimeUnit.SECONDS.sleep(1);

                } catch (Exception e) {
                    log.error("Exception", e);
                }

            }

            log.info("Delay task has stopped");
        }
    }
}


================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/SortArrayMap.java
================================================
package com.crossoverjie.cim.common.data.construct;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.apache.curator.shaded.com.google.common.collect.Sets;

/**
 * Function:根据 key 排序的 Map
 *
 * @author crossoverJie
 * Date: 2019-02-25 18:17
 * @since JDK 1.8
 */
public class SortArrayMap extends AbstractMap<String, String> {

    /**
     * 核心数组
     */
    private Node[] buckets;

    private static final int DEFAULT_SIZE = 10;

    /**
     * 数组大小
     */
    private int size = 0;

    public SortArrayMap() {
        buckets = new Node[DEFAULT_SIZE];
    }

    /**
     * 写入数据
     * @param key
     * @param value
     */
    public void add(Long key, String value) {
        checkSize(size + 1);
        Node node = new Node(key, value);
        buckets[size++] = node;
    }

    public SortArrayMap remove(String value) {
        List<Node> list = new ArrayList<>(Arrays.asList(buckets));
        list.removeIf(next -> next != null && next.value.equals(value));
        buckets = list.toArray(new Node[0]);
        return this;
    }

    /**
     * 校验是否需要扩容
     * @param size
     */
    private void checkSize(int size) {
        if (size >= buckets.length) {
            //扩容自身的 3/2
            int oldLen = buckets.length;
            int newLen = oldLen + (oldLen >> 1);
            buckets = Arrays.copyOf(buckets, newLen);
        }
    }

    /**
     * 顺时针取出数据
     * @param key
     * @return
     */
    public String firstNodeValue(long key) {
        if (size == 0) {
            return null;
        }
        for (Node bucket : buckets) {
            if (bucket == null) {
                break;
            }
            if (bucket.key >= key) {
                return bucket.value;
            }
        }

        return buckets[0].value;

    }

    /**
     * 排序
     */
    public void sort() {
        Arrays.sort(buckets, 0, size, (o1, o2) -> {
            if (o1.key > o2.key) {
                return 1;
            } else {
                return 0;
            }
        });
    }

    public void print() {
        for (Node bucket : buckets) {
            if (bucket == null) {
                continue;
            }
            System.out.println(bucket.toString());
        }
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public void clear() {
        buckets = new Node[DEFAULT_SIZE];
        size = 0;
    }

    @Override
    public Set<Entry<String, String>> entrySet() {
        Set<Entry<String, String>> set = Sets.newHashSet();
        for (Node bucket : buckets) {
            set.add(new SimpleEntry<>(String.valueOf(bucket.key), bucket.value));
        }
        return set;
    }

    @Override
    public Set<String> keySet() {
        Set<String> set = Sets.newHashSet();
        for (Node bucket : buckets) {
            if (bucket == null) {
                continue;
            }
            set.add(bucket.value);
        }
        return set;
    }

    /**
     * 数据节点
     */
    private class Node {
        public Long key;
        public String value;

        public Node(Long key, String value) {
            this.key = key;
            this.value = value;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "key=" + key +
                    ", value='" + value + '\'' +
                    '}';
        }

    }

}


================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/TrieTree.java
================================================
package com.crossoverjie.cim.common.data.construct;

import com.crossoverjie.cim.common.util.StringUtil;

import java.util.ArrayList;
import java.util.List;

/**
 * Function:字典树字符前缀模糊匹配
 *
 * @author crossoverJie
 *         Date: 2019/1/7 18:58
 * @since JDK 1.8
 */
public class TrieTree {

    /**
     * 大小写都可保存
     */
    private static final int CHILDREN_LENGTH = 26 * 2;

    /**
     * 存放的最大字符串长度
     */
    private static final int MAX_CHAR_LENGTH = 16;

    private static final char UPPERCASE_STAR = 'A';

    /**
     * 小写就要 -71
     */
    private static final char LOWERCASE_STAR = 'G';

    private Node root;

    public TrieTree() {
        root = new Node();
    }

    /**
     * 写入
     *
     * @param data
     */
    public void insert(String data) {
        this.insert(this.root, data);
    }

    private void insert(Node root, String data) {
        char[] chars = data.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            char aChar = chars[i];
            int index;
            if (Character.isUpperCase(aChar)) {
                index = aChar - UPPERCASE_STAR;
            } else {
                //小写就要 -71
                index = aChar - LOWERCASE_STAR;
            }


            if (index >= 0 && index < CHILDREN_LENGTH) {
                if (root.children[index] == null) {
                    Node node = new Node();
                    root.children[index] = node;
                    root.children[index].data = chars[i];

                }

                //最后一个字符设置标志
                if (i + 1 == chars.length) {
                    root.children[index].isEnd = true;
                }

                //指向下一节点
                root = root.children[index];
            }

        }
    }


    /**
     * 递归深度遍历
     *
     * @param key
     * @return
     */
    public List<String> prefixSearch(String key) {
        List<String> value = new ArrayList<String>();
        if (StringUtil.isEmpty(key)) {
            return value;
        }

        char k = key.charAt(0);
        int index;
        if (Character.isUpperCase(k)) {
            index = k - UPPERCASE_STAR;
        } else {
            index = k - LOWERCASE_STAR;

        }
        if (root.children != null && root.children[index] != null) {
            return query(root.children[index], value,
                    key.substring(1), String.valueOf(k));
        }
        return value;
    }

    private List<String> query(Node child, List<String> value, String key, String result) {

        if (child.isEnd && key == null) {
            value.add(result);
        }
        if (StringUtil.isNotEmpty(key)) {
            char ca = key.charAt(0);

            int index;
            if (Character.isUpperCase(ca)) {
                index = ca - UPPERCASE_STAR;
            } else {
                index = ca - LOWERCASE_STAR;

            }

            if (child.children[index] != null) {
                query(child.children[index], value, key.substring(1).equals("") ? null : key.substring(1), result + ca);
            }
        } else {
            for (int i = 0; i < CHILDREN_LENGTH; i++) {
                if (child.children[i] == null) {
                    continue;
                }

                int j;
                if (Character.isUpperCase(child.children[i].data)) {
                    j = UPPERCASE_STAR + i;
                } else {
                    j = LOWERCASE_STAR + i;
                }

                char temp = (char) j;
                query(child.children[i], value, null, result + temp);
            }
        }

        return value;
    }


    /**
     * 查询所有
     *
     * @return
     */
    public List<String> all() {
        char[] chars = new char[MAX_CHAR_LENGTH];
        List<String> value = depth(this.root, new ArrayList<String>(), chars, 0);
        return value;
    }


    public List<String> depth(Node node, List<String> list, char[] chars, int index) {
        if (node.children == null || node.children.length == 0) {
            return list;
        }

        Node[] children = node.children;

        for (int i = 0; i < children.length; i++) {
            Node child = children[i];

            if (child == null) {
                continue;
            }

            if (child.isEnd) {
                chars[index] = child.data;

                char[] temp = new char[index + 1];
                for (int j = 0; j < chars.length; j++) {
                    if (chars[j] == 0) {
                        continue;
                    }

                    temp[j] = chars[j];
                }
                list.add(String.valueOf(temp));
                return list;
            } else {
                chars[index] = child.data;

                index++;

                depth(child, list, chars, index);

                index = 0;
            }
        }


        return list;
    }


    /**
     * 字典树节点
     */
    private class Node {
        /**
         * 是否为最后一个字符
         */
        public boolean isEnd = false;

        /**
         * 如果只是查询,则不需要存储数据
         */
        public char data;

        public Node[] children = new Node[CHILDREN_LENGTH];

    }
}


================================================
FILE: cim-common/src/main/java/com/crossoverjie/cim/common/enums/StatusEnum.java
================================================
package com.crossoverjie.cim.common.enums;

import java.util.ArrayList;
import java.util.List;

/**
 * @author crossoverJie
 */

public enum StatusEnum {

    /** 成功 */
    SUCCESS("9000", "Success"),
    /** 成功 */
    FALLBACK("8000", "FALL_BACK"),
    /** 参数校验失败**/
    VALIDATION_FAIL("3000", "invalid argument"),
    /** 失败 */
    FAIL("4000", "Failure"),

    /** 重复登录 */
    REPEAT_LOGIN("5000", "Repeat login, log out an account please!"),

    /** 请求限流 */
    REQUEST_LIMIT("6000", "请求限流"),

    /** 账号不在线 */
    OFF_LINE("7000", "You selected user is offline!, please try again later!"),

    SERVER_NOT_AVAILABLE("7100", "cim server is not available, please try again later!"),

    RECONNECT_FAIL("7200", "Reconnect fail, continue to retry!"),
    /** 登录信息不匹配 */
    ACCOUNT_NOT_MATCH("9100", "The User information you have used is incorrect!"),

    OFFLINE_MESSAGE_STORAGE_ERROR("9200", "Offline message storage error!"),

    OFFLINE_MESSAGE_FETCH_ERROR("9201", "Offline message fetch error!"),

    OFFLINE_MESSAGE_DELETE_ERROR("9202", "Offline message delete error!");


    /** 枚举值码 */
    private final String code;

    /** 枚举描述 */
    private final String message;

    /**
     * 构建一个 StatusEnum 。
     * @param code 枚举值码。
     * @param message 枚举描述。
     */
    private StatusEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }

    /**
     * 得到枚举值码。
     * @return 枚举值码。
     */
    public String getCode() {
        return code;
    }

    /**
     * 得到枚举描述。
     * @return 枚举描述。
     */
    public String getMessage() {
        return message;
    }

    /**
     * 得到枚举值码。
     * @return 枚举值码。
     */
    public String code() {
        return code;
    }

    /**
     * 得到枚举描述。
     * @return 枚举描述。
     */
    public String message() {
        return message;
    }

    /**
     * 通过枚举值码查找枚举值。
     * @param code 查找枚举值的枚举值码。
     * @return 枚举值码对应的枚举值。
     * @throws IllegalArgumentException 如果 code 没有对应的 StatusEnum 。
     */
    public static StatusEnum findStatus(String code) {
        for (StatusEnum status : values()) {
            if (status.getCode().equals(code)) {
                return status;
            }
        }
        throw new IllegalArgumentException("ResultInfo StatusEnum not legal:" + code);
    }

    /**
     * 获取全部枚举值。
     *
     * @return 全部枚举值。
     */
    public static List<StatusEnum> getAllStatus() {
        List<StatusEnum> list = new ArrayList<StatusEnum>();
        for (StatusEnum status : values()) {
            list.add(status);
        }
        return list;
    }

    /**
     * 获取全部枚举值码。
     *
     * @return 全部枚举值码。
     */
    public static List<String> getAllStatusCode() {
        List<String> list = new ArrayList<String>();
        for (StatusEnum status : values()) {
            list.add(status.code());
    
Download .txt
gitextract_k26pvt4s/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   └── bug_report.md
│   └── workflows/
│       ├── docker.yml
│       ├── maven.yml
│       └── reusable_run_tests.yml
├── .gitignore
├── CLAUDE.md
├── LICENSE
├── Makefile
├── README-zh.md
├── README.md
├── checkstyle/
│   ├── checkstyle.xml
│   └── suppressions.xml
├── cim-client/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── crossoverjie/
│       │   │           └── cim/
│       │   │               └── client/
│       │   │                   ├── CIMClientApplication.java
│       │   │                   ├── config/
│       │   │                   │   ├── AppConfiguration.java
│       │   │                   │   ├── BeanConfig.java
│       │   │                   │   └── SwaggerConfig.java
│       │   │                   ├── scanner/
│       │   │                   │   └── Scan.java
│       │   │                   ├── service/
│       │   │                   │   ├── InnerCommand.java
│       │   │                   │   ├── InnerCommandContext.java
│       │   │                   │   ├── MsgHandle.java
│       │   │                   │   ├── MsgLogger.java
│       │   │                   │   ├── ShutDownSign.java
│       │   │                   │   └── impl/
│       │   │                   │       ├── AsyncMsgLogger.java
│       │   │                   │       ├── EchoServiceImpl.java
│       │   │                   │       ├── MsgCallBackListener.java
│       │   │                   │       ├── MsgHandler.java
│       │   │                   │       └── command/
│       │   │                   │           ├── CloseAIModelCommand.java
│       │   │                   │           ├── DelayMsgCommand.java
│       │   │                   │           ├── EchoInfoCommand.java
│       │   │                   │           ├── EmojiCommand.java
│       │   │                   │           ├── OpenAIModelCommand.java
│       │   │                   │           ├── PrefixSearchCommand.java
│       │   │                   │           ├── PrintAllCommand.java
│       │   │                   │           ├── PrintOnlineUsersCommand.java
│       │   │                   │           ├── QueryHistoryCommand.java
│       │   │                   │           └── ShutDownCommand.java
│       │   │                   └── util/
│       │   │                       └── SpringBeanFactory.java
│       │   └── resources/
│       │       ├── application.yaml
│       │       └── banner.txt
│       └── test/
│           ├── java/
│           │   └── com/
│           │       └── crossoverjie/
│           │           └── cim/
│           │               ├── client/
│           │               │   └── service/
│           │               │       ├── InnerCommandContextTest.java
│           │               │       └── impl/
│           │               │           └── AsyncMsgLoggerTest.java
│           │               └── server/
│           │                   └── test/
│           │                       ├── CommonTest.java
│           │                       └── EchoTest.java
│           └── resources/
│               └── application.yaml
├── cim-client-sdk/
│   ├── README.md
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   └── java/
│       │       └── com/
│       │           └── crossoverjie/
│       │               └── cim/
│       │                   └── client/
│       │                       └── sdk/
│       │                           ├── Client.java
│       │                           ├── ClientBuilder.java
│       │                           ├── ClientState.java
│       │                           ├── Event.java
│       │                           ├── FetchOfflineMsgJob.java
│       │                           ├── ReConnectManager.java
│       │                           ├── RouteManager.java
│       │                           ├── impl/
│       │                           │   ├── ClientBuilderImpl.java
│       │                           │   ├── ClientConfigurationData.java
│       │                           │   └── ClientImpl.java
│       │                           └── io/
│       │                               ├── CIMClientHandle.java
│       │                               ├── CIMClientHandleInitializer.java
│       │                               ├── MessageListener.java
│       │                               ├── ReconnectCheck.java
│       │                               └── backoff/
│       │                                   ├── BackoffStrategy.java
│       │                                   └── RandomBackoff.java
│       └── test/
│           ├── java/
│           │   └── com/
│           │       └── crossoverjie/
│           │           └── cim/
│           │               └── client/
│           │                   └── sdk/
│           │                       ├── ClientTest.java
│           │                       └── OfflineMsgTest.java
│           └── resources/
│               ├── application-route.yaml
│               └── init.sql
├── cim-common/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── crossoverjie/
│       │   │           └── cim/
│       │   │               └── common/
│       │   │                   ├── constant/
│       │   │                   │   └── Constants.java
│       │   │                   ├── core/
│       │   │                   │   └── proxy/
│       │   │                   │       ├── DynamicUrl.java
│       │   │                   │       ├── Request.java
│       │   │                   │       └── RpcProxyManager.java
│       │   │                   ├── data/
│       │   │                   │   └── construct/
│       │   │                   │       ├── RingBufferWheel.java
│       │   │                   │       ├── SortArrayMap.java
│       │   │                   │       └── TrieTree.java
│       │   │                   ├── enums/
│       │   │                   │   ├── StatusEnum.java
│       │   │                   │   └── SystemCommandEnum.java
│       │   │                   ├── exception/
│       │   │                   │   ├── CIMException.java
│       │   │                   │   └── GenericException.java
│       │   │                   ├── kit/
│       │   │                   │   └── HeartBeatHandler.java
│       │   │                   ├── metastore/
│       │   │                   │   ├── AbstractConfiguration.java
│       │   │                   │   ├── MetaStore.java
│       │   │                   │   ├── ZkConfiguration.java
│       │   │                   │   └── ZkMetaStoreImpl.java
│       │   │                   ├── pojo/
│       │   │                   │   ├── CIMUserInfo.java
│       │   │                   │   └── RouteInfo.java
│       │   │                   ├── req/
│       │   │                   │   └── BaseRequest.java
│       │   │                   ├── res/
│       │   │                   │   ├── BaseResponse.java
│       │   │                   │   └── NULLBody.java
│       │   │                   ├── route/
│       │   │                   │   └── algorithm/
│       │   │                   │       ├── RouteHandle.java
│       │   │                   │       ├── consistenthash/
│       │   │                   │       │   ├── AbstractConsistentHash.java
│       │   │                   │       │   ├── ConsistentHashHandle.java
│       │   │                   │       │   ├── SortArrayMapConsistentHash.java
│       │   │                   │       │   └── TreeMapConsistentHash.java
│       │   │                   │       ├── loop/
│       │   │                   │       │   └── LoopHandle.java
│       │   │                   │       └── random/
│       │   │                   │           └── RandomHandle.java
│       │   │                   └── util/
│       │   │                       ├── HttpClient.java
│       │   │                       ├── NettyAttrUtil.java
│       │   │                       ├── RouteInfoParseUtil.java
│       │   │                       ├── SnowflakeIdWorker.java
│       │   │                       └── StringUtil.java
│       │   ├── proto/
│       │   │   └── cim.proto
│       │   └── resources/
│       │       └── log4j.properties
│       └── test/
│           └── java/
│               └── com/
│                   └── crossoverjie/
│                       └── cim/
│                           └── common/
│                               ├── CommonTest.java
│                               ├── core/
│                               │   └── proxy/
│                               │       └── RpcProxyManagerTest.java
│                               ├── data/
│                               │   └── construct/
│                               │       ├── RingBufferWheelTest.java
│                               │       ├── ScheduledTest.java
│                               │       ├── SortArrayMapTest.java
│                               │       ├── TimerTest.java
│                               │       └── TrieTreeTest.java
│                               ├── enums/
│                               │   └── SystemCommandEnumTypeTest.java
│                               ├── metastore/
│                               │   └── MetaStoreTest.java
│                               ├── route/
│                               │   └── algorithm/
│                               │       ├── consistenthash/
│                               │       │   ├── ConsistentHashHandleTest.java
│                               │       │   ├── RangeCheckTestUtil.java
│                               │       │   ├── SortArrayMapConsistentHashTest.java
│                               │       │   └── TreeMapConsistentHashTest.java
│                               │       ├── loop/
│                               │       │   └── LoopHandleTest.java
│                               │       └── random/
│                               │           └── RandomHandleTest.java
│                               └── util/
│                                   ├── HttpClientTest.java
│                                   └── ProtocolTest.java
├── cim-forward-route/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── crossoverjie/
│       │   │           └── cim/
│       │   │               └── route/
│       │   │                   ├── RouteApplication.java
│       │   │                   ├── config/
│       │   │                   │   ├── AppConfiguration.java
│       │   │                   │   ├── BeanConfig.java
│       │   │                   │   ├── MySqlPersistenceConfig.java
│       │   │                   │   ├── OfflineMsgStoreConfig.java
│       │   │                   │   └── SwaggerConfig.java
│       │   │                   ├── constant/
│       │   │                   │   └── Constant.java
│       │   │                   ├── controller/
│       │   │                   │   └── RouteController.java
│       │   │                   ├── exception/
│       │   │                   │   └── ExceptionHandlingController.java
│       │   │                   ├── factory/
│       │   │                   │   └── OfflineMsgFactory.java
│       │   │                   ├── kit/
│       │   │                   │   └── NetAddressIsReachable.java
│       │   │                   ├── service/
│       │   │                   │   ├── AccountService.java
│       │   │                   │   ├── CommonBizService.java
│       │   │                   │   ├── OfflineMsgService.java
│       │   │                   │   ├── UserInfoCacheService.java
│       │   │                   │   └── impl/
│       │   │                   │       ├── AccountServiceRedisImpl.java
│       │   │                   │       ├── OfflineMsgServiceImpl.java
│       │   │                   │       └── UserInfoCacheServiceImpl.java
│       │   │                   └── util/
│       │   │                       └── SpringBeanFactory.java
│       │   └── resources/
│       │       ├── application.yaml
│       │       ├── banner.txt
│       │       └── lua/
│       │           └── offLine.lua
│       └── test/
│           ├── java/
│           │   ├── CommonTest.java
│           │   └── com/
│           │       └── crossoverjie/
│           │           └── cim/
│           │               └── route/
│           │                   └── service/
│           │                       └── impl/
│           │                           ├── AbstractBaseTest.java
│           │                           ├── AccountServiceRedisImplTest.java
│           │                           ├── RedisTest.java
│           │                           └── UserInfoCacheServiceImplTest.java
│           └── resources/
│               ├── application.yaml
│               └── init.sql
├── cim-integration-test/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   └── java/
│       │       └── com/
│       │           └── crossoverjie/
│       │               └── cim/
│       │                   └── client/
│       │                       └── sdk/
│       │                           ├── route/
│       │                           │   ├── AbstractRouteBaseTest.java
│       │                           │   └── OfflineMsgStoreRouteBaseTest.java
│       │                           └── server/
│       │                               └── AbstractServerBaseTest.java
│       └── test/
│           └── resources/
│               ├── application-client.yaml
│               └── application-route.yaml
├── cim-persistence/
│   ├── cim-persistence-api/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           └── java/
│   │               └── com/
│   │                   └── crossoverjie/
│   │                       └── cim/
│   │                           └── persistence/
│   │                               └── api/
│   │                                   ├── config/
│   │                                   │   └── BeanConfig.java
│   │                                   ├── pojo/
│   │                                   │   ├── OfflineMsg.java
│   │                                   │   └── OfflineMsgLastSendRecord.java
│   │                                   ├── service/
│   │                                   │   └── OfflineMsgStore.java
│   │                                   └── vo/
│   │                                       └── req/
│   │                                           └── SaveOfflineMsgReqVO.java
│   ├── cim-persistence-mysql/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           ├── java/
│   │           │   └── com/
│   │           │       └── crossoverjie/
│   │           │           └── cim/
│   │           │               └── persistence/
│   │           │                   └── mysql/
│   │           │                       ├── config/
│   │           │                       │   └── MyBatisConfig.java
│   │           │                       ├── offlinemsg/
│   │           │                       │   ├── OfflineMsgDb.java
│   │           │                       │   └── mapper/
│   │           │                       │       ├── OfflineMsgLastSendRecordMapper.java
│   │           │                       │       └── OfflineMsgMapper.java
│   │           │                       └── util/
│   │           │                           └── MapToJsonTypeHandler.java
│   │           └── resources/
│   │               └── mapper/
│   │                   ├── OfflineMsgLastSendRecordMapper.xml
│   │                   └── OfflineMsgMapper.xml
│   ├── cim-persistence-redis/
│   │   ├── pom.xml
│   │   └── src/
│   │       └── main/
│   │           ├── java/
│   │           │   └── com/
│   │           │       └── crossoverjie/
│   │           │           └── cim/
│   │           │               └── persistence/
│   │           │                   └── redis/
│   │           │                       ├── OfflineMsgBuffer.java
│   │           │                       ├── constant/
│   │           │                       │   └── Constant.java
│   │           │                       └── kit/
│   │           │                           └── OfflineMsgScriptExecutor.java
│   │           └── resources/
│   │               └── lua/
│   │                   ├── deleteOfflineMsg.lua
│   │                   ├── fetchOfflineMsg.lua
│   │                   └── saveOfflineMsg.lua
│   └── pom.xml
├── cim-rout-api/
│   ├── pom.xml
│   └── src/
│       └── main/
│           └── java/
│               └── com/
│                   └── crossoverjie/
│                       └── cim/
│                           └── route/
│                               └── api/
│                                   ├── RouteApi.java
│                                   └── vo/
│                                       ├── req/
│                                       │   ├── ChatReqVO.java
│                                       │   ├── LoginReqVO.java
│                                       │   ├── OfflineMsgReqVO.java
│                                       │   ├── P2PReqVO.java
│                                       │   ├── RegisterInfoReqVO.java
│                                       │   └── SendMsgReqVO.java
│                                       └── res/
│                                           ├── CIMServerResVO.java
│                                           ├── RegisterInfoResVO.java
│                                           └── SendMsgResVO.java
├── cim-server/
│   ├── pom.xml
│   └── src/
│       ├── main/
│       │   ├── java/
│       │   │   └── com/
│       │   │       └── crossoverjie/
│       │   │           └── cim/
│       │   │               └── server/
│       │   │                   ├── CIMServerApplication.java
│       │   │                   ├── config/
│       │   │                   │   ├── AppConfiguration.java
│       │   │                   │   ├── BeanConfig.java
│       │   │                   │   └── SwaggerConfig.java
│       │   │                   ├── controller/
│       │   │                   │   └── IndexController.java
│       │   │                   ├── handle/
│       │   │                   │   └── CIMServerHandle.java
│       │   │                   ├── init/
│       │   │                   │   └── CIMServerInitializer.java
│       │   │                   ├── kit/
│       │   │                   │   ├── RegistryMetaStore.java
│       │   │                   │   ├── RouteHandler.java
│       │   │                   │   └── ServerHeartBeatHandlerImpl.java
│       │   │                   ├── server/
│       │   │                   │   └── CIMServer.java
│       │   │                   └── util/
│       │   │                       ├── SessionSocketHolder.java
│       │   │                       └── SpringBeanFactory.java
│       │   └── resources/
│       │       ├── application.yaml
│       │       └── banner.txt
│       └── test/
│           └── com/
│               └── crossoverjie/
│                   └── cim/
│                       └── server/
│                           └── util/
│                               └── NettyAttrUtilTest.java
├── cim-server-api/
│   ├── pom.xml
│   └── src/
│       └── main/
│           └── java/
│               └── com/
│                   └── crossoverjie/
│                       └── cim/
│                           └── server/
│                               └── api/
│                                   ├── ServerApi.java
│                                   └── vo/
│                                       ├── req/
│                                       │   └── SendMsgReqVO.java
│                                       └── res/
│                                           ├── OfflineMsgResVO.java
│                                           ├── SaveOfflineMsgResVO.java
│                                           └── SendMsgResVO.java
├── doc/
│   └── QA.md
├── docker/
│   ├── README.md
│   ├── allin1-ubuntu.Dockerfile
│   ├── client-ubuntu.Dockerfile
│   ├── supervisord.conf
│   └── wait-for-it.sh
├── pom.xml
├── script/
│   ├── build.sh
│   ├── deploy.sh
│   ├── route-startup.sh
│   └── server-startup.sh
└── sql/
    ├── 01schema.sql
    ├── offline_msg.sql
    └── offline_msg_last_send_record.sql
Download .txt
SYMBOL INDEX (872 symbols across 168 files)

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/Client.java
  type Client (line 13) | public interface Client extends Closeable {
    method builder (line 15) | static ClientBuilder builder() {
    method sendP2P (line 19) | default void sendP2P(P2PReqVO p2PReqVO) throws Exception {
    method sendP2PAsync (line 23) | CompletableFuture<Void> sendP2PAsync(P2PReqVO p2PReqVO);
    method sendGroup (line 25) | default void sendGroup(String msg) throws Exception {
    method sendGroupAsync (line 29) | CompletableFuture<Void> sendGroupAsync(String msg);
    method getState (line 31) | ClientState.State getState();
    method getAuth (line 33) | ClientConfigurationData.Auth getAuth();
    method getOnlineUser (line 35) | Set<CIMUserInfo> getOnlineUser() throws Exception;
    method getServerInfo (line 37) | Optional<CIMServerResVO> getServerInfo();

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ClientBuilder.java
  type ClientBuilder (line 14) | public interface ClientBuilder {
    method build (line 16) | Client build();
    method auth (line 17) | ClientBuilder auth(ClientConfigurationData.Auth auth);
    method routeUrl (line 18) | ClientBuilder routeUrl(String routeUrl);
    method loginRetryCount (line 19) | ClientBuilder loginRetryCount(int loginRetryCount);
    method event (line 20) | ClientBuilder event(Event event);
    method reconnectCheck (line 21) | ClientBuilder reconnectCheck(ReconnectCheck reconnectCheck);
    method okHttpClient (line 22) | ClientBuilder okHttpClient(OkHttpClient okHttpClient);
    method messageListener (line 23) | ClientBuilder messageListener(MessageListener messageListener);
    method callbackThreadPool (line 24) | ClientBuilder callbackThreadPool(ThreadPoolExecutor callbackThreadPool);
    method backoffStrategy (line 25) | ClientBuilder backoffStrategy(BackoffStrategy backoffStrategy);

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ClientState.java
  class ClientState (line 5) | public abstract class ClientState {
    type State (line 9) | public enum State {
    method setState (line 16) | public void setState(State s) {
    method getState (line 20) | public State getState() {

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/Event.java
  type Event (line 3) | public interface Event {
    method debug (line 4) | void debug(String msg, Object... replace);
    method info (line 5) | void info(String msg, Object... replace);
    method warn (line 6) | void warn(String msg, Object... replace);
    method error (line 7) | void error(String msg, Object... replace);
    method fatal (line 8) | void fatal(Client client);
    class DefaultEvent (line 10) | class DefaultEvent implements Event {
      method debug (line 11) | @Override
      method info (line 16) | @Override
      method warn (line 21) | @Override
      method error (line 26) | @Override
      method fatal (line 31) | @Override

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/FetchOfflineMsgJob.java
  class FetchOfflineMsgJob (line 6) | public class FetchOfflineMsgJob extends RingBufferWheel.Task {
    method FetchOfflineMsgJob (line 12) | public FetchOfflineMsgJob(RouteManager routeManager, ClientConfigurati...
    method run (line 18) | @Override

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ReConnectManager.java
  class ReConnectManager (line 12) | public final class ReConnectManager {
    method reConnect (line 21) | public void reConnect(ChannelHandlerContext ctx) {
    method reConnectSuccess (line 36) | public void reConnectSuccess() {
    method buildExecutor (line 44) | private void buildExecutor() {
    class ClientHeartBeatHandle (line 54) | private static class ClientHeartBeatHandle implements HeartBeatHandler {
      method process (line 56) | @Override
    method createReConnectManager (line 62) | public static ReConnectManager createReConnectManager() {
    method createHeartBeatHandler (line 66) | public static HeartBeatHandler createHeartBeatHandler() {

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/RouteManager.java
  class RouteManager (line 20) | public class RouteManager {
    method RouteManager (line 26) | public RouteManager(String routeUrl, OkHttpClient okHttpClient, Event ...
    method getServer (line 31) | public CIMServerResVO getServer(LoginReqVO loginReqVO) throws Exception {
    method sendP2P (line 47) | public CompletableFuture<Void> sendP2P(CompletableFuture<Void> future,...
    method sendGroupMsg (line 62) | public CompletableFuture<Void> sendGroupMsg(ChatReqVO chatReqVO) {
    method offLine (line 72) | public void offLine(Long userId) {
    method onlineUser (line 77) | public Set<CIMUserInfo> onlineUser() throws Exception {
    method fetchOfflineMsgs (line 82) | public void fetchOfflineMsgs(Long userId) {

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientBuilderImpl.java
  class ClientBuilderImpl (line 13) | public class ClientBuilderImpl implements ClientBuilder {
    method ClientBuilderImpl (line 18) | public ClientBuilderImpl() {
    method ClientBuilderImpl (line 21) | public ClientBuilderImpl(ClientConfigurationData conf) {
    method build (line 25) | @Override
    method auth (line 30) | @Override
    method routeUrl (line 39) | @Override
    method loginRetryCount (line 48) | @Override
    method event (line 54) | @Override
    method reconnectCheck (line 60) | @Override
    method okHttpClient (line 66) | @Override
    method messageListener (line 72) | @Override
    method callbackThreadPool (line 78) | @Override
    method backoffStrategy (line 84) | @Override

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientConfigurationData.java
  class ClientConfigurationData (line 17) | @Data
    class Auth (line 25) | @Data

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientImpl.java
  class ClientImpl (line 47) | @Slf4j
    method ClientImpl (line 78) | public ClientImpl(ClientConfigurationData conf) {
    method postConnectionSetup (line 110) | private void postConnectionSetup() {
    method connectServer (line 115) | private void connectServer(Consumer<Void> success) {
    method doConnectServer (line 142) | private CompletableFuture<Boolean> doConnectServer() {
    method userLogin (line 162) | private Optional<CIMServerResVO> userLogin(CompletableFuture<Boolean> ...
    method doConnectServer (line 179) | private void doConnectServer(CIMServerResVO cimServer, CompletableFutu...
    method loginServer (line 199) | private void loginServer() {
    method reconnect (line 218) | public void reconnect() throws Exception {
    method close (line 237) | @Override
    method sendP2P (line 249) | @Override
    method sendGroup (line 254) | @Override
    method recordSendLog (line 259) | private void recordSendLog(CompletableFuture<Void> future, String msgW...
    method sendP2PAsync (line 272) | @Override
    method sendGroupAsync (line 279) | @Override
    method getAuth (line 285) | @Override
    method getState (line 290) | @Override
    method getOnlineUser (line 295) | @Override
    method getServerInfo (line 300) | @Override

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/CIMClientHandle.java
  class CIMClientHandle (line 17) | @ChannelHandler.Sharable
    method userEventTriggered (line 21) | @Override
    method channelActive (line 42) | @Override
    method channelInactive (line 48) | @Override
    method channelRead0 (line 60) | @Override
    method exceptionCaught (line 90) | @Override

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/CIMClientHandleInitializer.java
  class CIMClientHandleInitializer (line 12) | public class CIMClientHandleInitializer extends ChannelInitializer<Chann...
    method initChannel (line 16) | @Override

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/MessageListener.java
  type MessageListener (line 6) | public interface MessageListener {
    method received (line 13) | void received(Client client, Map<String, String> properties, String msg);

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/ReconnectCheck.java
  type ReconnectCheck (line 5) | public interface ReconnectCheck {
    method isNeedReconnect (line 11) | boolean isNeedReconnect(Client client);

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/backoff/BackoffStrategy.java
  type BackoffStrategy (line 11) | public interface BackoffStrategy {
    method nextBackoff (line 15) | long nextBackoff();
    method runBackoff (line 21) | default void runBackoff() throws InterruptedException {

FILE: cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/backoff/RandomBackoff.java
  class RandomBackoff (line 9) | public class RandomBackoff implements BackoffStrategy {
    method nextBackoff (line 11) | @Override

FILE: cim-client-sdk/src/test/java/com/crossoverjie/cim/client/sdk/ClientTest.java
  class ClientTest (line 27) | @Slf4j
    method tearDown (line 31) | @AfterEach
    method groupChat (line 36) | @Test
    method testP2PChat (line 111) | @Test
    method testReconnect (line 242) | @Test
    method offLineAndOnline (line 328) | @Test
    method testClose (line 408) | @Test
    method testIncorrectUser (line 438) | @Test

FILE: cim-client-sdk/src/test/java/com/crossoverjie/cim/client/sdk/OfflineMsgTest.java
  class OfflineMsgTest (line 20) | @Slf4j
    method testP2POfflineChatRedis (line 23) | @Test
    method testP2POfflineChatMysql (line 142) | @Test

FILE: cim-client-sdk/src/test/resources/init.sql
  type `offline_msg` (line 2) | CREATE TABLE IF NOT EXISTS `offline_msg`
  type offline_msg_last_send_record (line 28) | CREATE TABLE offline_msg_last_send_record

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/CIMClientApplication.java
  class CIMClientApplication (line 12) | @Slf4j
    method main (line 17) | public static void main(String[] args) {
    method run (line 22) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/config/AppConfiguration.java
  class AppConfiguration (line 14) | @Component

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/config/BeanConfig.java
  class BeanConfig (line 32) | @Configuration
    method buildClient (line 45) | @Bean
    method okHttpClient (line 74) | @Bean
    method buildCallerThread (line 90) | @Bean("callBackThreadPool")
    method bufferWheel (line 102) | @Bean

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/config/SwaggerConfig.java
  class SwaggerConfig (line 10) | @Configuration
    method createRestApi (line 13) | @Bean
    method apiInfo (line 19) | private Info apiInfo() {
    method contact (line 28) | private Contact contact() {

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/scanner/Scan.java
  class Scan (line 17) | public class Scan implements Runnable {
    method Scan (line 25) | public Scan() {
    method run (line 31) | @SneakyThrows

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/InnerCommand.java
  type InnerCommand (line 10) | public interface InnerCommand {
    method process (line 16) | void process(String msg) throws Exception;

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/InnerCommandContext.java
  class InnerCommandContext (line 19) | @Slf4j
    method getInstance (line 28) | public InnerCommand getInstance(String command) {

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/MsgHandle.java
  type MsgHandle (line 10) | public interface MsgHandle {
    method sendMsg (line 17) | void sendMsg(String msg) throws Exception;
    method checkMsg (line 27) | boolean checkMsg(String msg);
    method innerCommand (line 35) | boolean innerCommand(String msg) throws Exception;
    method openAIModel (line 41) | void openAIModel();
    method closeAIModel (line 46) | void closeAIModel();

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/MsgLogger.java
  type MsgLogger (line 10) | public interface MsgLogger {
    method log (line 16) | void log(String msg);
    method stop (line 22) | void stop();
    method query (line 29) | String query(String key);

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/ShutDownSign.java
  class ShutDownSign (line 12) | @Component
    method shutdown (line 19) | public void shutdown() {
    method checkStatus (line 23) | public boolean checkStatus() {

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/AsyncMsgLogger.java
  class AsyncMsgLogger (line 31) | @Slf4j
    method log (line 48) | @Override
    class Worker (line 60) | private class Worker extends Thread {
      method run (line 63) | @Override
    method writeLog (line 78) | private void writeLog(String msg) {
    method startMsgLogger (line 107) | private void startMsgLogger() {
    method stop (line 119) | @Override
    method query (line 125) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/EchoServiceImpl.java
  class EchoServiceImpl (line 20) | @Service
    method debug (line 31) | @Override
    method info (line 36) | @Override
    method warn (line 49) | @Override
    method error (line 54) | @Override
    method fatal (line 59) | @Override
    method print (line 72) | private String print(String msg, Object... place) {

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/MsgCallBackListener.java
  class MsgCallBackListener (line 17) | public class MsgCallBackListener implements MessageListener {
    method MsgCallBackListener (line 23) | public MsgCallBackListener(MsgLogger msgLogger, Event event) {
    method received (line 29) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/MsgHandler.java
  class MsgHandler (line 20) | @Slf4j
    method sendMsg (line 33) | @Override
    method normalChat (line 42) | private void normalChat(String msg) throws Exception {
    method aiChat (line 60) | private void aiChat(String msg) {
    method checkMsg (line 69) | @Override
    method innerCommand (line 78) | @Override
    method openAIModel (line 93) | @Override
    method closeAIModel (line 98) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/CloseAIModelCommand.java
  class CloseAIModelCommand (line 16) | @Service
    method process (line 26) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/DelayMsgCommand.java
  class DelayMsgCommand (line 18) | @Service
    method process (line 31) | @Override
    class DelayMsgJob (line 49) | private class DelayMsgJob extends RingBufferWheel.Task {
      method DelayMsgJob (line 53) | public DelayMsgJob(String msg) {
      method run (line 57) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/EchoInfoCommand.java
  class EchoInfoCommand (line 16) | @Service
    method process (line 25) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/EmojiCommand.java
  class EmojiCommand (line 19) | @Service
    method process (line 26) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/OpenAIModelCommand.java
  class OpenAIModelCommand (line 15) | @Service
    method process (line 22) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrefixSearchCommand.java
  class PrefixSearchCommand (line 21) | @Slf4j
    method process (line 31) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrintAllCommand.java
  class PrintAllCommand (line 17) | @Service
    method process (line 24) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrintOnlineUsersCommand.java
  class PrintOnlineUsersCommand (line 19) | @Slf4j
    method process (line 29) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/QueryHistoryCommand.java
  class QueryHistoryCommand (line 17) | @Slf4j
    method process (line 27) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/ShutDownCommand.java
  class ShutDownCommand (line 23) | @Slf4j
    method process (line 46) | @Override

FILE: cim-client/src/main/java/com/crossoverjie/cim/client/util/SpringBeanFactory.java
  class SpringBeanFactory (line 8) | @Component
    method getBean (line 12) | public static <T> T getBean(Class<T> c) {
    method getBean (line 17) | public static <T> T getBean(String name, Class<T> clazz) {
    method setApplicationContext (line 21) | @Override

FILE: cim-client/src/test/java/com/crossoverjie/cim/client/service/InnerCommandContextTest.java
  class InnerCommandContextTest (line 11) | @SpringBootTest(classes = CIMClientApplication.class)
    method execute (line 18) | @Test
    method execute3 (line 26) | public void execute3() throws Exception {
    method execute4 (line 33) | @Test
    method execute5 (line 40) | @Test
    method execute6 (line 47) | @Test
    method execute7 (line 54) | @Test
    method execute8 (line 62) | public void execute8() throws Exception {
    method execute9 (line 69) | @Test
    method execute10 (line 76) | @Test
    method quit (line 86) | public void quit() throws Exception {

FILE: cim-client/src/test/java/com/crossoverjie/cim/client/service/impl/AsyncMsgLoggerTest.java
  class AsyncMsgLoggerTest (line 13) | @SpringBootTest(classes = CIMClientApplication.class)
    method writeLog (line 22) | @Test
    method query (line 33) | @Test

FILE: cim-client/src/test/java/com/crossoverjie/cim/server/test/CommonTest.java
  class CommonTest (line 39) | @Slf4j
    method searchMsg2 (line 45) | @Test
    method log (line 82) | @Test
    method emoji (line 110) | @Test
    method emoji2 (line 127) | @Test
    method deSerialize (line 138) | public void deSerialize() throws Exception {
    method json (line 149) | @Test
    class Gen (line 181) | private static class Gen<T, R> {
    type TestInterface (line 186) | interface TestInterface {
      method login (line 187) | Gen<String, P2PReqVO> login();
    method test1 (line 191) | @Test
    method getGenericTypeOfBaseResponse (line 212) | public static Type getGenericTypeOfBaseResponse() {

FILE: cim-client/src/test/java/com/crossoverjie/cim/server/test/EchoTest.java
  class EchoTest (line 13) | public class EchoTest {
    method echo (line 14) | @Test
    method echo2 (line 24) | @Test
    method echo3 (line 34) | @Test
    method echo4 (line 43) | @Test
    method echo5 (line 52) | @Test
    method echo6 (line 62) | @Test
    method log (line 72) | private String log(String msg, String... place) {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/constant/Constants.java
  class Constants (line 10) | public class Constants {
    class MetaKey (line 24) | public static class MetaKey {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/RpcProxyManager.java
  class RpcProxyManager (line 27) | @Slf4j
    method RpcProxyManager (line 42) | private RpcProxyManager(Class<T> clazz, String url, OkHttpClient okHtt...
    method RpcProxyManager (line 48) | private RpcProxyManager(Class<T> clazz, OkHttpClient okHttpClient) {
    method RpcProxyManager (line 55) | private RpcProxyManager() {
    method create (line 67) | public static <T> T create(Class<T> clazz, String url, OkHttpClient ok...
    method create (line 71) | public static <T> T create(Class<T> clazz, OkHttpClient okHttpClient) {
    method getInstance (line 80) | @SuppressWarnings("unchecked")
    class ProxyInvocation (line 89) | private class ProxyInvocation implements InvocationHandler {
      method invoke (line 100) | @Override
    method getGenericTypeOfBaseResponse (line 173) | private Type getGenericTypeOfBaseResponse(Method declaredMethod) {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/RingBufferWheel.java
  class RingBufferWheel (line 23) | @Slf4j
    method RingBufferWheel (line 69) | public RingBufferWheel(ExecutorService executorService) {
    method RingBufferWheel (line 82) | public RingBufferWheel(ExecutorService executorService, int bufferSize) {
    method addTask (line 97) | public int addTask(Task task) {
    method cancel (line 137) | public boolean cancel(int id) {
    method taskSize (line 174) | public int taskSize() {
    method taskMapSize (line 182) | public int taskMapSize() {
    method start (line 189) | public void start() {
    method stop (line 209) | public void stop(boolean force) {
    method get (line 234) | private Set<Task> get(int index) {
    method put (line 238) | private void put(int key, Set<Task> tasks) {
    method remove (line 248) | private Set<Task> remove(int key) {
    method size2Notify (line 277) | private void size2Notify() {
    method powerOf2 (line 289) | private boolean powerOf2(int target) {
    method mod (line 301) | private int mod(int target, int mod) {
    method cycleNum (line 307) | private int cycleNum(int target, int mod) {
    class Task (line 315) | public abstract static class Task extends Thread {
      method run (line 328) | @Override
      method getKey (line 332) | public int getKey() {
      method setKey (line 340) | public void setKey(int key) {
      method getCycleNum (line 344) | public int getCycleNum() {
      method setCycleNum (line 348) | private void setCycleNum(int cycleNum) {
      method getIndex (line 352) | public int getIndex() {
      method setIndex (line 356) | private void setIndex(int index) {
      method getTaskId (line 360) | public int getTaskId() {
      method setTaskId (line 364) | public void setTaskId(int taskId) {
    class TriggerJob (line 370) | private class TriggerJob implements Runnable {
      method run (line 372) | @Override

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/SortArrayMap.java
  class SortArrayMap (line 17) | public class SortArrayMap extends AbstractMap<String, String> {
    method SortArrayMap (line 31) | public SortArrayMap() {
    method add (line 40) | public void add(Long key, String value) {
    method remove (line 46) | public SortArrayMap remove(String value) {
    method checkSize (line 57) | private void checkSize(int size) {
    method firstNodeValue (line 71) | public String firstNodeValue(long key) {
    method sort (line 91) | public void sort() {
    method print (line 101) | public void print() {
    method size (line 110) | @Override
    method clear (line 115) | @Override
    method entrySet (line 121) | @Override
    method keySet (line 130) | @Override
    class Node (line 145) | private class Node {
      method Node (line 149) | public Node(Long key, String value) {
      method toString (line 154) | @Override

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/TrieTree.java
  class TrieTree (line 15) | public class TrieTree {
    method TrieTree (line 36) | public TrieTree() {
    method insert (line 45) | public void insert(String data) {
    method insert (line 49) | private void insert(Node root, String data) {
    method prefixSearch (line 89) | public List<String> prefixSearch(String key) {
    method query (line 110) | private List<String> query(Node child, List<String> value, String key,...
    method all (line 156) | public List<String> all() {
    method depth (line 163) | public List<String> depth(Node node, List<String> list, char[] chars, ...
    class Node (line 209) | private class Node {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/enums/StatusEnum.java
  type StatusEnum (line 10) | public enum StatusEnum {
    method StatusEnum (line 54) | private StatusEnum(String code, String message) {
    method getCode (line 63) | public String getCode() {
    method getMessage (line 71) | public String getMessage() {
    method code (line 79) | public String code() {
    method message (line 87) | public String message() {
    method findStatus (line 97) | public static StatusEnum findStatus(String code) {
    method getAllStatus (line 111) | public static List<StatusEnum> getAllStatus() {
    method getAllStatusCode (line 124) | public static List<String> getAllStatusCode() {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/enums/SystemCommandEnum.java
  type SystemCommandEnum (line 13) | public enum SystemCommandEnum {
    method SystemCommandEnum (line 43) | private SystemCommandEnum(String commandType, String desc, String claz...
    method getCommandType (line 53) | public String getCommandType() {
    method getClazz (line 60) | public String getClazz() {
    method getDesc (line 68) | public String getDesc() {
    method code (line 76) | public String code() {
    method message (line 84) | public String message() {
    method getAllStatusCode (line 93) | public static Map<String, String> getAllStatusCode() {
    method getAllClazz (line 101) | public static Map<String, String> getAllClazz() {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/exception/CIMException.java
  class CIMException (line 13) | public class CIMException extends GenericException {
    method CIMException (line 16) | public CIMException(String errorCode, String errorMessage) {
    method CIMException (line 22) | public CIMException(Exception e, String errorCode, String errorMessage) {
    method CIMException (line 28) | public CIMException(String message) {
    method CIMException (line 33) | public CIMException(StatusEnum statusEnum) {
    method CIMException (line 39) | public CIMException(StatusEnum statusEnum, String message) {
    method CIMException (line 45) | public CIMException(Exception oriEx) {
    method CIMException (line 49) | public CIMException(Throwable oriEx) {
    method CIMException (line 53) | public CIMException(String message, Exception oriEx) {
    method CIMException (line 58) | public CIMException(String message, Throwable oriEx) {
    method isResetByPeer (line 64) | public static boolean isResetByPeer(String msg) {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/exception/GenericException.java
  class GenericException (line 12) | public class GenericException extends RuntimeException implements Serial...
    method GenericException (line 17) | public GenericException() {
    method GenericException (line 20) | public GenericException(String message) {
    method GenericException (line 24) | public GenericException(Exception oriEx) {
    method GenericException (line 28) | public GenericException(Exception oriEx, String message) {
    method GenericException (line 32) | public GenericException(Throwable oriEx) {
    method GenericException (line 36) | public GenericException(String message, Exception oriEx) {
    method GenericException (line 40) | public GenericException(String message, Throwable oriEx) {
    method getErrorCode (line 44) | public String getErrorCode() {
    method setErrorCode (line 48) | public void setErrorCode(String errorCode) {
    method getErrorMessage (line 52) | public String getErrorMessage() {
    method setErrorMessage (line 56) | public void setErrorMessage(String errorMessage) {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/kit/HeartBeatHandler.java
  type HeartBeatHandler (line 12) | public interface HeartBeatHandler {
    method process (line 19) | void process(ChannelHandlerContext ctx) throws Exception;

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/metastore/AbstractConfiguration.java
  class AbstractConfiguration (line 9) | @Data

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/metastore/MetaStore.java
  type MetaStore (line 9) | public interface MetaStore {
    method initialize (line 11) | void initialize(AbstractConfiguration<?> configuration) throws Exception;
    method getAvailableServerList (line 18) | Set<String> getAvailableServerList() throws Exception;
    method addServer (line 24) | void addServer(String ip, int cimServerPort, int httpPort) throws Exce...
    method listenServerList (line 31) | void listenServerList(ChildListener childListener) throws Exception;
    method rebuildCache (line 37) | void rebuildCache() throws Exception;
    type ChildListener (line 39) | interface ChildListener {
      method childChanged (line 46) | void childChanged(String parentPath, List<String> currentChildren) t...

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/metastore/ZkConfiguration.java
  class ZkConfiguration (line 8) | public class ZkConfiguration extends AbstractConfiguration<RetryPolicy> {
    method ZkConfiguration (line 9) | ZkConfiguration(String metaServiceUri, int timeout, RetryPolicy retryP...

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/metastore/ZkMetaStoreImpl.java
  class ZkMetaStoreImpl (line 21) | @Slf4j
    method initialize (line 29) | @Override
    method getAvailableServerList (line 35) | @Override
    method addServer (line 49) | @Override
    method listenServerList (line 65) | @Override
    method rebuildCache (line 77) | @Override
    method watchedGetChildren (line 85) | private List<String> watchedGetChildren(CuratorFramework client, Strin...
    method createEphemeral (line 93) | private void createEphemeral(CuratorFramework client, String path, byt...
    method create (line 98) | private void create(CuratorFramework client, String path, byte[] paylo...
    method watchedGetChildren (line 103) | private void watchedGetChildren(CuratorFramework client, String path, ...

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/pojo/CIMUserInfo.java
  class CIMUserInfo (line 14) | @Data

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/pojo/RouteInfo.java
  class RouteInfo (line 14) | @Data

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/req/BaseRequest.java
  class BaseRequest (line 12) | public class BaseRequest {
    method BaseRequest (line 23) | public BaseRequest() {
    method getReqNo (line 27) | public String getReqNo() {
    method setReqNo (line 31) | public void setReqNo(String reqNo) {
    method getTimeStamp (line 35) | public int getTimeStamp() {
    method setTimeStamp (line 39) | public void setTimeStamp(int timeStamp) {
    method toString (line 44) | @Override

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/res/BaseResponse.java
  class BaseResponse (line 10) | public class BaseResponse<T> implements Serializable {
    method BaseResponse (line 22) | public BaseResponse() {}
    method BaseResponse (line 24) | public BaseResponse(T dataBody) {
    method BaseResponse (line 28) | public BaseResponse(String code, String message) {
    method BaseResponse (line 33) | public BaseResponse(String code, String message, T dataBody) {
    method BaseResponse (line 39) | public BaseResponse(String code, String message, String reqNo, T dataB...
    method create (line 46) | public static <T> BaseResponse<T> create(T t) {
    method create (line 50) | public static <T> BaseResponse<T> create(T t, StatusEnum statusEnum) {
    method createSuccess (line 54) | public static <T> BaseResponse<T> createSuccess(T t, String message) {
    method createFail (line 59) | public static <T> BaseResponse<T> createFail(T t, String message) {
    method create (line 63) | public static <T> BaseResponse<T> create(T t, StatusEnum statusEnum, S...
    method getCode (line 69) | public String getCode() {
    method setCode (line 73) | public void setCode(String code) {
    method getMessage (line 77) | public String getMessage() {
    method setMessage (line 81) | public void setMessage(String message) {
    method getDataBody (line 85) | public T getDataBody() {
    method setDataBody (line 89) | public void setDataBody(T dataBody) {
    method getReqNo (line 93) | public String getReqNo() {
    method setReqNo (line 97) | public void setReqNo(String reqNo) {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/res/NULLBody.java
  class NULLBody (line 10) | public class NULLBody {
    method NULLBody (line 11) | public NULLBody() {}
    method create (line 13) | public static NULLBody create() {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/RouteHandle.java
  type RouteHandle (line 13) | public interface RouteHandle {
    method routeServer (line 22) | String routeServer(List<String> values, String key);
    method removeExpireServer (line 24) | List<String> removeExpireServer(RouteInfo routeInfo);

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/AbstractConsistentHash.java
  class AbstractConsistentHash (line 16) | public abstract class AbstractConsistentHash {
    method add (line 23) | protected abstract void add(long key, String value);
    method remove (line 30) | protected abstract Map<String, String> remove(String value);
    method clear (line 35) | protected abstract void clear();
    method sort (line 40) | protected void sort() { }
    method getFirstNodeValue (line 47) | protected abstract String getFirstNodeValue(String value);
    method process (line 55) | public String process(List<String> values, String key) {
    method hash (line 71) | public Long hash(String value) {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/ConsistentHashHandle.java
  class ConsistentHashHandle (line 18) | public class ConsistentHashHandle implements RouteHandle {
    method setHash (line 21) | public void setHash(AbstractConsistentHash hash) {
    method routeServer (line 25) | @Override
    method removeExpireServer (line 30) | @Override

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/SortArrayMapConsistentHash.java
  class SortArrayMapConsistentHash (line 15) | public class SortArrayMapConsistentHash extends AbstractConsistentHash {
    method add (line 24) | @Override
    method remove (line 33) | @Override
    method sort (line 39) | @Override
    method getSortArrayMap (line 48) | @VisibleForTesting
    method clear (line 53) | @Override
    method getFirstNodeValue (line 58) | @Override

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/TreeMapConsistentHash.java
  class TreeMapConsistentHash (line 19) | public class TreeMapConsistentHash extends AbstractConsistentHash {
    method add (line 27) | @Override
    method remove (line 36) | @Override
    method clear (line 46) | @Override
    method getTreeMap (line 55) | @VisibleForTesting
    method getFirstNodeValue (line 60) | @Override

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/loop/LoopHandle.java
  class LoopHandle (line 19) | public class LoopHandle implements RouteHandle {
    method routeServer (line 24) | @Override
    method removeExpireServer (line 38) | @Override

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/random/RandomHandle.java
  class RandomHandle (line 19) | public class RandomHandle implements RouteHandle {
    method routeServer (line 22) | @Override
    method removeExpireServer (line 34) | @Override

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/util/HttpClient.java
  class HttpClient (line 18) | public final class HttpClient {
    method post (line 22) | public static Response post(OkHttpClient okHttpClient, String params, ...
    method get (line 39) | public static Response get(OkHttpClient okHttpClient, String url) thro...

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/util/NettyAttrUtil.java
  class NettyAttrUtil (line 14) | public class NettyAttrUtil {
    method updateReaderTime (line 19) | public static void updateReaderTime(Channel channel, Long time) {
    method getReaderTime (line 23) | public static Long getReaderTime(Channel channel) {
    method getAttribute (line 33) | private static String getAttribute(Channel channel, AttributeKey<Strin...

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/util/RouteInfoParseUtil.java
  class RouteInfoParseUtil (line 15) | public class RouteInfoParseUtil {
    method parse (line 17) | public static RouteInfo parse(String info) {
    method parse (line 26) | public static String parse(RouteInfo routeInfo) {
    method RouteInfoParseUtil (line 30) | private RouteInfoParseUtil() {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/util/SnowflakeIdWorker.java
  class SnowflakeIdWorker (line 8) | public class SnowflakeIdWorker {
    method tilNextMillis (line 20) | private long tilNextMillis(long lastTs) {
    method nextId (line 28) | public synchronized long nextId() {

FILE: cim-common/src/main/java/com/crossoverjie/cim/common/util/StringUtil.java
  class StringUtil (line 10) | public class StringUtil {
    method StringUtil (line 11) | public StringUtil() {
    method isNullOrEmpty (line 14) | public static boolean isNullOrEmpty(String str) {
    method isEmpty (line 18) | public static boolean isEmpty(String str) {
    method isNotEmpty (line 22) | public static boolean isNotEmpty(String str) {
    method formatLike (line 26) | public static String formatLike(String str) {

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/CommonTest.java
  class CommonTest (line 15) | public class CommonTest {
    method test2 (line 18) | @Test
    method test (line 24) | @Test
    method mod (line 44) | private int mod(int target, int mod) {
    method cycleNum (line 49) | private int cycleNum(int target, int mod) {
    method is2 (line 54) | private boolean is2(int target) {
    method cycle (line 68) | private void cycle() throws InterruptedException {

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/core/proxy/RpcProxyManagerTest.java
  class RpcProxyManagerTest (line 14) | class RpcProxyManagerTest {
    method testGet (line 16) | @Test
    method testPost (line 26) | @Test
    method testUrl (line 41) | @Test
    method testFail (line 90) | @Test
    method testGeneric (line 103) | @Test
    type Echo (line 116) | interface Echo {
      method echo (line 117) | @Request(url = "sample-request?author=beeceptor")
      method echoTarget (line 120) | @Request(url = "sample-request?author=beeceptor")
      method echoTarget (line 122) | EchoResponse echoTarget(EchoRequest message, @DynamicUrl(useMethodEn...
      method echoTarget (line 123) | @Request(url = "sample-request?author=beeceptor")
      method request (line 125) | EchoResponse request(@DynamicUrl() String url, EchoRequest message);
      method fail (line 126) | @Request(url = "sample-request?author=beeceptor")
      method echoGeneric (line 129) | @Request(url = "sample-request?author=beeceptor")
    class EchoRequest (line 133) | @Data
    class CIMServerResVO (line 140) | @Data
    class EchoGeneric (line 151) | @Data
    class EchoResponse (line 163) | @NoArgsConstructor
      class HeadersDTO (line 184) | @NoArgsConstructor
      class ParsedQueryParamsDTO (line 203) | @NoArgsConstructor
      class ParsedBodyDTO (line 210) | @NoArgsConstructor
    type Github (line 222) | interface Github {
      method crossoverjie (line 223) | @Request(method = Request.GET)
      method torvalds (line 226) | @Request(method = Request.GET)
    class GithubResponse (line 230) | @Data

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/RingBufferWheelTest.java
  class RingBufferWheelTest (line 16) | @Slf4j
    method main (line 19) | public static void main(String[] args) throws Exception {
    method test8 (line 25) | private static void test8() throws Exception {
    class ByteTask (line 42) | private static class ByteTask extends RingBufferWheel.Task {
      method ByteTask (line 46) | public ByteTask(int size) {
      method run (line 50) | @Override
    method test1 (line 56) | private static void test1() throws InterruptedException {
    method test2 (line 74) | private static void test2() throws InterruptedException {
    method test3 (line 105) | private static void test3() throws InterruptedException {
    method test4 (line 124) | private static void test4() throws InterruptedException {
    method test5 (line 144) | private static void test5() throws InterruptedException {
    method test6 (line 162) | private static void test6() throws InterruptedException {
    method test7 (line 183) | private static void test7() throws InterruptedException {
    method concurrentTest (line 213) | private static void concurrentTest() throws Exception {
    class Job (line 244) | private static class Job extends RingBufferWheel.Task {
      method Job (line 248) | public Job(int num) {
      method run (line 252) | @Override
    class Task (line 258) | private static class Task extends RingBufferWheel.Task {
      method run (line 260) | @Override
    method hashTimerTest (line 268) | public static void hashTimerTest() {

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/ScheduledTest.java
  class ScheduledTest (line 16) | @Slf4j
    method main (line 19) | public static void main(String[] args) {

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/SortArrayMapTest.java
  class SortArrayMapTest (line 7) | public class SortArrayMapTest {
    method ad (line 11) | @Test
    method add (line 21) | @Test
    method add2 (line 31) | @Test
    method add3 (line 42) | @Test
    method firstNode (line 55) | @Test
    method firstNode2 (line 70) | @Test
    method firstNode3 (line 85) | @Test
    method firstNode4 (line 100) | @Test
    method add4 (line 115) | @Test
    method add5 (line 129) | @Test
    method add6 (line 146) | @Test
    method add7 (line 161) | @Test
    method add8 (line 176) | @Test

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/TimerTest.java
  class TimerTest (line 14) | @Slf4j
    method main (line 17) | public static void main(String[] args) {

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/TrieTreeTest.java
  class TrieTreeTest (line 7) | public class TrieTreeTest {
    method insert (line 9) | @Test
    method all (line 17) | @Test
    method all2 (line 33) | @Test
    method prefixSea (line 49) | @Test
    method prefixSea2 (line 69) | @Test
    method prefixSea3 (line 89) | @Test
    method prefixSea4 (line 109) | @Test
    method prefixSea5 (line 129) | @Test
    method prefixSearch (line 149) | @Test
    method prefixSearch2 (line 173) | @Test
    method prefixSearch3 (line 187) | @Test
    method prefixSearch4 (line 201) | @Test
    method prefixSearch44 (line 217) | @Test
    method prefixSearch5 (line 231) | @Test
    method prefixSearch6 (line 249) | @Test
    method prefixSearch7 (line 267) | @Test
    method prefixSearch8 (line 285) | @Test
    method prefixSearch9 (line 299) | @Test
    method prefixSearch10 (line 318) | @Test

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/enums/SystemCommandEnumTypeTest.java
  class SystemCommandEnumTypeTest (line 6) | public class SystemCommandEnumTypeTest {
    method getAllStatusCode (line 9) | @Test

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/metastore/MetaStoreTest.java
  class MetaStoreTest (line 15) | public class MetaStoreTest {
    method testZk (line 20) | @SneakyThrows
    method createEphemeral (line 57) | public static void createEphemeral(CuratorFramework client, String pat...
    method create (line 62) | public static void create(CuratorFramework client, String path, byte[]...
    method watchedGetChildren (line 68) | public static List<String> watchedGetChildren(CuratorFramework client,...
    method watchedGetChildren (line 76) | public static List<String> watchedGetChildren(CuratorFramework client,...
    method curatorWatcherGetChildren (line 84) | public static List<String> curatorWatcherGetChildren(CuratorFramework ...
    method zkClientTest (line 93) | @SneakyThrows

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/ConsistentHashHandleTest.java
  class ConsistentHashHandleTest (line 12) | class ConsistentHashHandleTest {
    method removeSortMapExpireServer (line 14) | @Test
    method removeTreeMapExpireServer (line 34) | @Test

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/RangeCheckTestUtil.java
  class RangeCheckTestUtil (line 11) | public class RangeCheckTestUtil {
    method assertInRange (line 13) | public static void assertInRange(int value, int l, int r) {

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/SortArrayMapConsistentHashTest.java
  class SortArrayMapConsistentHashTest (line 11) | public class SortArrayMapConsistentHashTest {
    method getFirstNodeValue (line 13) | @Test
    method getFirstNodeValue2 (line 28) | @Test
    method getFirstNodeValue3 (line 44) | @Test
    method getFirstNodeValue4 (line 60) | @Test
    method getFirstNodeValue5 (line 76) | @Test
    method getFirstNodeValue6 (line 92) | @Test
    method getFirstNodeValue7 (line 108) | @Test
    method getFirstNodeValue8 (line 125) | @Test
    method testVirtualNode (line 141) | @Test

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/TreeMapConsistentHashTest.java
  class TreeMapConsistentHashTest (line 11) | public class TreeMapConsistentHashTest {
    method getFirstNodeValue (line 15) | @Test
    method getFirstNodeValue2 (line 32) | @Test
    method getFirstNodeValue3 (line 48) | @Test
    method getFirstNodeValue4 (line 63) | @Test
    method testVirtualNode (line 79) | @Test

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/loop/LoopHandleTest.java
  class LoopHandleTest (line 12) | class LoopHandleTest {
    method removeExpireServer (line 14) | @Test

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/random/RandomHandleTest.java
  class RandomHandleTest (line 12) | class RandomHandleTest {
    method removeExpireServer (line 14) | @Test

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/util/HttpClientTest.java
  class HttpClientTest (line 10) | public class HttpClientTest {
    method before (line 14) | @Before
    method call (line 24) | @Test

FILE: cim-common/src/test/java/com/crossoverjie/cim/common/util/ProtocolTest.java
  class ProtocolTest (line 8) | public class ProtocolTest {
    method testProtocol (line 10) | @Test
    method encode (line 31) | public static byte[] encode(Request protocol) {
    method decode (line 41) | public static Request decode(byte[] bytes) throws InvalidProtocolBuffe...

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/RouteApplication.java
  class RouteApplication (line 13) | @Slf4j
    method main (line 21) | public static void main(String[] args) {
    method run (line 26) | @Override

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/AppConfiguration.java
  class AppConfiguration (line 13) | @Component
    method getZkConnectTimeout (line 35) | public int getZkConnectTimeout() {
    method getPort (line 39) | public int getPort() {
    method setPort (line 43) | public void setPort(int port) {
    method getZkRoot (line 47) | public String getZkRoot() {
    method setZkRoot (line 51) | public void setZkRoot(String zkRoot) {
    method getZkAddr (line 55) | public String getZkAddr() {
    method setZkAddr (line 59) | public void setZkAddr(String zkAddr) {
    method getRouteWay (line 63) | public String getRouteWay() {
    method setRouteWay (line 67) | public void setRouteWay(String routeWay) {
    method getConsistentHashWay (line 71) | public String getConsistentHashWay() {
    method setConsistentHashWay (line 75) | public void setConsistentHashWay(String consistentHashWay) {

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/BeanConfig.java
  class BeanConfig (line 39) | @Configuration
    method metaStore (line 47) | @Bean
    method redisTemplate (line 69) | @Bean
    method okHttpClient (line 84) | @Bean
    method buildRouteHandle (line 94) | @Bean
    method userInfoCache (line 114) | @Bean("userInfoCache")
    method serverApi (line 133) | @Bean
    method snowflakeIdWorker (line 138) | @Bean

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/MySqlPersistenceConfig.java
  class MySqlPersistenceConfig (line 14) | @Configuration

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/OfflineMsgStoreConfig.java
  class OfflineMsgStoreConfig (line 20) | @Configuration
    method offlineMsgDbStore (line 23) | @Bean
    method offlineMsgBufferStore (line 29) | @Bean

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/SwaggerConfig.java
  class SwaggerConfig (line 9) | @Configuration
    method createRestApi (line 12) | @Bean
    method apiInfo (line 18) | private Info apiInfo() {
    method contact (line 27) | private Contact contact() {

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/constant/Constant.java
  class Constant (line 10) | public final class Constant {
    class OfflineStoreMode (line 29) | public static final class OfflineStoreMode {

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/controller/RouteController.java
  class RouteController (line 47) | @Slf4j
    method groupRoute (line 74) | @Operation(summary = "群聊 API")
    method p2pRoute (line 112) | @Operation(summary = "私聊 API")
    method offLine (line 143) | @Operation(summary = "客户端下线")
    method login (line 167) | @Operation(summary = "登录并获取服务器")
    method registerAccount (line 205) | @Operation(summary = "注册账号")
    method onlineUser (line 228) | @Operation(summary = "获取所有在线用户")
    method fetchOfflineMsgs (line 242) | @Operation(summary = "Client fetch offline messages")

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/exception/ExceptionHandlingController.java
  class ExceptionHandlingController (line 17) | @Slf4j
    method handleAllExceptions (line 20) | @ExceptionHandler(CIMException.class)

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/factory/OfflineMsgFactory.java
  class OfflineMsgFactory (line 17) | @Service
    method OfflineMsgFactory (line 22) | public OfflineMsgFactory(SnowflakeIdWorker idWorker) {
    method createFromVo (line 26) | public OfflineMsg createFromVo(SaveOfflineMsgReqVO vo) {
    method createPropertiesMap (line 43) | private Map<String, String> createPropertiesMap(SaveOfflineMsgReqVO vo) {

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/kit/NetAddressIsReachable.java
  class NetAddressIsReachable (line 15) | @Slf4j
    method checkAddressReachable (line 26) | public static boolean checkAddressReachable(String address, int port, ...
    method NetAddressIsReachable (line 42) | private NetAddressIsReachable() {

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/AccountService.java
  type AccountService (line 19) | public interface AccountService {
    method register (line 27) | RegisterInfoResVO register(RegisterInfoResVO info) throws Exception;
    method login (line 35) | StatusEnum login(LoginReqVO loginReqVO) throws Exception;
    method saveRouteInfo (line 43) | void saveRouteInfo(LoginReqVO loginReqVO, String msg) throws Exception;
    method loadRouteRelated (line 49) | Map<Long, CIMServerResVO> loadRouteRelated();
    method loadRouteRelatedByUserId (line 56) | Optional<CIMServerResVO> loadRouteRelatedByUserId(Long userId);
    method pushMsg (line 66) | void pushMsg(CIMServerResVO cimServerResVO, long sendUserId, ChatReqVO...
    method offLine (line 73) | void offLine(Long userId);

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/CommonBizService.java
  class CommonBizService (line 20) | @Component
    method checkServerAvailable (line 32) | @SneakyThrows

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/OfflineMsgService.java
  type OfflineMsgService (line 9) | public interface OfflineMsgService {
    method fetchOfflineMsgs (line 16) | void fetchOfflineMsgs(CIMServerResVO cimServerResVO, Long receiveUserId);
    method saveOfflineMsg (line 22) | void saveOfflineMsg(P2PReqVO p2pRequest);

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/UserInfoCacheService.java
  type UserInfoCacheService (line 15) | public interface UserInfoCacheService {
    method loadUserInfoByUserId (line 23) | Optional<CIMUserInfo> loadUserInfoByUserId(Long userId);
    method saveAndCheckUserLoginStatus (line 31) | boolean saveAndCheckUserLoginStatus(Long userId) throws Exception;
    method onlineUser (line 37) | Set<CIMUserInfo> onlineUser();

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/impl/AccountServiceRedisImpl.java
  class AccountServiceRedisImpl (line 45) | @Slf4j
    method register (line 58) | @Override
    method login (line 76) | @Override
    method saveRouteInfo (line 99) | @Override
    method loadRouteRelated (line 105) | @Override
    method loadRouteRelatedByUserId (line 129) | @Override
    method parseServerInfo (line 143) | private void parseServerInfo(Map<Long, CIMServerResVO> routes, String ...
    method pushMsg (line 153) | @Override
    method offLine (line 171) | @Override

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/impl/OfflineMsgServiceImpl.java
  class OfflineMsgServiceImpl (line 25) | @Slf4j
    method fetchOfflineMsgs (line 41) | @Override
    method saveOfflineMsg (line 69) | @Override

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/impl/UserInfoCacheServiceImpl.java
  class UserInfoCacheServiceImpl (line 26) | @Slf4j
    method loadUserInfoByUserId (line 36) | @Override
    method saveAndCheckUserLoginStatus (line 42) | @Override
    method onlineUser (line 49) | @Override

FILE: cim-forward-route/src/main/java/com/crossoverjie/cim/route/util/SpringBeanFactory.java
  class SpringBeanFactory (line 8) | @Component
    method getBean (line 12) | public static <T> T getBean(Class<T> c) {
    method getBean (line 17) | public static <T> T getBean(String name, Class<T> clazz) {
    method setApplicationContext (line 21) | @Override

FILE: cim-forward-route/src/test/java/CommonTest.java
  class CommonTest (line 11) | public class CommonTest {
    method test (line 13) | @Test

FILE: cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/AbstractBaseTest.java
  class AbstractBaseTest (line 14) | public class AbstractBaseTest {
    method before (line 41) | @BeforeAll
    method after (line 61) | @AfterAll

FILE: cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/AccountServiceRedisImplTest.java
  class AccountServiceRedisImplTest (line 13) | @Slf4j
    method loadRouteRelated (line 20) | @Test

FILE: cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/RedisTest.java
  class RedisTest (line 11) | @SpringBootTest(classes = RouteApplication.class)
    method test (line 17) | @Test

FILE: cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/UserInfoCacheServiceImplTest.java
  class UserInfoCacheServiceImplTest (line 13) | @Slf4j
    method checkUserLoginStatus (line 20) | @Test
    method onlineUser (line 26) | @Test

FILE: cim-forward-route/src/test/resources/init.sql
  type `offline_msg` (line 2) | CREATE TABLE IF NOT EXISTS `offline_msg`
  type offline_msg_last_send_record (line 28) | CREATE TABLE offline_msg_last_send_record

FILE: cim-integration-test/src/main/java/com/crossoverjie/cim/client/sdk/route/AbstractRouteBaseTest.java
  class AbstractRouteBaseTest (line 15) | public abstract class AbstractRouteBaseTest extends AbstractServerBaseTe...
    method startRoute (line 21) | public void startRoute(String offlineModel) {
    method close (line 34) | public void close() {
    method registerAccount (line 40) | public Long registerAccount(String userName) throws Exception {

FILE: cim-integration-test/src/main/java/com/crossoverjie/cim/client/sdk/route/OfflineMsgStoreRouteBaseTest.java
  class OfflineMsgStoreRouteBaseTest (line 15) | public class OfflineMsgStoreRouteBaseTest extends AbstractRouteBaseTest {
    method startRoute (line 19) | @Override
    method close (line 59) | @Override

FILE: cim-integration-test/src/main/java/com/crossoverjie/cim/client/sdk/server/AbstractServerBaseTest.java
  class AbstractServerBaseTest (line 15) | public abstract class AbstractServerBaseTest {
    method starSingleServer (line 30) | public void starSingleServer() {
    method stopSingle (line 40) | public void stopSingle() {
    method startTwoServer (line 45) | public void startTwoServer() {
    method stopServer (line 72) | public void stopServer(Integer port) {
    method stopTwoServer (line 76) | public void stopTwoServer() {
    method close (line 80) | public void close() {

FILE: cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/config/BeanConfig.java
  class BeanConfig (line 18) | @Configuration("persistenceBeanConfig")
    method stringObjectRedisTemplate (line 28) | @Bean
    method hashMapper (line 40) | @Bean

FILE: cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/pojo/OfflineMsg.java
  class OfflineMsg (line 16) | @Data

FILE: cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/pojo/OfflineMsgLastSendRecord.java
  class OfflineMsgLastSendRecord (line 14) | @Data

FILE: cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/service/OfflineMsgStore.java
  type OfflineMsgStore (line 13) | public interface OfflineMsgStore {
    method save (line 20) | void save(OfflineMsg offlineMsg);
    method fetch (line 28) | List<OfflineMsg> fetch(Long userId);
    method markDelivered (line 36) | void markDelivered(Long userId, List<Long> messageIds);

FILE: cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/vo/req/SaveOfflineMsgReqVO.java
  class SaveOfflineMsgReqVO (line 15) | @Data

FILE: cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/config/MyBatisConfig.java
  class MyBatisConfig (line 13) | @Configuration
    method configurationCustomizer (line 15) | @Bean

FILE: cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/offlinemsg/OfflineMsgDb.java
  class OfflineMsgDb (line 17) | public class OfflineMsgDb implements OfflineMsgStore {
    method OfflineMsgDb (line 22) | public OfflineMsgDb(OfflineMsgMapper offlineMsgMapper, OfflineMsgLastS...
    method save (line 27) | @Override
    method fetch (line 32) | @Override
    method markDelivered (line 37) | @Override

FILE: cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/offlinemsg/mapper/OfflineMsgLastSendRecordMapper.java
  type OfflineMsgLastSendRecordMapper (line 11) | @Mapper
    method saveLatestMessageId (line 14) | void saveLatestMessageId(@Param("receiveUserId") Long receiveUserId, @...

FILE: cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/offlinemsg/mapper/OfflineMsgMapper.java
  type OfflineMsgMapper (line 16) | @Mapper
    method insert (line 19) | int insert(OfflineMsg msg);
    method insertBatch (line 21) | int insertBatch(@Param("offlineMsgs") List<OfflineMsg> offlineMsgs);
    method fetchOfflineMsgsWithCursor (line 23) | List<OfflineMsg> fetchOfflineMsgsWithCursor(@Param("receiveUserId") Lo...
    method updateStatus (line 25) | int updateStatus(
    method fetchOfflineMsgIdsWithCursor (line 29) | List<Long> fetchOfflineMsgIdsWithCursor(@Param("receiveUserId") Long r...

FILE: cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/util/MapToJsonTypeHandler.java
  class MapToJsonTypeHandler (line 19) | public class MapToJsonTypeHandler extends BaseTypeHandler<Map<String, St...
    method setNonNullParameter (line 22) | @Override
    method getNullableResult (line 32) | @Override
    method getNullableResult (line 37) | @Override
    method getNullableResult (line 42) | @Override
    method parseJson (line 47) | private Map<String, String> parseJson(String json) {

FILE: cim-persistence/cim-persistence-redis/src/main/java/com/crossoverjie/cim/persistence/redis/OfflineMsgBuffer.java
  class OfflineMsgBuffer (line 28) | @Slf4j
    method OfflineMsgBuffer (line 35) | public OfflineMsgBuffer(OfflineMsgScriptExecutor scriptExecutor, Integ...
    method ensureValidTtlOrDefault (line 41) | private int ensureValidTtlOrDefault(Integer configuredDays) {
    method save (line 45) | @Override
    method fetch (line 55) | @Override
    method markDelivered (line 75) | @Override

FILE: cim-persistence/cim-persistence-redis/src/main/java/com/crossoverjie/cim/persistence/redis/constant/Constant.java
  class Constant (line 8) | public final class Constant {

FILE: cim-persistence/cim-persistence-redis/src/main/java/com/crossoverjie/cim/persistence/redis/kit/OfflineMsgScriptExecutor.java
  class OfflineMsgScriptExecutor (line 25) | @Component
    method OfflineMsgScriptExecutor (line 31) | public OfflineMsgScriptExecutor(RedisTemplate<String, Object> redisTem...
    method saveOfflineMsg (line 55) | public Long saveOfflineMsg(OfflineMsg msg, Integer messageTtlDays) {
    method serialize (line 66) | private String serialize(OfflineMsg msg) {
    method fetchOfflineMsgs (line 75) | public List<String> fetchOfflineMsgs(Long userId, Integer size) {
    method deleteOfflineMsg (line 84) | public Long deleteOfflineMsg(Long userId, List<Long> msgIds) {

FILE: cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/RouteApi.java
  type RouteApi (line 24) | public interface RouteApi {
    method groupRoute (line 33) | BaseResponse<NULLBody> groupRoute(ChatReqVO groupReqVO);
    method p2pRoute (line 41) | BaseResponse<NULLBody> p2pRoute(P2PReqVO p2pRequest);
    method offLine (line 51) | BaseResponse<NULLBody> offLine(ChatReqVO groupReqVO);
    method login (line 59) | BaseResponse<CIMServerResVO> login(LoginReqVO loginReqVO) throws Excep...
    method registerAccount (line 68) | BaseResponse<RegisterInfoResVO> registerAccount(RegisterInfoReqVO regi...
    method onlineUser (line 76) | @Request(method = Request.GET)
    method fetchOfflineMsgs (line 80) | BaseResponse<NULLBody> fetchOfflineMsgs(OfflineMsgReqVO offlineMsgReqVO);

FILE: cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/ChatReqVO.java
  class ChatReqVO (line 21) | @EqualsAndHashCode(callSuper = true)
    method toString (line 39) | @Override

FILE: cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/LoginReqVO.java
  class LoginReqVO (line 13) | @AllArgsConstructor
    method getUserId (line 18) | public Long getUserId() {
    method setUserId (line 22) | public void setUserId(Long userId) {
    method getUserName (line 26) | public String getUserName() {
    method setUserName (line 30) | public void setUserName(String userName) {
    method toString (line 34) | @Override

FILE: cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/OfflineMsgReqVO.java
  class OfflineMsgReqVO (line 15) | @Builder

FILE: cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/P2PReqVO.java
  class P2PReqVO (line 20) | @Builder
    method P2PReqVO (line 43) | public P2PReqVO() {
    method P2PReqVO (line 46) | public P2PReqVO(Long userId, Long receiveUserId, String msg) {
    method P2PReqVO (line 51) | public P2PReqVO(Long userId, Long receiveUserId, String msg, List<Stri...
    method getReceiveUserId (line 58) | public Long getReceiveUserId() {
    method setReceiveUserId (line 62) | public void setReceiveUserId(Long receiveUserId) {
    method getMsg (line 66) | public String getMsg() {
    method setMsg (line 70) | public void setMsg(String msg) {
    method getUserId (line 74) | public Long getUserId() {
    method setUserId (line 78) | public void setUserId(Long userId) {
    method toString (line 82) | @Override

FILE: cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/RegisterInfoReqVO.java
  class RegisterInfoReqVO (line 15) | public class RegisterInfoReqVO extends BaseRequest {
    method getUserName (line 21) | public String getUserName() {
    method setUserName (line 25) | public void setUserName(String userName) {
    method toString (line 29) | @Override

FILE: cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/SendMsgReqVO.java
  class SendMsgReqVO (line 15) | public class SendMsgReqVO extends BaseRequest {
    method getMsg (line 25) | public String getMsg() {
    method setMsg (line 29) | public void setMsg(String msg) {
    method getId (line 33) | public long getId() {
    method setId (line 37) | public void setId(long id) {

FILE: cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/res/CIMServerResVO.java
  class CIMServerResVO (line 15) | @Data

FILE: cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/res/RegisterInfoResVO.java
  class RegisterInfoResVO (line 12) | public class RegisterInfoResVO implements Serializable {
    method RegisterInfoResVO (line 16) | public RegisterInfoResVO(Long userId, String userName) {
    method getUserId (line 21) | public Long getUserId() {
    method setUserId (line 25) | public void setUserId(Long userId) {
    method getUserName (line 29) | public String getUserName() {
    method setUserName (line 33) | public void setUserName(String userName) {
    method toString (line 37) | @Override

FILE: cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/res/SendMsgResVO.java
  class SendMsgResVO (line 10) | public class SendMsgResVO {
    method getMsg (line 13) | public String getMsg() {
    method setMsg (line 17) | public void setMsg(String msg) {

FILE: cim-server-api/src/main/java/com/crossoverjie/cim/server/api/ServerApi.java
  type ServerApi (line 15) | public interface ServerApi {
    method sendMsg (line 23) | BaseResponse<SendMsgResVO> sendMsg(SendMsgReqVO sendMsgReqVO, @Dynamic...

FILE: cim-server-api/src/main/java/com/crossoverjie/cim/server/api/vo/req/SendMsgReqVO.java
  class SendMsgReqVO (line 24) | @Builder
    method SendMsgReqVO (line 49) | public SendMsgReqVO() {
    method SendMsgReqVO (line 52) | public SendMsgReqVO(String msg, Long userId, List<String> batchMsg, Ba...
    method getMsg (line 59) | public String getMsg() {
    method setMsg (line 63) | public void setMsg(String msg) {
    method toString (line 68) | @Override

FILE: cim-server-api/src/main/java/com/crossoverjie/cim/server/api/vo/res/OfflineMsgResVO.java
  class OfflineMsgResVO (line 8) | public class OfflineMsgResVO {
    method getMsg (line 12) | public String getMsg() {
    method setMsg (line 16) | public void setMsg(String msg) {

FILE: cim-server-api/src/main/java/com/crossoverjie/cim/server/api/vo/res/SaveOfflineMsgResVO.java
  class SaveOfflineMsgResVO (line 3) | public class SaveOfflineMsgResVO {
    method getMsg (line 7) | public String getMsg() {
    method setMsg (line 11) | public void setMsg(String msg) {

FILE: cim-server-api/src/main/java/com/crossoverjie/cim/server/api/vo/res/SendMsgResVO.java
  class SendMsgResVO (line 10) | public class SendMsgResVO {
    method getMsg (line 13) | public String getMsg() {
    method setMsg (line 17) | public void setMsg(String msg) {

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/CIMServerApplication.java
  class CIMServerApplication (line 18) | @SpringBootApplication
    method main (line 32) | public static void main(String[] args) {
    method run (line 37) | @Override

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/config/AppConfiguration.java
  class AppConfiguration (line 13) | @Component
    method getRouteUrl (line 31) | public String getRouteUrl() {
    method setRouteUrl (line 35) | public void setRouteUrl(String routeUrl) {
    method getZkConnectTimeout (line 45) | public int getZkConnectTimeout() {
    method getZkRoot (line 49) | public String getZkRoot() {
    method setZkRoot (line 53) | public void setZkRoot(String zkRoot) {
    method getZkAddr (line 57) | public String getZkAddr() {
    method setZkAddr (line 61) | public void setZkAddr(String zkAddr) {
    method isZkSwitch (line 65) | public boolean isZkSwitch() {
    method setZkSwitch (line 69) | public void setZkSwitch(boolean zkSwitch) {
    method getCimServerPort (line 73) | public int getCimServerPort() {
    method setCimServerPort (line 77) | public void setCimServerPort(int cimServerPort) {
    method getHeartBeatTime (line 81) | public long getHeartBeatTime() {
    method setHeartBeatTime (line 85) | public void setHeartBeatTime(long heartBeatTime) {

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/config/BeanConfig.java
  class BeanConfig (line 22) | @Configuration
    method okHttpClient (line 32) | @Bean
    method metaStore (line 42) | @Bean
    method heartBeat (line 51) | @Bean(value = "heartBeat")
    method routeApi (line 60) | @Bean

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/config/SwaggerConfig.java
  class SwaggerConfig (line 9) | @Configuration
    method createRestApi (line 11) | @Bean
    method apiInfo (line 17) | private Info apiInfo() {
    method contact (line 26) | private Contact contact() {

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/controller/IndexController.java
  class IndexController (line 25) | @Controller
    method sendMsg (line 38) | @Override

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/handle/CIMServerHandle.java
  class CIMServerHandle (line 29) | @ChannelHandler.Sharable
    method channelInactive (line 41) | @Override
    method userEventTriggered (line 56) | @Override
    method channelRead0 (line 73) | @Override
    method exceptionCaught (line 100) | @Override

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/init/CIMServerInitializer.java
  class CIMServerInitializer (line 20) | public class CIMServerInitializer extends ChannelInitializer<Channel> {
    method initChannel (line 24) | @Override

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/kit/RegistryMetaStore.java
  class RegistryMetaStore (line 19) | @Slf4j
    method RegistryMetaStore (line 30) | public RegistryMetaStore(MetaStore metaStore, String ip, int cimServer...
    method run (line 38) | @SneakyThrows

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/kit/RouteHandler.java
  class RouteHandler (line 19) | @Component
    method userOffLine (line 32) | public void userOffLine(CIMUserInfo userInfo, NioSocketChannel channel) {
    method clearRouteInfo (line 50) | public void clearRouteInfo(CIMUserInfo userInfo) {

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/kit/ServerHeartBeatHandlerImpl.java
  class ServerHeartBeatHandlerImpl (line 21) | @Service
    method process (line 32) | @Override

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/server/CIMServer.java
  class CIMServer (line 29) | @Component
    method start (line 48) | @PostConstruct
    method destroy (line 69) | @PreDestroy
    method sendMsg (line 81) | public void sendMsg(SendMsgReqVO sendMsgReqVO) {

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/util/SessionSocketHolder.java
  class SessionSocketHolder (line 16) | public class SessionSocketHolder {
    method saveSession (line 20) | public static void saveSession(Long userId, String userName) {
    method removeSession (line 24) | public static void removeSession(Long userId) {
    method put (line 33) | public static void put(Long id, NioSocketChannel socketChannel) {
    method get (line 37) | public static NioSocketChannel get(Long id) {
    method getRelationShip (line 41) | public static Map<Long, NioSocketChannel> getRelationShip() {
    method remove (line 45) | public static void remove(NioSocketChannel nioSocketChannel) {
    method getUserId (line 54) | public static CIMUserInfo getUserId(NioSocketChannel nioSocketChannel) {

FILE: cim-server/src/main/java/com/crossoverjie/cim/server/util/SpringBeanFactory.java
  class SpringBeanFactory (line 8) | @Component
    method getBean (line 12) | public static <T> T getBean(Class<T> c) {
    method getBean (line 17) | public static <T> T getBean(String name, Class<T> clazz) {
    method setApplicationContext (line 21) | @Override

FILE: cim-server/src/test/com/crossoverjie/cim/server/util/NettyAttrUtilTest.java
  class NettyAttrUtilTest (line 8) | public class NettyAttrUtilTest {
    method test (line 10) | @Test

FILE: sql/offline_msg.sql
  type offline_msg (line 1) | CREATE TABLE offline_msg (

FILE: sql/offline_msg_last_send_record.sql
  type offline_msg_last_send_record (line 1) | CREATE TABLE offline_msg_last_send_record
Condensed preview — 226 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (474K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 565,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: 'Please check if there are similar issues before s"
  },
  {
    "path": ".github/workflows/docker.yml",
    "chars": 1327,
    "preview": "name: Multi-Platform Docker Build\n\non:\n  push:\n    tags: ['image-*']\n  workflow_dispatch:    # keep this to allow manual"
  },
  {
    "path": ".github/workflows/maven.yml",
    "chars": 770,
    "preview": "# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow executi"
  },
  {
    "path": ".github/workflows/reusable_run_tests.yml",
    "chars": 782,
    "preview": "name: A reusable workflow to build and run the unit test suite\n\non:\n  workflow_call:\n    secrets:\n      codecov_token:\n "
  },
  {
    "path": ".gitignore",
    "chars": 2186,
    "preview": "# Created by .ignore support plugin (hsz.mobi)\n### macOS template\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon m"
  },
  {
    "path": "CLAUDE.md",
    "chars": 779,
    "preview": "# CIM Project Guide\n\nCIM (Cross-platform Instant Messaging) is a Java-based instant messaging framework.\n\n## Requirement"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2018 crossoverJie\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "Makefile",
    "chars": 1356,
    "preview": "IMAGE_NAME = allin1-ubuntu\nREGISTRY = ghcr.io\nOWNER = $(shell git config --get remote.origin.url | sed -e 's/.*github.co"
  },
  {
    "path": "README-zh.md",
    "chars": 8527,
    "preview": "\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://cod"
  },
  {
    "path": "README.md",
    "chars": 11443,
    "preview": "\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://cod"
  },
  {
    "path": "checkstyle/checkstyle.xml",
    "chars": 6231,
    "preview": "<?xml version=\"1.0\"?>\n<!DOCTYPE module PUBLIC\n        \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\"\n        \"htt"
  },
  {
    "path": "checkstyle/suppressions.xml",
    "chars": 1073,
    "preview": "<?xml version=\"1.0\"?>\n<!DOCTYPE suppressions PUBLIC\n        \"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN\""
  },
  {
    "path": "cim-client/pom.xml",
    "chars": 3383,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/CIMClientApplication.java",
    "chars": 733,
    "preview": "package com.crossoverjie.cim.client;\n\nimport com.crossoverjie.cim.client.scanner.Scan;\nimport lombok.extern.slf4j.Slf4j;"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/config/AppConfiguration.java",
    "chars": 864,
    "preview": "package com.crossoverjie.cim.client.config;\n\nimport lombok.Data;\nimport org.springframework.beans.factory.annotation.Val"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/config/BeanConfig.java",
    "chars": 3808,
    "preview": "package com.crossoverjie.cim.client.config;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.cim."
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/config/SwaggerConfig.java",
    "chars": 874,
    "preview": "package com.crossoverjie.cim.client.config;\n\nimport io.swagger.v3.oas.models.OpenAPI;\nimport io.swagger.v3.oas.models.in"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/scanner/Scan.java",
    "chars": 1304,
    "preview": "package com.crossoverjie.cim.client.scanner;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim."
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/InnerCommand.java",
    "chars": 264,
    "preview": "package com.crossoverjie.cim.client.service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n * Date: 2019-01-27 19:26\n * @"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/InnerCommandContext.java",
    "chars": 1258,
    "preview": "package com.crossoverjie.cim.client.service;\n\nimport com.crossoverjie.cim.client.service.impl.command.PrintAllCommand;\ni"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/MsgHandle.java",
    "chars": 713,
    "preview": "package com.crossoverjie.cim.client.service;\n\n/**\n * Function:消息处理器\n *\n * @author crossoverJie\n * Date: 2018/12/26 11:11"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/MsgLogger.java",
    "chars": 398,
    "preview": "package com.crossoverjie.cim.client.service;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2019/1/6 15:2"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/ShutDownSign.java",
    "chars": 433,
    "preview": "package com.crossoverjie.cim.client.service;\n\nimport org.springframework.stereotype.Component;\n\n/**\n * Function:\n *\n * @"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/AsyncMsgLogger.java",
    "chars": 3990,
    "preview": "package com.crossoverjie.cim.client.service.impl;\n\nimport com.crossoverjie.cim.client.config.AppConfiguration;\nimport co"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/EchoServiceImpl.java",
    "chars": 2692,
    "preview": "package com.crossoverjie.cim.client.service.impl;\n\nimport com.crossoverjie.cim.client.config.AppConfiguration;\nimport co"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/MsgCallBackListener.java",
    "chars": 1020,
    "preview": "package com.crossoverjie.cim.client.service.impl;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverji"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/MsgHandler.java",
    "chars": 2397,
    "preview": "package com.crossoverjie.cim.client.service.impl;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverji"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/CloseAIModelCommand.java",
    "chars": 716,
    "preview": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.cros"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/DelayMsgCommand.java",
    "chars": 1583,
    "preview": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.cros"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/EchoInfoCommand.java",
    "chars": 792,
    "preview": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.cro"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/EmojiCommand.java",
    "chars": 1184,
    "preview": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.cros"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/OpenAIModelCommand.java",
    "chars": 664,
    "preview": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.service.InnerCommand;\nimpo"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrefixSearchCommand.java",
    "chars": 1435,
    "preview": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.cro"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrintAllCommand.java",
    "chars": 1043,
    "preview": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.cros"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/PrintOnlineUsersCommand.java",
    "chars": 1210,
    "preview": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.cro"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/QueryHistoryCommand.java",
    "chars": 826,
    "preview": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.cros"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/service/impl/command/ShutDownCommand.java",
    "chars": 1795,
    "preview": "package com.crossoverjie.cim.client.service.impl.command;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.cro"
  },
  {
    "path": "cim-client/src/main/java/com/crossoverjie/cim/client/util/SpringBeanFactory.java",
    "chars": 757,
    "preview": "package com.crossoverjie.cim.client.util;\n\nimport org.springframework.beans.BeansException;\nimport org.springframework.c"
  },
  {
    "path": "cim-client/src/main/resources/application.yaml",
    "chars": 565,
    "preview": "spring:\n  application:\n    name: cim-client\n\n# web port\nserver:\n  port: 8082\n\nlogging:\n  level:\n    root: error\n\n# enabl"
  },
  {
    "path": "cim-client/src/main/resources/banner.txt",
    "chars": 177,
    "preview": "      _              ___          __\n ____(_)_ _     ____/ (_)__ ___  / /_\n/ __/ /  ' \\   / __/ / / -_) _ \\/ __/\n\\__/_/_"
  },
  {
    "path": "cim-client/src/test/java/com/crossoverjie/cim/client/service/InnerCommandContextTest.java",
    "chars": 2620,
    "preview": "package com.crossoverjie.cim.client.service;\n\nimport com.crossoverjie.cim.client.CIMClientApplication;\nimport com.crosso"
  },
  {
    "path": "cim-client/src/test/java/com/crossoverjie/cim/client/service/impl/AsyncMsgLoggerTest.java",
    "chars": 963,
    "preview": "package com.crossoverjie.cim.client.service.impl;\n\nimport com.crossoverjie.cim.client.CIMClientApplication;\nimport com.c"
  },
  {
    "path": "cim-client/src/test/java/com/crossoverjie/cim/server/test/CommonTest.java",
    "chars": 7734,
    "preview": "package com.crossoverjie.cim.server.test;\n\n\nimport com.crossoverjie.cim.common.core.proxy.RpcProxyManager;\nimport com.cr"
  },
  {
    "path": "cim-client/src/test/java/com/crossoverjie/cim/server/test/EchoTest.java",
    "chars": 2583,
    "preview": "package com.crossoverjie.cim.server.test;\n\nimport org.junit.Assert;\nimport org.junit.Test;\n\n/**\n * Function:\n *\n * @auth"
  },
  {
    "path": "cim-client/src/test/resources/application.yaml",
    "chars": 714,
    "preview": "spring:\n  application:\n    name: cim-client\n  main:\n    # this will not be used to create real spring context, because d"
  },
  {
    "path": "cim-client-sdk/README.md",
    "chars": 730,
    "preview": "\n```java\n    var auth1 = ClientConfigurationData.Auth.builder()\n    .userId(id)\n    .userName(cj)\n    .build();\n    \n   "
  },
  {
    "path": "cim-client-sdk/pom.xml",
    "chars": 1506,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/Client.java",
    "chars": 1107,
    "preview": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientBuilderImpl;\nimport com.cros"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ClientBuilder.java",
    "chars": 997,
    "preview": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;\nimport co"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ClientState.java",
    "chars": 498,
    "preview": "package com.crossoverjie.cim.client.sdk;\n\nimport java.util.concurrent.atomic.AtomicReference;\n\npublic abstract class Cli"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/Event.java",
    "chars": 905,
    "preview": "package com.crossoverjie.cim.client.sdk;\n\npublic interface Event {\n    void debug(String msg, Object... replace);\n    vo"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/FetchOfflineMsgJob.java",
    "chars": 737,
    "preview": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;\nimport co"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/ReConnectManager.java",
    "chars": 2199,
    "preview": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientImpl;\nimport com.crossoverji"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/RouteManager.java",
    "chars": 3348,
    "preview": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientImpl;\nimport com.crossoverji"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientBuilderImpl.java",
    "chars": 2602,
    "preview": "package com.crossoverjie.cim.client.sdk.impl;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport com.crossoverjie.ci"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientConfigurationData.java",
    "chars": 1556,
    "preview": "package com.crossoverjie.cim.client.sdk.impl;\n\nimport com.crossoverjie.cim.client.sdk.Event;\nimport com.crossoverjie.cim"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/impl/ClientImpl.java",
    "chars": 10985,
    "preview": "package com.crossoverjie.cim.client.sdk.impl;\n\nimport static com.crossoverjie.cim.common.enums.StatusEnum.RECONNECT_FAIL"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/CIMClientHandle.java",
    "chars": 3852,
    "preview": "package com.crossoverjie.cim.client.sdk.io;\n\nimport com.crossoverjie.cim.client.sdk.ClientState;\nimport com.crossoverjie"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/CIMClientHandleInitializer.java",
    "chars": 1120,
    "preview": "package com.crossoverjie.cim.client.sdk.io;\n\nimport com.crossoverjie.cim.common.protocol.Response;\nimport io.netty.chann"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/MessageListener.java",
    "chars": 344,
    "preview": "package com.crossoverjie.cim.client.sdk.io;\n\nimport com.crossoverjie.cim.client.sdk.Client;\nimport java.util.Map;\n\npubli"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/ReconnectCheck.java",
    "chars": 364,
    "preview": "package com.crossoverjie.cim.client.sdk.io;\n\nimport com.crossoverjie.cim.client.sdk.Client;\n\npublic interface ReconnectC"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/backoff/BackoffStrategy.java",
    "chars": 512,
    "preview": "package com.crossoverjie.cim.client.sdk.io.backoff;\n\nimport java.util.concurrent.TimeUnit;\n\n/**\n * @author:qjj\n * @creat"
  },
  {
    "path": "cim-client-sdk/src/main/java/com/crossoverjie/cim/client/sdk/io/backoff/RandomBackoff.java",
    "chars": 305,
    "preview": "package com.crossoverjie.cim.client.sdk.io.backoff;\n\n/**\n * @author:qjj\n * @create: 2024-09-21 12:22\n * @Description: ra"
  },
  {
    "path": "cim-client-sdk/src/test/java/com/crossoverjie/cim/client/sdk/ClientTest.java",
    "chars": 18026,
    "preview": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;\nimport co"
  },
  {
    "path": "cim-client-sdk/src/test/java/com/crossoverjie/cim/client/sdk/OfflineMsgTest.java",
    "chars": 10191,
    "preview": "package com.crossoverjie.cim.client.sdk;\n\nimport com.crossoverjie.cim.client.sdk.impl.ClientConfigurationData;\nimport co"
  },
  {
    "path": "cim-client-sdk/src/test/resources/application-route.yaml",
    "chars": 1055,
    "preview": "spring:\n  application:\n    name:\n      cim-forward-route\n  data:\n    redis:\n      host: 127.0.0.1\n      port: 6379\n     "
  },
  {
    "path": "cim-client-sdk/src/test/resources/init.sql",
    "chars": 768,
    "preview": "-- 创建表\nCREATE TABLE IF NOT EXISTS `offline_msg`\n(\n    `id`\n                   BIGINT\n        PRIMARY\n            KEY\n   "
  },
  {
    "path": "cim-common/pom.xml",
    "chars": 3937,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/constant/Constants.java",
    "chars": 1058,
    "preview": "package com.crossoverjie.cim.common.constant;\n\n/**\n * Function:常量\n *\n * @author crossoverJie\n *         Date: 28/03/2018"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/DynamicUrl.java",
    "chars": 391,
    "preview": "package com.crossoverjie.cim.common.core.proxy;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Re"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/Request.java",
    "chars": 346,
    "preview": "package com.crossoverjie.cim.common.core.proxy;\n\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.Rete"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/core/proxy/RpcProxyManager.java",
    "chars": 8376,
    "preview": "package com.crossoverjie.cim.common.core.proxy;\n\nimport static com.crossoverjie.cim.common.enums.StatusEnum.VALIDATION_F"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/RingBufferWheel.java",
    "chars": 9776,
    "preview": "package com.crossoverjie.cim.common.data.construct;\n\nimport lombok.extern.slf4j.Slf4j;\n\nimport java.util.HashSet;\nimport"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/SortArrayMap.java",
    "chars": 3487,
    "preview": "package com.crossoverjie.cim.common.data.construct;\n\nimport java.util.AbstractMap;\nimport java.util.ArrayList;\nimport ja"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/data/construct/TrieTree.java",
    "chars": 5187,
    "preview": "package com.crossoverjie.cim.common.data.construct;\n\nimport com.crossoverjie.cim.common.util.StringUtil;\n\nimport java.ut"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/enums/StatusEnum.java",
    "chars": 2880,
    "preview": "package com.crossoverjie.cim.common.enums;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * @author crossover"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/enums/SystemCommandEnum.java",
    "chars": 2591,
    "preview": "package com.crossoverjie.cim.common.enums;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * Function:\n *\n * @aut"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/exception/CIMException.java",
    "chars": 1701,
    "preview": "package com.crossoverjie.cim.common.exception;\n\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\n\n/**\n * Function:\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/exception/GenericException.java",
    "chars": 1279,
    "preview": "package com.crossoverjie.cim.common.exception;\n\nimport java.io.Serializable;\n\n/**\n * Function:\n *\n * @author crossoverJi"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/kit/HeartBeatHandler.java",
    "chars": 354,
    "preview": "package com.crossoverjie.cim.common.kit;\n\nimport io.netty.channel.ChannelHandlerContext;\n\n/**\n * Function:\n *\n * @author"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/metastore/AbstractConfiguration.java",
    "chars": 278,
    "preview": "package com.crossoverjie.cim.common.metastore;\n\nimport lombok.Builder;\nimport lombok.Data;\n\n/**\n * @author crossverJie\n "
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/metastore/MetaStore.java",
    "chars": 1201,
    "preview": "package com.crossoverjie.cim.common.metastore;\n\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * @author crossoverJi"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/metastore/ZkConfiguration.java",
    "chars": 338,
    "preview": "package com.crossoverjie.cim.common.metastore;\n\nimport org.apache.curator.RetryPolicy;\n\n/**\n * @author crossoverJie\n */\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/metastore/ZkMetaStoreImpl.java",
    "chars": 3930,
    "preview": "package com.crossoverjie.cim.common.metastore;\n\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverj"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/pojo/CIMUserInfo.java",
    "chars": 365,
    "preview": "package com.crossoverjie.cim.common.pojo;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok.NoArgsCon"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/pojo/RouteInfo.java",
    "chars": 370,
    "preview": "package com.crossoverjie.cim.common.pojo;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lombok.Data;\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/req/BaseRequest.java",
    "chars": 1069,
    "preview": "package com.crossoverjie.cim.common.req;\n\n\nimport io.swagger.v3.oas.annotations.media.Schema;\n\n/**\n * Function:\n * @auth"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/res/BaseResponse.java",
    "chars": 2176,
    "preview": "package com.crossoverjie.cim.common.res;\n\n\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverjie"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/res/NULLBody.java",
    "chars": 296,
    "preview": "package com.crossoverjie.cim.common.res;\n\n/**\n * Function:空对象,用在泛型中,表示没有额外的请求参数或者返回参数\n *\n * @author crossoverJie\n *     "
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/RouteHandle.java",
    "chars": 559,
    "preview": "package com.crossoverjie.cim.common.route.algorithm;\n\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport java.uti"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/AbstractConsistentHash.java",
    "chars": 2358,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport java.io.UnsupportedEncodingException;\nimport"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/ConsistentHashHandle.java",
    "chars": 952,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\n"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/SortArrayMapConsistentHash.java",
    "chars": 1489,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport com.crossoverjie.cim.common.data.construct.S"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/TreeMapConsistentHash.java",
    "chars": 2025,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/loop/LoopHandle.java",
    "chars": 1281,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.loop;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport c"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/route/algorithm/random/RandomHandle.java",
    "chars": 1185,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.random;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/util/HttpClient.java",
    "chars": 1509,
    "preview": "package com.crossoverjie.cim.common.util;\n\nimport okhttp3.MediaType;\nimport okhttp3.OkHttpClient;\nimport okhttp3.Request"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/util/NettyAttrUtil.java",
    "chars": 925,
    "preview": "package com.crossoverjie.cim.common.util;\n\nimport io.netty.channel.Channel;\nimport io.netty.util.Attribute;\nimport io.ne"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/util/RouteInfoParseUtil.java",
    "chars": 896,
    "preview": "package com.crossoverjie.cim.common.util;\n\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.crossov"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/util/SnowflakeIdWorker.java",
    "chars": 1430,
    "preview": "package com.crossoverjie.cim.common.util;\n\n/**\n * @author zhongcanyu\n * @date 2025/5/18\n * @description\n */\npublic class"
  },
  {
    "path": "cim-common/src/main/java/com/crossoverjie/cim/common/util/StringUtil.java",
    "chars": 654,
    "preview": "package com.crossoverjie.cim.common.util;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 22/05/2018 15:16"
  },
  {
    "path": "cim-common/src/main/proto/cim.proto",
    "chars": 567,
    "preview": "syntax = \"proto3\";\npackage com.crossoverjie.cim.common.protocol;\noption java_package = \"com.crossoverjie.cim.common.prot"
  },
  {
    "path": "cim-common/src/main/resources/log4j.properties",
    "chars": 773,
    "preview": "# Global logging configuration\r\nlog4j.rootLogger=DEBUG,CONSOLE,LOGFILE\r\n\r\nlog4j.appender.CONSOLE=org.apache.log4j.Consol"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/CommonTest.java",
    "chars": 1626,
    "preview": "package com.crossoverjie.cim.common;\n\nimport java.time.LocalDate;\nimport java.time.LocalTime;\nimport java.util.concurren"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/core/proxy/RpcProxyManagerTest.java",
    "chars": 8115,
    "preview": "package com.crossoverjie.cim.common.core.proxy;\n\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.f"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/RingBufferWheelTest.java",
    "chars": 8369,
    "preview": "package com.crossoverjie.cim.common.data.construct;\n\nimport com.google.common.util.concurrent.ThreadFactoryBuilder;\nimpo"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/ScheduledTest.java",
    "chars": 850,
    "preview": "package com.crossoverjie.cim.common.data.construct;\n\nimport com.google.common.util.concurrent.ThreadFactoryBuilder;\nimpo"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/SortArrayMapTest.java",
    "chars": 4998,
    "preview": "package com.crossoverjie.cim.common.data.construct;\n\nimport java.util.SortedMap;\nimport java.util.TreeMap;\nimport org.ju"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/TimerTest.java",
    "chars": 706,
    "preview": "package com.crossoverjie.cim.common.data.construct;\n\nimport java.util.Timer;\nimport java.util.TimerTask;\nimport lombok.e"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/data/construct/TrieTreeTest.java",
    "chars": 8824,
    "preview": "package com.crossoverjie.cim.common.data.construct;\n\nimport java.util.List;\nimport org.junit.Assert;\nimport org.junit.Te"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/enums/SystemCommandEnumTypeTest.java",
    "chars": 552,
    "preview": "package com.crossoverjie.cim.common.enums;\n\nimport java.util.Map;\nimport org.junit.Test;\n\npublic class SystemCommandEnum"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/metastore/MetaStoreTest.java",
    "chars": 3836,
    "preview": "package com.crossoverjie.cim.common.metastore;\n\nimport java.util.List;\nimport java.util.concurrent.TimeUnit;\nimport lomb"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/ConsistentHashHandleTest.java",
    "chars": 2131,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport static org.junit.jupiter.api.Assertions.asse"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/RangeCheckTestUtil.java",
    "chars": 349,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport org.junit.Assert;\n\n/**\n * @description: TODO"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/SortArrayMapConsistentHashTest.java",
    "chars": 5257,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport com.crossoverjie.cim.common.data.construct.S"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/consistenthash/TreeMapConsistentHashTest.java",
    "chars": 2931,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.consistenthash;\n\nimport java.util.ArrayList;\nimport java.util.HashSe"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/loop/LoopHandleTest.java",
    "chars": 1128,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.loop;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport com"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/route/algorithm/random/RandomHandleTest.java",
    "chars": 1028,
    "preview": "package com.crossoverjie.cim.common.route.algorithm.random;\n\nimport static org.junit.jupiter.api.Assertions.*;\n\nimport c"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/util/HttpClientTest.java",
    "chars": 999,
    "preview": "package com.crossoverjie.cim.common.util;\n\nimport com.alibaba.fastjson.JSONObject;\nimport java.io.IOException;\nimport ja"
  },
  {
    "path": "cim-common/src/test/java/com/crossoverjie/cim/common/util/ProtocolTest.java",
    "chars": 1203,
    "preview": "package com.crossoverjie.cim.common.util;\n\nimport com.crossoverjie.cim.common.protocol.BaseCommand;\nimport com.crossover"
  },
  {
    "path": "cim-forward-route/pom.xml",
    "chars": 4374,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/RouteApplication.java",
    "chars": 874,
    "preview": "package com.crossoverjie.cim.route;\n\nimport lombok.extern.slf4j.Slf4j;\nimport org.springframework.boot.CommandLineRunner"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/AppConfiguration.java",
    "chars": 1532,
    "preview": "package com.crossoverjie.cim.route.config;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.spring"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/BeanConfig.java",
    "chars": 5218,
    "preview": "package com.crossoverjie.cim.route.config;\n\nimport com.crossoverjie.cim.common.core.proxy.RpcProxyManager;\nimport com.cr"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/MySqlPersistenceConfig.java",
    "chars": 706,
    "preview": "package com.crossoverjie.cim.route.config;\n\nimport org.mybatis.spring.annotation.MapperScan;\nimport org.springframework."
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/OfflineMsgStoreConfig.java",
    "chars": 1683,
    "preview": "package com.crossoverjie.cim.route.config;\n\nimport com.crossoverjie.cim.persistence.mysql.offlinemsg.OfflineMsgDb;\nimpor"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/config/SwaggerConfig.java",
    "chars": 886,
    "preview": "package com.crossoverjie.cim.route.config;\n\nimport io.swagger.v3.oas.models.OpenAPI;\nimport io.swagger.v3.oas.models.inf"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/constant/Constant.java",
    "chars": 708,
    "preview": "package com.crossoverjie.cim.route.constant;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2018/9/10 14:"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/controller/RouteController.java",
    "chars": 9562,
    "preview": "package com.crossoverjie.cim.route.controller;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossove"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/exception/ExceptionHandlingController.java",
    "chars": 899,
    "preview": "package com.crossoverjie.cim.route.exception;\n\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.cro"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/factory/OfflineMsgFactory.java",
    "chars": 2140,
    "preview": "package com.crossoverjie.cim.route.factory;\n\nimport com.crossoverjie.cim.common.constant.Constants;\nimport com.crossover"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/kit/NetAddressIsReachable.java",
    "chars": 1001,
    "preview": "package com.crossoverjie.cim.route.kit;\n\nimport java.io.IOException;\nimport java.net.InetSocketAddress;\nimport java.net."
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/AccountService.java",
    "chars": 1674,
    "preview": "package com.crossoverjie.cim.route.service;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossoverji"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/CommonBizService.java",
    "chars": 1461,
    "preview": "package com.crossoverjie.cim.route.service;\n\nimport com.crossoverjie.cim.common.pojo.RouteInfo;\nimport com.crossoverjie."
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/OfflineMsgService.java",
    "chars": 539,
    "preview": "package com.crossoverjie.cim.route.service;\n\nimport com.crossoverjie.cim.route.api.vo.req.P2PReqVO;\nimport com.crossover"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/UserInfoCacheService.java",
    "chars": 776,
    "preview": "package com.crossoverjie.cim.route.service;\n\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\n\nimport java.util.Opti"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/impl/AccountServiceRedisImpl.java",
    "chars": 6585,
    "preview": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.crossoverjie.cim.common.constant.Constants;\nimport com.cros"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/impl/OfflineMsgServiceImpl.java",
    "chars": 3307,
    "preview": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.crossoverjie.cim.common.constant.Constants;\nimport com.cros"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/service/impl/UserInfoCacheServiceImpl.java",
    "chars": 2055,
    "preview": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crosso"
  },
  {
    "path": "cim-forward-route/src/main/java/com/crossoverjie/cim/route/util/SpringBeanFactory.java",
    "chars": 756,
    "preview": "package com.crossoverjie.cim.route.util;\n\nimport org.springframework.beans.BeansException;\nimport org.springframework.co"
  },
  {
    "path": "cim-forward-route/src/main/resources/application.yaml",
    "chars": 1637,
    "preview": "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    "
  },
  {
    "path": "cim-forward-route/src/main/resources/banner.txt",
    "chars": 180,
    "preview": "      _                         __\n ____(_)_ _      _______  __ __/ /____\n/ __/ /  ' \\    / __/ _ \\/ // / __/ -_)\n\\__/_/"
  },
  {
    "path": "cim-forward-route/src/main/resources/lua/offLine.lua",
    "chars": 67,
    "preview": "\nredis.call('DEL', KEYS[1])\n\nredis.call('SREM', ARGV[1], ARGV[2])\n\n"
  },
  {
    "path": "cim-forward-route/src/test/java/CommonTest.java",
    "chars": 388,
    "preview": "import com.crossoverjie.cim.route.kit.NetAddressIsReachable;\nimport org.junit.Test;\n\n/**\n * Function:\n *\n * @author cros"
  },
  {
    "path": "cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/AbstractBaseTest.java",
    "chars": 2291,
    "preview": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.clevercloud.testcontainers.zookeeper.ZooKeeperContainer;\nim"
  },
  {
    "path": "cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/AccountServiceRedisImplTest.java",
    "chars": 978,
    "preview": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.alibaba.fastjson.JSON;\nimport com.crossoverjie.cim.route.Ro"
  },
  {
    "path": "cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/RedisTest.java",
    "chars": 743,
    "preview": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.crossoverjie.cim.route.RouteApplication;\nimport org.junit.j"
  },
  {
    "path": "cim-forward-route/src/test/java/com/crossoverjie/cim/route/service/impl/UserInfoCacheServiceImplTest.java",
    "chars": 1058,
    "preview": "package com.crossoverjie.cim.route.service.impl;\n\nimport com.alibaba.fastjson.JSON;\nimport com.crossoverjie.cim.common.p"
  },
  {
    "path": "cim-forward-route/src/test/resources/application.yaml",
    "chars": 1506,
    "preview": "spring:\n  application:\n    name: cim-forward-route\n  data:\n    redis:\n      host: 127.0.0.1\n      port: 6379\n      jedis"
  },
  {
    "path": "cim-forward-route/src/test/resources/init.sql",
    "chars": 768,
    "preview": "-- 创建表\nCREATE TABLE IF NOT EXISTS `offline_msg`\n(\n    `id`\n                   BIGINT\n        PRIMARY\n            KEY\n   "
  },
  {
    "path": "cim-integration-test/pom.xml",
    "chars": 2366,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www"
  },
  {
    "path": "cim-integration-test/src/main/java/com/crossoverjie/cim/client/sdk/route/AbstractRouteBaseTest.java",
    "chars": 1987,
    "preview": "package com.crossoverjie.cim.client.sdk.route;\n\nimport com.crossoverjie.cim.common.res.BaseResponse;\nimport com.crossove"
  },
  {
    "path": "cim-integration-test/src/main/java/com/crossoverjie/cim/client/sdk/route/OfflineMsgStoreRouteBaseTest.java",
    "chars": 2445,
    "preview": "package com.crossoverjie.cim.client.sdk.route;\n\nimport com.crossoverjie.cim.route.RouteApplication;\nimport com.crossover"
  },
  {
    "path": "cim-integration-test/src/main/java/com/crossoverjie/cim/client/sdk/server/AbstractServerBaseTest.java",
    "chars": 3332,
    "preview": "package com.crossoverjie.cim.client.sdk.server;\n\nimport com.clevercloud.testcontainers.zookeeper.ZooKeeperContainer;\nimp"
  },
  {
    "path": "cim-integration-test/src/test/resources/application-client.yaml",
    "chars": 567,
    "preview": "spring:\n  application:\n    name: cim-client\n\n# web port\nserver:\n  port: 8082\n\nlogging:\n  level:\n    root: error\n\n# enabl"
  },
  {
    "path": "cim-integration-test/src/test/resources/application-route.yaml",
    "chars": 1205,
    "preview": "spring:\n  application:\n    name:\n      cim-forward-route\n  data:\n    redis:\n      host: 127.0.0.1\n      port: 6379\n     "
  },
  {
    "path": "cim-persistence/cim-persistence-api/pom.xml",
    "chars": 1187,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
  },
  {
    "path": "cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/config/BeanConfig.java",
    "chars": 1606,
    "preview": "package com.crossoverjie.cim.persistence.api.config;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport lombok."
  },
  {
    "path": "cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/pojo/OfflineMsg.java",
    "chars": 683,
    "preview": "package com.crossoverjie.cim.persistence.api.pojo;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Builder;\nimport lomb"
  },
  {
    "path": "cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/pojo/OfflineMsgLastSendRecord.java",
    "chars": 421,
    "preview": "package com.crossoverjie.cim.persistence.api.pojo;\n\nimport lombok.AllArgsConstructor;\nimport lombok.Data;\nimport lombok."
  },
  {
    "path": "cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/service/OfflineMsgStore.java",
    "chars": 665,
    "preview": "package com.crossoverjie.cim.persistence.api.service;\n\n\nimport com.crossoverjie.cim.persistence.api.pojo.OfflineMsg;\n\nim"
  },
  {
    "path": "cim-persistence/cim-persistence-api/src/main/java/com/crossoverjie/cim/persistence/api/vo/req/SaveOfflineMsgReqVO.java",
    "chars": 906,
    "preview": "package com.crossoverjie.cim.persistence.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\nimport io.swag"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/pom.xml",
    "chars": 1604,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/config/MyBatisConfig.java",
    "chars": 637,
    "preview": "package com.crossoverjie.cim.persistence.mysql.config;\n\nimport com.crossoverjie.cim.persistence.mysql.util.MapToJsonType"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/offlinemsg/OfflineMsgDb.java",
    "chars": 1562,
    "preview": "package com.crossoverjie.cim.persistence.mysql.offlinemsg;\n\nimport com.crossoverjie.cim.persistence.api.pojo.OfflineMsg;"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/offlinemsg/mapper/OfflineMsgLastSendRecordMapper.java",
    "chars": 402,
    "preview": "package com.crossoverjie.cim.persistence.mysql.offlinemsg.mapper;\n\nimport org.apache.ibatis.annotations.Mapper;\nimport o"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/offlinemsg/mapper/OfflineMsgMapper.java",
    "chars": 814,
    "preview": "package com.crossoverjie.cim.persistence.mysql.offlinemsg.mapper;\n\n\n\nimport com.crossoverjie.cim.persistence.api.pojo.Of"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/java/com/crossoverjie/cim/persistence/mysql/util/MapToJsonTypeHandler.java",
    "chars": 1790,
    "preview": "package com.crossoverjie.cim.persistence.mysql.util;\n\nimport com.fasterxml.jackson.core.type.TypeReference;\nimport com.f"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/resources/mapper/OfflineMsgLastSendRecordMapper.xml",
    "chars": 658,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org"
  },
  {
    "path": "cim-persistence/cim-persistence-mysql/src/main/resources/mapper/OfflineMsgMapper.xml",
    "chars": 3006,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/pom.xml",
    "chars": 1225,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/java/com/crossoverjie/cim/persistence/redis/OfflineMsgBuffer.java",
    "chars": 3376,
    "preview": "package com.crossoverjie.cim.persistence.redis;\n\nimport com.crossoverjie.cim.common.enums.StatusEnum;\nimport com.crossov"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/java/com/crossoverjie/cim/persistence/redis/constant/Constant.java",
    "chars": 664,
    "preview": "package com.crossoverjie.cim.persistence.redis.constant;\n\n/**\n * @author zhongcanyu\n * @date 2025/6/14\n * @description\n "
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/java/com/crossoverjie/cim/persistence/redis/kit/OfflineMsgScriptExecutor.java",
    "chars": 3595,
    "preview": "package com.crossoverjie.cim.persistence.redis.kit;\n\nimport com.crossoverjie.cim.persistence.api.pojo.OfflineMsg;\nimport"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/resources/lua/deleteOfflineMsg.lua",
    "chars": 265,
    "preview": "local msgPrefix = KEYS[1]\nlocal userIdxPrefix = KEYS[2]\nlocal userId = ARGV[1]\nlocal userListKey = userIdxPrefix .. user"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/resources/lua/fetchOfflineMsg.lua",
    "chars": 441,
    "preview": "local userId = ARGV[1]\nlocal rangeSize = tonumber(ARGV[2])\nlocal msgPrefix = KEYS[1]\nlocal userIdxPrefix = KEYS[2]\nlocal"
  },
  {
    "path": "cim-persistence/cim-persistence-redis/src/main/resources/lua/saveOfflineMsg.lua",
    "chars": 521,
    "preview": "local msgPrefix = KEYS[1]\nlocal userIdxPrefix = KEYS[2]\nlocal msgId = ARGV[1]\nlocal receiveUserId = ARGV[2]\nlocal msgVal"
  },
  {
    "path": "cim-persistence/pom.xml",
    "chars": 1352,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
  },
  {
    "path": "cim-rout-api/pom.xml",
    "chars": 1253,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/RouteApi.java",
    "chars": 2075,
    "preview": "package com.crossoverjie.cim.route.api;\n\nimport com.crossoverjie.cim.common.core.proxy.Request;\nimport com.crossoverjie."
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/ChatReqVO.java",
    "chars": 1164,
    "preview": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\n\nimport io.swagger.v"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/LoginReqVO.java",
    "chars": 861,
    "preview": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\nimport lombok.AllArg"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/OfflineMsgReqVO.java",
    "chars": 615,
    "preview": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport io.swagger.v3.oas.annotations.media.Schema;\nimport jakarta.valida"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/P2PReqVO.java",
    "chars": 2162,
    "preview": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\n\nimport io.swagger.v"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/RegisterInfoReqVO.java",
    "chars": 858,
    "preview": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\n\nimport io.swagger.v"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/req/SendMsgReqVO.java",
    "chars": 894,
    "preview": "package com.crossoverjie.cim.route.api.vo.req;\n\nimport com.crossoverjie.cim.common.req.BaseRequest;\n\nimport io.swagger.v"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/res/CIMServerResVO.java",
    "chars": 456,
    "preview": "package com.crossoverjie.cim.route.api.vo.res;\n\nimport java.io.Serializable;\nimport lombok.AllArgsConstructor;\nimport lo"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/res/RegisterInfoResVO.java",
    "chars": 909,
    "preview": "package com.crossoverjie.cim.route.api.vo.res;\n\nimport java.io.Serializable;\n\n/**\n * Function:\n *\n * @author crossoverJi"
  },
  {
    "path": "cim-rout-api/src/main/java/com/crossoverjie/cim/route/api/vo/res/SendMsgResVO.java",
    "chars": 325,
    "preview": "package com.crossoverjie.cim.route.api.vo.res;\n\n/**\n * Function:\n *\n * @author crossoverJie\n *         Date: 2017/6/26 1"
  },
  {
    "path": "cim-server/pom.xml",
    "chars": 2762,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/CIMServerApplication.java",
    "chars": 1271,
    "preview": "package com.crossoverjie.cim.server;\n\nimport com.crossoverjie.cim.common.metastore.MetaStore;\nimport com.crossoverjie.ci"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/config/AppConfiguration.java",
    "chars": 1765,
    "preview": "package com.crossoverjie.cim.server.config;\n\nimport org.springframework.beans.factory.annotation.Value;\nimport org.sprin"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/config/BeanConfig.java",
    "chars": 1780,
    "preview": "package com.crossoverjie.cim.server.config;\n\nimport com.crossoverjie.cim.common.core.proxy.RpcProxyManager;\nimport com.c"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/config/SwaggerConfig.java",
    "chars": 872,
    "preview": "package com.crossoverjie.cim.server.config;\n\nimport io.swagger.v3.oas.models.OpenAPI;\nimport io.swagger.v3.oas.models.in"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/controller/IndexController.java",
    "chars": 1810,
    "preview": "package com.crossoverjie.cim.server.controller;\n\nimport com.crossoverjie.cim.common.core.proxy.DynamicUrl;\nimport com.cr"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/handle/CIMServerHandle.java",
    "chars": 3765,
    "preview": "package com.crossoverjie.cim.server.handle;\n\nimport com.crossoverjie.cim.common.exception.CIMException;\nimport com.cross"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/init/CIMServerInitializer.java",
    "chars": 1328,
    "preview": "package com.crossoverjie.cim.server.init;\n\nimport com.crossoverjie.cim.common.protocol.Request;\nimport com.crossoverjie."
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/kit/RegistryMetaStore.java",
    "chars": 1629,
    "preview": "package com.crossoverjie.cim.server.kit;\n\nimport com.crossoverjie.cim.common.metastore.MetaStore;\nimport com.crossoverji"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/kit/RouteHandler.java",
    "chars": 1341,
    "preview": "package com.crossoverjie.cim.server.kit;\n\nimport com.crossoverjie.cim.common.pojo.CIMUserInfo;\nimport com.crossoverjie.c"
  },
  {
    "path": "cim-server/src/main/java/com/crossoverjie/cim/server/kit/ServerHeartBeatHandlerImpl.java",
    "chars": 1607,
    "preview": "package com.crossoverjie.cim.server.kit;\n\nimport com.crossoverjie.cim.common.kit.HeartBeatHandler;\nimport com.crossoverj"
  }
]

// ... and 26 more files (download for full content)

About this extraction

This page contains the full source code of the crossoverJie/cim GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 226 files (423.0 KB), approximately 113.6k tokens, and a symbol index with 872 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!