1 /***************************************************************************
2     Copyright (C) 2001-2009 Robby Stephenson <robby@periapsis.org>
3  ***************************************************************************/
4 
5 /***************************************************************************
6  *                                                                         *
7  *   This program is free software; you can redistribute it and/or         *
8  *   modify it under the terms of the GNU General Public License as        *
9  *   published by the Free Software Foundation; either version 2 of        *
10  *   the License or (at your option) version 3 or any later version        *
11  *   accepted by the membership of KDE e.V. (or its successor approved     *
12  *   by the membership of KDE e.V.), which shall act as a proxy            *
13  *   defined in Section 14 of version 3 of the license.                    *
14  *                                                                         *
15  *   This program is distributed in the hope that it will be useful,       *
16  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
17  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
18  *   GNU General Public License for more details.                          *
19  *                                                                         *
20  *   You should have received a copy of the GNU General Public License     *
21  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
22  *                                                                         *
23  ***************************************************************************/
24 
25 #include "entry.h"
26 #include "entrygroup.h"
27 #include "collection.h"
28 #include "field.h"
29 #include "derivedvalue.h"
30 #include "utils/string_utils.h"
31 #include "utils/stringset.h"
32 #include "tellico_debug.h"
33 
34 #include <KLocalizedString>
35 
36 using namespace Tellico;
37 using namespace Tellico::Data;
38 using Tellico::Data::Entry;
39 
Entry(Tellico::Data::CollPtr coll_)40 Entry::Entry(Tellico::Data::CollPtr coll_) : QSharedData(), m_coll(coll_), m_id(-1) {
41 #ifndef NDEBUG
42   if(!coll_) {
43     myWarning() << "null collection pointer!";
44   }
45 #endif
46 }
47 
Entry(Tellico::Data::CollPtr coll_,Data::ID id_)48 Entry::Entry(Tellico::Data::CollPtr coll_, Data::ID id_) : QSharedData(), m_coll(coll_), m_id(id_) {
49 #ifndef NDEBUG
50   if(!coll_) {
51     myWarning() << "null collection pointer!";
52   }
53 #endif
54 }
55 
Entry(const Entry & entry_)56 Entry::Entry(const Entry& entry_) :
57     QSharedData(entry_),
58     m_coll(entry_.m_coll),
59     m_id(-1),
60     m_fieldValues(entry_.m_fieldValues),
61     m_formattedFields(entry_.m_formattedFields) {
62   // special case for creation date since it gets set in Collection::addEntry IF cdate is empty
63   m_fieldValues.remove(QStringLiteral("cdate"));
64   m_fieldValues.remove(QStringLiteral("mdate"));
65 }
66 
operator =(const Entry & other_)67 Entry& Entry::operator=(const Entry& other_) {
68   if(this == &other_) return *this;
69 
70 //  static_cast<QSharedData&>(*this) = static_cast<const QSharedData&>(other_);
71   m_coll = other_.m_coll;
72   m_id = other_.m_id;
73   m_fieldValues = other_.m_fieldValues;
74   // special case for creation date field which gets set in Collection::addEntry IF cdate is empty
75   m_fieldValues.remove(QStringLiteral("cdate"));
76   m_fieldValues.remove(QStringLiteral("mdate"));
77   m_formattedFields = other_.m_formattedFields;
78   return *this;
79 }
80 
~Entry()81 Entry::~Entry() {
82 }
83 
collection() const84 Tellico::Data::CollPtr Entry::collection() const {
85   return m_coll;
86 }
87 
setCollection(Tellico::Data::CollPtr coll_)88 void Entry::setCollection(Tellico::Data::CollPtr coll_) {
89   if(coll_ == m_coll) {
90 //    myDebug() << "already belongs to collection!";
91     return;
92   }
93   // special case adding a book to a bibtex collection
94   // it would be better to do this in a real object-oriented way, but this should work
95   const bool addEntryType = m_coll->type() == Collection::Book &&
96                             coll_->type() == Collection::Bibtex &&
97                             !m_coll->hasField(QStringLiteral("entry-type"));
98   m_coll = coll_;
99   m_id = -1;
100   // set this after changing the m_coll pointer since setField() checks field validity
101   if(addEntryType) {
102     setField(QStringLiteral("entry-type"), QStringLiteral("book"));
103   }
104 }
105 
title() const106 QString Entry::title() const {
107   return field(QStringLiteral("title"));
108 }
109 
field(const QString & fieldName_) const110 QString Entry::field(const QString& fieldName_) const {
111   return field(m_coll->fieldByName(fieldName_));
112 }
113 
field(Tellico::Data::FieldPtr field_) const114 QString Entry::field(Tellico::Data::FieldPtr field_) const {
115   if(!field_) {
116     return QString();
117   }
118 
119   if(field_->hasFlag(Field::Derived)) {
120     DerivedValue dv(field_);
121     return dv.value(EntryPtr(const_cast<Entry*>(this)), false);
122   }
123 
124   return m_fieldValues.value(field_->name());
125 }
126 
formattedField(const QString & fieldName_,FieldFormat::Request request_) const127 QString Entry::formattedField(const QString& fieldName_, FieldFormat::Request request_) const {
128   return formattedField(m_coll->fieldByName(fieldName_), request_);
129 }
130 
formattedField(Tellico::Data::FieldPtr field_,FieldFormat::Request request_) const131 QString Entry::formattedField(Tellico::Data::FieldPtr field_, FieldFormat::Request request_) const {
132   if(!field_) {
133     return QString();
134   }
135 
136   // don't format the value unless it's requested to do so
137   if(request_ == FieldFormat::AsIsFormat) {
138     return field(field_);
139   }
140 
141   const FieldFormat::Type flag = field_->formatType();
142   if(field_->hasFlag(Field::Derived)) {
143     DerivedValue dv(field_);
144     // format sub fields and whole string
145     return FieldFormat::format(dv.value(EntryPtr(const_cast<Entry*>(this)), true), flag, request_);
146   }
147 
148   // if auto format is not set or FormatNone, then just return the value
149   if(flag == FieldFormat::FormatNone) {
150     return m_coll->prepareText(field(field_));
151   }
152 
153   if(!m_formattedFields.contains(field_->name())) {
154     QString formattedValue;
155     if(field_->type() == Field::Table) {
156       QStringList rows;
157       // we only format the first column
158       foreach(const QString& row, FieldFormat::splitTable(field(field_))) {
159         QStringList columns = FieldFormat::splitRow(row);
160         QStringList newValues;
161         if(!columns.isEmpty()) {
162           foreach(const QString& value, FieldFormat::splitValue(columns.at(0))) {
163             newValues << FieldFormat::format(value, field_->formatType(), FieldFormat::DefaultFormat);
164           }
165           columns.replace(0, newValues.join(FieldFormat::delimiterString()));
166         }
167         rows << columns.join(FieldFormat::columnDelimiterString());
168       }
169       formattedValue = rows.join(FieldFormat::rowDelimiterString());
170     } else {
171       QStringList values;
172       if(field_->hasFlag(Field::AllowMultiple)) {
173         values = FieldFormat::splitValue(field(field_));
174       } else {
175         values << field(field_);
176       }
177       QStringList formattedValues;
178       foreach(const QString& value, values) {
179         formattedValues << FieldFormat::format(m_coll->prepareText(value), flag, request_);
180       }
181       formattedValue = formattedValues.join(FieldFormat::delimiterString());
182     }
183     if(!formattedValue.isEmpty()) {
184       m_formattedFields.insert(field_->name(), Tellico::shareString(formattedValue));
185     }
186     return formattedValue;
187   }
188   // otherwise, just look it up
189   return m_formattedFields.value(field_->name());
190 }
191 
setField(Tellico::Data::FieldPtr field_,const QString & value_,bool updateMDate_)192 bool Entry::setField(Tellico::Data::FieldPtr field_, const QString& value_, bool updateMDate_) {
193   return setField(field_->name(), value_, updateMDate_);
194 }
195 
196 // updating the modified date of the entry is expensive with the call to QDate::currentDate
197 // when loading a collection from a file (in particular), it's faster to ignore that date
setField(const QString & name_,const QString & value_,bool updateMDate_)198 bool Entry::setField(const QString& name_, const QString& value_, bool updateMDate_) {
199   if(name_.isEmpty()) {
200     myWarning() << "empty field name for value:" << value_;
201     return false;
202   }
203 
204   if(m_coll->fields().isEmpty()) {
205     myDebug() << "collection has no fields, can't add -" << name_;
206     return false;
207   }
208 
209   if(!m_coll->hasField(name_)) {
210     myDebug() << "unknown collection entry field -" << name_
211               << "in collection" << m_coll->title();
212     myDebug() <<  "not adding" << value_;
213     return false;
214   }
215 
216   const bool wasDifferent = field(name_) != value_;
217   const bool res = setFieldImpl(name_, value_);
218   // returning true means entry was successfully modified
219   if(res && wasDifferent && updateMDate_ &&
220      name_ != QLatin1String("mdate") && m_coll->hasField(QStringLiteral("mdate"))) {
221     setFieldImpl(QStringLiteral("mdate"), QDate::currentDate().toString(Qt::ISODate));
222   }
223   return res;
224 }
225 
setFieldImpl(const QString & name_,const QString & value_)226 bool Entry::setFieldImpl(const QString& name_, const QString& value_) {
227   // an empty value means remove the field
228   if(value_.isEmpty()) {
229     if(m_fieldValues.remove(name_)) {
230       invalidateFormattedFieldValue(name_);
231     }
232     return true;
233   }
234 
235   if(m_coll && !m_coll->isAllowed(name_, value_)) {
236     myDebug() << "for" << name_ << ", value is not allowed -" << value_;
237     return false;
238   }
239 
240   Data::FieldPtr f = m_coll->fieldByName(name_);
241   if(!f) {
242     return false;
243   }
244 
245   // the string store is probable only useful for fields with auto-completion or choice/number/bool
246   bool shareType = f->type() == Field::Choice ||
247                    f->type() == Field::Bool ||
248                    f->type() == Field::Image ||
249                    f->type() == Field::Rating ||
250                    f->type() == Field::Number;
251   if(!(f->hasFlag(Field::AllowMultiple)) &&
252      (shareType ||
253       (f->type() == Field::Line && (f->flags() & Field::AllowCompletion)))) {
254     m_fieldValues.insert(Tellico::shareString(name_), Tellico::shareString(value_));
255   } else {
256     m_fieldValues.insert(Tellico::shareString(name_), value_);
257   }
258   invalidateFormattedFieldValue(name_);
259   return true;
260 }
261 
addToGroup(EntryGroup * group_)262 bool Entry::addToGroup(EntryGroup* group_) {
263   if(!group_ || m_groups.contains(group_)) {
264     return false;
265   }
266 
267   m_groups.push_back(group_);
268   group_->append(EntryPtr(this));
269   return true;
270 }
271 
removeFromGroup(EntryGroup * group_)272 bool Entry::removeFromGroup(EntryGroup* group_) {
273   // if the removal isn't successful, just return
274   bool success = m_groups.removeOne(group_);
275   success = group_->removeOne(EntryPtr(this)) && success;
276 //  myDebug() << "removing from group - " << group_->fieldName() << "--" << group_->groupName();
277   if(!success) {
278     myDebug() << "failed!";
279   }
280   return success;
281 }
282 
clearGroups()283 void Entry::clearGroups() {
284   m_groups.clear();
285 }
286 
287 // this function gets called before m_groups is updated. In fact, it is used to
288 // update that list. This is the function that actually parses the field values
289 // and returns the list of the group names.
groupNamesByFieldName(const QString & fieldName_) const290 QStringList Entry::groupNamesByFieldName(const QString& fieldName_) const {
291 //  myDebug() << fieldName_;
292   FieldPtr f = m_coll->fieldByName(fieldName_);
293   if(!f) {
294     myWarning() << "no field named" << fieldName_;
295     return QStringList();
296   }
297 
298   StringSet groups;
299   // check table before multiple since tables are always multiple
300   if(f->type() == Field::Table) {
301     // we only take groups from the first column
302     foreach(const QString& row, FieldFormat::splitTable(field(f))) {
303       const QStringList columns = FieldFormat::splitRow(row);
304       const QStringList values = columns.isEmpty() ? QStringList() : FieldFormat::splitValue(columns.at(0));
305       foreach(const QString& value, values) {
306         groups.add(FieldFormat::format(value, f->formatType(), FieldFormat::DefaultFormat));
307       }
308     }
309   } else if(f->hasFlag(Field::AllowMultiple)) {
310     // use a string split instead of regexp split, since we've already enforced the space after the semi-comma
311     groups.add(FieldFormat::splitValue(formattedField(f), FieldFormat::StringSplit));
312   } else {
313     groups.add(formattedField(f));
314   }
315 
316   // possible to be empty for no value
317   // but we want to populate an empty group
318   return groups.isEmpty() ? QStringList(QString()) : groups.values();
319 }
320 
isOwned()321 bool Entry::isOwned() {
322   return (m_coll && m_id > -1 && m_coll->entryCount() > 0 && m_coll->entries().contains(EntryPtr(this)));
323 }
324 
325 // an empty string means invalidate all
invalidateFormattedFieldValue(const QString & name_)326 void Entry::invalidateFormattedFieldValue(const QString& name_) {
327   if(name_.isEmpty()) {
328     m_formattedFields.clear();
329   } else if(!m_formattedFields.isEmpty()) {
330     m_formattedFields.remove(name_);
331   }
332 }
333