reSIProcate/repro  9694
MySqlDb.cxx
Go to the documentation of this file.
00001 #include <cassert>
00002 #include <fcntl.h>
00003 
00004 #ifdef HAVE_CONFIG_H
00005 #include "config.h"
00006 #endif
00007 
00008 #ifdef USE_MYSQL
00009 
00010 #ifdef WIN32
00011 #include <errmsg.h>
00012 #else 
00013 #include <mysql/errmsg.h>
00014 #endif
00015 
00016 #include "rutil/Data.hxx"
00017 #include "rutil/DataStream.hxx"
00018 #include "rutil/Logger.hxx"
00019 #include "rutil/ParseBuffer.hxx"
00020 
00021 #include "repro/AbstractDb.hxx"
00022 #include "repro/MySqlDb.hxx"
00023 
00024 
00025 using namespace resip;
00026 using namespace repro;
00027 using namespace std;
00028 
00029 #define RESIPROCATE_SUBSYSTEM Subsystem::REPRO
00030 
00031 extern "C"
00032 {
00033    void mysqlThreadEnd(void*)
00034    {
00035       mysql_thread_end();
00036    }
00037 }
00038 
00039 // This class helps ensure that each thread using the MySQL API's 
00040 // initialize by calling mysql_thread_init before calling any mySQL functions
00041 class MySQLInitializer
00042 {
00043    public:
00044       MySQLInitializer()
00045       {
00046          ThreadIf::tlsKeyCreate(mThreadStorage, mysqlThreadEnd);
00047       }
00048       ~MySQLInitializer()
00049       {
00050          ThreadIf::tlsKeyDelete(mThreadStorage);
00051       }
00052       void setInitialized()
00053       {
00054          ThreadIf::tlsSetValue(mThreadStorage, (void*) true);
00055       }
00056       bool isInitialized()
00057       {
00058          // Note:  if value is not set yet then 0 (false) is returned
00059          return ThreadIf::tlsGetValue(mThreadStorage) != 0; 
00060       }
00061 
00062    private:
00063       ThreadIf::TlsKey mThreadStorage;
00064 };
00065 static MySQLInitializer g_MySQLInitializer;
00066 
00067 MySqlDb::MySqlDb(const Data& server, 
00068                  const Data& user, 
00069                  const Data& password, 
00070                  const Data& databaseName, 
00071                  unsigned int port, 
00072                  const Data& customUserAuthQuery) :
00073    mDBServer(server),
00074    mDBUser(user),
00075    mDBPassword(password),
00076    mDBName(databaseName),
00077    mDBPort(port),
00078    mCustomUserAuthQuery(customUserAuthQuery),
00079    mConn(0),
00080    mConnected(false)
00081 { 
00082    InfoLog( << "Using MySQL DB with server=" << server << ", user=" << user << ", dbName=" << databaseName << ", port=" << port);
00083 
00084    for (int i=0;i<MaxTable;i++)
00085    {
00086       mResult[i]=0;
00087    }
00088 
00089    mysql_library_init(0, 0, 0);
00090    if(!mysql_thread_safe())
00091    {
00092       ErrLog( << "Repro uses MySQL from multiple threads - you MUST link with a thread safe version of the mySQL client library!");
00093    }
00094    else
00095    {
00096       connectToDatabase();
00097    }
00098 }
00099 
00100 
00101 MySqlDb::~MySqlDb()
00102 {
00103    disconnectFromDatabase();
00104 }
00105 
00106 void
00107 MySqlDb::initialize() const
00108 {
00109    if(!g_MySQLInitializer.isInitialized())
00110    {
00111       g_MySQLInitializer.setInitialized();
00112       mysql_thread_init();
00113    }
00114 }
00115 
00116 void
00117 MySqlDb::disconnectFromDatabase() const
00118 {
00119    if(mConn)
00120    {
00121       for (int i=0;i<MaxTable;i++)
00122       {
00123          if (mResult[i])
00124          {  
00125             mysql_free_result(mResult[i]); 
00126             mResult[i]=0;
00127          }
00128       }
00129    
00130       mysql_close(mConn);
00131       mConn = 0;
00132       mConnected = false;
00133    }
00134 }
00135 
00136 int 
00137 MySqlDb::connectToDatabase() const
00138 {
00139    // Disconnect from database first (if required)
00140    disconnectFromDatabase();
00141 
00142    // Now try to connect
00143    assert(mConn == 0);
00144    assert(mConnected == false);
00145 
00146    mConn = mysql_init(0);
00147    if(mConn == 0)
00148    {
00149       ErrLog( << "MySQL init failed: insufficient memory.");
00150       return CR_OUT_OF_MEMORY;
00151    }
00152 
00153    MYSQL* ret = mysql_real_connect(mConn,
00154                                    mDBServer.c_str(),   // hostname
00155                                    mDBUser.c_str(),     // user
00156                                    mDBPassword.c_str(), // password
00157                                    mDBName.c_str(),     // DB
00158                                    mDBPort,             // port
00159                                    0,                   // unix socket file
00160                                    0);                  // client flags
00161 
00162    if (ret == 0)
00163    { 
00164       int rc = mysql_errno(mConn);
00165       ErrLog( << "MySQL connect failed: error=" << rc << ": " << mysql_error(mConn));
00166       mysql_close(mConn); 
00167       mConn = 0;
00168       mConnected = false;
00169       return rc;
00170    }
00171    else
00172    {
00173       mConnected = true;
00174       return 0;
00175    }
00176 }
00177 
00178 int
00179 MySqlDb::query(const Data& queryCommand, MYSQL_RES** result) const
00180 {
00181    int rc = 0;
00182 
00183    initialize();
00184 
00185    DebugLog( << "MySqlDb::query: executing query: " << queryCommand);
00186 
00187    Lock lock(mMutex);
00188    if(mConn == 0 || !mConnected)
00189    {
00190       rc = connectToDatabase();
00191    }
00192    if(rc == 0)
00193    {
00194       assert(mConn!=0);
00195       assert(mConnected);
00196       rc = mysql_query(mConn,queryCommand.c_str());
00197       if(rc != 0)
00198       {
00199          rc = mysql_errno(mConn);
00200          if(rc == CR_SERVER_GONE_ERROR ||
00201             rc == CR_SERVER_LOST)
00202          {
00203             // First failure is a connection error - try to re-connect and then try again
00204             rc = connectToDatabase();
00205             if(rc == 0)
00206             {
00207                // OK - we reconnected - try query again
00208                rc = mysql_query(mConn,queryCommand.c_str());
00209                if( rc != 0)
00210                {
00211                   ErrLog( << "MySQL query failed: error=" << mysql_errno(mConn) << ": " << mysql_error(mConn));
00212                }
00213             }
00214          }
00215          else
00216          {
00217             ErrLog( << "MySQL query failed: error=" << mysql_errno(mConn) << ": " << mysql_error(mConn));
00218          }
00219       }
00220    }
00221 
00222    // Now store result - if pointer to result pointer was supplied and no errors
00223    if(rc == 0 && result)
00224    {
00225       *result = mysql_store_result(mConn);
00226       if(*result == 0)
00227       {
00228          rc = mysql_errno(mConn);
00229          if(rc != 0)
00230          {
00231             ErrLog( << "MySQL store result failed: error=" << rc << ": " << mysql_error(mConn));
00232          }
00233       }
00234    }
00235 
00236    if(rc != 0)
00237    {
00238       ErrLog( << " SQL Command was: " << queryCommand) ;
00239    }
00240    return rc;
00241 }
00242 
00243 int
00244 MySqlDb::singleResultQuery(const Data& queryCommand, std::vector<Data>& fields) const
00245 {
00246    MYSQL_RES* result=0;
00247    int rc = query(queryCommand, &result);
00248       
00249    if(rc == 0)
00250    {
00251       if(result == 0)
00252       {
00253          return rc;
00254       }
00255 
00256       MYSQL_ROW row = mysql_fetch_row(result);
00257       if(row)
00258       {
00259          for(unsigned int i = 0; i < result->field_count; i++)
00260          {
00261             fields.push_back(Data(row[i]));
00262          }
00263       }
00264       else
00265       {
00266          rc = mysql_errno(mConn);
00267          if(rc != 0)
00268          {
00269             ErrLog( << "MySQL fetch row failed: error=" << rc << ": " << mysql_error(mConn));
00270          }
00271       }
00272       mysql_free_result(result);
00273    }
00274    return rc;
00275 }
00276 
00277 resip::Data& 
00278 MySqlDb::escapeString(const resip::Data& str, resip::Data& escapedStr) const
00279 {
00280    escapedStr.truncate2(mysql_real_escape_string(mConn, (char*)escapedStr.getBuf(str.size()*2+1), str.c_str(), str.size()));
00281    return escapedStr;
00282 }
00283 
00284 bool 
00285 MySqlDb::addUser(const AbstractDb::Key& key, const AbstractDb::UserRecord& rec)
00286 { 
00287    Data command;
00288    {
00289       DataStream ds(command);
00290       ds << "REPLACE INTO users SET user='" << rec.user 
00291          << "', domain='" << rec.domain
00292          << "', realm='" << rec.realm
00293          << "', passwordHash='" << rec.passwordHash
00294          << "', name='" << rec.name
00295          << "', email='" << rec.email
00296          << "', forwardAddress='" << rec.forwardAddress
00297          << "'";
00298    }
00299    return query(command, 0) == 0;
00300 }
00301 
00302 
00303 void 
00304 MySqlDb::eraseUser(const AbstractDb::Key& key )
00305 { 
00306    Data command;
00307    {
00308       DataStream ds(command);
00309       ds << "DELETE FROM users ";
00310       userWhereClauseToDataStream(key, ds);
00311    }
00312    query(command, 0);
00313 }
00314 
00315 
00316 AbstractDb::UserRecord 
00317 MySqlDb::getUser( const AbstractDb::Key& key ) const
00318 {
00319    AbstractDb::UserRecord  ret;
00320 
00321    Data command;
00322    {
00323       DataStream ds(command);
00324       ds << "SELECT user, domain, realm, passwordHash, name, email, forwardAddress FROM users ";
00325       userWhereClauseToDataStream(key, ds);
00326    }
00327    
00328    MYSQL_RES* result=0;
00329    if(query(command, &result) != 0)
00330    {
00331       return ret;
00332    }
00333    
00334    if (result==0)
00335    {
00336       ErrLog( << "MySQL store result failed: error=" << mysql_errno(mConn) << ": " << mysql_error(mConn));
00337       return ret;
00338    }
00339 
00340    MYSQL_ROW row = mysql_fetch_row(result);
00341    if (row)
00342    {
00343       ret.user           = Data(row[0]);
00344       ret.domain         = Data(row[1]);
00345       ret.realm          = Data(row[2]);
00346       ret.passwordHash   = Data(row[3]);
00347       ret.name           = Data(row[4]);
00348       ret.email          = Data(row[5]);
00349       ret.forwardAddress = Data(row[6]);
00350    }
00351 
00352    mysql_free_result(result);
00353 
00354    return ret;
00355 }
00356 
00357 
00358 resip::Data 
00359 MySqlDb::getUserAuthInfo(  const AbstractDb::Key& key ) const
00360 { 
00361    std::vector<Data> ret;
00362 
00363    Data command;
00364    {
00365       DataStream ds(command);
00366       Data user;
00367       Data domain;
00368       getUserAndDomainFromKey(key, user, domain);
00369       ds << "SELECT passwordHash FROM users WHERE user = '" << user << "' AND domain = '" << domain << "' ";
00370    
00371       // Note: domain is empty when querying for HTTP admin user - for this special user, 
00372       // we will only check the repro db, by not adding the UNION statement below
00373       if(!mCustomUserAuthQuery.empty() && !domain.empty())  
00374       {
00375          ds << " UNION " << mCustomUserAuthQuery;
00376          ds.flush();
00377          command.replace("$user", user);
00378          command.replace("$domain", domain);
00379       }
00380    }
00381 
00382    if(singleResultQuery(command, ret) != 0 || ret.size() == 0)
00383    {
00384       return Data::Empty;
00385    }
00386    
00387    DebugLog( << "Auth password is " << ret.front());
00388    
00389    return ret.front();
00390 }
00391 
00392 
00393 AbstractDb::Key 
00394 MySqlDb::firstUserKey()
00395 {  
00396    // free memory from previous search 
00397    if (mResult[UserTable])
00398    {
00399       mysql_free_result(mResult[UserTable]); 
00400       mResult[UserTable] = 0;
00401    }
00402    
00403    Data command("SELECT user, domain FROM users");
00404 
00405    if(query(command, &mResult[UserTable]) != 0)
00406    {
00407       return Data::Empty;
00408    }
00409 
00410    if(mResult[UserTable] == 0)
00411    {
00412       ErrLog( << "MySQL store result failed: error=" << mysql_errno(mConn) << ": " << mysql_error(mConn));
00413       return Data::Empty;
00414    }
00415    
00416    return nextUserKey();
00417 }
00418 
00419 
00420 AbstractDb::Key 
00421 MySqlDb::nextUserKey()
00422 { 
00423    if(mResult[UserTable] == 0)
00424    { 
00425       return Data::Empty;
00426    }
00427    
00428    MYSQL_ROW row = mysql_fetch_row(mResult[UserTable]);
00429    if (!row)
00430    {
00431       mysql_free_result(mResult[UserTable]); 
00432       mResult[UserTable] = 0;
00433       return Data::Empty;
00434    }
00435    Data user(row[0]);
00436    Data domain(row[1]);
00437    
00438    return user+"@"+domain;
00439 }
00440 
00441 
00442 bool 
00443 MySqlDb::dbWriteRecord(const Table table, 
00444                        const resip::Data& pKey, 
00445                        const resip::Data& pData)
00446 {
00447    Data command;
00448 
00449    // Check if there is a secondary key or not and get it's value
00450    char* secondaryKey;
00451    unsigned int secondaryKeyLen;
00452    Data escapedKey;
00453    if(AbstractDb::getSecondaryKey(table, pKey, pData, (void**)&secondaryKey, &secondaryKeyLen) == 0)
00454    {
00455       Data sKey(Data::Share, secondaryKey, secondaryKeyLen);
00456       DataStream ds(command);
00457       ds << "REPLACE INTO " << tableName(table)
00458          << " SET attr='" << escapeString(pKey, escapedKey)
00459          << "', attr2='" << escapeString(sKey, escapedKey)
00460          << "', value='"  << pData.base64encode()
00461          << "'";
00462    }
00463    else
00464    {
00465       DataStream ds(command);
00466       ds << "REPLACE INTO " << tableName(table) 
00467          << " SET attr='" << escapeString(pKey, escapedKey)
00468          << "', value='"  << pData.base64encode()
00469          << "'";
00470    }
00471 
00472    return query(command, 0) == 0;
00473 }
00474 
00475 bool 
00476 MySqlDb::dbReadRecord(const Table table, 
00477                       const resip::Data& pKey, 
00478                       resip::Data& pData) const
00479 { 
00480    Data command;
00481    Data escapedKey;
00482    {
00483       DataStream ds(command);
00484       ds << "SELECT value FROM " << tableName(table) 
00485          << " WHERE attr='" << escapeString(pKey, escapedKey)
00486          << "'";
00487    }
00488 
00489    MYSQL_RES* result = 0;
00490    if(query(command, &result) != 0)
00491    {
00492       return false;
00493    }
00494 
00495    if (result == 0)
00496    {
00497       ErrLog( << "MySQL store result failed: error=" << mysql_errno(mConn) << ": " << mysql_error(mConn));
00498       return false;
00499    }
00500    else
00501    {
00502       bool success = false;
00503       MYSQL_ROW row=mysql_fetch_row(result);
00504       if(row)
00505       {
00506          pData = Data(Data::Share, row[0], (Data::size_type)strlen(row[0])).base64decode();
00507          success = true;
00508       }
00509       mysql_free_result(result);
00510       return success;
00511    }
00512 }
00513 
00514 
00515 void 
00516 MySqlDb::dbEraseRecord(const Table table, 
00517                        const resip::Data& pKey,
00518                        bool isSecondaryKey) // allows deleting records from a table that supports secondary keying using a secondary key
00519 { 
00520    Data command;
00521    {
00522       DataStream ds(command);
00523       Data escapedKey;
00524       ds << "DELETE FROM " << tableName(table);
00525       if(isSecondaryKey)
00526       {
00527          ds << " WHERE attr2='" << escapeString(pKey, escapedKey) << "'";
00528       }
00529       else
00530       {
00531          ds << " WHERE attr='" << escapeString(pKey, escapedKey) << "'";
00532       }
00533    }   
00534    query(command, 0);
00535 }
00536 
00537 
00538 resip::Data 
00539 MySqlDb::dbNextKey(const Table table, bool first)
00540 { 
00541    if(first)
00542    {
00543       // free memory from previous search 
00544       if (mResult[table])
00545       {
00546          mysql_free_result(mResult[table]); 
00547          mResult[table] = 0;
00548       }
00549       
00550       Data command;
00551       {
00552          DataStream ds(command);
00553          ds << "SELECT attr FROM " << tableName(table);
00554       }
00555       
00556       if(query(command, &mResult[table]) != 0)
00557       {
00558          return Data::Empty;
00559       }
00560 
00561       if (mResult[table] == 0)
00562       {
00563          ErrLog( << "MySQL store result failed: error=" << mysql_errno(mConn) << ": " << mysql_error(mConn));
00564          return Data::Empty;
00565       }
00566    }
00567    else
00568    {
00569       if (mResult[table] == 0)
00570       { 
00571          return Data::Empty;
00572       }
00573    }
00574    
00575    MYSQL_ROW row = mysql_fetch_row(mResult[table]);
00576    if (!row)
00577    {
00578       mysql_free_result(mResult[table]); 
00579       mResult[table] = 0;
00580       return Data::Empty;
00581    }
00582 
00583    return Data(row[0]);
00584 }
00585 
00586 
00587 bool 
00588 MySqlDb::dbNextRecord(const Table table,
00589                       const resip::Data& key,
00590                       resip::Data& data,
00591                       bool forUpdate,  // specifying to add SELECT ... FOR UPDATE so the rows are locked
00592                       bool first)  // return false if no more
00593 {
00594    if(first)
00595    {
00596       // free memory from previous search 
00597       if (mResult[table])
00598       {
00599          mysql_free_result(mResult[table]); 
00600          mResult[table] = 0;
00601       }
00602       
00603       Data command;
00604       {
00605          DataStream ds(command);
00606          ds << "SELECT value FROM " << tableName(table);
00607          if(!key.empty())
00608          {
00609             Data escapedKey;
00610             // dbNextRecord is used to iterator through database tables that support duplication records
00611             // it is only appropriate for MySQL tables that contain the attr2 non-unique index (secondary key)
00612             ds << " WHERE attr2='" << escapeString(key, escapedKey) << "'";
00613          }
00614          if(forUpdate)
00615          {
00616             ds << " FOR UPDATE";
00617          }
00618       }
00619 
00620       if(query(command, &mResult[table]) != 0)
00621       {
00622          return false;
00623       }
00624 
00625       if (mResult[table] == 0)
00626       {
00627          ErrLog( << "MySQL store result failed: error=" << mysql_errno(mConn) << ": " << mysql_error(mConn));
00628          return false;
00629       }
00630    }
00631    
00632    if (mResult[table] == 0)
00633    { 
00634       return false;
00635    }
00636    
00637    MYSQL_ROW row = mysql_fetch_row(mResult[table]);
00638    if (!row)
00639    {
00640       mysql_free_result(mResult[table]); 
00641       mResult[table] = 0;
00642       return false;
00643    }
00644 
00645    data = Data(Data::Share, row[0], (Data::size_type)strlen(row[0])).base64decode();
00646 
00647    return true;
00648 }
00649 
00650 bool 
00651 MySqlDb::dbBeginTransaction(const Table table)
00652 {
00653    Data command("SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ");
00654    if(query(command, 0) == 0)
00655    {
00656       command = "START TRANSACTION";
00657       return query(command, 0) == 0;
00658    }
00659    return false;
00660 }
00661 
00662 bool 
00663 MySqlDb::dbCommitTransaction(const Table table)
00664 {
00665    Data command("COMMIT");
00666    return query(command, 0) == 0;
00667 }
00668 
00669 bool 
00670 MySqlDb::dbRollbackTransaction(const Table table)
00671 {
00672    Data command("ROLLBACK");
00673    return query(command, 0) == 0;
00674 }
00675 
00676 static const char usersavp[] = "usersavp";
00677 static const char routesavp[] = "routesavp";
00678 static const char aclsavp[] = "aclsavp";
00679 static const char configsavp[] = "configsavp";
00680 static const char staticregsavp[] = "staticregsavp";
00681 static const char filtersavp[] = "filtersavp";
00682 static const char siloavp[] = "siloavp";
00683 
00684 const char*
00685 MySqlDb::tableName(Table table) const
00686 {
00687    switch (table)
00688    {
00689       case UserTable:
00690          assert(false);  // usersavp is not used!
00691          return usersavp;
00692       case RouteTable:
00693          return routesavp;
00694       case AclTable:
00695          return aclsavp; 
00696       case ConfigTable:
00697          return configsavp;
00698       case StaticRegTable:
00699          return staticregsavp;
00700       case FilterTable:
00701          return filtersavp;
00702       case SiloTable:
00703          return siloavp;
00704       default:
00705          assert(0);
00706    }
00707    return 0;
00708 }
00709 
00710 void 
00711 MySqlDb::userWhereClauseToDataStream(const Key& key, DataStream& ds) const
00712 {
00713    Data user;
00714    Data domain;
00715    getUserAndDomainFromKey(key, user, domain);
00716    ds << " WHERE user='" << user
00717       << "' AND domain='" << domain
00718       << "'";      
00719 }
00720    
00721 void
00722 MySqlDb::getUserAndDomainFromKey(const Key& key, Data& user, Data& domain) const
00723 {
00724    ParseBuffer pb(key);
00725    const char* start = pb.position();
00726    pb.skipToOneOf("@");
00727    pb.data(user, start);
00728    const char* anchor = pb.skipChar();
00729    pb.skipToEnd();
00730    pb.data(domain, anchor);
00731 }
00732 
00733 #endif // USE_MYSQL
00734 
00735 /* ====================================================================
00736  * The Vovida Software License, Version 1.0 
00737  * 
00738  * Copyright (c) 2000 Vovida Networks, Inc.  All rights reserved.
00739  * 
00740  * Redistribution and use in source and binary forms, with or without
00741  * modification, are permitted provided that the following conditions
00742  * are met:
00743  * 
00744  * 1. Redistributions of source code must retain the above copyright
00745  *    notice, this list of conditions and the following disclaimer.
00746  * 
00747  * 2. Redistributions in binary form must reproduce the above copyright
00748  *    notice, this list of conditions and the following disclaimer in
00749  *    the documentation and/or other materials provided with the
00750  *    distribution.
00751  * 
00752  * 3. The names "VOCAL", "Vovida Open Communication Application Library",
00753  *    and "Vovida Open Communication Application Library (VOCAL)" must
00754  *    not be used to endorse or promote products derived from this
00755  *    software without prior written permission. For written
00756  *    permission, please contact vocal@vovida.org.
00757  *
00758  * 4. Products derived from this software may not be called "VOCAL", nor
00759  *    may "VOCAL" appear in their name, without prior written
00760  *    permission of Vovida Networks, Inc.
00761  * 
00762  * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED
00763  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
00764  * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND
00765  * NON-INFRINGEMENT ARE DISCLAIMED.  IN NO EVENT SHALL VOVIDA
00766  * NETWORKS, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT DAMAGES
00767  * IN EXCESS OF $1,000, NOR FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
00768  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
00769  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
00770  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
00771  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
00772  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
00773  * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
00774  * DAMAGE.
00775  * 
00776  * ====================================================================
00777  */