diff --git a/src/blackmisc/db/datastoreobjectlist.cpp b/src/blackmisc/db/datastoreobjectlist.cpp index 3af38884f..42eac5540 100644 --- a/src/blackmisc/db/datastoreobjectlist.cpp +++ b/src/blackmisc/db/datastoreobjectlist.cpp @@ -16,6 +16,7 @@ #include "blackmisc/aviation/aircrafticaocodelist.h" #include "blackmisc/aviation/airlineicaocodelist.h" #include "blackmisc/db/dbinfolist.h" +#include "blackmisc/db/distributionlist.h" #include "blackmisc/simulation/aircraftmodellist.h" #include "blackmisc/simulation/distributorlist.h" @@ -170,6 +171,7 @@ namespace BlackMisc template class BLACKMISC_EXPORT_DEFINE_TEMPLATE IDatastoreObjectList; template class BLACKMISC_EXPORT_DEFINE_TEMPLATE IDatastoreObjectList; template class BLACKMISC_EXPORT_DEFINE_TEMPLATE IDatastoreObjectList; + template class BLACKMISC_EXPORT_DEFINE_TEMPLATE IDatastoreObjectList; template class BLACKMISC_EXPORT_DEFINE_TEMPLATE IDatastoreObjectList; template class BLACKMISC_EXPORT_DEFINE_TEMPLATE IDatastoreObjectList; template class BLACKMISC_EXPORT_DEFINE_TEMPLATE IDatastoreObjectList; diff --git a/src/blackmisc/db/datastoreobjectlist.h b/src/blackmisc/db/datastoreobjectlist.h index b16e73f70..bf1000d44 100644 --- a/src/blackmisc/db/datastoreobjectlist.h +++ b/src/blackmisc/db/datastoreobjectlist.h @@ -13,7 +13,6 @@ #define BLACKMISC_DB_DATABASEOBJECTLIST_H #include "blackmisc/timestampobjectlist.h" - #include #include #include @@ -70,6 +69,7 @@ namespace BlackMisc extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE IDatastoreObjectList; extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE IDatastoreObjectList; extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE IDatastoreObjectList; + extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE IDatastoreObjectList; extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE IDatastoreObjectList; extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE IDatastoreObjectList; extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE IDatastoreObjectList; diff --git a/src/blackmisc/db/db.h b/src/blackmisc/db/db.h index 4f718a891..4c48f86e5 100644 --- a/src/blackmisc/db/db.h +++ b/src/blackmisc/db/db.h @@ -21,5 +21,7 @@ #include "blackmisc/db/dbinfolist.h" #include "blackmisc/db/dbinfo.h" #include "blackmisc/db/dbflags.h" +#include "blackmisc/db/distribution.h" +#include "blackmisc/db/distributionlist.h" #endif // guard diff --git a/src/blackmisc/db/distribution.cpp b/src/blackmisc/db/distribution.cpp new file mode 100644 index 000000000..76b623971 --- /dev/null +++ b/src/blackmisc/db/distribution.cpp @@ -0,0 +1,283 @@ +/* Copyright (C) 2017 + * swift project community / contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +#include "distribution.h" +#include "blackconfig/buildconfig.h" +#include + +using namespace BlackConfig; +using namespace BlackMisc; +using namespace BlackMisc::Network; + +namespace BlackMisc +{ + namespace Db + { + CDistribution::CDistribution() + { } + + CDistribution::CDistribution(const QString &channel, bool restricted) : + m_channel(channel.trimmed().toUpper()), m_restricted(restricted) + { } + + void CDistribution::setChannel(const QString &channel) + { + m_channel = channel.trimmed().toUpper(); + } + + QStringList CDistribution::getPlatforms() const + { + QStringList platforms = m_platformFiles.keys(); + platforms.sort(); + return platforms; + } + + QString CDistribution::guessPlatform() const + { + const QStringList platforms(getPlatforms()); + if (platforms.isEmpty()) { return ""; } + QStringList reduced; + for (const QString &p : platforms) + { + if (CBuildConfig::isRunningOnWindowsNtPlatform()) + { + if (!p.contains("win", Qt::CaseInsensitive)) continue; + } + else if (CBuildConfig::isRunningOnLinuxPlatform()) + { + if (!p.contains("linux", Qt::CaseInsensitive)) continue; + } + else if (CBuildConfig::isRunningOnMacOSXPlatform()) + { + if (!p.contains("mac", Qt::CaseInsensitive) || !p.contains("osx", Qt::CaseInsensitive)) continue; + } + reduced << p; + } + + if (reduced.size() > 1) + { + // further reduce by VATSIM flag + QStringList furtherReduced; + for (const QString &p : reduced) + { + if (CBuildConfig::isVatsimVersion()) + { + if (p.contains("vatsim")) { furtherReduced << p; } + } + else + { + if (!p.contains("vatsim")) { furtherReduced << p; } + } + } + if (!furtherReduced.isEmpty()) { reduced = furtherReduced; } + } + + if (reduced.isEmpty()) { return ""; } + return reduced.front(); + } + + QString CDistribution::getVersionString(const QString &platform) const + { + if (platform.isEmpty()) { return ""; } + return m_platformVersions.value(platform); + } + + QVersionNumber CDistribution::getQVersion(const QString &platform) const + { + return QVersionNumber::fromString(getVersionString(platform)); + } + + QString CDistribution::getFilename(const QString &platform) const + { + if (platform.isEmpty()) { return ""; } + return m_platformFiles.value(platform); + } + + void CDistribution::addDownloadUrl(const CUrl &url) + { + if (url.isEmpty()) { return; } + this->m_downloadUrls.push_back(url); + } + + bool CDistribution::hasDownloadUrls() const + { + return !this->m_downloadUrls.isEmpty(); + } + + QString CDistribution::convertToQString(bool i18n) const + { + return convertToQString(", ", i18n); + } + + QString CDistribution::convertToQString(const QString &separator, bool i18n) const + { + return QLatin1String("channel: ") % + this->getChannel() % + separator % + QLatin1String("download URLs: ") % + getDownloadUrls().toQString(i18n) % + separator % + QLatin1String("platforms: ") % + getPlatforms().join(", ") % + separator % + QLatin1String("timestamp: ") % + this->getFormattedUtcTimestampYmdhms(); + } + + CVariant CDistribution::propertyByIndex(const BlackMisc::CPropertyIndex &index) const + { + if (index.isMyself()) { return CVariant::from(*this); } + if (IDatastoreObjectWithIntegerKey::canHandleIndex(index)) { return IDatastoreObjectWithIntegerKey::propertyByIndex(index); } + + const ColumnIndex i = index.frontCasted(); + switch (i) + { + case IndexChannel: + return CVariant::fromValue(this->m_channel); + case IndexDownloadUrls: + return CVariant::fromValue(this->m_downloadUrls); + case IndexRestricted: + return CVariant::fromValue(this->m_restricted); + case IndexPlatforms: + return CVariant::fromValue(this->getPlatforms()); + case IndexPlatformFiles: + return CVariant::fromValue(this->m_platformFiles); + default: + return CValueObject::propertyByIndex(index); + } + } + + void CDistribution::setPropertyByIndex(const CPropertyIndex &index, const CVariant &variant) + { + if (index.isMyself()) { (*this) = variant.to(); return; } + if (IDatastoreObjectWithIntegerKey::canHandleIndex(index)) + { + IDatastoreObjectWithIntegerKey::setPropertyByIndex(index, variant); + return; + } + + const ColumnIndex i = index.frontCasted(); + switch (i) + { + case IndexChannel: + this->setChannel(variant.value()); + break; + case IndexDownloadUrls: + this->m_downloadUrls = variant.value(); + break; + case IndexRestricted: + this->m_restricted = variant.toBool(); + break; + case IndexPlatformFiles: + this->m_platformFiles = variant.value(); + break; + default: + CValueObject::setPropertyByIndex(index, variant); + break; + } + } + + CDistribution CDistribution::fromDatabaseJson(const QJsonObject &json, const QString &prefix) + { + Q_UNUSED(prefix); // not nested + + const QString channel(json.value("channel").toString()); + const bool restricted(json.value("restricted").toBool()); + CDistribution distribution(channel, restricted); + + // add the URLs + for (int i = 0; i < 5; i++) + { + const QString key = "url" + QString::number(i); + const QString url = json.value(key).toString(); + if (url.isEmpty()) { continue; } + distribution.addDownloadUrl(CUrl(url)); + } + + const QJsonObject platforms = json.value("platforms").toObject(); + const QStringList platformsKeys = platforms.keys(); + if (platformsKeys.isEmpty()) { return CDistribution(); } // no platforms, then the whole distribution is useless + for (const QString platformKey : platformsKeys) + { + QStringList platformFileNames; + QJsonArray platformFiles = platforms.value(platformKey).toArray(); + for (const QJsonValue &platformFile : platformFiles) + { + const QString filename = platformFile.toString(); + if (filename.isEmpty()) { continue; } + platformFileNames << filename; + } + + const QPair latestFileInfo = findLatestVersion(platformFileNames); + if (!latestFileInfo.first.isEmpty()) + { + distribution.m_platformFiles.insert(platformKey, latestFileInfo.first); + distribution.m_platformVersions.insert(platformKey, latestFileInfo.second.toString()); + } + } + + distribution.setKeyAndTimestampFromDatabaseJson(json); + return distribution; + } + + QVersionNumber CDistribution::versionNumberFromFilename(const QString &filename) + { + if (filename.isEmpty()) { return QVersionNumber(); } + + // swift-installer-linux-64-0.7.3_2017-03-25_11-24-50.run + static const QRegExp firstSegments("\\d+\\.\\d+\\.\\d+"); + if (firstSegments.indexIn(filename) < 0) + { + return QVersionNumber(); // no version, invalid + } + QString v = firstSegments.cap(0); // first 3 segments, like 0.9.3 + if (!v.endsWith('.')) { v += '.'; } + + static const QRegExp ts1("\\d{4}.\\d{2}.\\d{2}.\\d{2}.\\d{2}.\\d{2}"); + if (ts1.indexIn(filename) < 0) + { + // version without timestamp + v += "0"; + return QVersionNumber::fromString(v); + } + + const QString versionTimestampString = BlackMisc::digitOnlyString(ts1.cap(0)); + const QDateTime versionTimestamp = QDateTime::fromString(versionTimestampString, "yyyyMMddHHmmss"); + const QString lastSegment = QString::number(CBuildConfig::buildTimestampAsVersionSegment(versionTimestamp)); + + v += lastSegment; + return QVersionNumber::fromString(v); + } + + QPair CDistribution::findLatestVersion(const QStringList &filenames) + { + if (filenames.isEmpty()) return QPair(); + if (filenames.size() == 1) return QPair(filenames.first(), versionNumberFromFilename(filenames.first())); + QString latest; + QVersionNumber latestVersion; + for (const QString &fn : filenames) + { + if (latest.isEmpty()) + { + latest = fn; + latestVersion = versionNumberFromFilename(fn); + continue; + } + + const QVersionNumber version = versionNumberFromFilename(fn); + if (version > latestVersion) + { + latest = fn; + latestVersion = versionNumberFromFilename(fn); + } + } + return QPair(latest, latestVersion); + } + } // ns +} // ns diff --git a/src/blackmisc/db/distribution.h b/src/blackmisc/db/distribution.h new file mode 100644 index 000000000..83e16799a --- /dev/null +++ b/src/blackmisc/db/distribution.h @@ -0,0 +1,146 @@ +/* Copyright (C) 2017 + * swift project community / contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +//! \file + +#ifndef BLACKMISC_DB_DISTRIBUTION_H +#define BLACKMISC_DB_DISTRIBUTION_H + +#include "blackmisc/blackmiscexport.h" +#include "blackmisc/network/urllist.h" +#include "blackmisc/db/datastore.h" +#include "blackmisc/settingscache.h" +#include "blackmisc/dictionary.h" +#include "blackmisc/valueobject.h" +#include "blackmisc/variant.h" + +#include +#include +#include + +namespace BlackMisc +{ + namespace Db + { + //! Dictionary for files per platform + using CPlatformDictionary = BlackMisc::CDictionary; + + //! CDistribution for channel + class BLACKMISC_EXPORT CDistribution : + public BlackMisc::CValueObject, + public BlackMisc::Db::IDatastoreObjectWithIntegerKey + { + public: + //! Properties by index + enum ColumnIndex + { + IndexChannel = BlackMisc::CPropertyIndex::GlobalIndexCDistribution, + IndexRestricted, + IndexDownloadUrls, + IndexPlatforms, + IndexPlatformFiles, + }; + + //! Default constructor + CDistribution(); + + //! Constructor + CDistribution(const QString &channel, bool restricted); + + //! Destructor. + ~CDistribution() {} + + //! Version channel (Alpha, Beta, Stable ..) + const QString &getChannel() const { return m_channel; } + + //! Set the channel + void setChannel(const QString &channel); + + //! Get platforms + QStringList getPlatforms() const; + + //! Guess platform + QString guessPlatform() const; + + //! Version for platform + QString getVersionString(const QString &platform) const; + + //! Version as QVersion + QVersionNumber getQVersion(const QString &platform) const; + + //! File for platform + QString getFilename(const QString &platform) const; + + //! Download URLs, i.e. here one can download installer + const BlackMisc::Network::CUrlList &getDownloadUrls() const { return m_downloadUrls; } + + //! Add URL, ignored if empty + void addDownloadUrl(const BlackMisc::Network::CUrl &url); + + //! At least one download URL? + bool hasDownloadUrls() const; + + //! Restricted channel? + bool isRestricted() const { return m_restricted; } + + //! \copydoc BlackMisc::Mixin::String::toQString + QString convertToQString(bool i18n = false) const; + + //! To string + QString convertToQString(const QString &separator, bool i18n = false) const; + + //! \copydoc BlackMisc::Mixin::Index::propertyByIndex + BlackMisc::CVariant propertyByIndex(const BlackMisc::CPropertyIndex &index) const; + + //! \copydoc BlackMisc::Mixin::Index::setPropertyByIndex + void setPropertyByIndex(const BlackMisc::CPropertyIndex &index, const BlackMisc::CVariant &variant); + + //! Object from database JSON format + static CDistribution fromDatabaseJson(const QJsonObject &json, const QString &prefix = {}); + + private: + QString m_channel; //!< for development + bool m_restricted = false; //!< restricted access (i.e. password for download needed) + BlackMisc::Network::CUrlList m_downloadUrls; //!< download URLs, here I get the installer + CPlatformDictionary m_platformFiles; //!< the latest file version per platform + CPlatformDictionary m_platformVersions; //!< the version per platform + + //! Extract version number from a file name + static QVersionNumber versionNumberFromFilename(const QString &filename); + + //! Find the file representing the latest version + static QPair findLatestVersion(const QStringList &filenames); + + BLACK_METACLASS( + CDistribution, + BLACK_METAMEMBER(dbKey), + BLACK_METAMEMBER(timestampMSecsSinceEpoch), + BLACK_METAMEMBER(channel), + BLACK_METAMEMBER(downloadUrls), + BLACK_METAMEMBER(platformFiles, 0, DisabledForComparison | DisabledForHashing), + BLACK_METAMEMBER(platformVersions, 0, DisabledForComparison | DisabledForHashing) + ); + }; + + //! Distribution settings: channel/platform + struct TDistributionSetting : public BlackMisc::TSettingTrait + { + //! \copydoc BlackMisc::TSettingTrait::key + static const char *key() { return "distribution"; } + + //! \copydoc BlackMisc::TSettingTrait::defaultValue + static QStringList defaultValue() { static const QStringList d{"", ""}; return d; } + }; + } // ns +} // ns + +Q_DECLARE_METATYPE(BlackMisc::Db::CDistribution) +Q_DECLARE_METATYPE(BlackMisc::Db::CPlatformDictionary) + +#endif // guard diff --git a/src/blackmisc/db/distributionlist.cpp b/src/blackmisc/db/distributionlist.cpp new file mode 100644 index 000000000..e2c0360cc --- /dev/null +++ b/src/blackmisc/db/distributionlist.cpp @@ -0,0 +1,50 @@ +/* Copyright (C) 2017 + * swift Project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +#include "distributionlist.h" +#include "blackmisc/stringutils.h" + +namespace BlackMisc +{ + namespace Db + { + CDistributionList::CDistributionList() { } + + CDistributionList::CDistributionList(const CSequence &other) : + CSequence(other) + { } + + QStringList CDistributionList::getChannels() const + { + QStringList channels; + for (const CDistribution &distribution : *this) + { + if (distribution.getChannel().isEmpty()) { continue; } + channels << distribution.getChannel(); + } + return channels; + } + + CDistribution CDistributionList::findByChannelOrDefault(const QString &channel) const + { + return this->findFirstByOrDefault(&CDistribution::getChannel, channel); + } + + CDistributionList CDistributionList::fromDatabaseJson(const QJsonArray &array) + { + CDistributionList distributions; + for (const QJsonValue &value : array) + { + const CDistribution distribution(CDistribution::fromDatabaseJson(value.toObject())); + distributions.push_back(distribution); + } + return distributions; + } + } // namespace +} // namespace diff --git a/src/blackmisc/db/distributionlist.h b/src/blackmisc/db/distributionlist.h new file mode 100644 index 000000000..c8f0a63de --- /dev/null +++ b/src/blackmisc/db/distributionlist.h @@ -0,0 +1,68 @@ +/* Copyright (C) 2017 + * swift project Community / Contributors + * + * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level + * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, + * including this file, may be copied, modified, propagated, or distributed except according to the terms + * contained in the LICENSE file. + */ + +//! \file + +#ifndef BLACKMISC_DB_DISTRIBUTIONLIST_H +#define BLACKMISC_DB_DISTRIBUTIONLIST_H + +#include "distribution.h" +#include "blackmisc/db/datastoreobjectlist.h" +#include "blackmisc/datacache.h" +#include "blackmisc/blackmiscexport.h" +#include "blackmisc/collection.h" +#include "blackmisc/sequence.h" +#include "blackmisc/variant.h" + +namespace BlackMisc +{ + namespace Db + { + //! Value object encapsulating a list of aircraft models + class BLACKMISC_EXPORT CDistributionList : + public BlackMisc::CSequence, + public BlackMisc::Db::IDatastoreObjectList, + public BlackMisc::Mixin::MetaType + { + public: + BLACKMISC_DECLARE_USING_MIXIN_METATYPE(CDistributionList) + + //! Empty constructor. + CDistributionList(); + + //! Construct from a base class object. + CDistributionList(const CSequence &other); + + //! All channels + QStringList getChannels() const; + + //! Find distribution by channels + CDistribution findByChannelOrDefault(const QString &channel) const; + + //! From database JSON + static CDistributionList fromDatabaseJson(const QJsonArray &array); + }; + + //! Trait for global setup data + struct TDistributionInfo : public BlackMisc::TDataTrait + { + //! Key in data cache + static const char *key() { return "distributions"; } + + //! First load is synchronous + static constexpr bool isPinned() { return true; } + }; + } // ns +} // ns + +Q_DECLARE_METATYPE(BlackMisc::Db::CDistributionList) +Q_DECLARE_METATYPE(BlackMisc::CCollection) +Q_DECLARE_METATYPE(BlackMisc::CSequence) + +#endif //guard diff --git a/src/blackmisc/db/registermetadatadb.cpp b/src/blackmisc/db/registermetadatadb.cpp index d1dc8e98c..6e854856e 100644 --- a/src/blackmisc/db/registermetadatadb.cpp +++ b/src/blackmisc/db/registermetadatadb.cpp @@ -19,6 +19,8 @@ namespace BlackMisc CDbInfo::registerMetadata(); CDbInfoList::registerMetadata(); CDbFlags::registerMetadata(); + CDistribution::registerMetadata(); + CDistributionList::registerMetadata(); } } // ns } // ns diff --git a/src/blackmisc/propertyindex.h b/src/blackmisc/propertyindex.h index ab952f5a9..251511a4f 100644 --- a/src/blackmisc/propertyindex.h +++ b/src/blackmisc/propertyindex.h @@ -137,7 +137,7 @@ namespace BlackMisc GlobalIndexIDatastoreString = 11100, GlobalIndexCDbInfo = 11200, GlobalIndexCGlobalSetup = 12000, - GlobalIndexCUpdateInfo = 12100, + GlobalIndexCDistribution = 12100, GlobalIndexCVatsimSetup = 12200, GlobalIndexCLauncherSetup = 12300, GlobalIndexCGuiStateDbOwnModelsComponent = 14000, diff --git a/src/blackmisc/timestampobjectlist.cpp b/src/blackmisc/timestampobjectlist.cpp index b53f175b8..1d97ab711 100644 --- a/src/blackmisc/timestampobjectlist.cpp +++ b/src/blackmisc/timestampobjectlist.cpp @@ -15,6 +15,7 @@ #include "blackmisc/aviation/airport.h" #include "blackmisc/aviation/airportlist.h" #include "blackmisc/db/dbinfolist.h" +#include "blackmisc/db/distributionlist.h" #include "blackmisc/network/textmessage.h" #include "blackmisc/network/textmessagelist.h" #include "blackmisc/simulation/distributorlist.h" @@ -114,6 +115,15 @@ namespace BlackMisc } } + template + void ITimestampObjectList::setUtcTime(qint64 msSinceEpoch) + { + for (ITimestampBased &tsObj : this->container()) + { + tsObj.setMSecsSinceEpoch(msSinceEpoch); + } + } + template void ITimestampObjectList::setInvalidTimestampsToCurrentUtcTime() { @@ -236,6 +246,7 @@ namespace BlackMisc template class BLACKMISC_EXPORT_DEFINE_TEMPLATE ITimestampObjectList; template class BLACKMISC_EXPORT_DEFINE_TEMPLATE ITimestampObjectList; template class BLACKMISC_EXPORT_DEFINE_TEMPLATE ITimestampObjectList; + template class BLACKMISC_EXPORT_DEFINE_TEMPLATE ITimestampObjectList; template class BLACKMISC_EXPORT_DEFINE_TEMPLATE ITimestampObjectList; template class BLACKMISC_EXPORT_DEFINE_TEMPLATE ITimestampObjectList; template class BLACKMISC_EXPORT_DEFINE_TEMPLATE ITimestampObjectList; diff --git a/src/blackmisc/timestampobjectlist.h b/src/blackmisc/timestampobjectlist.h index 4f92140be..9162037be 100644 --- a/src/blackmisc/timestampobjectlist.h +++ b/src/blackmisc/timestampobjectlist.h @@ -53,6 +53,9 @@ namespace BlackMisc //! Set all timestamps to now void setCurrentUtcTime(); + //! Set all timestamps to given time + void setUtcTime(qint64 msSinceEpoch); + //! Set invalid timestamps to now void setInvalidTimestampsToCurrentUtcTime(); @@ -130,6 +133,8 @@ namespace BlackMisc { class CDbInfo; class CDbInfoList; + class CDistribution; + class CDistributionList; } namespace Simulation @@ -154,6 +159,7 @@ namespace BlackMisc extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE ITimestampObjectList; extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE ITimestampObjectList; extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE ITimestampObjectList; + extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE ITimestampObjectList; extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE ITimestampObjectList; extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE ITimestampObjectList; extern template class BLACKMISC_EXPORT_DECLARE_TEMPLATE ITimestampObjectList;