File indexing completed on 2024-04-28 05:08:17

0001 /***************************************************************************
0002     Copyright (C) 2001-2009 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "derivedvalue.h"
0026 #include "collection.h"
0027 #include "fieldformat.h"
0028 #include "utils/stringset.h"
0029 #include "tellico_debug.h"
0030 
0031 #include <QStack>
0032 
0033 using namespace Tellico::Data;
0034 using Tellico::Data::DerivedValue;
0035 
0036 const QRegularExpression DerivedValue::s_templateFieldsRx(QLatin1String("%\\{([^:]+):?.*?\\}"));
0037 
0038 DerivedValue::DerivedValue(const QString& valueTemplate_) : m_valueTemplate(valueTemplate_) {
0039 }
0040 
0041 DerivedValue::DerivedValue(FieldPtr field_) {
0042   Q_ASSERT(field_);
0043   if(!field_->hasFlag(Field::Derived)) {
0044     myWarning() << "using DerivedValue for non-derived field";
0045   } else {
0046     m_valueTemplate = field_->property(QStringLiteral("template"));
0047     m_fieldName = field_->name();
0048   }
0049 }
0050 
0051 bool DerivedValue::isRecursive(Collection* coll_) const {
0052   Q_ASSERT(coll_);
0053   StringSet fieldNamesFound;
0054   if(!m_fieldName.isEmpty()) {
0055     fieldNamesFound.add(m_fieldName);
0056   }
0057 
0058   QStack<QString> fieldsToCheck;
0059   foreach(const QString& key, templateFields()) {
0060     fieldsToCheck.push(key);
0061   }
0062   while(!fieldsToCheck.isEmpty()) {
0063     QString fieldName = fieldsToCheck.pop();
0064     FieldPtr f = coll_->fieldByName(fieldName);
0065     if(!f) {
0066       f = coll_->fieldByTitle(fieldName);
0067     }
0068     if(!f) {
0069       continue;
0070     }
0071     if(fieldNamesFound.has(f->name())) {
0072       // we have recursion
0073       myLog() << "found recursion, refers to" << f->name() << "more than once";
0074       return true;
0075     } else {
0076       fieldNamesFound.add(f->name());
0077     }
0078     if(f->hasFlag(Field::Derived)) {
0079       DerivedValue dv(f);
0080       foreach(const QString& key, dv.templateFields()) {
0081         fieldsToCheck.push(key);
0082       }
0083     }
0084   }
0085   return false;
0086 }
0087 
0088 QString DerivedValue::value(EntryPtr entry_, bool formatted_) const {
0089   Q_ASSERT(entry_);
0090   Q_ASSERT(entry_->collection());
0091   if(!entry_ || !entry_->collection()) {
0092     return m_valueTemplate;
0093   }
0094 
0095   QString result;
0096 
0097   int endPos;
0098   int curPos = 0;
0099   int pctPos = m_valueTemplate.indexOf(QLatin1Char('%'), curPos);
0100   while(pctPos != -1 && pctPos+1 < m_valueTemplate.length()) {
0101     if(m_valueTemplate.at(pctPos+1) == QLatin1Char('{')) {
0102       endPos = m_valueTemplate.indexOf(QLatin1Char('}'), pctPos+2);
0103       if(endPos > -1) {
0104         result += m_valueTemplate.midRef(curPos, pctPos-curPos)
0105                 + templateKeyValue(entry_, m_valueTemplate.mid(pctPos+2, endPos-pctPos-2), formatted_);
0106         curPos = endPos+1;
0107       } else {
0108         break;
0109       }
0110     } else {
0111       result += m_valueTemplate.midRef(curPos, pctPos-curPos+1);
0112       curPos = pctPos+1;
0113     }
0114     pctPos = m_valueTemplate.indexOf(QLatin1Char('%'), curPos);
0115   }
0116   result += m_valueTemplate.midRef(curPos, m_valueTemplate.length()-curPos);
0117 //  myDebug() << "format_ << " = " << result;
0118   // sometimes field value might empty, resulting in multiple consecutive white spaces
0119   // so let's simplify that...
0120   return result.simplified();
0121 }
0122 
0123 void DerivedValue::initRegularExpression() const {
0124   m_keyRx.setPattern(QLatin1String("^([^:]+):?(-?\\d*)/?(.*)$"));
0125 }
0126 
0127 // format is something like "%{year} %{author}"
0128 QStringList DerivedValue::templateFields() const {
0129   QStringList list;
0130   auto i = s_templateFieldsRx.globalMatch(m_valueTemplate);
0131   while(i.hasNext()) {
0132     auto match = i.next();
0133     list << match.captured(1);
0134   }
0135   return list;
0136 }
0137 
0138 QString DerivedValue::templateKeyValue(EntryPtr entry_, const QString& key_, bool formatted_) const {
0139   // @id is used often, so avoid regex if possible
0140   if(key_ == QLatin1String("@id")) {
0141     return QString::number(entry_->id());
0142   }
0143 
0144   if(m_keyRx.pattern().isEmpty()) {
0145     initRegularExpression();
0146   }
0147   auto match = m_keyRx.match(key_);
0148   if(!match.hasMatch()) {
0149     myDebug() << "unmatched regexp for" << key_;
0150     return QLatin1String("%{") + key_ + QLatin1Char('}');
0151   }
0152 
0153   const QString fieldName = match.captured(1);
0154   FieldPtr field = entry_->collection()->fieldByName(fieldName);
0155   if(!field) {
0156     // allow the user to also use field titles
0157     field = entry_->collection()->fieldByTitle(fieldName);
0158   }
0159   if(!field) {
0160     if(fieldName == QLatin1String("id")) {
0161       // '@id' is the best way to use it, but formerly, we allowed just 'id'
0162       return QString::number(entry_->id());
0163     } else {
0164       return QLatin1String("%{") + key_ + QLatin1Char('}');
0165     }
0166   }
0167   // field name, followed by optional colon, optional value index (negative), and words after slash
0168   int pos = match.captured(2).toInt();
0169   QString result;
0170   if(pos == 0) {
0171     // insert field value
0172     result = formatted_ ? entry_->formattedField(field) : entry_->field(field);
0173   } else {
0174     QStringList values;
0175     if(field->type() ==  Field::Table) {
0176       // for tables, only take first column
0177       QStringList rows = FieldFormat::splitTable(formatted_ ? entry_->formattedField(field) : entry_->field(field));
0178       foreach(const QString& row, rows) {
0179         const QStringList rowValues = FieldFormat::splitRow(row);
0180         if(!rowValues.isEmpty()) {
0181           values.append(rowValues.at(0));
0182         }
0183       }
0184     } else {
0185       values = FieldFormat::splitValue(formatted_ ? entry_->formattedField(field) : entry_->field(field));
0186     }
0187     if(pos < 0) {
0188       pos += values.count();
0189       if(pos < 0) {
0190         pos = 0;
0191       }
0192     } else {
0193       // a position of 1 is actually index 0
0194       --pos;
0195     }
0196     // use value() instead of at() since not sure within bounds
0197     result = values.value(pos);
0198   }
0199 
0200   const QString func = match.captured(3);
0201   if(func.contains(QLatin1Char('u'))) {
0202     result = result.toUpper();
0203   }
0204   if(func.contains(QLatin1Char('l'))) {
0205     result = result.toLower();
0206   }
0207 
0208   return result;
0209 }