From fa7b3db0bc1fc480fbbad69c87a4f8ce91196aec Mon Sep 17 00:00:00 2001 From: YiFang Xiao Date: Wed, 6 Dec 2023 15:17:37 +0800 Subject: [PATCH] opt: use qtcpsocket signal to implement the dictserver protocol --- src/dict/dictionary.hh | 4 + src/dict/dictserver.cc | 1055 +++++++++++++++++++++------------------- 2 files changed, 564 insertions(+), 495 deletions(-) diff --git a/src/dict/dictionary.hh b/src/dict/dictionary.hh index 010429ffa..2fed491be 100644 --- a/src/dict/dictionary.hh +++ b/src/dict/dictionary.hh @@ -226,6 +226,8 @@ public: hasAnyData( false ) { } +signals: + void finishedArticle( QString articleText ); protected: bool hasAnyData; // With this being false, dataSize() always returns -1 @@ -235,6 +237,8 @@ protected: /// A helper class for synchronous word search implementations. class WordSearchRequestInstant: public WordSearchRequest { + Q_OBJECT + public: WordSearchRequestInstant() diff --git a/src/dict/dictserver.cc b/src/dict/dictserver.cc index e7d7ae555..420f06337 100644 --- a/src/dict/dictserver.cc +++ b/src/dict/dictserver.cc @@ -3,11 +3,11 @@ #include "dictserver.hh" #include "wstring_qt.hh" +#include #include #include #include #include -#include "gddebug.hh" #include "htmlescape.hh" #include @@ -23,156 +23,170 @@ enum { namespace { -#define MAX_MATCHES_COUNT 60 -bool readLine( QTcpSocket & socket, QString & line, QString & errorString, QAtomicInt & isCancelled ) -{ - line.clear(); - errorString.clear(); - if ( socket.state() != QTcpSocket::ConnectedState ) - return false; - - for ( ;; ) { - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - return false; +enum class DictServerState { + START, + CONNECT, + DB, + DB_DATA, + DB_DATA_FINISHED, + CLIENT, + AUTH, + OPTION, + MATCH, + MATCH_DATA, + FINISHED, + DEFINE, + DEFINE_DATA, +}; - if ( socket.canReadLine() ) { - QByteArray reply = socket.readLine(); - line = QString::fromUtf8( reply.data(), reply.size() ); - return true; - } +#define MAX_MATCHES_COUNT 60 - if ( !socket.waitForReadyRead( 2000 ) ) { - errorString = - "Data reading error: socket error " + QString::number( socket.error() ) + ": \"" + socket.errorString() + "\""; - break; - } - } - return false; -} -bool connectToServer( QTcpSocket & socket, QString const & url, QString & errorString, QAtomicInt & isCancelled ) +void disconnectFromServer( QTcpSocket & socket ) { - QUrl serverUrl( url ); - quint16 port = serverUrl.port( DefaultPort ); - QString reply; - - for ( ;; ) { - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - return false; + if ( socket.state() == QTcpSocket::ConnectedState ) + socket.write( QByteArray( "QUIT\r\n" ) ); - socket.connectToHost( serverUrl.host(), port ); - if ( socket.state() != QTcpSocket::ConnectedState ) { - if ( !socket.waitForConnected( 2000 ) ) - break; - } + socket.disconnectFromHost(); +} - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - return false; - if ( !readLine( socket, reply, errorString, isCancelled ) ) - break; +class DictServerImpl: public QObject +{ + Q_OBJECT - if ( !reply.isEmpty() && reply.left( 3 ) != "220" ) { - errorString = "Server refuse connection: " + reply; - return false; - } + QString url; + QString errorString; - QString msgId = reply.mid( reply.lastIndexOf( " " ) ).trimmed(); - socket.write( QByteArray( "CLIENT GoldenDict\r\n" ) ); - if ( !socket.waitForBytesWritten( 1000 ) ) - break; + QString msgId; + QString client; - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - return false; +public: + QTcpSocket socket; + DictServerState state; + QMutex mutex; - if ( !readLine( socket, reply, errorString, isCancelled ) ) - break; - if ( !serverUrl.userInfo().isEmpty() ) { - QString authCommand = QString( "AUTH " ); - QString authString = msgId; + DictServerImpl( QObject * parent, QString const & url_, QString client_ ): + QObject( parent ), + url( url_ ), + client( client_ ) + { + } - int pos = serverUrl.userInfo().indexOf( QRegularExpression( "[:;]" ) ); - if ( pos > 0 ) { - authCommand += serverUrl.userInfo().left( pos ); - authString += serverUrl.userInfo().mid( pos + 1 ); - } - else - authCommand += serverUrl.userInfo(); + void run( std::function< void() > callback ) + { + int pos = url.indexOf( "://" ); + if ( pos < 0 ) + url = "dict://" + url; - authCommand += " "; - authCommand += QCryptographicHash::hash( authString.toUtf8(), QCryptographicHash::Md5 ).toHex(); - authCommand += "\r\n"; + QUrl serverUrl( url ); + quint16 port = serverUrl.port( DefaultPort ); + QString reply; + socket.connectToHost( serverUrl.host(), port ); + state = DictServerState::CONNECT; + connect( &socket, &QTcpSocket::connected, this, [ this ]() { + } ); - socket.write( authCommand.toUtf8() ); + connect( &socket, &QTcpSocket::errorOccurred, this, []( QAbstractSocket::SocketError error ) { + qDebug() << "socket error message: " << error; + } ); + connect( &socket, &QTcpSocket::readyRead, this, [ this, callback ]() { + QMutexLocker const _( &mutex ); - if ( !socket.waitForBytesWritten( 1000 ) ) - break; + if ( state == DictServerState::CONNECT ) { + QByteArray reply = socket.readLine(); + qDebug() << "received:" << reply; - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - return false; + if ( !reply.isEmpty() && reply.left( 3 ) != "220" ) { + errorString = "Server refuse connection: " + reply; + return; + } - if ( !readLine( socket, reply, errorString, isCancelled ) ) - break; + msgId = reply.mid( reply.lastIndexOf( " " ) ).trimmed(); - if ( reply.left( 3 ) != "230" ) { - errorString = "Authentication error: " + reply; - return false; + state = DictServerState::CLIENT; + socket.write( QString( "CLIENT %1\r\n" ).arg( client ).toUtf8() ); } - } - socket.write( QByteArray( "OPTION MIME\r\n" ) ); + else if ( state == DictServerState::CLIENT ) { + QByteArray reply = socket.readLine(); + qDebug() << "received:" << reply; - if ( !socket.waitForBytesWritten( 1000 ) ) - break; + QUrl serverUrl( url ); + if ( !serverUrl.userInfo().isEmpty() ) { + QString authCommand = QString( "AUTH " ); + QString authString = msgId; - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - return false; + int pos = serverUrl.userInfo().indexOf( QRegularExpression( "[:;]" ) ); + if ( pos > 0 ) { + authCommand += serverUrl.userInfo().left( pos ); + authString += serverUrl.userInfo().mid( pos + 1 ); + } + else + authCommand += serverUrl.userInfo(); - if ( !readLine( socket, reply, errorString, isCancelled ) ) - break; + authCommand += " "; + authCommand += QCryptographicHash::hash( authString.toUtf8(), QCryptographicHash::Md5 ).toHex(); + authCommand += "\r\n"; - if ( reply.left( 3 ) != "250" ) { - // RFC 2229, 3.10.1.1: - // OPTION MIME is a REQUIRED server capability, - // all DICT servers MUST implement this command. - errorString = "Server doesn't support mime capability: " + reply; - return false; - } + state = DictServerState::AUTH; + socket.write( authCommand.toUtf8() ); + } + else { + const QByteArray & data = QByteArray( "OPTION MIME\r\n" ); + socket.write( data ); + qDebug() << "write:" << data; + state = DictServerState::OPTION; + } + } + else if ( ( state == DictServerState::OPTION ) || ( state == DictServerState::AUTH ) ) { + QByteArray reply = socket.readLine(); + qDebug() << "received option:" << reply; + + if ( reply.left( 3 ) != "250" ) { + // RFC 2229, 3.10.1.1: + // OPTION MIME is a REQUIRED server capability, + // all DICT servers MUST implement this command. + errorString = "Server doesn't support mime capability: " + reply; + return; + } - return true; - } + state = DictServerState::FINISHED; - if ( !Utils::AtomicInt::loadAcquire( isCancelled ) ) - errorString = QString( "Server connection fault, socket error %1: \"%2\"" ) - .arg( QString::number( socket.error() ) ) - .arg( socket.errorString() ); - return false; -} + if ( callback ) { + callback(); + } + } + } ); + } -void disconnectFromServer( QTcpSocket & socket ) -{ - if ( socket.state() == QTcpSocket::ConnectedState ) - socket.write( QByteArray( "QUIT\r\n" ) ); + ~DictServerImpl() override + { + disconnectFromServer( socket ); + } +}; - socket.disconnectFromHost(); -} class DictServerDictionary: public Dictionary::Class { + Q_OBJECT + string name; QString url, icon; quint32 langId; QString errorString; - QTcpSocket socket; QStringList databases; QStringList strategies; QStringList serverDatabases; + DictServerState state; + QMutex mutex; + QTcpSocket socket; + QString msgId; public: - DictServerDictionary( string const & id, string const & name_, QString const & url_, @@ -202,14 +216,77 @@ class DictServerDictionary: public Dictionary::Class socket.connectToHost( serverUrl.host(), port ); connect( &socket, &QTcpSocket::connected, this, [ this ]() { //initialize the description. - getServerDatabasesAfterConnect(); + QString req = QString( "SHOW DB\r\n" ); + socket.write( req.toUtf8() ); + state = DictServerState::DB; } ); - connect( &socket, &QTcpSocket::stateChanged, this, []( QAbstractSocket::SocketState state ) { - qDebug() << "socket state change: " << state; + + connect( this, &DictServerDictionary::finishDatabase, this, [ this ]() { + socket.write( QByteArray( "CLIENT GoldenDict\r\n" ) ); } ); + connect( &socket, &QTcpSocket::errorOccurred, this, []( QAbstractSocket::SocketError error ) { qDebug() << "socket error message: " << error; } ); + connect( &socket, &QTcpSocket::readyRead, this, [ this ]() { + QMutexLocker const _( &mutex ); + QByteArray reply = socket.readLine(); + qDebug() << "received:" << reply; + if ( state == DictServerState::DB ) { + + if ( reply.left( 3 ) == "110" ) { + state = DictServerState::DB_DATA; + int countPos = reply.indexOf( ' ', 4 ); + // Get databases count + int count = reply.mid( 4, countPos > 4 ? countPos - 4 : -1 ).toInt(); + + // Read databases + int x = 0; + for ( ; x < count; x++ ) { + reply = socket.readLine(); + + if ( reply.isEmpty() ) { + return; + } + reply = reply.trimmed(); + + qDebug() << "receive db:" << reply; + + if ( reply[ 0 ] == '.' ) { + state = DictServerState::DB_DATA_FINISHED; + emit finishDatabase(); + return; + } + + if ( !reply.isEmpty() ) + serverDatabases.append( reply ); + } + + qDebug() << "db count:" << x; + if ( x == count ) { + emit finishDatabase(); + } + } + } + else if ( state == DictServerState::DB_DATA ) { + while ( !reply.isEmpty() ) { + + qDebug() << "receive db:" << reply; + if ( reply[ 0 ] == '.' ) { + state = DictServerState::DB_DATA_FINISHED; + emit finishDatabase(); + return; + } + + reply = reply.trimmed(); + + if ( !reply.isEmpty() ) + serverDatabases.append( reply ); + + reply = socket.readLine(); + } + } + } ); } ~DictServerDictionary() override @@ -259,7 +336,10 @@ class DictServerDictionary: public Dictionary::Class friend class DictServerWordSearchRequest; friend class DictServerArticleRequest; - void getServerDatabasesAfterConnect(); + +private: +signals: + void finishDatabase(); }; void DictServerDictionary::loadIcon() noexcept @@ -280,93 +360,103 @@ void DictServerDictionary::loadIcon() noexcept QString const & DictServerDictionary::getDescription() { if ( dictionaryDescription.isEmpty() ) { - dictionaryDescription = QCoreApplication::translate( "DictServer", "Url: " ) + url + "\n"; - dictionaryDescription += QCoreApplication::translate( "DictServer", "Databases: " ) + databases.join( ", " ) + "\n"; + dictionaryDescription = QCoreApplication::translate( "DictServer", "Url: " ) + url + "
"; + dictionaryDescription += QCoreApplication::translate( "DictServer", "Databases: " ) + "
"; + for ( const auto & serverDatabase : databases ) + dictionaryDescription += serverDatabase + "
"; dictionaryDescription += QCoreApplication::translate( "DictServer", "Search strategies: " ) + strategies.join( ", " ); if ( !serverDatabases.isEmpty() ) { - dictionaryDescription += "\n\n"; + dictionaryDescription += "

"; dictionaryDescription += QCoreApplication::translate( "DictServer", "Server databases" ) + " (" - + QString::number( serverDatabases.size() ) + "):"; + + QString::number( serverDatabases.size() ) + "):" + "
"; for ( const auto & serverDatabase : serverDatabases ) - dictionaryDescription += "\n" + serverDatabase; + dictionaryDescription += serverDatabase + "
"; } } return dictionaryDescription; } -void DictServerDictionary::getServerDatabasesAfterConnect() + +class DictServerWordSearchRequest: public Dictionary::WordSearchRequest { + Q_OBJECT QAtomicInt isCancelled; + wstring word; + QString errorString; + DictServerDictionary & dict; - for ( ;; ) { - QString req = QString( "SHOW DB\r\n" ); - socket.write( req.toUtf8() ); - socket.waitForBytesWritten( 1000 ); + QStringList matchesList; + int currentStrategy = 0; + int currentDatabase = 0; - QString reply; + DictServerState state = DictServerState::START; + QString msgId; - if ( !readLine( socket, reply, errorString, isCancelled ) ) - return; + DictServerImpl * dictImpl; - if ( reply.left( 3 ) == "110" ) { - int countPos = reply.indexOf( ' ', 4 ); - // Get databases count - int count = reply.mid( 4, countPos > 4 ? countPos - 4 : -1 ).toInt(); +public: - // Read databases - for ( int x = 0; x < count; x++ ) { - if ( !readLine( socket, reply, errorString, isCancelled ) ) - break; + DictServerWordSearchRequest( wstring word_, DictServerDictionary & dict_ ): + word( std::move( word_ ) ), + dict( dict_ ), + dictImpl( new DictServerImpl( this, dict_.url, "GoldenDict-w" ) ) + { - if ( reply[ 0 ] == '.' ) - break; + connect( this, &DictServerWordSearchRequest::finishedMatches, this, [ this ]() { + state = DictServerState::FINISHED; - while ( reply.endsWith( '\r' ) || reply.endsWith( '\n' ) ) - reply.chop( 1 ); + matchesList.removeDuplicates(); + int countn = qMin( matchesList.size(), MAX_MATCHES_COUNT ); - if ( !reply.isEmpty() ) - serverDatabases.append( reply ); + if ( countn ) { + QMutexLocker _( &dataMutex ); + for ( int x = 0; x < countn; x++ ) + matches.emplace_back( gd::toWString( matchesList.at( x ) ) ); } + finish(); + } ); - break; - } - else { - gdWarning( "Retrieving databases from \"%s\" fault: %s\n", getName().c_str(), reply.toUtf8().data() ); - break; - } - } -} + this->run(); -class DictServerWordSearchRequest: public Dictionary::WordSearchRequest -{ - QAtomicInt isCancelled; - wstring word; - QString errorString; - QFuture< void > f; - DictServerDictionary & dict; - QTcpSocket * socket; + dictImpl->run( [ this ]() { + state = dictImpl->state; -public: + matchNext(); + } ); + } - DictServerWordSearchRequest( wstring const & word_, DictServerDictionary & dict_ ): - word( word_ ), - dict( dict_ ), - socket( 0 ) + void matchNext() { - f = QtConcurrent::run( [ this ]() { - this->run(); - } ); + if ( currentDatabase >= dict.databases.size() ) { + currentStrategy++; + currentDatabase = 0; + } + if ( currentStrategy >= dict.strategies.size() ) { + emit finishedMatches(); + return; + } + state = DictServerState::MATCH; + + QString matchReq = QString( "MATCH " ) + dict.databases.at( currentDatabase ) + " " + + dict.strategies.at( currentStrategy ) + " \"" + QString::fromStdU32String( word ) + "\"\r\n"; + dictImpl->socket.write( matchReq.toUtf8() ); + currentDatabase++; } void run(); - ~DictServerWordSearchRequest() override - { - f.waitForFinished(); - } + ~DictServerWordSearchRequest() override = default; void cancel() override; + + void addMatchedWord( const QString & ); + +private: + void readMatchData( QByteArray & reply ); + +signals: + void finishedMatches(); }; void DictServerWordSearchRequest::run() @@ -376,130 +466,73 @@ void DictServerWordSearchRequest::run() return; } - socket = new QTcpSocket; + connect( &dictImpl->socket, &QTcpSocket::readyRead, this, [ this ]() { + QMutexLocker const _( &dictImpl->mutex ); + if ( state == DictServerState::MATCH ) { + QByteArray reply = dictImpl->socket.readLine(); + qDebug() << "receive match:" << reply; + auto code = reply.left( 3 ); - if ( !socket ) { - finish(); - return; - } + if ( reply.left( 3 ) != "152" ) { - if ( connectToServer( *socket, dict.url, errorString, isCancelled ) ) { - QStringList matchesList; - - for ( int ns = 0; ns < dict.strategies.size(); ns++ ) { - for ( int i = 0; i < dict.databases.size(); i++ ) { - QString matchReq = QString( "MATCH " ) + dict.databases.at( i ) + " " + dict.strategies.at( ns ) + " \"" - + QString::fromStdU32String( word ) + "\"\r\n"; - socket->write( matchReq.toUtf8() ); - socket->waitForBytesWritten( 1000 ); - - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - break; - - QString reply; - - if ( !readLine( *socket, reply, errorString, isCancelled ) ) - break; - - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - break; - - if ( reply.left( 3 ) == "250" ) { - // "OK" reply - matches info will be later - if ( !readLine( *socket, reply, errorString, isCancelled ) ) - break; - - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - break; - } - - if ( reply.left( 3 ) == "552" ) { - // No matches - continue; - } - - if ( reply[ 0 ] == '5' || reply[ 0 ] == '4' ) { - // Database error - gdWarning( "Find matches in \"%s\", database \"%s\", strategy \"%s\" fault: %s\n", - dict.getName().c_str(), - dict.databases.at( i ).toUtf8().data(), - dict.strategies.at( ns ).toUtf8().data(), - reply.toUtf8().data() ); - continue; - } - - if ( reply.left( 3 ) == "152" ) { - // Matches found - int countPos = reply.indexOf( ' ', 4 ); - - // Get matches count - int count = reply.mid( 4, countPos > 4 ? countPos - 4 : -1 ).toInt(); - - // Read matches - for ( int x = 0; x <= count; x++ ) { - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - break; + matchNext(); + } + else { + state = DictServerState::MATCH_DATA; - if ( !readLine( *socket, reply, errorString, isCancelled ) ) - break; + readMatchData( reply ); + } + } + else if ( state == DictServerState::MATCH_DATA ) { + QByteArray reply = dictImpl->socket.readLine(); + qDebug() << "receive match data:" << reply; + readMatchData( reply ); + } + } ); - if ( reply[ 0 ] == '.' ) - break; + connect( &dictImpl->socket, &QTcpSocket::errorOccurred, this, [ this ]( QAbstractSocket::SocketError error ) { + qDebug() << "socket error message: " << error; + cancel(); + } ); +} +void DictServerWordSearchRequest::readMatchData( QByteArray & reply ) +{ + do { - while ( reply.endsWith( '\r' ) || reply.endsWith( '\n' ) ) - reply.chop( 1 ); + if ( reply.isEmpty() ) { + return; + } - int pos = reply.indexOf( ' ' ); - if ( pos >= 0 ) { - QString word = reply.mid( pos + 1 ); - if ( word.endsWith( '\"' ) ) - word.chop( 1 ); - if ( word[ 0 ] == '\"' ) - word = word.mid( 1 ); + reply = reply.trimmed(); - matchesList.append( word ); - } - } - if ( Utils::AtomicInt::loadAcquire( isCancelled ) || !errorString.isEmpty() ) - break; - } - } + if ( reply == "." ) { + //discard left response. such as + // 250 ok [d/m/c = 0/3/20; 0.000r 0.000u 0.000s] + while ( !dictImpl->socket.readLine().isEmpty() ) {} + matchNext(); + return; + } - if ( Utils::AtomicInt::loadAcquire( isCancelled ) || !errorString.isEmpty() ) - break; + if ( reply.left( 3 ) == "152" ) { + reply = this->dictImpl->socket.readLine(); - matchesList.removeDuplicates(); - if ( matchesList.size() >= MAX_MATCHES_COUNT ) - break; + continue; } - if ( !Utils::AtomicInt::loadAcquire( isCancelled ) && errorString.isEmpty() ) { - matchesList.removeDuplicates(); - - int count = matchesList.size(); - if ( count > MAX_MATCHES_COUNT ) - count = MAX_MATCHES_COUNT; + int pos = reply.indexOf( ' ' ); + if ( pos >= 0 ) { + QString word = reply.mid( pos + 1 ); + if ( word.endsWith( '\"' ) ) + word.chop( 1 ); + if ( word[ 0 ] == '\"' ) + word = word.mid( 1 ); - if ( count ) { - QMutexLocker _( &dataMutex ); - for ( int x = 0; x < count; x++ ) - matches.emplace_back( gd::toWString( matchesList.at( x ) ) ); - } + this->addMatchedWord( word ); } - } - if ( !errorString.isEmpty() ) - gdWarning( "Prefix find in \"%s\" fault: %s\n", dict.getName().c_str(), errorString.toUtf8().data() ); + reply = this->dictImpl->socket.readLine(); - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - socket->abort(); - else - disconnectFromServer( *socket ); - - delete socket; - socket = nullptr; - if ( !Utils::AtomicInt::loadAcquire( isCancelled ) ) - finish(); + } while ( true ); } void DictServerWordSearchRequest::cancel() @@ -507,95 +540,192 @@ void DictServerWordSearchRequest::cancel() isCancelled.ref(); finish(); } +void DictServer::DictServerWordSearchRequest::addMatchedWord( const QString & str ) +{ + matchesList.append( str ); + + if ( matchesList.size() >= MAX_MATCHES_COUNT ) { + emit finishedMatches(); + } +} class DictServerArticleRequest: public Dictionary::DataRequest { QAtomicInt isCancelled; wstring word; QString errorString; - QFuture< void > f; DictServerDictionary & dict; - QTcpSocket * socket; + string articleData; + + int currentDatabase = 0; + DictServerState state; + bool contentInHtml = false; + public: - DictServerArticleRequest( wstring const & word_, DictServerDictionary & dict_ ): - word( word_ ), + DictServerImpl * dictImpl; + DictServerArticleRequest( wstring word_, DictServerDictionary & dict_ ): + word( std::move( word_ ) ), dict( dict_ ), - socket( 0 ) + dictImpl( new DictServerImpl( this, dict_.url, "GoldenDict-t" ) ) { - f = QtConcurrent::run( [ this ]() { - this->run(); + connect( this, &DictServerArticleRequest::finishedArticle, this, [ this ]( QString articleText ) { + if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) { + cancel(); + return; + } + + qDebug() << articleText; + + static QRegularExpression phonetic( R"(\\([^\\]+)\\)", + QRegularExpression::CaseInsensitiveOption ); // phonetics: \stuff\ ... + static QRegularExpression divs_inside_phonetic( "]*)>]*)>", + QRegularExpression::CaseInsensitiveOption ); + static QRegularExpression refs( R"(\{([^\{\}]+)\})", + QRegularExpression::CaseInsensitiveOption ); // links: {stuff} + static QRegularExpression links( "", + QRegularExpression::CaseInsensitiveOption ); + static QRegularExpression tags( "<[^>]*>", QRegularExpression::CaseInsensitiveOption ); + + string articleStr; + if ( contentInHtml ) + articleStr = articleText.toUtf8().data(); + else + articleStr = Html::preformat( articleText.toUtf8().data() ); + + articleText = QString::fromUtf8( articleStr.c_str(), articleStr.size() ); + int pos; + if ( !contentInHtml ) { + articleText = articleText.replace( refs, R"(\1)" ); + + pos = 0; + QString articleNewText; + + // Handle phonetics + + QRegularExpressionMatchIterator it = phonetic.globalMatch( articleText ); + while ( it.hasNext() ) { + QRegularExpressionMatch match = it.next(); + articleNewText += articleText.mid( pos, match.capturedStart() - pos ); + pos = match.capturedEnd(); + + QString phonetic_text = match.captured( 1 ); + phonetic_text.replace( divs_inside_phonetic, R"()" ); + + articleNewText += R"()" + phonetic_text + ""; + } + if ( pos ) { + articleNewText += articleText.mid( pos ); + articleText = articleNewText; + articleNewText.clear(); + } + + // Handle links + + pos = 0; + it = links.globalMatch( articleText ); + while ( it.hasNext() ) { + QRegularExpressionMatch match = it.next(); + articleNewText += articleText.mid( pos, match.capturedStart() - pos ); + pos = match.capturedEnd(); + + QString link = match.captured( 1 ); + link.replace( tags, " " ); + link.replace( " ", " " ); + + QString newLink = match.captured(); + newLink.replace( 30, + match.capturedLength( 1 ), + QString::fromUtf8( QUrl::toPercentEncoding( link.simplified() ) ) ); + articleNewText += newLink; + } + if ( pos ) { + articleNewText += articleText.mid( pos ); + articleText = articleNewText; + articleNewText.clear(); + } + } + + articleData += string( "
" ) + articleText.toUtf8().data() + "
"; + + + if ( !articleData.empty() ) { + appendString( articleData ); + articleData.clear(); + + hasAnyData = true; + } + + defineNext(); + } ); + this->run(); + + dictImpl->run( [ this ]() { + state = dictImpl->state; + state = DictServerState::DEFINE; + + defineNext(); + } ); + + QTimer::singleShot( 5000, this, [ this ]() { + qDebug() << "Server takes too much time to response"; + cancel(); } ); } void run(); - ~DictServerArticleRequest() - { - f.waitForFinished(); - } + ~DictServerArticleRequest() override = default; void cancel() override; + void readData( QByteArray reply ); + bool defineNext(); }; -void DictServerArticleRequest::run() +bool DictServerArticleRequest::defineNext() { - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) { + if ( currentDatabase >= dict.databases.size() ) { finish(); - return; + return false; } + QString defineReq = + QString( "DEFINE " ) + dict.databases.at( currentDatabase ) + " \"" + QString::fromStdU32String( word ) + "\"\r\n"; + dictImpl->socket.write( defineReq.toUtf8() ); + qDebug() << "define req:" << defineReq; + currentDatabase++; + return true; +} - socket = new QTcpSocket; - - if ( !socket ) { +void DictServerArticleRequest::run() +{ + if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) { finish(); return; } - if ( connectToServer( *socket, dict.url, errorString, isCancelled ) ) { - string articleData; - - for ( int i = 0; i < dict.databases.size(); i++ ) { - QString defineReq = - QString( "DEFINE " ) + dict.databases.at( i ) + " \"" + QString::fromStdU32String( word ) + "\"\r\n"; - socket->write( defineReq.toUtf8() ); - socket->waitForBytesWritten( 1000 ); - QString reply; - - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - break; - - if ( !readLine( *socket, reply, errorString, isCancelled ) ) - break; - - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - break; + connect( &dictImpl->socket, &QTcpSocket::readyRead, this, [ this ]() { + QMutexLocker const _( &dictImpl->mutex ); + if ( state == DictServerState::DEFINE ) { + QByteArray reply = dictImpl->socket.readLine(); + qDebug() << "receive define:" << reply; + //work around to fix ,some extra response . if ( reply.left( 3 ) == "250" ) { - // "OK" reply - matches info will be later - if ( !readLine( *socket, reply, errorString, isCancelled ) ) - break; - - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - break; + reply = dictImpl->socket.readLine(); + if ( reply.isEmpty() ) { + return; + } } + //no match if ( reply.left( 3 ) == "552" ) { - // No matches found - continue; - } - - if ( reply[ 0 ] == '5' || reply[ 0 ] == '4' ) { - // Database error - gdWarning( "Articles request from \"%s\", database \"%s\" fault: %s\n", - dict.getName().c_str(), - dict.databases.at( i ).toUtf8().data(), - reply.toUtf8().data() ); - continue; + defineNext(); + return; } + auto code = reply.left( 3 ); if ( reply.left( 3 ) == "150" ) { // Articles found int countPos = reply.indexOf( ' ', 4 ); @@ -607,195 +737,130 @@ void DictServerArticleRequest::run() // Read articles for ( int x = 0; x < count; x++ ) { - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - break; - - if ( !readLine( *socket, reply, errorString, isCancelled ) ) - break; - - if ( reply.left( 3 ) == "250" ) - break; - - if ( reply.left( 3 ) == "151" ) { - int pos = 4; - int endPos; - - // Skip requested word - if ( reply[ pos ] == '\"' ) - endPos = reply.indexOf( '\"', pos + 1 ) + 1; - else - endPos = reply.indexOf( ' ', pos ); - - if ( endPos < pos ) { - // It seems mailformed string - break; - } - - pos = endPos + 1; + reply = dictImpl->socket.readLine(); + if ( reply.isEmpty() ) { + state = DictServerState::DEFINE_DATA; + return; + } + readData( reply ); + } + state = DictServerState::DEFINE_DATA; + } + } + else if ( state == DictServerState::DEFINE_DATA ) { + QByteArray reply = dictImpl->socket.readLine(); + qDebug() << "receive define data:" << reply; + while ( true ) { + if ( reply.isEmpty() ) + return; + readData( reply ); + reply = dictImpl->socket.readLine(); + } + } + } ); - QString dbID, dbName; + connect( &dictImpl->socket, &QTcpSocket::errorOccurred, this, [ this ]( QAbstractSocket::SocketError error ) { + qDebug() << "socket error message: " << error; + cancel(); + } ); +} - // Retrieve database ID - endPos = reply.indexOf( ' ', pos ); +void DictServerArticleRequest::readData( QByteArray reply ) +{ + if ( reply.isEmpty() ) + return; - if ( endPos < pos ) { - // It seems mailformed string - break; - } + if ( reply.left( 3 ) == "250" ) + return; - dbID = reply.mid( pos, endPos - pos ); + if ( reply.left( 3 ) == "151" ) { + int pos = 4; + int endPos; - // Retrieve database ID - pos = endPos + 1; - endPos = reply.indexOf( ' ', pos ); - if ( reply[ pos ] == '\"' ) - endPos = reply.indexOf( '\"', pos + 1 ) + 1; - else - endPos = reply.indexOf( ' ', pos ); + // Skip requested word + if ( reply[ pos ] == '\"' ) + endPos = reply.indexOf( '\"', pos + 1 ) + 1; + else + endPos = reply.indexOf( ' ', pos ); - if ( endPos < pos ) { - // It seems mailformed string - break; - } + if ( endPos < pos ) { + // It seems mailformed string + return; + } - dbName = reply.mid( pos, endPos - pos ); - if ( dbName.endsWith( '\"' ) ) - dbName.chop( 1 ); - if ( dbName[ 0 ] == '\"' ) - dbName = dbName.mid( 1 ); + pos = endPos + 1; - articleData += string( "
From " ) + dbName.toUtf8().data() + " [" - + dbID.toUtf8().data() + "]:" + "
"; + QString dbID, dbName; - // Retreive MIME headers if any + // Retrieve database ID + endPos = reply.indexOf( ' ', pos ); - static QRegularExpression contentTypeExpr( "Content-Type\\s*:\\s*text/html", - QRegularExpression::CaseInsensitiveOption ); + if ( endPos < pos ) { + // It seems mailformed string + return; + } - bool contentInHtml = false; - for ( ;; ) { - if ( !readLine( *socket, reply, errorString, isCancelled ) ) - break; + dbID = reply.mid( pos, endPos - pos ); - if ( reply == "\r\n" ) - break; + // Retrieve database ID + pos = endPos + 1; + endPos = reply.indexOf( ' ', pos ); + if ( reply[ pos ] == '\"' ) + endPos = reply.indexOf( '\"', pos + 1 ) + 1; + else + endPos = reply.indexOf( ' ', pos ); - QRegularExpressionMatch match = contentTypeExpr.match( reply ); - if ( match.hasMatch() ) - contentInHtml = true; - } + if ( endPos < pos ) { + // It seems mailformed string + return; + } - // Retrieve article text + dbName = reply.mid( pos, endPos - pos ); + if ( dbName.endsWith( '\"' ) ) + dbName.chop( 1 ); + if ( dbName[ 0 ] == '\"' ) + dbName = dbName.mid( 1 ); - articleText.clear(); - for ( ;; ) { - if ( !readLine( *socket, reply, errorString, isCancelled ) ) - break; + articleData += string( "
" ) + dbName.toUtf8().data() + "[" + dbID.toUtf8().data() + + "]" + "
"; - if ( reply == ".\r\n" ) - break; + // Retreive MIME headers if any - articleText += reply; - } + static QRegularExpression contentTypeExpr( "Content-Type\\s*:\\s*text/html", + QRegularExpression::CaseInsensitiveOption ); - if ( Utils::AtomicInt::loadAcquire( isCancelled ) || !errorString.isEmpty() ) - break; - - static QRegularExpression phonetic( R"(\\([^\\]+)\\)", - QRegularExpression::CaseInsensitiveOption ); // phonetics: \stuff\ ... - static QRegularExpression divs_inside_phonetic( "
]*)>]*)>", - QRegularExpression::CaseInsensitiveOption ); - static QRegularExpression refs( R"(\{([^\{\}]+)\})", - QRegularExpression::CaseInsensitiveOption ); // links: {stuff} - static QRegularExpression links( "", - QRegularExpression::CaseInsensitiveOption ); - static QRegularExpression tags( "<[^>]*>", QRegularExpression::CaseInsensitiveOption ); - - string articleStr; - if ( contentInHtml ) - articleStr = articleText.toUtf8().data(); - else - articleStr = Html::preformat( articleText.toUtf8().data() ); - - articleText = QString::fromUtf8( articleStr.c_str(), articleStr.size() ); - if ( !contentInHtml ) { - articleText = articleText.replace( refs, R"(\1)" ); - - pos = 0; - QString articleNewText; - - // Handle phonetics - - QRegularExpressionMatchIterator it = phonetic.globalMatch( articleText ); - while ( it.hasNext() ) { - QRegularExpressionMatch match = it.next(); - articleNewText += articleText.mid( pos, match.capturedStart() - pos ); - pos = match.capturedEnd(); - - QString phonetic_text = match.captured( 1 ); - phonetic_text.replace( divs_inside_phonetic, R"()" ); - - articleNewText += "" + phonetic_text + ""; - } - if ( pos ) { - articleNewText += articleText.mid( pos ); - articleText = articleNewText; - articleNewText.clear(); - } - - // Handle links - - pos = 0; - it = links.globalMatch( articleText ); - while ( it.hasNext() ) { - QRegularExpressionMatch match = it.next(); - articleNewText += articleText.mid( pos, match.capturedStart() - pos ); - pos = match.capturedEnd(); - - QString link = match.captured( 1 ); - link.replace( tags, " " ); - link.replace( " ", " " ); - - QString newLink = match.captured(); - newLink.replace( 30, - match.capturedLength( 1 ), - QString::fromUtf8( QUrl::toPercentEncoding( link.simplified() ) ) ); - articleNewText += newLink; - } - if ( pos ) { - articleNewText += articleText.mid( pos ); - articleText = articleNewText; - articleNewText.clear(); - } - } + for ( ;; ) { + reply = dictImpl->socket.readLine(); + if ( reply.isEmpty() ) + return; - articleData += string( "
" ) + articleText.toUtf8().data() + "
"; - } + if ( reply == "\r\n" ) + break; - if ( Utils::AtomicInt::loadAcquire( isCancelled ) || !errorString.isEmpty() ) - break; - } - } + QRegularExpressionMatch match = contentTypeExpr.match( reply ); + if ( match.hasMatch() ) + contentInHtml = true; } - if ( !Utils::AtomicInt::loadAcquire( isCancelled ) && errorString.isEmpty() && !articleData.empty() ) { - appendString( articleData ); + QString articleText; + // Retrieve article text + + articleText.clear(); + for ( ;; ) { + reply = dictImpl->socket.readLine(); + if ( reply.isEmpty() ) + return; + + qDebug() << "reply data:" << reply; + if ( reply == ".\r\n" ) { + //discard all left message. + while ( !dictImpl->socket.readLine().isEmpty() ) {} + emit finishedArticle( articleText ); + return; + } - hasAnyData = true; + articleText += reply; } } - - if ( !errorString.isEmpty() ) - gdWarning( "Articles request from \"%s\" fault: %s\n", dict.getName().c_str(), errorString.toUtf8().data() ); - - if ( Utils::AtomicInt::loadAcquire( isCancelled ) ) - socket->abort(); - else - disconnectFromServer( *socket ); - - delete socket; - socket = nullptr; - if ( !Utils::AtomicInt::loadAcquire( isCancelled ) ) - finish(); } void DictServerArticleRequest::cancel() @@ -849,5 +914,5 @@ vector< sptr< Dictionary::Class > > makeDictionaries( Config::DictServers const return result; } - -} // namespace DictServer +#include "dictserver.moc" +} // namespace DictServer \ No newline at end of file