⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 lumaqqmessagemanager.java

📁 类似于MSN
💻 JAVA
📖 第 1 页 / 共 2 页
字号:
/*
* 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);
			}
		} 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 + -