001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ade.configuration.formatters;
029
030import org.opencms.util.CmsUUID;
031import org.opencms.xml.containerpage.I_CmsFormatterBean;
032
033import java.util.ArrayList;
034import java.util.Collection;
035import java.util.HashMap;
036import java.util.HashSet;
037import java.util.List;
038import java.util.Map;
039import java.util.Optional;
040import java.util.Set;
041import java.util.function.Predicate;
042
043import com.google.common.collect.HashMultimap;
044
045/**
046 * Helper class for keeping track of which keys map to which formatters, and which formatters are active,
047 * when evaluating the sitemap configuration.
048 *
049 * <p>Formatters can now have multiple keys, which makes overriding them in more specific sitemap/master configurations more complex.
050 * Let X and Y be active sitemap/master configurations for the currently requested page, with Y being more specific than X. If X adds a formatter F1
051 * with keys A, B, C and Y adds a different formatter F2 with an overlapping set of keys C, D, E, then as a result, formatter F1
052 * should be disabled and formatter F2 should be used for the keys A, B, C, D, E (even though it does not have A and B
053 * as configured keys).
054 *
055 * <p>You use an instance of this class by adding/removing formatters in the order these operations are defined by the sitemap configuration
056 * and finally calling the getFormattersWithAdditionalKeys() method at the end, which augments the formatters it returns by adding the appropriate keys.
057 */
058public class CmsFormatterIndex {
059
060    /** Table which maps ids to formatter keys. */
061    private HashMultimap<CmsUUID, String> m_keysById = HashMultimap.create();
062
063    /** Table which maps formatter keys to ids. */
064    private HashMultimap<String, CmsUUID> m_idsByKey = HashMultimap.create();
065
066    /** Map of formatters by id. */
067    private Map<CmsUUID, I_CmsFormatterBean> m_formattersById = new HashMap<>();
068
069    /**
070     * Adds the given formatter.
071     *
072     * <p>If there are any direct or indirect overlaps with the keys of already added formatters, these
073     * formatters will be removed and their keys mapped to the new formatter.
074     *
075     * @param formatter the formatter to add
076     */
077    public void addFormatter(I_CmsFormatterBean formatter) {
078
079        String id = formatter.getId();
080        if (CmsUUID.isValidUUID(id)) {
081            CmsUUID uuid = new CmsUUID(id);
082
083            Set<String> relatedKeys = new HashSet<>();
084            Set<CmsUUID> relatedIds = new HashSet<>();
085            collectRelatedKeysAndIds(formatter.getAllKeys(), relatedKeys, relatedIds);
086
087            for (CmsUUID relatedId : relatedIds) {
088                m_keysById.removeAll(relatedId);
089                m_formattersById.remove(relatedId);
090            }
091            for (String relatedKey : relatedKeys) {
092                m_idsByKey.removeAll(relatedKey);
093            }
094
095            m_formattersById.put(uuid, formatter);
096            for (String relatedKey : relatedKeys) {
097                m_idsByKey.put(relatedKey, uuid);
098            }
099            m_keysById.putAll(uuid, relatedKeys);
100        }
101    }
102
103    /**
104     * Gets the final map of active formatters, with their formatter keys replaced by the total set of keys under which they should be available.
105     *
106     * @return the map of formatters by id
107     */
108    public Map<CmsUUID, I_CmsFormatterBean> getFormattersWithAdditionalKeys() {
109
110        Map<CmsUUID, I_CmsFormatterBean> result = new HashMap<>();
111        for (Map.Entry<CmsUUID, I_CmsFormatterBean> entry : m_formattersById.entrySet()) {
112            CmsUUID id = entry.getKey();
113            Set<String> keys = m_keysById.get(id);
114            I_CmsFormatterBean formatter = entry.getValue();
115            Optional<I_CmsFormatterBean> formatterWithKeys = formatter.withKeys(keys);
116            if (formatterWithKeys.isPresent()) {
117                result.put(id, formatterWithKeys.orElse(null));
118            }
119        }
120        return result;
121    }
122
123    /**
124     * Removes the formatter with the given id.
125     *
126     * @param id the formatter id
127     */
128    public void remove(CmsUUID id) {
129
130        Set<String> keys = m_keysById.removeAll(id);
131        for (String key : keys) {
132            m_idsByKey.remove(key, id);
133        }
134        m_formattersById.remove(id);
135    }
136
137    /**
138     * Removes all formatters matching the given predicate
139     *
140     * @param condition the condition to use for checking which formatters should be removed
141     */
142    public void removeIf(Predicate<I_CmsFormatterBean> condition) {
143
144        Set<CmsUUID> toRemove = new HashSet<>();
145        for (Map.Entry<CmsUUID, I_CmsFormatterBean> entry : m_formattersById.entrySet()) {
146            if (condition.test(entry.getValue())) {
147                toRemove.add(entry.getKey());
148            }
149        }
150        toRemove.forEach(id -> remove(id));
151    }
152
153    /**
154     * Collects all related keys and IDs which need to be removed or updated when adding a new formatter with a given set of keys.
155     *
156     * @param initialKeys the keys of the new formatter
157     * @param visitedKeys the set in which the related keys should be stored
158     * @param visitedIds the set in which the related IDs should be stored
159     */
160    private void collectRelatedKeysAndIds(
161        Collection<String> initialKeys,
162        Set<String> visitedKeys,
163        Set<CmsUUID> visitedIds) {
164
165        visitedKeys.clear();
166        visitedIds.clear();
167        List<String> todo = new ArrayList<>(initialKeys);
168        while (todo.size() > 0) {
169            String key = todo.remove(todo.size() - 1);
170            if (visitedKeys.contains(key)) {
171                continue;
172            }
173            visitedKeys.add(key);
174            for (CmsUUID id : m_idsByKey.get(key)) {
175                visitedIds.add(id);
176                for (String relatedKey : m_keysById.get(id)) {
177                    todo.add(relatedKey);
178                }
179            }
180        }
181    }
182
183}