/*
Copyright (C) 2011 monte
This file is part of PSP NetParty.
PSP NetParty 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 3 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, see <http://www.gnu.org/licenses/>.
*/
package pspnetparty.lib.engine;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;
import org.apache.lucene.analysis.cjk.CJKAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriter.MaxFieldLength;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;
import pspnetparty.lib.FileContentCache;
import pspnetparty.lib.ILogger;
import pspnetparty.lib.Utility;
import pspnetparty.lib.constants.IServerRegistry;
import pspnetparty.lib.constants.ProtocolConstants;
import pspnetparty.lib.constants.ProtocolConstants.Search;
import pspnetparty.lib.socket.IProtocol;
import pspnetparty.lib.socket.IProtocolDriver;
import pspnetparty.lib.socket.IProtocolMessageHandler;
import pspnetparty.lib.socket.IServer;
import pspnetparty.lib.socket.IServerListener;
import pspnetparty.lib.socket.ISocketConnection;
import pspnetparty.lib.socket.TextProtocolDriver;
public class SearchEngine {
private ILogger logger;
private RAMDirectory ramDirectory;
private IndexWriter indexWriter;
private IndexSearcher indexSearcher;
private QueryParser searchParser;
private ConcurrentHashMap<String, PlayRoom> playRoomEntries;
private ConcurrentHashMap<SearchProtocolDriver, Object> searchClientConnections;
private IServerRegistry serverNetwork;
private ConcurrentSkipListMap<SearchStatusProtocolDriver, Object> portalConnections;
private SearchStatusProtocolDriver roomDataSource;
private FileContentCache loginMessageFile = new FileContentCache();
private int maxUsers = 30;
private int descriptionMaxLength = 100;
private int maxSearchResults = 50;
private boolean isAcceptingPortal = true;
private int updateCount = 0;
public SearchEngine(IServer server, ILogger logger, IServerRegistry net) throws IOException {
this.logger = logger;
serverNetwork = net;
playRoomEntries = new ConcurrentHashMap<String, PlayRoom>();
searchClientConnections = new ConcurrentHashMap<SearchProtocolDriver, Object>();
portalConnections = new ConcurrentSkipListMap<SearchEngine.SearchStatusProtocolDriver, Object>();
ramDirectory = new RAMDirectory();
indexWriter = new IndexWriter(ramDirectory, new CJKAnalyzer(Version.LUCENE_30, new HashSet<String>()), true,
MaxFieldLength.UNLIMITED);
searchParser = new QueryParser(Version.LUCENE_30, "title", indexWriter.getAnalyzer());
server.addServerListener(new IServerListener() {
@Override
public void log(String message) {
SearchEngine.this.logger.log(message);
}
@Override
public void serverStartupFinished() {
}
@Override
public void serverShutdownFinished() {
}
});
server.addProtocol(new SearchProtocol());
server.addProtocol(new SearchStatusProtocol());
}
public int getCurrentUsers() {
return searchClientConnections.size();
}
public int getMaxUsers() {
return maxUsers;
}
public void setMaxUsers(int maxUsers) {
if (maxUsers < 0)
return;
this.maxUsers = maxUsers;
notifyServerStatus();
}
public int getDescriptionMaxLength() {
return descriptionMaxLength;
}
public void setDescriptionMaxLength(int descriptionMaxLength) {
if (descriptionMaxLength < 1)
return;
this.descriptionMaxLength = descriptionMaxLength;
}
public int getMaxSearchResults() {
return maxSearchResults;
}
public void setMaxSearchResults(int maxSearchResults) {
if (maxSearchResults < 1)
return;
this.maxSearchResults = maxSearchResults;
}
public void setLoginMessageFile(String loginMessageFile) {
this.loginMessageFile.setFile(loginMessageFile);
}
public boolean isAcceptingPortal() {
return isAcceptingPortal;
}
public void setAcceptingPortal(boolean isAcceptingPortal) {
if (!isAcceptingPortal) {
for (SearchStatusProtocolDriver portal : portalConnections.keySet()) {
portal.getConnection().disconnect();
}
portalConnections.clear();
}
this.isAcceptingPortal = isAcceptingPortal;
}
public int getRoomEntryCount() {
return playRoomEntries.size();
}
public String allRoomsToString() {
StringBuilder sb = new StringBuilder();
for (Entry<String, PlayRoom> entry : playRoomEntries.entrySet()) {
PlayRoom room = entry.getValue();
sb.append(room.getRoomAddress()).append('\t');
sb.append(room.getMasterName()).append('\t');
sb.append(room.getTitle()).append('\t');
sb.append(room.getCurrentPlayers());
sb.append(" / ");
sb.append(room.getMaxPlayers());
sb.append('\n');
}
return sb.toString();
}
public String allPortalsToString() {
StringBuilder sb = new StringBuilder();
for (SearchStatusProtocolDriver portal : portalConnections.keySet()) {
InetSocketAddress address = portal.getConnection().getRemoteAddress();
sb.append(address.getAddress().getHostAddress() + ":" + address.getPort());
if (portal == roomDataSource) {
sb.append(" *");
}
sb.append('\n');
}
return sb.toString();
}
private Field docFieldAddress = new Field("address", "", Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS);
private Field docFieldTitle = new Field("title", "", Field.Store.NO, Field.Index.ANALYZED);
private Field docFieldMasterName = new Field("masterName", "", Field.Store.NO, Field.Index.ANALYZED);
private Field docFieldServerAddress = new Field("serverAddress", "", Field.Store.NO, Field.Index.ANALYZED);
private Field docFieldHasPassword = new Field("hasPassword", "", Field.Store.NO, Field.Index.NOT_ANALYZED);
private Field docFieldIsVacant = new Field("isVacant", "", Field.Store.NO, Field.Index.NOT_ANALYZED);
private Field docFieldSource = new Field("source", "", Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS);
private void updateRoomEntry(PlayRoom room, String source) throws IOException {
Document doc = new Document();
docFieldAddress.setValue(room.getRoomAddress());
docFieldTitle.setValue(room.getTitle().replace(" ", " "));
docFieldMasterName.setValue(room.getMasterName().replace(" ", " "));
docFieldServerAddress.setValue(room.getServerAddress());
docFieldHasPassword.setValue(room.hasPassword() ? "y" : "n");
docFieldIsVacant.setValue(room.getCurrentPlayers() < room.getMaxPlayers() ? "y" : "n");
docFieldSource.setValue(source);
doc.add(docFieldAddress);
doc.add(docFieldTitle);
doc.add(docFieldMasterName);
doc.add(docFieldServerAddress);
doc.add(docFieldHasPassword);
doc.add(docFieldIsVacant);
doc.add(docFieldSource);
indexWriter.updateDocument(new Term("address", room.getRoomAddress()), doc);
indexWriter.commit();
updateCount++;
if (updateCount > 10) {
indexWriter.optimize();
updateCount = 0;
}
indexSearcher = null;
}
private void notifyServerStatus() {
StringBuilder sb = new StringBuilder();
sb.append(ProtocolConstants.SERVER_STATUS);
sb.append(TextProtocolDriver.ARGUMENT_SEPARATOR);
sb.append(searchClientConnections.size());
sb.append(TextProtocolDriver.ARGUMENT_SEPARATOR);
sb.append(maxUsers);
ByteBuffer buffer = Utility.encode(sb);
for (SearchProtocolDriver client : searchClientConnections.keySet()) {
buffer.position(0);
client.getConnection().send(buffer);
}
for (SearchStatusProtocolDriver portal : portalConnections.keySet()) {
buffer.position(0);
portal.getConnection().send(buffer);
}
}
public void notifyAllClients(String message) {
ByteBuffer buffer = Utility.encode(ProtocolConstants.Room.NOTIFY_FROM_ADMIN + TextProtocolDriver.ARGUMENT_SEPARATOR + message);
for (SearchProtocolDriver client : searchClientConnections.keySet()) {
buffer.position(0);
client.getConnection().send(buffer);
}
}
private void appendLoginMessage(StringBuilder sb) {
String loginMessage = loginMessageFile.getContent();
if (!Utility.isEmpty(loginMessage)) {
sb.append(TextProtocolDriver.MESSAGE_SEPARATOR);
sb.append(ProtocolConstants.Room.NOTIFY_FROM_ADMIN);
sb.append(TextProtocolDriver.ARGUMENT_SEPARATOR);
sb.append(loginMessage);
}
}
private class SearchStatusProtocol implements IProtocol {
@Override
public String getProtocol() {
return ProtocolConstants.PROTOCOL_SEARCH_STATUS;
}
@Override
public IProtocolDriver createDriver(ISocketConnection connection) {
if (!isAcceptingPortal)
return null;
serverNetwork.reload();
if (!serverNetwork.isValidPortalServer(connection.getRemoteAddress().getAddress()))
return null;
SearchStatusProtocolDriver driver = new SearchStatusProtocolDriver(connection);
logger.log("ポータルから接続されました: " + driver.address);
if (roomDataSource == null) {
connection.send(Utility.encode(ProtocolConstants.SearchStatus.COMMAND_ASK_ROOM_DATA));
roomDataSource = driver;
logger.log("部屋情報ソースに設定: " + driver.address);
}
portalConnections.put(driver, this);
notifyServerStatus();
return driver;
}
@Override
public void log(String message) {
logger.log(message);
}
}
private class SearchStatusProtocolDriver extends TextProtocolDriver implements Comparable<SearchStatusProtocolDriver> {
private String address;
private final long connectedTime;
private SearchStatusProtocolDriver(ISocketConnection connection) {
super(connection, portalHandlers);
address = Utility.socketAddressToStringByIP(connection.getRemoteAddress());
connectedTime = System.currentTimeMillis();
}
@Override
public void log(String message) {
logger.log(message);
}
@Override
public void connectionDisconnected() {
portalConnections.remove(this);
logger.log("ポータルから切断されました: " + address);
if (this == roomDataSource) {
playRoomEntries.clear();
try {
indexWriter.deleteAll();
indexWriter.commit();
indexWriter.optimize();
updateCount = 0;
indexSearcher = null;
} catch (IOException e) {
logger.log(Utility.stackTraceToString(e));
}
if (portalConnections.isEmpty() || !isAcceptingPortal) {
roomDataSource = null;
logger.log("部屋情報ソース: なし");
} else {
roomDataSource = portalConnections.firstKey();
roomDataSource.getConnection().send(Utility.encode(ProtocolConstants.SearchStatus.COMMAND_ASK_ROOM_DATA));
String remoteAddress = Utility.socketAddressToStringByIP(roomDataSource.getConnection().getRemoteAddress());
logger.log("部屋情報ソースに設定: " + remoteAddress);
}
}
}
@Override
public int compareTo(SearchStatusProtocolDriver p) {
int diff = (int) (connectedTime - p.connectedTime);
if (diff == 0)
return address.compareTo(p.address);
return diff;
}
@Override
public void errorProtocolNumber(String number) {
}
}
private final HashMap<String, IProtocolMessageHandler> portalHandlers = new HashMap<String, IProtocolMessageHandler>();
{
portalHandlers.put(ProtocolConstants.SearchStatus.NOTIFY_ROOM_SERVER_REMOVED, new IProtocolMessageHandler() {
@Override
public boolean process(IProtocolDriver driver, String source) {
try {
Term term = new Term("source", source);
Query query = new TermQuery(term);
IndexSearcher localIndexSearcher = indexSearcher;
if (localIndexSearcher == null)
localIndexSearcher = indexSearcher = new IndexSearcher(ramDirectory);
TopDocs docs = localIndexSearcher.search(query, Integer.MAX_VALUE);
ScoreDoc[] hits = docs.scoreDocs;
for (int i = 0; i < hits.length; i++) {
Document d = localIndexSearcher.doc(hits[i].doc);
String roomAddress = d.get("address");
playRoomEntries.remove(roomAddress);
}
indexWriter.deleteDocuments(term);
indexWriter.commit();
indexSearcher = null;
} catch (CorruptIndexException e) {
} catch (IOException e) {
}
return true;
}
});
portalHandlers.put(ProtocolConstants.SearchStatus.NOTIFY_ROOM_CREATED, new IProtocolMessageHandler() {
@Override
public boolean process(IProtocolDriver driver, String argument) {
// R hostname:port hostname:port masterName title currentPlayers
// maxPlayers hasPassword createdTime description
final String[] tokens = argument.split(TextProtocolDriver.ARGUMENT_SEPARATOR, -1);
if (tokens.length != 9)
return true;
try {
String source = tokens[0];
String server = tokens[1];
if (Utility.isEmpty(server))
server = source;
String masterName = tokens[2];
String title = tokens[3];
int currentPlayers = Integer.parseInt(tokens[4]);
int maxPlayers = Integer.parseInt(tokens[5]);
boolean hasPassword = "Y".equals(tokens[6]);
long created = Long.parseLong(tokens[7]);
String description = Utility.trim(tokens[8], descriptionMaxLength);
PlayRoom room = new PlayRoom(source, server, masterName, title, hasPassword, currentPlayers, maxPlayers, created);
room.setDescription(description);
updateRoomEntry(room, source);
playRoomEntries.put(room.getRoomAddress(), room);
} catch (NumberFormatException e) {
} catch (IOException e) {
}
return true;
}
});
portalHandlers.put(ProtocolConstants.SearchStatus.NOTIFY_ROOM_UPDATED, new IProtocolMessageHandler() {
@Override
public boolean process(IProtocolDriver driver, String argument) {
// U hostname:port:master title maxPlayers hasPassword
// description
String[] tokens = argument.split(TextProtocolDriver.ARGUMENT_SEPARATOR, -1);
if (tokens.length != 5)
return true;
try {
String address = tokens[0];
String title = tokens[1];
int maxPlayers = Integer.parseInt(tokens[2]);
boolean hasPassword = "Y".equals(tokens[3]);
String description = Utility.trim(tokens[4], descriptionMaxLength);
PlayRoom room = playRoomEntries.get(address);
if (room == null)
return true;
room.setTitle(title);
room.setMaxPlayers(maxPlayers);
room.setHasPassword(hasPassword);
room.setDescription(description);
updateRoomEntry(room, room.getSourceServer());
} catch (NumberFormatException e) {
} catch (IOException e) {
logger.log(Utility.stackTraceToString(e));
}
return true;
}
});
portalHandlers.put(ProtocolConstants.SearchStatus.NOTIFY_ROOM_DELETED, new IProtocolMessageHandler() {
@Override
public boolean process(IProtocolDriver driver, String argument) {
// NRD hostname:port:master
String address = argument;
PlayRoom room = playRoomEntries.remove(address);
if (room == null)
return true;
try {
indexWriter.deleteDocuments(new Term("address", address));
indexWriter.commit();
indexSearcher = null;
} catch (IOException e) {
logger.log(Utility.stackTraceToString(e));
}
return true;
}
});
portalHandlers.put(ProtocolConstants.SearchStatus.NOTIFY_ROOM_PLAYER_COUNT_CHANGED, new IProtocolMessageHandler() {
@Override
public boolean process(IProtocolDriver driver, String argument) {
// NRPC hostname:port:master playerCount
String[] tokens = argument.split(TextProtocolDriver.ARGUMENT_SEPARATOR, -1);
if (tokens.length != 2)
return true;
String address = tokens[0];
PlayRoom room = playRoomEntries.get(address);
if (room == null)
return true;
try {
int playerCount = Integer.parseInt(tokens[1]);
room.setCurrentPlayers(playerCount);
updateRoomEntry(room, room.getSourceServer());
} catch (NumberFormatException e) {
} catch (IOException e) {
}
return true;
}
});
}
private class SearchProtocol implements IProtocol {
@Override
public String getProtocol() {
return ProtocolConstants.PROTOCOL_SEARCH;
}
@Override
public IProtocolDriver createDriver(ISocketConnection connection) {
SearchProtocolDriver driver = new SearchProtocolDriver(connection);
return driver;
}
@Override
public void log(String message) {
logger.log(message);
}
}
private class SearchProtocolDriver extends TextProtocolDriver {
private SearchProtocolDriver(ISocketConnection connection) {
super(connection, loginHandlers);
}
@Override
public void log(String message) {
logger.log(message);
}
@Override
public void connectionDisconnected() {
searchClientConnections.remove(this);
notifyServerStatus();
}
@Override
public void errorProtocolNumber(String number) {
}
}
private final HashMap<String, IProtocolMessageHandler> loginHandlers = new HashMap<String, IProtocolMessageHandler>();
private final HashMap<String, IProtocolMessageHandler> searchHandlers = new HashMap<String, IProtocolMessageHandler>();
{
loginHandlers.put(Search.COMMAND_LOGIN, new IProtocolMessageHandler() {
@Override
public boolean process(IProtocolDriver driver, String argument) {
SearchProtocolDriver client = (SearchProtocolDriver) driver;
if (searchClientConnections.size() >= maxUsers) {
client.getConnection().send(Utility.encode(ProtocolConstants.Search.ERROR_LOGIN_BEYOND_CAPACITY));
return false;
}
StringBuilder sb = new StringBuilder();
sb.append(ProtocolConstants.Search.COMMAND_LOGIN);
appendLoginMessage(sb);
client.getConnection().send(Utility.encode(sb));
searchClientConnections.put(client, this);
notifyServerStatus();
client.setMessageHandlers(searchHandlers);
return true;
}
});
searchHandlers.put(Search.COMMAND_SEARCH, new IProtocolMessageHandler() {
@Override
public boolean process(IProtocolDriver driver, String argument) {
SearchProtocolDriver client = (SearchProtocolDriver) driver;
// S server ngServer roomMaster ngRoomMaster title ngTitle
// hasPassword onlyVacant
String[] tokens = argument.split(TextProtocolDriver.ARGUMENT_SEPARATOR, -1);
if (tokens.length != 8)
return false;
String roomServer = tokens[0].trim();
String ngRoomServer = tokens[1].trim();
String roomMaster = tokens[2].replace(" ", " ").replaceAll(" {2,}", " ").trim();
String ngRoomMaster = tokens[3].replace(" ", " ").replaceAll(" {2,}", " ").trim();
String title = tokens[4].replace(" ", " ").replaceAll(" {2,}", " ").trim();
String ngTitle = tokens[5].replace(" ", " ").replaceAll(" {2,}", " ").trim();
boolean hasPassword = "Y".equals(tokens[6]);
boolean onlyVacant = "Y".equals(tokens[7]);
StringBuilder queryBuilder = new StringBuilder();
queryBuilder.append("hasPassword:").append(hasPassword ? 'y' : 'n');
if (!Utility.isEmpty(roomServer)) {
queryBuilder.append(" AND ");
queryBuilder.append("serverAddress:").append(QueryParser.escape(roomServer));// .append('*');
}
if (!Utility.isEmpty(ngRoomServer)) {
queryBuilder.append(" AND ");
queryBuilder.append("-serverAddress:").append(QueryParser.escape(ngRoomServer));// .append('*');
}
if (!Utility.isEmpty(title)) {
appendQuery(queryBuilder, "title", title);
}
if (!Utility.isEmpty(ngTitle)) {
appendQuery(queryBuilder, "-title", ngTitle);
}
if (!Utility.isEmpty(roomMaster)) {
appendQuery(queryBuilder, "masterName", roomMaster);
}
if (!Utility.isEmpty(ngRoomMaster)) {
appendQuery(queryBuilder, "-masterName", ngRoomMaster);
}
if (onlyVacant) {
queryBuilder.append(" AND isVacant:y");
}
try {
Query query = searchParser.parse(queryBuilder.toString());
// System.out.println(queryBuilder);
// System.out.println(query);
IndexSearcher localIndexSearcher = indexSearcher;
if (localIndexSearcher == null)
indexSearcher = localIndexSearcher = new IndexSearcher(ramDirectory);
TopDocs docs = localIndexSearcher.search(query, maxSearchResults);
ScoreDoc[] hits = docs.scoreDocs;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hits.length; i++) {
Document d = localIndexSearcher.doc(hits[i].doc);
String address = d.get("address");
PlayRoom room = playRoomEntries.get(address);
if (room == null) {
logger.log("Defunct document: address = " + address);
continue;
}
sb.append(ProtocolConstants.Search.COMMAND_SEARCH);
sb.append(TextProtocolDriver.ARGUMENT_SEPARATOR);
sb.append(room.getServerAddress());
sb.append(TextProtocolDriver.ARGUMENT_SEPARATOR);
sb.append(room.getMasterName());
sb.append(TextProtocolDriver.ARGUMENT_SEPARATOR);
sb.append(room.getTitle());
sb.append(TextProtocolDriver.ARGUMENT_SEPARATOR);
sb.append(room.getCurrentPlayers());
sb.append(TextProtocolDriver.ARGUMENT_SEPARATOR);
sb.append(room.getMaxPlayers());
sb.append(TextProtocolDriver.ARGUMENT_SEPARATOR);
sb.append(room.hasPassword() ? "Y" : "N");
sb.append(TextProtocolDriver.ARGUMENT_SEPARATOR);
sb.append(room.getCreatedTime());
sb.append(TextProtocolDriver.ARGUMENT_SEPARATOR);
sb.append(room.getDescription());
sb.append(TextProtocolDriver.MESSAGE_SEPARATOR);
}
sb.append(ProtocolConstants.Search.COMMAND_SEARCH);
client.getConnection().send(Utility.encode(sb));
} catch (IOException e) {
logger.log(Utility.stackTraceToString(e));
} catch (Exception e) {
logger.log(Utility.stackTraceToString(e));
}
return true;
}
});
}
private static void appendQuery(StringBuilder sb, String field, String query) {
String[] tokens = query.split(" ");
for (String s : tokens) {
s = s.replaceAll("^\\*+", "");
if (Utility.isEmpty(s))
continue;
if (sb.length() > 0)
sb.append(" AND ");
sb.append(field).append(':');
sb.append(QueryParser.escape(s));
if (s.matches("[\\x20-\\x7E]+"))
sb.append('*');
}
}
}