You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
667 lines
23 KiB
C++
667 lines
23 KiB
C++
/***************************************************************************
|
|
* Copyright (C) gempa GmbH *
|
|
* All rights reserved. *
|
|
* Contact: gempa GmbH (seiscomp-dev@gempa.de) *
|
|
* *
|
|
* GNU Affero General Public License Usage *
|
|
* This file may be used under the terms of the GNU Affero *
|
|
* Public License version 3.0 as published by the Free Software Foundation *
|
|
* and appearing in the file LICENSE included in the packaging of this *
|
|
* file. Please review the following information to ensure the GNU Affero *
|
|
* Public License version 3.0 requirements will be met: *
|
|
* https://www.gnu.org/licenses/agpl-3.0.html. *
|
|
* *
|
|
* Other Usage *
|
|
* Alternatively, this file may be used in accordance with the terms and *
|
|
* conditions contained in a signed written agreement between you and *
|
|
* gempa GmbH. *
|
|
***************************************************************************/
|
|
|
|
#ifndef SEISCOMP_DATAMODEL_DATABASEARCHIVE_H__
|
|
#define SEISCOMP_DATAMODEL_DATABASEARCHIVE_H__
|
|
|
|
#include <seiscomp/core/baseobject.h>
|
|
#include <seiscomp/core/strings.h>
|
|
#include <seiscomp/core/io.h>
|
|
#include <seiscomp/io/database.h>
|
|
#include <seiscomp/datamodel/publicobject.h>
|
|
|
|
#include <list>
|
|
#include <mutex>
|
|
|
|
|
|
namespace Seiscomp {
|
|
namespace DataModel {
|
|
|
|
|
|
DEFINE_SMARTPOINTER(DatabaseArchive);
|
|
|
|
|
|
/**
|
|
* \brief An iterator class to iterate over a sequence of database objects.
|
|
* The iteration is done on the database side. Only the current object
|
|
* is hold in memory unless a reference of previous objects has been
|
|
* stored somewhere else.
|
|
* The iterator does not destroy or end a started query in its
|
|
* destructor. The query will be finished after iterating over all
|
|
* objects. To stop an iteration you have to call close() explicitly
|
|
* unless you receive a nullptr object at the end of iteration.
|
|
*/
|
|
class SC_SYSTEM_CORE_API DatabaseIterator : public Seiscomp::Core::BaseObject {
|
|
// ----------------------------------------------------------------------
|
|
// Public types
|
|
// ----------------------------------------------------------------------
|
|
public:
|
|
typedef IO::DatabaseInterface::OID OID;
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Xstruction
|
|
// ----------------------------------------------------------------------
|
|
protected:
|
|
//! Protected c'tor used by DatabaseArchive
|
|
DatabaseIterator(DatabaseArchive *database,
|
|
const Seiscomp::Core::RTTI *rtti);
|
|
|
|
public:
|
|
//! C'tor
|
|
DatabaseIterator();
|
|
//! Copy c'tor
|
|
DatabaseIterator(const DatabaseIterator &iter);
|
|
|
|
//! D'tor
|
|
~DatabaseIterator();
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Public interface
|
|
// ----------------------------------------------------------------------
|
|
public:
|
|
Object *get() const;
|
|
|
|
//! Returns the current result column count
|
|
size_t fieldCount() const;
|
|
|
|
//! Returns the current result field
|
|
const char *field(size_t index) const;
|
|
|
|
DatabaseIterator &operator=(const DatabaseIterator& it);
|
|
Object* operator*() const;
|
|
|
|
DatabaseIterator &operator++();
|
|
DatabaseIterator &operator++(int);
|
|
|
|
//! Returns if the current objectiterator is valid
|
|
//! and has a valid result set
|
|
bool valid() const;
|
|
|
|
//! Steps to the next result set and returns valid()
|
|
bool next();
|
|
|
|
/**
|
|
* Closes the iterator and ends the started query.
|
|
* This method is useful if you want to break an
|
|
* iteration and to start a new one.
|
|
*/
|
|
void close();
|
|
|
|
//! Returns the number of elements read
|
|
size_t count() const;
|
|
|
|
Core::Time lastModified() const {
|
|
if ( _lastModified ) return *_lastModified;
|
|
throw Core::ValueException("DatabaseIterator.lastModified is not set");
|
|
}
|
|
|
|
OID oid() const { return _oid; }
|
|
OID parentOid() const { return _parent_oid; }
|
|
|
|
//! Returns whether the object has been read from the database
|
|
//! directly (false) or fetched from the global object pool (true)
|
|
bool cached() const { return _cached; }
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Implementation
|
|
// ----------------------------------------------------------------------
|
|
private:
|
|
Object *fetch() const;
|
|
|
|
|
|
private:
|
|
const Seiscomp::Core::RTTI *_rtti;
|
|
DatabaseArchive *_reader;
|
|
mutable size_t _count;
|
|
ObjectPtr _object;
|
|
|
|
mutable OID _oid;
|
|
mutable OID _parent_oid;
|
|
mutable bool _cached;
|
|
mutable OPT(Core::Time) _lastModified;
|
|
|
|
//! Make DatabaseArchive a friend class
|
|
friend class DatabaseArchive;
|
|
};
|
|
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
|
|
|
|
|
|
|
|
|
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
|
class SC_SYSTEM_CORE_API DatabaseObjectWriter : protected Visitor {
|
|
// ----------------------------------------------------------------------
|
|
// Xstruction
|
|
// ----------------------------------------------------------------------
|
|
public:
|
|
DatabaseObjectWriter(DatabaseArchive& archive,
|
|
bool addToDatabase = true, int batchSize = 1);
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Public interface
|
|
// ----------------------------------------------------------------------
|
|
public:
|
|
//! Does the actual writing
|
|
bool operator()(Object* object);
|
|
bool operator()(Object* object, const std::string& parentID);
|
|
|
|
//! Returns the number of handled objects
|
|
int count() const { return _count; }
|
|
//! Returns the number of errors while writing
|
|
int errors() const { return _errors; }
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Visitor interface
|
|
// ----------------------------------------------------------------------
|
|
protected:
|
|
bool visit(PublicObject* publicObject);
|
|
void visit(Object* object);
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Implementation
|
|
// ----------------------------------------------------------------------
|
|
private:
|
|
bool write(Object* object);
|
|
|
|
private:
|
|
DatabaseArchive& _archive;
|
|
std::string _parentID;
|
|
bool _addObjects;
|
|
int _errors;
|
|
int _count;
|
|
int _batchSize;
|
|
};
|
|
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
|
|
|
|
|
|
|
|
|
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
|
/**
|
|
* \brief A class containing basic functionality to read and write
|
|
* \brief schema objects from and to a database.
|
|
*/
|
|
class SC_SYSTEM_CORE_API DatabaseArchive : protected Core::Archive,
|
|
public Observer {
|
|
// ----------------------------------------------------------------------
|
|
// Public types
|
|
// ----------------------------------------------------------------------
|
|
public:
|
|
typedef IO::DatabaseInterface::OID OID;
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Xstruction
|
|
// ----------------------------------------------------------------------
|
|
public:
|
|
//! Constructor
|
|
//! @param dbDriver The database driver used to access a
|
|
//! database. If the database has not been
|
|
//! opened by the DatabaseReader it will not
|
|
//! be closed by it. Neither by destruction nor
|
|
//! by calling DatabaseReader::close().
|
|
DatabaseArchive(Seiscomp::IO::DatabaseInterface* dbDriver);
|
|
|
|
//! Destructor
|
|
virtual ~DatabaseArchive();
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Public Interface
|
|
// ----------------------------------------------------------------------
|
|
public:
|
|
using Seiscomp::Core::Archive::version;
|
|
using Seiscomp::Core::Archive::versionMajor;
|
|
using Seiscomp::Core::Archive::versionMinor;
|
|
using Seiscomp::Core::Archive::isVersion;
|
|
using Seiscomp::Core::Archive::isLowerVersion;
|
|
using Seiscomp::Core::Archive::isHigherVersion;
|
|
using Seiscomp::Core::Archive::supportsVersion;
|
|
|
|
|
|
//! Implements derived method
|
|
//! @param dataSource user:password@host:port/database
|
|
bool open(const char* dataSource);
|
|
|
|
//! Implements derived method
|
|
void close();
|
|
|
|
//! Returns the used database driver
|
|
Seiscomp::IO::DatabaseInterface* driver() const;
|
|
|
|
//! Sets the database driver to use
|
|
void setDriver(Seiscomp::IO::DatabaseInterface*);
|
|
|
|
//! Returns if the archive is in an erroneous state eg after setting
|
|
//! a database interface.
|
|
bool hasError() const;
|
|
|
|
//! Returns the error message if hasError() return true
|
|
const std::string errorMsg() const;
|
|
|
|
void benchmarkQueries(int count);
|
|
|
|
/**
|
|
* Reads a public object from the database.
|
|
* @param classType The type of the object to be read. The type has
|
|
* to be derived from PublicObject
|
|
* @param publicId The publicId of the public object
|
|
* @return An unmanaged object pointer. The ownership goes over
|
|
* to the caller.
|
|
*/
|
|
PublicObject* getObject(const Seiscomp::Core::RTTI& classType,
|
|
const std::string& publicID);
|
|
|
|
/**
|
|
* Returns an iterator over all objects of a given type.
|
|
* @param parentID The publicID of the parent object. When empty,
|
|
* an iterator for all objects with type 'classType'
|
|
* is returned.
|
|
* @param classType The type of the objects to iterate over.
|
|
* @param ignorePublicObject If true then the PublicObject table will
|
|
* not be joined. That might be important if
|
|
* during a schema evolution an objects turned
|
|
* into a PublicObject but an old version
|
|
* should be read.
|
|
* @return The database iterator
|
|
*/
|
|
DatabaseIterator getObjects(const std::string& parentID,
|
|
const Seiscomp::Core::RTTI& classType,
|
|
bool ignorePublicObject = false);
|
|
|
|
/**
|
|
* Returns an iterator over all objects of a given type for a parent
|
|
* object.
|
|
* @param parent The parent object. When nullptr, an iterator for all
|
|
* objects with type 'classType' is returned.
|
|
* @param classType The type of the objects to iterate over.
|
|
* @param ignorePublicObject If true then the PublicObject table will
|
|
* not be joined. That might be important if
|
|
* during a schema evolution an objects turned
|
|
* into a PublicObject but an old version
|
|
* should be read.
|
|
* @return The database iterator
|
|
*/
|
|
DatabaseIterator getObjects(const PublicObject* parent,
|
|
const Seiscomp::Core::RTTI& classType,
|
|
bool ignorePublicObject = false);
|
|
|
|
/**
|
|
* Returns the number of objects of a given type.
|
|
* @param parentID The publicID of the parent object. When empty,
|
|
* an iterator for all objects with type 'classType'
|
|
* is returned.
|
|
* @param classType The type of the objects to iterate over.
|
|
* @return The object count
|
|
*/
|
|
size_t getObjectCount(const std::string& parentID,
|
|
const Seiscomp::Core::RTTI& classType);
|
|
|
|
/**
|
|
* Returns the number of objects of a given type for a parent
|
|
* object.
|
|
* @param parent The parent object. When nullptr, an iterator for all
|
|
* objects with type 'classType' is returned.
|
|
* @param classType The type of the objects to iterate over.
|
|
* @return The object count
|
|
*/
|
|
size_t getObjectCount(const PublicObject* parent,
|
|
const Seiscomp::Core::RTTI &classType);
|
|
|
|
|
|
//! Returns the database id for an object
|
|
//! @return The id or 0 of no id was cached for this object
|
|
OID getCachedId(const Object*) const;
|
|
|
|
/**
|
|
* Returns the publicID of the parent object if any.
|
|
* @param object The PublicObject whose parent is queried.
|
|
* @return The publicID of the parent or an empty string.
|
|
*/
|
|
std::string parentPublicID(const PublicObject* object);
|
|
|
|
/**
|
|
* Writes an object into the database.
|
|
* @param object The object to be written
|
|
* @param parentId The publicID of the parent object used
|
|
* when no parent pointer is set.
|
|
*/
|
|
bool write(Object* object, const std::string &parentID = "");
|
|
|
|
/**
|
|
* Updates an object in the database. While serializing the
|
|
* index attributes (Archive hint: INDEX_ATTRIBUTE) will not be
|
|
* recognized when object is not a PublicObject. Then the index
|
|
* attributes will be used to lookup the corresponding row in the
|
|
* object table. The publicID is readonly in any case.
|
|
* @param object The object to be updated
|
|
* @param parentId The publicID of the parent object used
|
|
* when no parent pointer is set.
|
|
*/
|
|
bool update(Object* object, const std::string &parentID = "");
|
|
|
|
/**
|
|
* Removes an object in the database. If the object is not a
|
|
* PublicObject the index attributes (serialized with Archive hint
|
|
* set to INDEX_ATTRIBUTE) will be used instead.
|
|
* @param object The object to be removed
|
|
* @param parentID The parentID of the parent objects used
|
|
* when no parent pointer is set.
|
|
*/
|
|
bool remove(Object* object, const std::string &parentID = "");
|
|
|
|
/**
|
|
* Adds a complete tree to the database.
|
|
* @param object The tree root to be written
|
|
* @param parentId The publicID of the parent object used
|
|
* when no parent pointer is set.
|
|
* @param objectsHandled Return count of number of objects
|
|
* written
|
|
*/
|
|
bool addTree(Object *object, const std::string &parentID = "",
|
|
int *objectsHandled = nullptr);
|
|
|
|
/**
|
|
* Removes an object tree from the database.
|
|
* @param object The object to be removed
|
|
* @param parentID The parentID of the parent objects used
|
|
* when no parent pointer is set.
|
|
* @param objectsHandled Return count of number of objects
|
|
* written
|
|
*/
|
|
bool removeTree(Object *object, const std::string &parentID = "",
|
|
int *objectsHandled = nullptr);
|
|
|
|
//! Returns an iterator for objects of a given type.
|
|
DatabaseIterator getObjectIterator(const std::string &query,
|
|
const Seiscomp::Core::RTTI &classType);
|
|
|
|
DatabaseIterator getObjectIterator(const std::string &query,
|
|
const Seiscomp::Core::RTTI *classType);
|
|
|
|
template <typename T>
|
|
std::string toString(const T& value) const { return Core::toString(value); }
|
|
|
|
std::string toString(const std::string &value) const;
|
|
std::string toString(const char *value) const;
|
|
std::string toString(const Core::Time& value) const;
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Protected Interface
|
|
// ----------------------------------------------------------------------
|
|
protected:
|
|
//! Implements derived method
|
|
bool create(const char* dataSource);
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Protected Archive Interface
|
|
// ----------------------------------------------------------------------
|
|
protected:
|
|
//! Reads an integer
|
|
virtual void read(std::int8_t& value);
|
|
virtual void read(std::int16_t& value);
|
|
virtual void read(std::int32_t& value);
|
|
virtual void read(std::int64_t& value);
|
|
//! Reads a float
|
|
virtual void read(float& value);
|
|
//! Reads a double
|
|
virtual void read(double& value);
|
|
//! Reads a float complex
|
|
virtual void read(std::complex<float>& value);
|
|
//! Reads a double complex
|
|
virtual void read(std::complex<double>& value);
|
|
//! Reads a boolean
|
|
virtual void read(bool& value);
|
|
|
|
//! Reads a vector of native types
|
|
virtual void read(std::vector<char>& value);
|
|
virtual void read(std::vector<int8_t>& value);
|
|
virtual void read(std::vector<int16_t>& value);
|
|
virtual void read(std::vector<int32_t>& value);
|
|
virtual void read(std::vector<int64_t>& value);
|
|
virtual void read(std::vector<float>& value);
|
|
virtual void read(std::vector<double>& value);
|
|
virtual void read(std::vector<std::string>& value);
|
|
virtual void read(std::vector<Core::Time>& value);
|
|
|
|
//! Reads a vector of complex doubles
|
|
virtual void read(std::vector<std::complex<double> >& value);
|
|
|
|
//! Reads a string
|
|
virtual void read(std::string& value);
|
|
|
|
//! Reads a time
|
|
virtual void read(Seiscomp::Core::Time& value);
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
// Write methods
|
|
// ------------------------------------------------------------------
|
|
protected:
|
|
//! Writes an integer
|
|
virtual void write(std::int8_t value);
|
|
virtual void write(std::int16_t value);
|
|
virtual void write(std::int32_t value);
|
|
virtual void write(std::int64_t value);
|
|
//! Writes a float
|
|
virtual void write(float value);
|
|
//! Writes a double
|
|
virtual void write(double value);
|
|
//! Writes a float complex
|
|
virtual void write(std::complex<float>& value);
|
|
//! Writes a double complex
|
|
virtual void write(std::complex<double>& value);
|
|
//! Writes a boolean
|
|
virtual void write(bool value);
|
|
|
|
//! Writes a vector of native types
|
|
virtual void write(std::vector<char>& value);
|
|
virtual void write(std::vector<int8_t>& value);
|
|
virtual void write(std::vector<int16_t>& value);
|
|
virtual void write(std::vector<int32_t>& value);
|
|
virtual void write(std::vector<int64_t>& value);
|
|
virtual void write(std::vector<float>& value);
|
|
virtual void write(std::vector<double>& value);
|
|
virtual void write(std::vector<std::string>& value);
|
|
virtual void write(std::vector<Core::Time>& value);
|
|
|
|
//! Writes a vector of complex doubles
|
|
virtual void write(std::vector<std::complex<double> >& value);
|
|
|
|
//! Writes a string
|
|
virtual void write(std::string& value);
|
|
|
|
//! Writes a time
|
|
virtual void write(Seiscomp::Core::Time& value);
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
// Protected observer interface
|
|
// ------------------------------------------------------------------
|
|
protected:
|
|
virtual void onObjectDestroyed(Object* object);
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
// Protected interface
|
|
// ------------------------------------------------------------------
|
|
protected:
|
|
//! Implements derived method
|
|
virtual bool locateObjectByName(const char* name, const char* targetClass, bool nullable);
|
|
//! Implements derived method
|
|
virtual bool locateNextObjectByName(const char* name, const char* targetClass);
|
|
//! Implements derived method
|
|
virtual void locateNullObjectByName(const char* name, const char* targetClass, bool first);
|
|
|
|
//! Implements derived method
|
|
virtual std::string determineClassName();
|
|
|
|
//! Implements derived method
|
|
virtual void setClassName(const char*);
|
|
|
|
//! Implements derived method
|
|
void serialize(RootType* object);
|
|
|
|
//! Implements derived method
|
|
void serialize(SerializeDispatcher&);
|
|
|
|
std::string buildQuery(const std::string& table,
|
|
const std::string& filter = "");
|
|
std::string buildExtendedQuery(const std::string& what,
|
|
const std::string& tables,
|
|
const std::string& filter = "");
|
|
|
|
bool validInterface() const;
|
|
|
|
//! Associates an objects with an id and caches
|
|
//! this information
|
|
void registerId(const Object*, OID id);
|
|
|
|
//! Returns the number of cached object
|
|
int getCacheSize() const;
|
|
|
|
//! Serializes an objects and registeres its id in the cache
|
|
void serializeObject(Object*);
|
|
|
|
Object* queryObject(const Seiscomp::Core::RTTI& classType,
|
|
const std::string& query);
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Privates types
|
|
// ----------------------------------------------------------------------
|
|
private:
|
|
typedef std::map<const Object*, OID> ObjectIdMap;
|
|
typedef std::map<std::string, OPT(std::string)> AttributeMap;
|
|
|
|
typedef std::pair<std::string, AttributeMap> ChildTable;
|
|
typedef std::list<ChildTable> ChildTables;
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Implementation
|
|
// ----------------------------------------------------------------------
|
|
private:
|
|
bool fetchVersion();
|
|
|
|
//! Removes an objects from the id cache
|
|
void removeId(Object*);
|
|
|
|
//! Returns the current field content
|
|
const char* cfield() const { return _field; }
|
|
|
|
std::string sfield() const { return std::string(_field, _fieldSize); }
|
|
|
|
//! Returns the current field size
|
|
size_t fieldSize() const { return _fieldSize; }
|
|
|
|
//! Writes an attribute into the attribute map
|
|
void writeAttrib(OPT_CR(std::string) value) const;
|
|
|
|
//! Reads an attribute from the query result
|
|
void readAttrib() const;
|
|
|
|
//! Resets the attribute prefix
|
|
void resetAttributePrefix() const;
|
|
|
|
//! Appends a name to the current attribute prefix
|
|
void pushAttributePrefix(const char* name) const;
|
|
|
|
//! Removes the last attribute prefix when using complex types
|
|
//! in one table.
|
|
void popAttribPrefix() const;
|
|
|
|
void renderAttributes(const AttributeMap&);
|
|
void renderValues(const AttributeMap&);
|
|
|
|
//! Returns an iterator for objects of a given type.
|
|
DatabaseIterator getObjectIterator(OID parentID,
|
|
const Seiscomp::Core::RTTI& classType,
|
|
bool ignorePublicObject = false);
|
|
|
|
//! Queries for the database id of a PublicObject for
|
|
//! a given publicID
|
|
OID publicObjectId(const std::string& publicId);
|
|
|
|
//! Queries for the database id of an Object
|
|
OID objectId(Object*, const std::string& parentID);
|
|
|
|
//! Insert a base object column and return its database id
|
|
OID insertObject();
|
|
|
|
//! Insert a PublicObject column and return its database id
|
|
OID insertPublicObject(const std::string& publicId);
|
|
|
|
//! Insert a row into a table
|
|
bool insertRow(const std::string& table,
|
|
const AttributeMap& attributes,
|
|
const std::string& parentId = "");
|
|
|
|
//! Delete an object with a given database id
|
|
bool deleteObject(OID id);
|
|
|
|
protected:
|
|
Seiscomp::IO::DatabaseInterfacePtr _db;
|
|
|
|
private:
|
|
std::string _errorMsg;
|
|
std::string _publicIDColumn;
|
|
|
|
mutable std::mutex _objectIdMutex;
|
|
mutable ObjectIdMap _objectIdCache;
|
|
mutable int _fieldIndex;
|
|
mutable const char* _field;
|
|
mutable size_t _fieldSize;
|
|
|
|
mutable AttributeMap _rootAttributes;
|
|
mutable AttributeMap _indexAttributes;
|
|
mutable AttributeMap* _objectAttributes;
|
|
mutable ChildTables _childTables;
|
|
mutable ChildTables::iterator _currentChildTable;
|
|
mutable int _childDepth;
|
|
|
|
mutable std::string _currentAttributePrefix;
|
|
mutable std::string _currentAttributeName;
|
|
mutable bool _ignoreIndexAttributes;
|
|
|
|
mutable int _prefixPos;
|
|
mutable std::string::size_type _prefixOffset[64];
|
|
|
|
bool _allowDbClose;
|
|
|
|
friend class DatabaseIterator;
|
|
friend class AttributeMapper;
|
|
friend class ValueMapper;
|
|
};
|
|
// <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
#endif
|