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            m_formattersById.put(uuid, formatter);
083
084            Set<String> relatedKeys = new HashSet<>();
085            Set<CmsUUID> relatedIds = new HashSet<>();
086            collectRelatedKeysAndIds(formatter.getAllKeys(), relatedKeys, relatedIds);
087
088            for (CmsUUID relatedId : relatedIds) {
089                m_keysById.removeAll(relatedId);
090                m_formattersById.remove(relatedId);
091            }
092            for (String relatedKey : relatedKeys) {
093                m_idsByKey.removeAll(relatedKey);
094            }
095            for (String relatedKey : relatedKeys) {
096                m_idsByKey.put(relatedKey, uuid);
097            }
098            m_keysById.putAll(uuid, relatedKeys);
099        }
100    }
101
102    /**
103     * Gets the final map of active formatters, with their formatter keys replaced by the total set of keys under which they should be available.
104     *
105     * @return the map of formatters by id
106     */
107    public Map<CmsUUID, I_CmsFormatterBean> getFormattersWithAdditionalKeys() {
108
109        Map<CmsUUID, I_CmsFormatterBean> result = new HashMap<>();
110        for (Map.Entry<CmsUUID, I_CmsFormatterBean> entry : m_formattersById.entrySet()) {
111            CmsUUID id = entry.getKey();
112            Set<String> keys = m_keysById.get(id);
113            I_CmsFormatterBean formatter = entry.getValue();
114            Optional<I_CmsFormatterBean> formatterWithKeys = formatter.withKeys(keys);
115            if (formatterWithKeys.isPresent()) {
116                result.put(id, formatterWithKeys.orElse(null));
117            }
118        }
119        return result;
120    }
121
122    /**
123     * Removes the formatter with the given id.
124     *
125     * @param id the formatter id
126     */
127    public void remove(CmsUUID id) {
128
129        Set<String> keys = m_keysById.removeAll(id);
130        for (String key : keys) {
131            m_idsByKey.remove(key, id);
132        }
133        m_formattersById.remove(id);
134    }
135
136    /**
137     * Removes all formatters matching the given predicate
138     *
139     * @param condition the condition to use for checking which formatters should be removed
140     */
141    public void removeIf(Predicate<I_CmsFormatterBean> condition) {
142
143        Set<CmsUUID> toRemove = new HashSet<>();
144        for (Map.Entry<CmsUUID, I_CmsFormatterBean> entry : m_formattersById.entrySet()) {
145            if (condition.test(entry.getValue())) {
146                toRemove.add(entry.getKey());
147            }
148        }
149        toRemove.forEach(id -> remove(id));
150    }
151
152    /**
153     * Collects all related keys and IDs which need to be removed or updated when adding a new formatter with a given set of keys.
154     *
155     * @param initialKeys the keys of the new formatter
156     * @param visitedKeys the set in which the related keys should be stored
157     * @param visitedIds the set in which the related IDs should be stored
158     */
159    private void collectRelatedKeysAndIds(
160        Collection<String> initialKeys,
161        Set<String> visitedKeys,
162        Set<CmsUUID> visitedIds) {
163
164        visitedKeys.clear();
165        visitedIds.clear();
166        List<String> todo = new ArrayList<>(initialKeys);
167        while (todo.size() > 0) {
168            String key = todo.remove(todo.size() - 1);
169            if (visitedKeys.contains(key)) {
170                continue;
171            }
172            visitedKeys.add(key);
173            for (CmsUUID id : m_idsByKey.get(key)) {
174                visitedIds.add(id);
175                for (String relatedKey : m_keysById.get(id)) {
176                    todo.add(relatedKey);
177                }
178            }
179        }
180    }
181
182}