File indexing completed on 2024-04-28 15:39:58

0001 // SPDX-FileCopyrightText: 2003 - 2022 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0002 // SPDX-FileCopyrightText: 2003 David Faure <faure@kde.org>
0003 // SPDX-FileCopyrightText: 2005 - 2007 Dirk Mueller <mueller@kde.org>
0004 // SPDX-FileCopyrightText: 2006 - 2007 Tuomas Suutari <tuomas@nepnep.net>
0005 // SPDX-FileCopyrightText: 2007 - 2008 Laurent Montel <montel@kde.org>
0006 // SPDX-FileCopyrightText: 2007 - 2010 Jan Kundrát <jkt@flaska.net>
0007 // SPDX-FileCopyrightText: 2008 - 2009 Henner Zeller <h.zeller@acm.org>
0008 // SPDX-FileCopyrightText: 2013 - 2024 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0009 // SPDX-FileCopyrightText: 2018 - 2022 Tobias Leupold <tl@stonemx.de>
0010 // SPDX-FileCopyrightText: 2018 Robert Krawitz <rlk@alum.mit.edu>
0011 // SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
0012 //
0013 // SPDX-License-Identifier: GPL-2.0-or-later
0014 
0015 #include "MemberMap.h"
0016 
0017 #include "Category.h"
0018 
0019 #include <kpabase/Logging.h>
0020 
0021 using namespace DB;
0022 
0023 MemberMap::MemberMap()
0024     : QObject(nullptr)
0025     , m_dirty(true)
0026     , m_loading(false)
0027 {
0028 }
0029 
0030 MemberMap::MemberMap(const MemberMap &other)
0031     : QObject(nullptr)
0032     , m_members(other.memberMap())
0033     , m_dirty(true)
0034     , m_loading(false)
0035 {
0036 }
0037 
0038 MemberMap &MemberMap::operator=(const MemberMap &other)
0039 {
0040     if (this != &other) {
0041         m_members = other.memberMap();
0042         m_dirty = true;
0043     }
0044     return *this;
0045 }
0046 
0047 QStringList MemberMap::groups(const QString &category) const
0048 {
0049     return QStringList(m_members[category].keys());
0050 }
0051 
0052 bool MemberMap::contains(const QString &category, const QString &item) const
0053 {
0054     return m_flatMembers.contains(category) && m_flatMembers[category].contains(item);
0055 }
0056 
0057 void MemberMap::markDirty(const QString &category)
0058 {
0059     if (m_loading)
0060         regenerateFlatList(category);
0061     else
0062         Q_EMIT dirty();
0063 }
0064 
0065 void MemberMap::deleteGroup(const QString &category, const QString &groupName)
0066 {
0067     if (!m_members.contains(category))
0068         return;
0069 
0070     if (m_members[category].remove(groupName) > 0) {
0071         m_dirty = true;
0072         markDirty(category);
0073     }
0074 }
0075 
0076 QStringList MemberMap::members(const QString &category, const QString &memberGroup, bool closure) const
0077 {
0078     if (!m_members.contains(category)) {
0079         return {};
0080     }
0081     if (closure) {
0082         if (m_dirty) {
0083             calculate();
0084         }
0085         const auto &members = m_closureMembers[category][memberGroup];
0086         return QStringList(members.begin(), members.end());
0087     } else {
0088         const auto &members = m_members[category][memberGroup];
0089         return QStringList(members.begin(), members.end());
0090     }
0091 }
0092 
0093 void MemberMap::setMembers(const QString &category, const QString &memberGroup, const QStringList &members)
0094 {
0095     Q_ASSERT(!category.isEmpty());
0096     Q_ASSERT(!memberGroup.isEmpty());
0097     StringSet allowedMembers(members.begin(), members.end());
0098 
0099     for (QStringList::const_iterator i = members.begin(); i != members.end(); ++i)
0100         if (!canAddMemberToGroup(category, memberGroup, *i))
0101             allowedMembers.remove(*i);
0102 
0103     m_members[category][memberGroup] = allowedMembers;
0104     m_dirty = true;
0105     markDirty(category);
0106 }
0107 
0108 bool MemberMap::isEmpty() const
0109 {
0110     return m_members.empty();
0111 }
0112 
0113 bool MemberMap::isGroup(const QString &category, const QString &item) const
0114 {
0115     return m_members.contains(category) && m_members[category].contains(item);
0116 }
0117 
0118 QMap<QString, StringSet> MemberMap::groupMap(const QString &category) const
0119 {
0120     if (!m_members.contains(category))
0121         return {};
0122 
0123     if (m_dirty)
0124         calculate();
0125 
0126     return m_closureMembers[category];
0127 }
0128 
0129 QStringList MemberMap::calculateClosure(QMap<QString, StringSet> &resultSoFar, const QString &category, const QString &group) const
0130 {
0131     resultSoFar[group] = StringSet(); // Prevent against cycles.
0132     const StringSet members = m_members[category][group];
0133     StringSet result = members;
0134     for (const auto &member : members) {
0135         if (resultSoFar.contains(member)) {
0136             result += resultSoFar[member];
0137         } else if (isGroup(category, member)) {
0138             const auto closure = calculateClosure(resultSoFar, category, member);
0139             const StringSet closureSet(closure.begin(), closure.end());
0140             result += closureSet;
0141         }
0142     }
0143 
0144     resultSoFar[group] = result;
0145     return QStringList(result.begin(), result.end());
0146 }
0147 
0148 void MemberMap::calculate() const
0149 {
0150     m_closureMembers.clear();
0151     // run through all categories
0152     for (QMap<QString, QMap<QString, StringSet>>::ConstIterator categoryIt = m_members.begin();
0153          categoryIt != m_members.end(); ++categoryIt) {
0154 
0155         QString category = categoryIt.key();
0156         QMap<QString, StringSet> groupMap = categoryIt.value();
0157 
0158         // Run through each of the groups for the given categories
0159         for (QMap<QString, StringSet>::const_iterator groupIt = groupMap.constBegin(); groupIt != groupMap.constEnd(); ++groupIt) {
0160             QString group = groupIt.key();
0161             if (m_closureMembers[category].find(group) == m_closureMembers[category].end()) {
0162                 (void)calculateClosure(m_closureMembers[category], category, group);
0163             }
0164         }
0165     }
0166     m_dirty = false;
0167 }
0168 
0169 void MemberMap::renameGroup(const QString &category, const QString &oldName, const QString &newName)
0170 {
0171     const auto sanitizedNewName = newName.trimmed();
0172     if (!m_members.contains(category))
0173         return;
0174     if (!m_members[category].contains(oldName))
0175         return;
0176     // Don't allow overwriting to avoid creating cycles
0177     if (m_members[category].contains(sanitizedNewName))
0178         return;
0179 
0180     m_dirty = true;
0181     markDirty(category);
0182     QMap<QString, StringSet> &groupMap = m_members[category];
0183     groupMap.insert(sanitizedNewName, m_members[category][oldName]);
0184     groupMap.remove(oldName);
0185     for (StringSet &set : groupMap) {
0186         if (set.contains(oldName)) {
0187             set.remove(oldName);
0188             set.insert(sanitizedNewName);
0189         }
0190     }
0191 }
0192 
0193 void MemberMap::deleteItem(DB::Category *category, const QString &name)
0194 {
0195     Q_ASSERT(category != nullptr);
0196     const auto categoryName = category->name();
0197     if (!m_members.contains(categoryName))
0198         return;
0199 
0200     int removed = 0;
0201     QMap<QString, StringSet> &groupMap = m_members[categoryName];
0202     for (StringSet &items : groupMap) {
0203         removed += items.remove(name);
0204     }
0205     removed += m_members[categoryName].remove(name);
0206 
0207     if (removed > 0) {
0208         m_dirty = true;
0209         markDirty(categoryName);
0210     }
0211 }
0212 
0213 void MemberMap::renameItem(DB::Category *category, const QString &oldName, const QString &newName)
0214 {
0215     Q_ASSERT(category != nullptr);
0216     const auto categoryName = category->name();
0217     const auto sanitizedNewName = newName.trimmed();
0218     if (!m_members.contains(categoryName))
0219         return;
0220     if (oldName == sanitizedNewName)
0221         return;
0222 
0223     bool changed = false;
0224     QMap<QString, StringSet> &groupMap = m_members[categoryName];
0225     for (StringSet &items : groupMap) {
0226         if (items.contains(oldName)) {
0227             changed = true;
0228             items.remove(oldName);
0229             items.insert(sanitizedNewName);
0230         }
0231     }
0232     if (groupMap.contains(oldName)) {
0233         changed = true;
0234         groupMap[sanitizedNewName] = groupMap[oldName];
0235         groupMap.remove(oldName);
0236     }
0237 
0238     if (changed) {
0239         m_dirty = true;
0240         markDirty(categoryName);
0241     }
0242 }
0243 
0244 void MemberMap::regenerateFlatList(const QString &category)
0245 {
0246     if (!m_members.contains(category))
0247         return;
0248 
0249     m_flatMembers[category].clear();
0250     for (const auto &group : qAsConst(m_members[category])) {
0251         for (const auto &tag : group) {
0252             m_flatMembers[category].insert(tag);
0253         }
0254     }
0255 }
0256 
0257 void MemberMap::addMemberToGroup(const QString &category, const QString &group, const QString &item)
0258 {
0259     // Only test for cycles after database is already loaded
0260     if (!m_loading && !canAddMemberToGroup(category, group, item)) {
0261         qCWarning(DBLog, "Inserting item %s into group %s/%s would create a cycle. Ignoring...", qPrintable(item), qPrintable(category), qPrintable(group));
0262         return;
0263     }
0264 
0265     if (item.isEmpty()) {
0266         qCWarning(DBLog, "Tried to insert null item into group %s/%s. Ignoring...", qPrintable(category), qPrintable(group));
0267         return;
0268     }
0269 
0270     m_members[category][group].insert(item);
0271     m_flatMembers[category].insert(item);
0272 
0273     if (m_loading) {
0274         m_dirty = true;
0275     } else if (!m_dirty) {
0276         // Update _closureMembers to avoid marking it dirty
0277 
0278         QMap<QString, StringSet> &categoryClosure = m_closureMembers[category];
0279 
0280         categoryClosure[group].insert(item);
0281 
0282         QMap<QString, StringSet>::const_iterator
0283             closureOfItem
0284             = categoryClosure.constFind(item);
0285         const StringSet *closureOfItemPtr(nullptr);
0286         if (closureOfItem != categoryClosure.constEnd()) {
0287             closureOfItemPtr = &(*closureOfItem);
0288             categoryClosure[group] += *closureOfItem;
0289         }
0290 
0291         for (QMap<QString, StringSet>::iterator i = categoryClosure.begin();
0292              i != categoryClosure.end(); ++i)
0293             if ((*i).contains(group)) {
0294                 (*i).insert(item);
0295                 if (closureOfItemPtr)
0296                     (*i) += *closureOfItemPtr;
0297             }
0298     }
0299 
0300     // If we are loading, we do *not* want to regenerate the list!
0301     if (!m_loading)
0302         Q_EMIT dirty();
0303 }
0304 
0305 void MemberMap::removeMemberFromGroup(const QString &category, const QString &group, const QString &item)
0306 {
0307     if (!m_members.contains(category))
0308         return;
0309     if (!m_members[category].contains(group))
0310         return;
0311 
0312     if (m_members[category][group].remove(item) > 0) {
0313         // We shouldn't be doing this very often, so just regenerate
0314         // the flat list
0315         regenerateFlatList(category);
0316         Q_EMIT dirty();
0317     }
0318 }
0319 
0320 void MemberMap::addGroup(const QString &category, const QString &group)
0321 {
0322     const auto sanitizedGroup = group.trimmed();
0323     if (sanitizedGroup.isEmpty())
0324         return;
0325 
0326     if (!m_members[category].contains(sanitizedGroup)) {
0327         m_members[category].insert(sanitizedGroup, StringSet());
0328         markDirty(category);
0329     }
0330 }
0331 
0332 void MemberMap::renameCategory(const QString &oldName, const QString &newName)
0333 {
0334     const auto sanitizedNewName = newName.trimmed();
0335     if (!m_members.contains(oldName))
0336         return;
0337     if (oldName == sanitizedNewName)
0338         return;
0339     if (m_members.contains(sanitizedNewName))
0340         return;
0341 
0342     m_members[sanitizedNewName] = m_members[oldName];
0343     m_members.remove(oldName);
0344     m_closureMembers[sanitizedNewName] = m_closureMembers[oldName];
0345     m_closureMembers.remove(oldName);
0346     if (!m_loading)
0347         Q_EMIT dirty();
0348 }
0349 
0350 void MemberMap::deleteCategory(const QString &category)
0351 {
0352     if (!m_members.contains(category))
0353         return;
0354 
0355     m_members.remove(category);
0356     m_closureMembers.remove(category);
0357     markDirty(category);
0358 }
0359 
0360 QMap<QString, StringSet> DB::MemberMap::inverseMap(const QString &category) const
0361 {
0362     QMap<QString, StringSet> res;
0363     const QMap<QString, StringSet> &map = m_members[category];
0364 
0365     for (QMap<QString, StringSet>::ConstIterator mapIt = map.begin(); mapIt != map.end(); ++mapIt) {
0366         QString group = mapIt.key();
0367         const StringSet members = mapIt.value();
0368         for (const auto &member : members) {
0369             res[member].insert(group);
0370         }
0371     }
0372     return res;
0373 }
0374 
0375 bool DB::MemberMap::hasPath(const QString &category, const QString &from, const QString &to) const
0376 {
0377     if (from == to)
0378         return true;
0379     else if (!m_members[category].contains(from))
0380         // Try to avoid calculate(), which is quite time consuming.
0381         return false;
0382     else {
0383         // return members(category, from, true).contains(to);
0384         if (m_dirty)
0385             calculate();
0386         return m_closureMembers[category][from].contains(to);
0387     }
0388 }
0389 
0390 void DB::MemberMap::setLoading(bool isLoading)
0391 {
0392     if (m_loading && !isLoading) {
0393         // TODO: Remove possible loaded cycles.
0394     }
0395     m_loading = isLoading;
0396 }
0397 
0398 #include "moc_MemberMap.cpp"
0399 
0400 // vi:expandtab:tabstop=4 shiftwidth=4: