|
|
|
/***************************************************************************
|
|
|
|
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"
|