LT Project: Library Book Records
The full library book record actually exists in two forms. The simpler owns its primary storage, of type IFieldAdder::storage_type; the second holds a templated type which is actually a filtered view of a record of the same type. These two coexist because the first is used where records are simply raed in from file and the second where the source data is stored in-memory for reuse.
Both derive from a somewhat lengthy interface:
class ILibraryBookRecord : public IMinimalLibraryBookRecord,
public IPrintableRecord,
public IFilterableRecord
{
public:
virtual ~ILibraryBookRecord();
virtual bool lessThan(const ILibraryBookRecord &inRec) const = 0;
virtual ILibraryBookRecord *clone() const = 0;
virtual void addToCollection(ILibraryRecordSet &inSet) = 0;
bool
operator<(const ILibraryBookRecord &inRec) const
{
return lessThan(inRec);
}
virtual std::string_view getAuthorLastName() const = 0;
protected:
static bool
IsAuthorExtensionField(const ELibraryRecord inRec)
{
return (inRec == ELibraryRecord::Primary_Author_Role)
|| (inRec == ELibraryRecord::Secondary_Author)
|| (inRec == ELibraryRecord::Secondary_Author_Roles);
}
};
The length of the interface is somewhat hidden by the fact that it is composed of three separate interfaces. IMinimalLibraryBookRecord we have seen already:
class IMinimalLibraryBookRecord
{
public:
virtual ~IMinimalLibraryBookRecord();
virtual Author getAuthor() const = 0;
virtual std::string_view getTitle() const = 0;
virtual int getTitleSortOffset() const = 0;
virtual void setTitleSortOffset(const int inVal) = 0;
};
The IPrintableRecord interface is an immutable interface allowing the formatting of different subsets of the object:
class IPrintableRecord
{
public:
virtual ~IPrintableRecord();
virtual void printField(const ELibraryRecord inRec,
IFormatterWrapper &inFormatter) const = 0;
virtual void printAll(IFormatterWrapper &inFormatter) const = 0;
virtual void printSome(IFormatterWrapper &inFormatter,
std::span<ELibraryRecord> inIncludes) const = 0;
};
The IFilterableRecord interface, again immutable, provides access to the criteria used to filter on the records:
class IFilterableRecord
{
public:
virtual ~IFilterableRecord();
virtual int getId() const = 0;
virtual bool matchesOnAuthor(std::string_view inName) const = 0;
virtual bool matchesOnCollection(const std::string &inName) const = 0;
virtual bool matchesOnTag(const std::string &inName) const = 0;
virtual bool isArchived() const = 0;
virtual bool isElectronic() const = 0;
virtual bool isUnderReconsideration() const = 0;
virtual bool isDeaccessioned() const = 0;
};
The simplest implementation of the interface is the NullObject version:
class NullLibraryBookRecord : public ILibraryBookRecord
{
public:
constexpr NullLibraryBookRecord() {}
~NullLibraryBookRecord() override;
void printField(const ELibraryRecord inRec,
IFormatterWrapper &inFormatter) const override;
void printSome(IFormatterWrapper &inFormatter,
std::span<ELibraryRecord> inIncludes) const override
{ }
void printAll(IFormatterWrapper &inFormatter) const override;
bool matchesOnAuthor(std::string_view inName) const override
{
return false;
}
Author getAuthor() const noexcept override
{
return {};
}
bool matchesOnCollection(const std::string &inName) const override
{
return false;
}
bool matchesOnTag(const std::string &inName) const override
{
return false;
}
bool isArchived() const override
{
return false;
}
bool isElectronic() const override
{
return false;
}
bool isUnderReconsideration() const override
{
return false;
}
bool isDeaccessioned() const override
{
return false;
}
bool lessThan(const ILibraryBookRecord &inRec) const override
{
if (inRec.getId() == 0)
return this < &inRec;
else
return true;
}
NullLibraryBookRecord * clone() const override
{
return new NullLibraryBookRecord();
}
void addToCollection(ILibraryRecordSet &inSet) override
{
}
int getId() const override
{
return 0;
}
std::string_view getTitle() const override
{
return {};
}
int getTitleSortOffset() const override
{
return 0;
}
void setTitleSortOffset(const int inVal) override
{ }
std::string_view getAuthorLastName() const override
{
return {};
}
};
This is interesting principally because the two main formatting functions are not no-ops -- they print out a message indicating a failure to retrieve a record., which is the most common use case for the deployment of the class:
void NullLibraryBookRecord::printField(const ELibraryRecord inRec,
IFormatterWrapper &inFormatter) const
{
inFormatter.format(ELibraryRecord::Library_Record_None,
"No record retrieved");
}
void NullLibraryBookRecord::printAll(IFormatterWrapper &inFormatter) const
{
inFormatter.format(ELibraryRecord::Library_Record_None,
"No record retrieved");
}
The first substantive implementation is the LibraryBookRecord class:
class LibraryBookRecord : public ILibraryBookRecord
{
class IAllocationStrategy
{
public:
virtual ~IAllocationStrategy();
virtual LibraryBookRecord *
getHeapAllocatedCopy(LibraryBookRecord &inVal) const = 0;
};
public:
LibraryBookRecord(const std::string &inRec, const IFieldAdderSet &inAdder,
const bool inBreakOutCollections,
const bool inBreakoutTags);
LibraryBookRecord(const BaseLibraryBookRecord &inRec,
const IFieldAdderSet &inAdder,
const bool inBreakOutCollections,
const bool inBreakoutTags);
void printField(const ELibraryRecord inRec,
IFormatterWrapper &inFormatter) const override;
void printAll(IFormatterWrapper &inFormatter) const override;
void printSome(IFormatterWrapper &inFormatter,
std::span<ELibraryRecord> inIncludes) const override;
bool matchesOnAuthor(std::string_view inName) const override;
Author getAuthor() const override;
bool matchesOnCollection(const std::string &inName) const override;
bool matchesOnTag(const std::string &inName) const override;
bool isArchived() const override;
bool isElectronic() const override;
bool isUnderReconsideration() const override;
bool isDeaccessioned() const override;
bool lessThan(const ILibraryBookRecord &inRec) const override;
LibraryBookRecord *clone() const override;
void addToCollection(ILibraryRecordSet &inSet) override;
std::string_view getTitle() const override;
void setIsOnHeap(const bool inVal)
{
if (inVal)
m_allocationStrategy = &s_HeapStrategy;
else
m_allocationStrategy = &s_StackStrategy;
}
constexpr int getId() const override
{
return m_secondaryData.getId();
}
int getTitleSortOffset() const override
{
return m_secondaryData.getTitleSortOffset();
}
void setTitleSortOffset(const int inVal) override
{
m_secondaryData.setTitleSortOffset(inVal);
}
static void SetBibliographicSort()
{
LibraryBookRecordSecondaryData::SetBibliographicSort();
}
static void SetTitleSort()
{
LibraryBookRecordSecondaryData::SetTitleSort();
}
std::string_view getAuthorLastName() const override;
private:
IFieldAdder::storage_type m_fields;
mutable LibraryBookRecordSecondaryData m_secondaryData;
IAllocationStrategy *m_allocationStrategy;
bool handleSpecialTypes(IFormatterWrapper &inFormatter,
const ELibraryRecord inType,
const std::string &inVal) const;
class StackAllocationStrategy : public IAllocationStrategy
{
public:
~StackAllocationStrategy() override;
LibraryBookRecord *
getHeapAllocatedCopy(LibraryBookRecord &inVal) const override
{
return inVal.clone();
}
};
class HeapAllocationStrategy : public IAllocationStrategy
{
public:
~HeapAllocationStrategy() override;
LibraryBookRecord *
getHeapAllocatedCopy(LibraryBookRecord &inVal) const override
{
return &inVal;
}
};
static StackAllocationStrategy s_StackStrategy;
static HeapAllocationStrategy s_HeapStrategy;
};
You will note that, in the interface, there is a clone() method which returns a bare pointer rather than a unique_ptr. This is deliberate.
During the process of identifying a match for criteria, a temporary instance is created, queried, and usually discarded. In most cases, this is done on the stack to minimize cost. In a few cases, the created version has been allocated on the heap. You will note that the two strategies return a pointer to a heap-allocated copy just by returning the instance itself, but will clone a stack-allocated copy.
clone() marks the instance as having been created on the heap:
LibraryBookRecord *
LibraryBookRecord::clone() const
{
LibraryBookRecord *rval = new LibraryBookRecord(*this);
rval->setIsOnHeap(true);
return rval;
}
and addToCollection(), which does double dispatch with a record set (not yet encountered), makes use of the strategy to return the appropriate pointer.
void LibraryBookRecord::addToCollection(ILibraryRecordSet &inSet)
{
inSet.addToCollection(m_allocationStrategy->getHeapAllocatedCopy(*this));
}
(The ultimate storage is
boost::ptr_vector<ILibraryBookRecord> m_records;
which is another reason why we don't need the unique_ptr -- the memory management is done by the boost container.)
The three printing functions are similar, but handle different slices of the data. printField() finds one entry and prints it if it is found. (The three fields from the AuthorExtension test, defined in the interface, are never printed from the raw data because they get handled in a more processed form.) The UnderReconsideration logic is required for a specific application formatting use.
void LibraryBookRecord::printField(const ELibraryRecord inRec,
IFormatterWrapper &inFormatter) const
{
if (IsAuthorExtensionField(inRec))
return;
if (isUnderReconsideration())
inFormatter.markAsUnderReconsideration();
if (auto iter = m_fields.find(inRec); iter != m_fields.end())
{
if (!handleSpecialTypes(inFormatter, inRec, iter->second))
inFormatter.format(inRec, iter->second);
}
}
printAll() and printSome() are very similar, except in that printSome() formats only a specified list of fields. (printSome() still prints the fields in the order of the underlying field storage. To rearrange the order, iterated calls to printField() are necessary; the alternative would be to accumulate fields for printing on one pass, reorder them to match the control list, and then do output, which would be both more elaborate and expensive than the prior approach.)
void LibraryBookRecord::printAll(IFormatterWrapper &inFormatter) const
{
if (isUnderReconsideration())
inFormatter.markAsUnderReconsideration();
std::ranges::for_each(m_fields | std::views::filter([](const auto &inVal) {
return !IsAuthorExtensionField(inVal.first);
}),
[&](const auto &inRec) {
auto [type, val] = inRec;
if (!handleSpecialTypes(inFormatter, type, val))
inFormatter.format(type, val);
});
}
void LibraryBookRecord::printSome(IFormatterWrapper &inFormatter,
std::span<ELibraryRecord> inIncludes) const
{
if (isUnderReconsideration())
inFormatter.markAsUnderReconsideration();
std::ranges::for_each(
m_fields | std::views::filter([&inIncludes](const auto &inVal) {
return std::ranges::any_of(inIncludes, [&](const auto &inField) {
return inField == inVal.first;
});
}),
[&](const auto &inRec) {
auto [type, val] = inRec;
if (!handleSpecialTypes(inFormatter, type, val))
inFormatter.format(type, val);
});
}
handleSpecialTypes() deals with the fields which have special logic, and sometimes special storage, associated with them.
bool LibraryBookRecord::handleSpecialTypes(IFormatterWrapper &inFormatter,
const ELibraryRecord inType,
const std::string &inVal) const
{
if (inType == ELibraryRecord::Title)
{
inFormatter.formatTitle(inType, inVal,
m_secondaryData.getPostTitleContent());
return true;
}
else if (inType == ELibraryRecord::Primary_Author)
{
std::string role;
if (IFieldAdder::storage_type::const_iterator iter
= m_fields.find(ELibraryRecord::Primary_Author_Role);
iter != m_fields.end())
role = iter->second;
else
role = "Author"s;
inFormatter.formatAuthor(m_secondaryData, inVal, role,
m_secondaryData.getSecondaryAuthors());
return true;
}
return false;
}
Many of the functions for matching defer to the secondary types common data, but a few reference the raw fields, e.g.:
bool LibraryBookRecord::isArchived() const
{
auto iter = m_fields.find(ELibraryRecord::Private_Comment);
return ((iter != m_fields.end())
&& iter->second.contains("Box"s));
}
bool LibraryBookRecord::isElectronic() const
{
auto iter = m_fields.find(ELibraryRecord::Media);
return ((iter != m_fields.end()) && (iter->second == "Ebook"s));
}
bool LibraryBookRecord::isUnderReconsideration() const
{
auto iter = m_fields.find(ELibraryRecord::Collections);
return ((iter != m_fields.end())
&& iter->second.contains("Reconsideration"s));
}
The constructors, as is frequently the case, do much of the work:
LibraryBookRecord::LibraryBookRecord(const std::string &inRec,
const IFieldAdderSet &inAdder,
const bool inBreakOutCollections,
const bool inBreakOutTags)
: m_secondaryData(0, inAdder.isBibliographic()),
m_allocationStrategy(&s_StackStrategy)
{
AdderScoper scoper(inAdder, m_secondaryData);
boost::tokenizer<boost::char_separator<char>> tokens(inRec, theSeparator);
ELibraryRecord type = ELibraryRecord::Book_Id;
std::ranges::for_each(tokens, [&](const auto &inVal) {
if (!inVal.empty())
{
inAdder.addField(m_fields, m_secondaryData, type, inVal);
if (type == ELibraryRecord::Book_Id)
m_secondaryData.setBookId(std::atoi(inVal.c_str()));
else
m_secondaryData.expandEntry(inBreakOutCollections, inBreakOutTags,
type, inVal);
}
else if (type == ELibraryRecord::Primary_Author)
{
inAdder.addField(m_fields, m_secondaryData, type, "Anonymous");
}
type = increment(type);
});
m_secondaryData.rationalizeTitleSortOffset();
}
LibraryBookRecord::LibraryBookRecord(const BaseLibraryBookRecord &inRec,
const IFieldAdderSet &inAdder,
const bool inBreakOutCollections,
const bool inBreakoutTags)
: m_secondaryData(inRec.getId(), inAdder.isBibliographic()),
m_allocationStrategy(&s_StackStrategy)
{
AdderScoper scoper(inAdder, m_secondaryData);
inRec.process([&](const ELibraryRecord type, const std::string &val) {
inAdder.addField(m_fields, m_secondaryData, type, val);
if (type == ELibraryRecord::Title)
m_secondaryData.setTitle(val);
m_secondaryData.expandEntry(inBreakOutCollections, inBreakoutTags, type,
val);
});
m_secondaryData.rationalizeTitleSortOffset();
}
(We'll be loking at BaseLibraryBookRec, the class used for the in-memory database, in the next post. The logic for it is simpler because some of the normalization takes place when its records are stored.)
(AdderScoper is a handy scoping class for setting/resetting the sort offset handle:
class AdderScoper
{
public:
AdderScoper(const IFieldAdderSet &inAdder,
IMinimalLibraryBookRecord &inRecord)
: m_adder(inAdder)
{
m_adder.setSortOffset(inRecord);
}
~AdderScoper() { m_adder.resetSortOffset(); }
private:
const IFieldAdderSet &m_adder;
};
)
Comments
Post a Comment