📄 lumaqqmessagemanager.java
字号:
/*
* LumaQQ - Java QQ Client
*
* Copyright (C) 2004 luma <stubma@163.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package edu.tsinghua.lumaqq;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Hashtable;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* LumaQQ的自定义聊天记录文件管理类,自定义的聊天记录文件格式为:(土到掉渣)
* 一. 文件头部分
* 1. 好友数量,2字节
* 2. 第一天的偏移,4字节
* 3. 最后一天的偏移,4字节
* 4. 最后一个日期项的偏移,4字节
* 二. 好友区部分(缺省初始化为500条好友记录,未用部分填0)
* 1. 好友号码,4字节
* 2. 第一条消息偏移,4字节
* 3. 最后一条消息偏移,4字节
* 三. 记录区部分
* 1. 日期记录区(一天只有一条,但是可能有很多段)
* i. 年, 2字节
* ii. 月,1字节
* iii. 日,1字节
* iv. 下一段的偏移,4字节
* v. 下一天的偏移,4字节
* vi. 下一月的偏移,4字节,只有在月中的第一天项上才有意义
* vii. 下一年的偏移,4字节,只有在年中的第一天项上才有意义
* viii.本天的消息数,4字节
* 2. 消息记录区(可以有若干条)
* i. 消息的从属,相当于第一级从属关系,如果是普通消息,这个是好友QQ,如果是系统消息,这个是10000
* ii. 好友的QQ号或者是群的内部ID,4字节,这个相当于第二级从属关系
* iii. 消息的发送者QQ号,4字节,这个相当于第三级从属关系,是最直接的消息引发者
* iv. 消息发送时间的毫秒数除1000,4字节
* v. 下一条消息偏移,4字节
* vi. 消息内容(是否加密由实现决定)
*
* 记录区部分是不断追加的
*
* @author 马若劼
*/
public class LumaQQMessageManager implements IMessage {
// 保存好友记录的bean
private class FriendEntry {
public long offset;
public int firstOffset;
public int lastOffset;
public FriendEntry() {
offset = firstOffset = lastOffset = 0;
}
}
// Log对象
private static Log log = LogFactory.getLog(LumaQQMessageManager.class);
// 单一实例
private static LumaQQMessageManager instance = null;
// 最多的好友数
private static final int MAX_FRIEND = 400;
// 文件头长度
private static final int HEADER_LENGTH = 14;
// 好友项长度
private static final int FRIEND_ENTRY_LENGTH = 12;
// 日期项长度
private static final int DATE_ENTRY_LENGTH = 24;
// 随机存取文件
private RandomAccessFile msgFile;
// 好友数目
private int friends;
// 好友列表,哈希表的键是QQ号,值是FriendEntry
private Hashtable friendHash;
// 第一天和最后一天的偏移
private int firstDayOffset, lastDayOffset;
// 最后一个日期项的偏移
private int lastDateEntryOffset;
// 最近的记录的年月日
private int lastYear, lastMonth, lastDay;
// 今天的年月日,这些变量用过临时变量
private int todayYear, todayMonth, todayDay;
// 当前搜索到的年月日,用做临时变量
private int searchYear, searchMonth, searchDay;
// 当前的日期记录项偏移和消息数,当前是指今天
private int todayOffset, todayNumber;
/**
* 私有构造函数
* @param userDir 用户目录
*/
private LumaQQMessageManager(String userDir) {
File file = new File(userDir + "/msg.db");
if(!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
log.error("无法创建聊天记录文件,聊天记录功能将无法使用");
msgFile = null;
return;
}
}
// 创建随机存取文件对象
try {
msgFile = new RandomAccessFile(file, "rw");
if(msgFile.length() <= FRIEND_ENTRY_LENGTH * MAX_FRIEND + HEADER_LENGTH) {
/* 文件长度不足,初始化文件为最初状态 */
msgFile.seek(0);
byte[] b = new byte[FRIEND_ENTRY_LENGTH * MAX_FRIEND + HEADER_LENGTH];
msgFile.write(b);
}
// 读取文件
msgFile.seek(0);
// 读取有记录的好友数
friends = (int)msgFile.readChar();
// 读取第一天的偏移和最后一天的偏移
firstDayOffset = msgFile.readInt();
lastDayOffset = msgFile.readInt();
lastDateEntryOffset = msgFile.readInt();
todayOffset = lastDateEntryOffset;
todayNumber = getMessageNumber(todayOffset);
// 读取好友的号码和偏移到哈希表中
msgFile.seek(HEADER_LENGTH);
friendHash = new Hashtable();
for(int i = 0; i < friends; i++) {
FriendEntry fe = new FriendEntry();
fe.offset = msgFile.getFilePointer();
Integer key = new Integer(msgFile.readInt());
fe.firstOffset = msgFile.readInt();
fe.lastOffset = msgFile.readInt();
friendHash.put(key, fe);
}
// 读取最大的日月年值
if(lastDateEntryOffset != 0) {
msgFile.seek(lastDateEntryOffset);
lastYear = msgFile.readChar();
lastMonth = msgFile.readByte();
lastDay = msgFile.readByte();
}
} catch (Exception e) {
log.error("无法读取聊天记录文件,聊天记录功能将无法使用");
msgFile = null;
}
}
/**
* 关闭聊天记录文件
*/
public void close() {
try {
msgFile.close();
} catch (IOException e) {
log.error(e.getMessage());
}
}
/**
* 得到单一实例
* @param userDir 用户目录
* @return IMessage对象
*/
public static IMessage getInstance(String userDir) {
if(instance != null)
instance.close();
instance = new LumaQQMessageManager(userDir);
return instance;
}
/* (non-Javadoc)
* @see edu.tsinghua.lumaqq.IMessage#saveMessage(int, int, int, java.lang.String, int)
*/
public void saveMessage(int owner, int src, int sender, String message, int time) {
if(msgFile == null) return;
try {
checkYMDRecord();
// 如果todayOffset等于-1说明有错,这条消息也不会被保存
if(todayOffset != -1) {
Integer key = new Integer(owner);
FriendEntry fe = null;
// 如果存在好友项,直接得到,如果不存在,新建一个,并写入该项
if(friendHash.containsKey(key))
fe = (FriendEntry)friendHash.get(key);
else
fe = createFriendEntry(key);
// 写入消息,得到消息的偏移
int offset = writeMessage(owner, src, sender, message, time);
// 如果这是这个好友的第一条消息,改变entry中的变量
if(fe.firstOffset == 0)
setFriendFirstOffset(fe, offset);
// 不管是不是第一条消息,lastOffset总要修改
// 如果不是第一条,需要修改消息链,其实这等于在链表中的尾部追加节点
if(fe.lastOffset != 0)
setNextMessageOffset(fe.lastOffset, offset);
setFriendLastOffset(fe, offset);
// 调整日期记录项的消息数目
setMessageNumber(todayOffset, ++todayNumber);
// 把改变同步到硬盘
msgFile.getFD().sync();
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
/**
* 创建一个新的好友项,写入,同时修改好友数量
* @param key
* @return
*/
private FriendEntry createFriendEntry(Integer key) throws IOException {
FriendEntry fe = new FriendEntry();
fe.offset = HEADER_LENGTH + FRIEND_ENTRY_LENGTH * friendHash.size();
msgFile.seek(fe.offset);
friendHash.put(key, fe);
msgFile.writeInt(key.intValue());
msgFile.seek(0);
msgFile.writeChar(friendHash.size());
return fe;
}
/**
* 设置下一条消息偏移
* @param i
* @param offset
*/
private void setNextMessageOffset(int offset, int nextOffset) throws IOException {
msgFile.seek(offset + 16);
msgFile.writeInt(nextOffset);
}
/**
* 得到下一条消息偏移
* @param offset 当前消息记录的偏移
* @return
* @throws IOException
*/
private int getNextMessageOffset(int offset) throws IOException {
msgFile.seek(offset + 16);
return msgFile.readInt();
}
/**
* 写入一条消息到文件最后
* @param owner
* @param src
* @param sender
* @param message
* @param time
* @return 消息的偏移
*/
private int writeMessage(int owner, int src, int sender, String message, int time) throws IOException {
int len = (int)msgFile.length();
msgFile.seek(len);
// 从属
msgFile.writeInt(owner);
// 好友QQ号或者群内部ID
msgFile.writeInt(src);
// 发送者QQ号
msgFile.writeInt(sender);
// 消息发送时间
msgFile.writeInt(time);
// 下一条消息偏移
msgFile.writeInt(0);
// 消息内容
msgFile.writeUTF(message);
return len;
}
/**
* 检查现在的日子和最近一天记录的日子是否相同,如果相同,不需要做什么
* 如果大于,需要添加一个新日期记录,如果小于,则要添加一个日期记录并
* 修改段值
*/
private void checkYMDRecord() {
getToday();
int ret = compareTodayWithLastDay();
if(ret != 0) {
todayOffset = addToday();
todayNumber = 0;
}
}
/**
* 添加一个日期记录项
* @return 返回这个日期记录项的偏移
*/
private int addToday() {
try {
int newOffset = writeToday();
adjustYMDSLink(newOffset);
setLastDateEntryOffset(newOffset);
return newOffset;
} catch (IOException e) {
return -1;
}
}
/**
* 修改最后一个日期记录项偏移值
* @param offset
* @throws IOException
*/
private void setLastDateEntryOffset(int offset) throws IOException {
msgFile.seek(10);
msgFile.writeInt(offset);
lastDateEntryOffset = offset;
readYMD(lastDateEntryOffset);
lastYear = searchYear;
lastMonth = searchMonth;
lastDay = searchDay;
}
/**
* 调整年月日段4条链
* @return
*/
private void adjustYMDSLink(int newOffset) {
try {
if(firstDayOffset == 0) {
/* 第一天的偏移为0说明现在一条日期记录都没有 */
setFirstDayOffset(newOffset);
setLastDayOffset(newOffset);
} else {
// 得到年偏移,今天的年有两种情况,可能等于也可能大于
int yearOffset = findYear(firstDayOffset);
if(yearOffset == 0) {
/* 如果offset等于0,那么这个是第一项,这是头部情况 */
if(todayYear < getYear(firstDayOffset))
setNextYearOffset(newOffset, firstDayOffset);
else if(todayMonth < getMonth(firstDayOffset)) {
setNextYearOffset(newOffset, getNextYearOffset(firstDayOffset));
setNextMonthOffset(newOffset, firstDayOffset);
setNextYearOffset(firstDayOffset, 0);
} else if(todayDay < getDay(firstDayOffset)) {
setNextYearOffset(newOffset, getNextDayOffset(firstDayOffset));
setNextDayOffset(newOffset, firstDayOffset);
setNextYearOffset(firstDayOffset, 0);
} else {
setNextSegmentOffset(newOffset, getNextSegmentOffset(firstDayOffset));
setNextSegmentOffset(firstDayOffset, newOffset);
}
setFirstDayOffset(newOffset);
} else if(todayYear > getYear(yearOffset)) {
/* 如果今天的年份大于搜索的位置,则可以插入 */
// 得到下一项,判断是否为尾部
int temp = getNextYearOffset(yearOffset);
if(temp == 0) {
setNextYearOffset(yearOffset, newOffset);
setLastDayOffset(newOffset);
} else {
/* 否则根据各个域的大小调整相应的链 */
if(todayYear < getYear(temp)) {
setNextYearOffset(yearOffset, newOffset);
setNextYearOffset(newOffset, temp);
} else if(todayMonth < getMonth(temp)) {
setNextYearOffset(yearOffset, newOffset);
setNextYearOffset(newOffset, getNextYearOffset(temp));
setNextMonthOffset(newOffset, temp);
setNextYearOffset(temp, 0);
} else if(todayDay < getDay(temp)) {
setNextYearOffset(yearOffset, newOffset);
setNextYearOffset(newOffset, getNextDayOffset(temp));
setNextDayOffset(newOffset, temp);
setNextYearOffset(temp, 0);
} else {
setNextSegmentOffset(newOffset, getNextSegmentOffset(temp));
setNextSegmentOffset(temp, newOffset);
}
}
} else {
int temp = getNextYearOffset(yearOffset);
/* 最后的情况是今天的年等于搜索的位置,需要继续判断条件 */
int monthOffset = findMonth(yearOffset);
int tempM = getNextMonthOffset(monthOffset);
if(todayMonth == getMonth(monthOffset)) {
/* 如果月是相等的,那么只能是调整日链 */
int dayOffset = findDay(monthOffset);
int tempD = getNextDayOffset(dayOffset);
if(temp == 0 && tempM == 0 && tempD == 0) {
setNextDayOffset(dayOffset, newOffset);
setLastDayOffset(newOffset);
} else {
if(todayDay < getDay(tempD)) {
setNextDayOffset(dayOffset, newOffset);
setNextDayOffset(newOffset, tempD);
} else {
setNextSegmentOffset(newOffset, getNextSegmentOffset(tempD));
setNextSegmentOffset(tempD, newOffset);
}
}
} else {
/* 如果不等于那今天的月肯定大于,调整月链 */
if(temp == 0 && tempM == 0) {
setNextMonthOffset(monthOffset, newOffset);
setLastDayOffset(newOffset);
} else {
if(todayMonth < getMonth(temp)) {
setNextMonthOffset(monthOffset, newOffset);
setNextMonthOffset(newOffset, tempM);
} else if(todayDay < getDay(temp)) {
setNextMonthOffset(monthOffset, newOffset);
setNextMonthOffset(newOffset, getNextMonthOffset(tempM));
setNextDayOffset(newOffset, tempM);
setNextMonthOffset(tempM, 0);
} else {
setNextSegmentOffset(newOffset, getNextSegmentOffset(temp));
setNextSegmentOffset(temp, newOffset);
}
}
}
}
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
/**
* 找到不大于今天年份的日期项偏移
* @param beginOffset
* @return
* @throws IOException
*/
private int findYear(int beginOffset) throws IOException {
int prevOffset = 0;
while(beginOffset != 0) {
readYMD(beginOffset);
if(compareTodayWithSearchDay() <= 0)
break;
prevOffset = beginOffset;
beginOffset = getNextYearOffset(beginOffset);
}
return prevOffset;
}
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -