You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tellico/src/entry.cpp

482 lines
15 KiB

/***************************************************************************
copyright : (C) 2001-2006 by Robby Stephenson
email : robby@periapsis.org
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of version 2 of the GNU General Public License as *
* published by the Free Software Foundation; *
* *
***************************************************************************/
#include "entry.h"
#include "collection.h"
#include "field.h"
#include "translators/bibtexhandler.h" // needed for BibtexHandler::cleanText()
#include "document.h"
#include "tellico_debug.h"
#include "tellico_utils.h"
#include "tellico_debug.h"
#include "latin1literal.h"
#include "../isbnvalidator.h"
#include "../lccnvalidator.h"
#include <tdelocale.h>
#include <tqregexp.h>
using Tellico::Data::Entry;
using Tellico::Data::EntryGroup;
EntryGroup::EntryGroup(const TQString& group, const TQString& field)
: TQObject(), EntryVec(), m_group(Tellico::shareString(group)), m_field(Tellico::shareString(field)) {
}
EntryGroup::~EntryGroup() {
// need a copy since we remove ourselves
EntryVec vec = *this;
for(Data::EntryVecIt entry = vec.begin(); entry != vec.end(); ++entry) {
entry->removeFromGroup(this);
}
}
bool Entry::operator==(const Entry& e1) {
// special case for file catalog, just check the url
if(m_coll && m_coll->type() == Collection::File &&
e1.m_coll && e1.m_coll->type() == Collection::File) {
// don't forget case where both could have empty urls
// but different values for other fields
TQString u = field(TQString::fromLatin1("url"));
if(!u.isEmpty()) {
// versions before 1.2.7 could have saved the url without the protocol
bool b = KURL::fromPathOrURL(u) == KURL::fromPathOrURL(e1.field(TQString::fromLatin1("url")));
if(b) {
return true;
} else {
Data::FieldPtr f = m_coll->fieldByName(TQString::fromLatin1("url"));
if(f && f->property(TQString::fromLatin1("relative")) == Latin1Literal("true")) {
return KURL(Document::self()->URL(), u) == KURL::fromPathOrURL(e1.field(TQString::fromLatin1("url")));
}
}
}
}
if(e1.m_fields.count() != m_fields.count()) {
return false;
}
for(StringMap::ConstIterator it = e1.m_fields.begin(); it != e1.m_fields.end(); ++it) {
if(!m_fields.contains(it.key()) || m_fields[it.key()] != it.data()) {
return false;
}
}
return true;
}
Entry::Entry(CollPtr coll_) : TDEShared(), m_coll(coll_), m_id(-1) {
#ifndef NDEBUG
if(!coll_) {
kdWarning() << "Entry() - null collection pointer!" << endl;
}
#endif
}
Entry::Entry(CollPtr coll_, int id_) : TDEShared(), m_coll(coll_), m_id(id_) {
#ifndef NDEBUG
if(!coll_) {
kdWarning() << "Entry() - null collection pointer!" << endl;
}
#endif
}
Entry::Entry(const Entry& entry_) :
TDEShared(entry_),
m_coll(entry_.m_coll),
m_id(-1),
m_fields(entry_.m_fields),
m_formattedFields(entry_.m_formattedFields) {
}
Entry& Entry::operator=(const Entry& other_) {
if(this == &other_) return *this;
// myDebug() << "Entry::operator=()" << endl;
static_cast<TDEShared&>(*this) = static_cast<const TDEShared&>(other_);
m_coll = other_.m_coll;
m_id = other_.m_id;
m_fields = other_.m_fields;
m_formattedFields = other_.m_formattedFields;
return *this;
}
Entry::~Entry() {
}
Tellico::Data::CollPtr Entry::collection() const {
return m_coll;
}
void Entry::setCollection(CollPtr coll_) {
if(coll_ == m_coll) {
myDebug() << "Entry::setCollection() - already belongs to collection!" << endl;
return;
}
// special case adding a book to a bibtex collection
// it would be better to do this in a real OOO way, but this should work
const bool addEntryType = m_coll->type() == Collection::Book &&
coll_->type() == Collection::Bibtex &&
!m_coll->hasField(TQString::fromLatin1("entry-type"));
m_coll = coll_;
m_id = -1;
// set this after changing the m_coll pointer since setField() checks field validity
if(addEntryType) {
setField(TQString::fromLatin1("entry-type"), TQString::fromLatin1("book"));
}
}
TQString Entry::title() const {
return formattedField(TQString::fromLatin1("title"));
}
TQString Entry::field(Data::FieldPtr field_, bool formatted_/*=false*/) const {
return field(field_->name(), formatted_);
}
TQString Entry::field(const TQString& fieldName_, bool formatted_/*=false*/) const {
if(formatted_) {
return formattedField(fieldName_);
}
FieldPtr f = m_coll->fieldByName(fieldName_);
if(!f) {
return TQString();
}
if(f->type() == Field::Dependent) {
return dependentValue(this, f->description(), false);
}
if(!m_fields.isEmpty() && m_fields.contains(fieldName_)) {
return m_fields[fieldName_];
}
return TQString();
}
TQString Entry::formattedField(Data::FieldPtr field_) const {
return formattedField(field_->name());
}
TQString Entry::formattedField(const TQString& fieldName_) const {
FieldPtr f = m_coll->fieldByName(fieldName_);
if(!f) {
return TQString();
}
Field::FormatFlag flag = f->formatFlag();
if(f->type() == Field::Dependent) {
if(flag == Field::FormatNone) {
return dependentValue(this, f->description(), false);
} else {
// format sub fields and whole string
return Field::format(dependentValue(this, f->description(), true), flag);
}
}
// if auto format is not set or FormatNone, then just return the value
if(flag == Field::FormatNone) {
return field(fieldName_);
}
if(m_formattedFields.isEmpty() || !m_formattedFields.contains(fieldName_)) {
TQString value = field(fieldName_);
if(!value.isEmpty()) {
// special for Bibtex collections
if(m_coll->type() == Collection::Bibtex) {
BibtexHandler::cleanText(value);
}
value = Field::format(value, flag);
m_formattedFields.insert(fieldName_, value);
}
return value;
}
// otherwise, just look it up
return m_formattedFields[fieldName_];
}
TQStringList Entry::fields(Data::FieldPtr field_, bool formatted_) const {
return fields(field_->name(), formatted_);
}
TQStringList Entry::fields(const TQString& field_, bool formatted_) const {
TQString s = formatted_ ? formattedField(field_) : field(field_);
if(s.isEmpty()) {
return TQStringList();
}
return Field::split(s, true);
}
bool Entry::setField(Data::FieldPtr field_, const TQString& value_) {
return setField(field_->name(), value_);
}
bool Entry::setField(const TQString& name_, const TQString& value_) {
if(name_.isEmpty()) {
kdWarning() << "Entry::setField() - empty field name for value: " << value_ << endl;
return false;
}
// an empty value means remove the field
if(value_.isEmpty()) {
if(!m_fields.isEmpty() && m_fields.contains(name_)) {
m_fields.remove(name_);
}
invalidateFormattedFieldValue(name_);
return true;
}
#ifndef NDEBUG
if(m_coll && (m_coll->fields().count() == 0 || !m_coll->hasField(name_))) {
myDebug() << "Entry::setField() - unknown collection entry field - "
<< name_ << endl;
return false;
}
#endif
if(m_coll && !m_coll->isAllowed(name_, value_)) {
myDebug() << "Entry::setField() - for " << name_
<< ", value is not allowed - " << value_ << endl;
return false;
}
Data::FieldPtr f = m_coll->fieldByName(name_);
if(!f) {
return false;
}
// the string store is probable only useful for fields with auto-completion or choice/number/bool
bool shareType = f->type() == Field::Choice ||
f->type() == Field::Bool ||
f->type() == Field::Image ||
f->type() == Field::Rating ||
f->type() == Field::Number;
if(!(f->flags() & Field::AllowMultiple) &&
(shareType ||
(f->type() == Field::Line && (f->flags() & Field::AllowCompletion)))) {
m_fields.insert(Tellico::shareString(name_), Tellico::shareString(value_));
} else {
m_fields.insert(Tellico::shareString(name_), value_);
}
invalidateFormattedFieldValue(name_);
return true;
}
bool Entry::addToGroup(EntryGroup* group_) {
if(!group_ || m_groups.contains(group_)) {
return false;
}
m_groups.push_back(group_);
group_->append(this);
// m_coll->groupModified(group_);
return true;
}
bool Entry::removeFromGroup(EntryGroup* group_) {
// if the removal isn't successful, just return
bool success = m_groups.remove(group_);
success = success && group_->remove(this);
// myDebug() << "Entry::removeFromGroup() - removing from group - "
// << group_->fieldName() << "::" << group_->groupName() << endl;
if(success) {
// m_coll->groupModified(group_);
} else {
myDebug() << "Entry::removeFromGroup() failed! " << endl;
}
return success;
}
void Entry::clearGroups() {
m_groups.clear();
}
// this function gets called before m_groups is updated. In fact, it is used to
// update that list. This is the function that actually parses the field values
// and returns the list of the group names.
TQStringList Entry::groupNamesByFieldName(const TQString& fieldName_) const {
// myDebug() << "Entry::groupsByfieldName() - " << fieldName_ << endl;
FieldPtr f = m_coll->fieldByName(fieldName_);
// easy if not allowing multiple values
if(!(f->flags() & Field::AllowMultiple)) {
TQString value = formattedField(fieldName_);
if(value.isEmpty()) {
return i18n(Collection::s_emptyGroupTitle);
} else {
return value;
}
}
TQStringList groups = fields(fieldName_, true);
if(groups.isEmpty()) {
return i18n(Collection::s_emptyGroupTitle);
} else if(f->type() == Field::Table) {
// quick hack for tables, how often will a user have "::" in their value?
// only use first column for group
TQStringList::Iterator it = groups.begin();
while(it != groups.end()) {
(*it) = (*it).section(TQString::fromLatin1("::"), 0, 0);
if((*it).isEmpty()) {
it = groups.remove(it); // points to next in list
} else {
++it;
}
}
}
return groups;
}
bool Entry::isOwned() {
return (m_coll && m_id > -1 && m_coll->entryCount() > 0 && m_coll->entries().contains(this));
}
// a null string means invalidate all
void Entry::invalidateFormattedFieldValue(const TQString& name_) {
if(name_.isNull()) {
m_formattedFields.clear();
} else if(!m_formattedFields.isEmpty() && m_formattedFields.contains(name_)) {
m_formattedFields.remove(name_);
}
}
// format is something like "%{year} %{author}"
TQString Entry::dependentValue(ConstEntryPtr entry_, const TQString& format_, bool formatted_) {
if(!entry_) {
return format_;
}
TQString result, fieldName;
FieldPtr field;
int endPos;
int curPos = 0;
int pctPos = format_.find('%', curPos);
while(pctPos != -1 && pctPos+1 < static_cast<int>(format_.length())) {
if(format_[pctPos+1] == '{') {
endPos = format_.find('}', pctPos+2);
if(endPos > -1) {
result += format_.mid(curPos, pctPos-curPos);
fieldName = format_.mid(pctPos+2, endPos-pctPos-2);
field = entry_->collection()->fieldByName(fieldName);
if(!field) {
// allow the user to also use field titles
field = entry_->collection()->fieldByTitle(fieldName);
}
if(field) {
// don't format, just capitalize
result += entry_->field(field, formatted_);
} else if(fieldName == Latin1Literal("id")) {
result += TQString::number(entry_->id());
} else {
result += format_.mid(pctPos, endPos-pctPos+1);
}
curPos = endPos+1;
} else {
break;
}
} else {
result += format_.mid(curPos, pctPos-curPos+1);
curPos = pctPos+1;
}
pctPos = format_.find('%', curPos);
}
result += format_.mid(curPos, format_.length()-curPos);
// myDebug() << "Entry::dependentValue() - " << format_ << " = " << result << endl;
// sometimes field value might empty, resulting in multiple consecutive white spaces
// so let's simplify that...
return result.simplifyWhiteSpace();
}
int Entry::compareValues(EntryPtr e1, EntryPtr e2, const TQString& f, ConstCollPtr c) {
return compareValues(e1, e2, c->fieldByName(f));
}
int Entry::compareValues(EntryPtr e1, EntryPtr e2, FieldPtr f) {
if(!e1 || !e2 || !f) {
return 0;
}
TQString s1 = e1->field(f).lower();
TQString s2 = e2->field(f).lower();
if(s1.isEmpty() || s2.isEmpty()) {
return 0;
}
// complicated string matching, here are the cases I want to match
// "bend it like beckham" == "bend it like beckham (widescreen edition)"
// "the return of the king" == "return of the king"
if(s1 == s2) {
return 5;
}
// special case for isbn
if(f->name() == Latin1Literal("isbn") && ISBNValidator::isbn10(s1) == ISBNValidator::isbn10(s2)) {
return 5;
}
if(f->name() == Latin1Literal("lccn") && LCCNValidator::formalize(s1) == LCCNValidator::formalize(s2)) {
return 5;
}
if(f->name() == Latin1Literal("arxiv")) {
// normalize and unVersion arxiv ID
s1.remove(TQRegExp(TQString::fromLatin1("^arxiv:"), false));
s1.remove(TQRegExp(TQString::fromLatin1("v\\d+$")));
s2.remove(TQRegExp(TQString::fromLatin1("^arxiv:"), false));
s2.remove(TQRegExp(TQString::fromLatin1("v\\d+$")));
if(s1 == s2) {
return 5;
}
}
if(f->formatFlag() == Field::FormatName) {
s1 = e1->field(f, true).lower();
s2 = e2->field(f, true).lower();
if(s1 == s2) {
return 5;
}
}
// try removing punctuation
TQRegExp notAlphaNum(TQString::fromLatin1("[^\\s\\w]"));
TQString s1a = s1; s1a.remove(notAlphaNum);
TQString s2a = s2; s2a.remove(notAlphaNum);
if(!s1a.isEmpty() && s1a == s2a) {
// myDebug() << "match without punctuation" << endl;
return 5;
}
Field::stripArticles(s1);
Field::stripArticles(s2);
if(!s1.isEmpty() && s1 == s2) {
// myDebug() << "match without articles" << endl;
return 3;
}
// try removing everything between parentheses
TQRegExp rx(TQString::fromLatin1("\\s*\\(.*\\)\\s*"));
s1.remove(rx);
s2.remove(rx);
if(!s1.isEmpty() && s1 == s2) {
// myDebug() << "match without parentheses" << endl;
return 2;
}
if(f->flags() & Field::AllowMultiple) {
TQStringList sl1 = e1->fields(f, false);
TQStringList sl2 = e2->fields(f, false);
int matches = 0;
for(TQStringList::ConstIterator it = sl1.begin(); it != sl1.end(); ++it) {
matches += sl2.contains(*it);
}
if(matches == 0 && f->formatFlag() == Field::FormatName) {
sl1 = e1->fields(f, true);
sl2 = e2->fields(f, true);
for(TQStringList::ConstIterator it = sl1.begin(); it != sl1.end(); ++it) {
matches += sl2.contains(*it);
}
}
return matches;
}
return 0;
}
#include "entry.moc"