add first parts of cereal extension
This commit is contained in:
parent
82fab9f7dc
commit
36a22f51de
24 changed files with 379 additions and 16 deletions
|
@ -51,6 +51,23 @@ else()
|
|||
set(ENABLE_hdf5 TRUE CACHE BOOL "enable hdf5")
|
||||
endif()
|
||||
|
||||
if(DEFINED ENABLE_cereal)
|
||||
set(ENABLE_cereal ${ENABLE_cereal} CACHE BOOL "enable hdf5")
|
||||
else()
|
||||
set(ENABLE_cereal TRUE CACHE BOOL "enable hdf5")
|
||||
endif()
|
||||
|
||||
find_package(cereal QUIET)
|
||||
if(cereal_FOUND)
|
||||
message(STATUS "found cereal")
|
||||
add_definitions(-DHAVE_CEREAL)
|
||||
else()
|
||||
message(STATUS "no cereal")
|
||||
if(ENABLE_cereal)
|
||||
message(FATAL_ERROR "cereal has been enabled but the cereal library has not been found")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_definitions(-DVERSION="${VERSION}")
|
||||
add_definitions(-DGIT_COMMIT="${GIT_HASH}")
|
||||
add_definitions(-DCXX_FLAGS="${CMAKE_CXX_FLAGS}")
|
||||
|
|
|
@ -8,4 +8,8 @@ if(ENABLE_hdf5)
|
|||
add_subdirectory(opt/hdf5)
|
||||
endif()
|
||||
|
||||
if(ENABLE_cereal)
|
||||
add_subdirectory(opt/cereal)
|
||||
endif()
|
||||
|
||||
install(DIRECTORY include/ DESTINATION ${INSTALL_PATH}/include/cnorxz)
|
||||
|
|
|
@ -30,6 +30,8 @@ namespace CNORXZ
|
|||
virtual const_iterator cend() const override;
|
||||
virtual bool isView() const override;
|
||||
|
||||
SERIALIZATION_FUNCTIONS;
|
||||
|
||||
private:
|
||||
Vector<T> mCont;
|
||||
};
|
||||
|
|
|
@ -38,4 +38,15 @@
|
|||
#define IS_SAME(a,b) std::is_same<a,b>::value
|
||||
#define IS_NOT_SAME(a,b) (not std::is_same<a,b>::value)
|
||||
|
||||
#ifdef HAVE_CEREAL
|
||||
#define SERIALIZATION_FUNCTIONS template <class Archive> void save(Archive& ar) const; \
|
||||
template <class Archive> void load(Archive& ar)
|
||||
#define SERIALIZATION_FUNCTIONS_NOPUB friend class cereal::access; \
|
||||
template <class Archive> void save(Archive& ar) const; \
|
||||
template <class Archive> void load(Archive& ar)
|
||||
#else
|
||||
#define SERIALIZATION_FUNCTIONS
|
||||
#define SERIALIZATION_FUNCTIONS_NOPUB
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
|
|
@ -100,7 +100,11 @@ namespace CNORXZ
|
|||
CRange(const CRange& in) = delete;
|
||||
CRange(SizeT size);
|
||||
|
||||
virtual Vector<Uuid> key() const override final;
|
||||
|
||||
SizeT mSize = 0;
|
||||
|
||||
SERIALIZATION_FUNCTIONS_NOPUB;
|
||||
};
|
||||
|
||||
template <>
|
||||
|
|
|
@ -529,15 +529,15 @@ namespace CNORXZ
|
|||
template <class... Ranges>
|
||||
void MRangeFactory<Ranges...>::make()
|
||||
{
|
||||
Vector<Uuid> key = iter<0,sizeof...(Ranges)>
|
||||
( [&](auto i) { return std::get<i>( mRs ); },
|
||||
[](const auto&... e) { return Vector<Uuid> { e->id()... }; } );
|
||||
const auto& info = typeid(MRange<Ranges...>);
|
||||
if(mRef != nullptr) {
|
||||
mProd = this->fromCreated(info, {mRef->id()});
|
||||
}
|
||||
mProd = this->fromCreated(info, key);
|
||||
if(mProd == nullptr) {
|
||||
RangePtr key = mProd = std::shared_ptr<MRange<Ranges...>>
|
||||
mProd = std::shared_ptr<MRange<Ranges...>>
|
||||
( new MRange<Ranges...>( mRs ) );
|
||||
if(mRef != nullptr) { key = mRef; }
|
||||
this->addToCreated(info, { key->id() }, mProd);
|
||||
this->addToCreated(info, key, mProd);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -621,6 +621,15 @@ namespace CNORXZ
|
|||
return MRangeFactory<Ranges...>( rs ).create();
|
||||
}
|
||||
|
||||
template <class... Ranges>
|
||||
Vector<Uuid> MRange<Ranges...>::key() const
|
||||
{
|
||||
Vector<Uuid> key = iter<0,sizeof...(Ranges)>
|
||||
( [&](auto i) { return std::get<i>( mRs ); },
|
||||
[](const auto&... e) { return Vector<Uuid> { e->id()... }; } );
|
||||
return key;
|
||||
}
|
||||
|
||||
/************************
|
||||
* MRange (private) *
|
||||
************************/
|
||||
|
|
|
@ -187,13 +187,15 @@ namespace CNORXZ
|
|||
SizeT getMeta(const MetaType& metaPos) const;
|
||||
|
||||
protected:
|
||||
MRange() = delete;
|
||||
MRange() = default;
|
||||
MRange(const MRange& in) = delete;
|
||||
MRange& operator=(const MRange& in) = delete;
|
||||
MRange(const Tuple<Sptr<Ranges>...>& rs);
|
||||
|
||||
Tuple<Sptr<Ranges>...> mRs;
|
||||
Arr<RangePtr,NR> mA;
|
||||
|
||||
virtual Vector<Uuid> key() const override final;
|
||||
private:
|
||||
|
||||
decltype(auto) mkA() const;
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
#include "memory/memory.h"
|
||||
#include "memory/memory.cc.h"
|
||||
|
||||
#ifdef HAVE_CEREAL
|
||||
#include <cereal/access.hpp>
|
||||
#endif
|
||||
|
||||
namespace CNORXZ
|
||||
{
|
||||
|
||||
|
@ -20,6 +24,9 @@ namespace CNORXZ
|
|||
|
||||
RangePtr create();
|
||||
|
||||
static RangePtr getRegistered(const TypeInfo& info, const RangePtr& r);
|
||||
// check if range with uuid of r exists; if yes, return existing one, else add r and return it
|
||||
|
||||
protected:
|
||||
|
||||
virtual void make() = 0;
|
||||
|
@ -36,9 +43,6 @@ namespace CNORXZ
|
|||
|
||||
};
|
||||
|
||||
Sptr<RangeFactoryBase> createRangeFactory(const char** dp);
|
||||
Sptr<RangeFactoryBase> createSingleRangeFactory(const Vector<char>*& d, int metaType, SizeT size);
|
||||
|
||||
class RangeBase
|
||||
{
|
||||
public:
|
||||
|
@ -70,6 +74,9 @@ namespace CNORXZ
|
|||
|
||||
RangeBase();
|
||||
RangeBase(const RangePtr& rel);
|
||||
|
||||
virtual Vector<Uuid> key() const = 0;
|
||||
|
||||
// delete copy/move???
|
||||
Uuid mId = {0,0};
|
||||
Wptr<RangeBase> mThis;
|
||||
|
|
|
@ -296,6 +296,12 @@ namespace CNORXZ
|
|||
return URangeFactory<MetaType>( space ).create();
|
||||
}
|
||||
|
||||
template <typename MetaType>
|
||||
Vector<Uuid> URange<MetaType>::key() const
|
||||
{
|
||||
return Vector<Uuid> { this->id() };
|
||||
}
|
||||
|
||||
/*******************
|
||||
* Range Casts *
|
||||
*******************/
|
||||
|
|
|
@ -104,12 +104,16 @@ namespace CNORXZ
|
|||
|
||||
private:
|
||||
|
||||
URange() = delete;
|
||||
URange() = default;
|
||||
URange(const URange& in) = delete;
|
||||
URange(const Vector<MetaType>& space);
|
||||
URange(Vector<MetaType>&& space);
|
||||
|
||||
Vector<MetaType> mSpace;
|
||||
|
||||
virtual Vector<Uuid> key() const override final;
|
||||
|
||||
SERIALIZATION_FUNCTIONS_NOPUB;
|
||||
};
|
||||
|
||||
template <typename MetaType>
|
||||
|
|
|
@ -108,6 +108,7 @@ namespace CNORXZ
|
|||
|
||||
Vector<RangePtr> mRVec;
|
||||
RangePtr mRef;
|
||||
|
||||
};
|
||||
|
||||
class YRange : public RangeInterface<YRange>
|
||||
|
@ -128,12 +129,16 @@ namespace CNORXZ
|
|||
|
||||
private:
|
||||
|
||||
YRange() = delete;
|
||||
YRange() = default;
|
||||
YRange(const YRange& a) = delete;
|
||||
YRange(const Vector<RangePtr>& rvec);
|
||||
YRange(Vector<RangePtr>&& rvec);
|
||||
|
||||
Vector<RangePtr> mRVec;
|
||||
|
||||
virtual Vector<Uuid> key() const override final;
|
||||
|
||||
SERIALIZATION_FUNCTIONS_NOPUB;
|
||||
};
|
||||
|
||||
RangePtr yrange(const Vector<RangePtr>& rs);
|
||||
|
|
|
@ -192,6 +192,11 @@ namespace CNORXZ
|
|||
return CRangeFactory( this->size() + rx->size() ).create();
|
||||
}
|
||||
|
||||
Vector<Uuid> CRange::key() const
|
||||
{
|
||||
return Vector<Uuid> { this->id() };
|
||||
}
|
||||
|
||||
/*******************
|
||||
* Range Casts *
|
||||
*******************/
|
||||
|
|
|
@ -35,6 +35,15 @@ namespace CNORXZ
|
|||
sCreated[info.hash_code()][rids] = r;
|
||||
}
|
||||
|
||||
RangePtr RangeFactoryBase::getRegistered(const TypeInfo& info, const RangePtr& r)
|
||||
{
|
||||
auto& rx = sCreated[info.hash_code()][r->key()];
|
||||
if(rx == nullptr){
|
||||
rx = r;
|
||||
}
|
||||
return rx;
|
||||
}
|
||||
|
||||
/******************
|
||||
* RangeBase *
|
||||
******************/
|
||||
|
|
|
@ -505,6 +505,14 @@ namespace CNORXZ
|
|||
return YRangeFactory( rvec ).create();
|
||||
}
|
||||
|
||||
Vector<Uuid> YRange::key() const
|
||||
{
|
||||
Vector<Uuid> key(mRVec.size());
|
||||
std::transform(mRVec.begin(), mRVec.end(), key.begin(),
|
||||
[&](const RangePtr& r) { return r->id(); } );
|
||||
return key;
|
||||
}
|
||||
|
||||
YRange::YRange(const Vector<RangePtr>& rvec) : mRVec(rvec) {}
|
||||
|
||||
YRange::YRange(Vector<RangePtr>&& rvec) : mRVec(std::forward<Vector<RangePtr>>(rvec)) {}
|
||||
|
|
6
src/opt/cereal/CMakeLists.txt
Normal file
6
src/opt/cereal/CMakeLists.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
|
||||
add_subdirectory(tests)
|
||||
|
||||
install(DIRECTORY include/ DESTINATION ${INSTALL_PATH}/include/cnorxz/cereal)
|
|
@ -1 +0,0 @@
|
|||
coming soon
|
28
src/opt/cereal/include/cer_array.cc.h
Normal file
28
src/opt/cereal/include/cer_array.cc.h
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
#ifndef __cxz_cereal_array_cc_h__
|
||||
#define __cxz_cereal_array_cc_h__
|
||||
|
||||
#include "base/base.h"
|
||||
#include "array/marray.h"
|
||||
#include <cereal/cereal.hpp>
|
||||
|
||||
namespace CNORXZ
|
||||
{
|
||||
template <typename T>
|
||||
template <class Archive>
|
||||
void MArray<T>::save(Archive& ar) const
|
||||
{
|
||||
ar(cereal::make_nvp("range", AB::mRange));
|
||||
ar(cereal::make_nvp("data", mCont));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
template <class Archive>
|
||||
void MArray<T>::load(Archive& ar)
|
||||
{
|
||||
ar(cereal::make_nvp("range", AB::mRange));
|
||||
ar(cereal::make_nvp("data", mCont));
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
91
src/opt/cereal/include/cer_ranges.cc.h
Normal file
91
src/opt/cereal/include/cer_ranges.cc.h
Normal file
|
@ -0,0 +1,91 @@
|
|||
|
||||
#ifndef __cxz_cereal_ranges_cc_h__
|
||||
#define __cxz_cereal_ranges_cc_h__
|
||||
|
||||
#include <cereal/cereal.hpp>
|
||||
#include <cereal/types/vector.hpp>
|
||||
|
||||
#include "base/base.h"
|
||||
#include "ranges/crange.h"
|
||||
#include "ranges/urange.h"
|
||||
#include "ranges/yrange.h"
|
||||
|
||||
namespace CNORXZ
|
||||
{
|
||||
/**************
|
||||
* save *
|
||||
**************/
|
||||
|
||||
template <class Archive>
|
||||
void save(Archive& ar, const Uuid& id)
|
||||
{
|
||||
ar(CEREAL_NVP(id.i1));
|
||||
ar(CEREAL_NVP(id.i2));
|
||||
}
|
||||
|
||||
template <class Archive>
|
||||
void CRange::save(Archive& ar) const
|
||||
{
|
||||
ar(cereal::make_nvp("uuid", RB::mId));
|
||||
ar(cereal::make_nvp("this", RB::mThis));
|
||||
ar(cereal::make_nvp("size", mSize));
|
||||
}
|
||||
|
||||
template <class MetaT>
|
||||
template <class Archive>
|
||||
void URange<MetaT>::save(Archive& ar) const
|
||||
{
|
||||
ar(cereal::make_nvp("uuid", RB::mId));
|
||||
ar(cereal::make_nvp("this", RB::mThis));
|
||||
ar(cereal::make_nvp("meta", mSpace));
|
||||
}
|
||||
|
||||
template <class Archive>
|
||||
void YRange::save(Archive& ar) const
|
||||
{
|
||||
ar(cereal::make_nvp("uuid", RB::mId));
|
||||
ar(cereal::make_nvp("this", RB::mThis));
|
||||
ar(cereal::make_nvp("sub", mRVec));
|
||||
}
|
||||
|
||||
/**************
|
||||
* load *
|
||||
**************/
|
||||
|
||||
template <class Archive>
|
||||
void load(Archive& ar, Uuid& id)
|
||||
{
|
||||
ar(CEREAL_NVP(id.i1));
|
||||
ar(CEREAL_NVP(id.i2));
|
||||
}
|
||||
|
||||
template <class Archive>
|
||||
void CRange::load(Archive& ar)
|
||||
{
|
||||
ar(cereal::make_nvp("uuid", RB::mId));
|
||||
ar(cereal::make_nvp("this", RB::mThis));
|
||||
ar(cereal::make_nvp("size", mSize));
|
||||
CXZ_ASSERT(RangePtr(RB::mThis).get() == this, "got corrupted range data"); // yes, cereal is that awesome! :)
|
||||
}
|
||||
|
||||
template <class MetaT>
|
||||
template <class Archive>
|
||||
void URange<MetaT>::load(Archive& ar)
|
||||
{
|
||||
ar(cereal::make_nvp("uuid", RB::mId));
|
||||
ar(cereal::make_nvp("this", RB::mThis));
|
||||
ar(cereal::make_nvp("meta", mSpace));
|
||||
CXZ_ASSERT(RangePtr(RB::mThis).get() == this, "got corrupted range data");
|
||||
}
|
||||
|
||||
template <class Archive>
|
||||
void YRange::load(Archive& ar)
|
||||
{
|
||||
ar(cereal::make_nvp("uuid", RB::mId));
|
||||
ar(cereal::make_nvp("this", RB::mThis));
|
||||
ar(cereal::make_nvp("sub", mRVec));
|
||||
CXZ_ASSERT(RangePtr(RB::mThis).get() == this, "got corrupted range data");
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
38
src/opt/cereal/include/cer_type_register.cc.h
Normal file
38
src/opt/cereal/include/cer_type_register.cc.h
Normal file
|
@ -0,0 +1,38 @@
|
|||
|
||||
#ifndef __cxz_cereal_type_register_cc_h__
|
||||
#define __cxz_cereal_type_register_cc_h__
|
||||
|
||||
#include "cer_ranges.cc.h"
|
||||
#include <cereal/types/polymorphic.hpp>
|
||||
|
||||
CEREAL_REGISTER_TYPE(CNORXZ::CRange);
|
||||
CEREAL_REGISTER_POLYMORPHIC_RELATION(CNORXZ::RangeBase, CNORXZ::CRange);
|
||||
|
||||
CEREAL_REGISTER_TYPE(CNORXZ::URange<CNORXZ::SizeT>);
|
||||
CEREAL_REGISTER_POLYMORPHIC_RELATION(CNORXZ::RangeBase, CNORXZ::URange<CNORXZ::SizeT>);
|
||||
|
||||
CEREAL_REGISTER_TYPE(CNORXZ::URange<CNORXZ::Int>);
|
||||
CEREAL_REGISTER_POLYMORPHIC_RELATION(CNORXZ::RangeBase, CNORXZ::URange<CNORXZ::Int>);
|
||||
|
||||
CEREAL_REGISTER_TYPE(CNORXZ::URange<CNORXZ::Double>);
|
||||
CEREAL_REGISTER_POLYMORPHIC_RELATION(CNORXZ::RangeBase, CNORXZ::URange<CNORXZ::Double>);
|
||||
|
||||
CEREAL_REGISTER_TYPE(CNORXZ::URange<CNORXZ::String>);
|
||||
CEREAL_REGISTER_POLYMORPHIC_RELATION(CNORXZ::RangeBase, CNORXZ::URange<CNORXZ::String>);
|
||||
|
||||
CEREAL_REGISTER_TYPE(CNORXZ::URange<CNORXZ::Vector<CNORXZ::SizeT>>);
|
||||
CEREAL_REGISTER_POLYMORPHIC_RELATION(CNORXZ::RangeBase, CNORXZ::URange<CNORXZ::Vector<CNORXZ::SizeT>>);
|
||||
|
||||
CEREAL_REGISTER_TYPE(CNORXZ::URange<CNORXZ::Vector<CNORXZ::Int>>);
|
||||
CEREAL_REGISTER_POLYMORPHIC_RELATION(CNORXZ::RangeBase, CNORXZ::URange<CNORXZ::Vector<CNORXZ::Int>>);
|
||||
|
||||
CEREAL_REGISTER_TYPE(CNORXZ::URange<CNORXZ::Vector<CNORXZ::Double>>);
|
||||
CEREAL_REGISTER_POLYMORPHIC_RELATION(CNORXZ::RangeBase, CNORXZ::URange<CNORXZ::Vector<CNORXZ::Double>>);
|
||||
|
||||
CEREAL_REGISTER_TYPE(CNORXZ::URange<CNORXZ::Vector<CNORXZ::String>>);
|
||||
CEREAL_REGISTER_POLYMORPHIC_RELATION(CNORXZ::RangeBase, CNORXZ::URange<CNORXZ::Vector<CNORXZ::String>>);
|
||||
|
||||
CEREAL_REGISTER_TYPE(CNORXZ::YRange);
|
||||
CEREAL_REGISTER_POLYMORPHIC_RELATION(CNORXZ::RangeBase, CNORXZ::YRange);
|
||||
|
||||
#endif
|
6
src/opt/cereal/include/cnorxz_cereal.h
Normal file
6
src/opt/cereal/include/cnorxz_cereal.h
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
#include "range_save_load.h"
|
||||
#include "range_save_load.cc.h"
|
||||
#include "cer_array.cc.h"
|
||||
#include "cer_ranges.cc.h"
|
||||
#include "cer_type_register.cc.h"
|
22
src/opt/cereal/include/range_save_load.cc.h
Normal file
22
src/opt/cereal/include/range_save_load.cc.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
|
||||
#ifndef __range_save_load_cc_h__
|
||||
#define __range_save_load_cc_h__
|
||||
|
||||
#include "ranges/range_base.h"
|
||||
#include "range_save_load.h"
|
||||
|
||||
namespace CNORXZ
|
||||
{
|
||||
namespace cer
|
||||
{
|
||||
template <class Archive>
|
||||
RangePtr save_load(Archive& ar, RangePtr& r)
|
||||
{
|
||||
ar(r);
|
||||
return r = RangeFactoryBase::getRegistered(r->type(), r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif
|
16
src/opt/cereal/include/range_save_load.h
Normal file
16
src/opt/cereal/include/range_save_load.h
Normal file
|
@ -0,0 +1,16 @@
|
|||
|
||||
#ifndef __range_save_load_h__
|
||||
#define __range_save_load_h__
|
||||
|
||||
#include "base/base.h"
|
||||
|
||||
namespace CNORXZ
|
||||
{
|
||||
namespace cer
|
||||
{
|
||||
template <class Archive>
|
||||
RangePtr save_load(Archive& ar, RangePtr& r);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
5
src/opt/cereal/tests/CMakeLists.txt
Normal file
5
src/opt/cereal/tests/CMakeLists.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
add_executable(certest cereal_unit_test.cc)
|
||||
add_dependencies(certest cnorxz)
|
||||
target_link_libraries(certest ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} cnorxz)
|
||||
add_test(NAME certest COMMAND certest)
|
59
src/opt/cereal/tests/cereal_unit_test.cc
Normal file
59
src/opt/cereal/tests/cereal_unit_test.cc
Normal file
|
@ -0,0 +1,59 @@
|
|||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include "cnorxz.h"
|
||||
#include "cnorxz_cereal.h"
|
||||
#include <cereal/archives/json.hpp>
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace CNORXZ;
|
||||
|
||||
class CerCRange_Test : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
|
||||
CerCRange_Test()
|
||||
{
|
||||
mSize = 7;
|
||||
mR = CRangeFactory(mSize).create();
|
||||
}
|
||||
|
||||
SizeT mSize;
|
||||
RangePtr mR;
|
||||
std::stringstream mS;
|
||||
};
|
||||
|
||||
TEST_F(CerCRange_Test, Serialize)
|
||||
{
|
||||
{
|
||||
cereal::JSONOutputArchive ar(mS);
|
||||
ar(mR);
|
||||
}
|
||||
std::cout << mS.str() << std::endl;
|
||||
RangePtr r = nullptr;
|
||||
{
|
||||
cereal::JSONInputArchive ar(mS);
|
||||
ar(r);
|
||||
}
|
||||
EXPECT_EQ(r->size(), mR->size());
|
||||
EXPECT_EQ(r->dim(), mR->dim());
|
||||
auto i1 = r->begin();
|
||||
auto i2 = mR->begin();
|
||||
auto i1e = r->end();
|
||||
auto i2e = mR->end();
|
||||
for(; i1 != i1e; ++i1, ++i2){
|
||||
EXPECT_TRUE(*i1 == *i2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
Loading…
Reference in a new issue