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.monitor;
029
030import org.opencms.file.CmsGroup;
031import org.opencms.file.CmsUser;
032import org.opencms.main.CmsLog;
033import org.opencms.security.CmsRole;
034import org.opencms.util.CmsUUID;
035
036import java.util.ArrayList;
037import java.util.Collections;
038import java.util.List;
039import java.util.Map;
040import java.util.concurrent.ConcurrentHashMap;
041
042import org.apache.commons.logging.Log;
043
044import com.google.common.cache.CacheBuilder;
045import com.google.common.cache.CacheLoader;
046import com.google.common.cache.LoadingCache;
047import com.google.common.collect.Interner;
048import com.google.common.collect.Interners;
049
050/**
051 * Cache for users' groups and data derived from those groups, like role membership.
052 *
053 * <p>The cache can be either flushed completely, or just for a single user id.
054 * The data for a user must be flushed when their group membership changes.
055 *
056 */
057public class CmsGroupListCache implements I_CmsMemoryMonitorable {
058
059    /**
060     * Internal entry which stores the cached data for a specific user id.<p>
061     */
062    class Entry implements I_CmsMemoryMonitorable {
063
064        /** Bare roles, with no OU information. */
065        private volatile List<CmsRole> m_bareRoles;
066
067        /** Cache for group lists. */
068        private Map<String, List<CmsGroup>> m_groupCache = createLRUCacheMap(GROUP_LISTS_PER_USER);
069
070        /** Cache for role memberships. */
071        private Map<String, Boolean> m_hasRoleCache = new ConcurrentHashMap<>();
072
073        /**
074         * Gets the cached bare roles (with no OU information).
075         *
076         * @return the cached bare roles
077         */
078        public List<CmsRole> getBareRoles() {
079
080            return m_bareRoles;
081        }
082
083        /**
084         * Gets the group list cache map.
085         *
086         * @return the group list cache
087         */
088        public Map<String, List<CmsGroup>> getGroupCache() {
089
090            return m_groupCache;
091        }
092
093        /**
094         * Gets the 'hasRole' cache map.
095         *
096         * @return the 'hasRole' cache
097         */
098        public Map<String, Boolean> getHasRoleCache() {
099
100            return m_hasRoleCache;
101        }
102
103        /**
104         * @see org.opencms.monitor.I_CmsMemoryMonitorable#getMemorySize()
105         */
106        public int getMemorySize() {
107
108            return (int)(CmsMemoryMonitor.getValueSize(m_groupCache)
109                + CmsMemoryMonitor.getValueSize(m_hasRoleCache)
110                + CmsMemoryMonitor.getValueSize(m_bareRoles));
111        }
112
113        /**
114         * Sets the cached bare roles (with no associated OU information).
115         *
116         * @param bareRoles the bare roles
117         */
118        public void setBareRoles(List<CmsRole> bareRoles) {
119
120            m_bareRoles = bareRoles;
121        }
122
123    }
124
125    /** Max cached group lists per user. Non-final, so can be adjusted at runtime. */
126    public static volatile int GROUP_LISTS_PER_USER = 32;
127
128    /** Log instance for this class. */
129    private static final Log LOG = CmsLog.getLog(CmsGroupListCache.class);
130
131    /** The internal cache used. */
132    private LoadingCache<CmsUUID, Entry> m_internalCache;
133
134    /** Interner for canonicalizing the role membership cache keys. */
135    private Interner<String> m_interner = Interners.newBuilder().concurrencyLevel(8).build();
136
137    /**
138     * Creates a new cache instance.
139     *
140     * @param size the maximum size
141     */
142    public CmsGroupListCache(int size) {
143
144        m_internalCache = CacheBuilder.newBuilder().concurrencyLevel(CmsMemoryMonitor.CONCURRENCY_LEVEL).maximumSize(
145            size).build(new CacheLoader<CmsUUID, Entry>() {
146
147                @Override
148                public Entry load(CmsUUID key) throws Exception {
149
150                    return new Entry();
151                }
152            });
153    }
154
155    /**
156     * Creates a thread safe LRU cache map based on the guava cache builder.<p>
157     *
158     * @param capacity the cache capacity
159     *
160     * @return the cache map
161     */
162    @SuppressWarnings("unchecked")
163    static <T, V> Map<T, V> createLRUCacheMap(int capacity) {
164
165        CacheBuilder<?, ?> builder = CacheBuilder.newBuilder().concurrencyLevel(4).maximumSize(capacity);
166        return (Map<T, V>)(builder.build().asMap());
167    }
168
169    /**
170     * Removes all cache entries.
171     */
172    public void clear() {
173
174        if (LOG.isInfoEnabled()) {
175            if (LOG.isDebugEnabled()) {
176                // when DEBUG level is enabled, log a dummy exception to generate a stack trace
177                LOG.debug("CmsGroupListCache.clear() called", new Exception("(dummy exception)"));
178            } else {
179                LOG.info("CmsGroupListCache.clear() called");
180            }
181        }
182        m_internalCache.invalidateAll();
183    }
184
185    /**
186     * Removes the cache entries for the given user id.
187     *
188     * @param idKey the user id
189     */
190    public void clearUser(CmsUUID idKey) {
191
192        if (idKey != null) {
193            m_internalCache.invalidate(idKey);
194        }
195    }
196
197    /**
198     * Gets the cached bare roles for the given user id, or null if none are cached.
199     *
200     * <p>These are just the roles of the user, but with no OU information.
201    
202     * @param userId the user id
203     * @return the bare roles for the user
204     */
205    public List<CmsRole> getBareRoles(CmsUUID userId) {
206
207        Entry entry = m_internalCache.getIfPresent(userId);
208        if (entry == null) {
209            return null;
210        }
211        return entry.getBareRoles();
212    }
213
214    /**
215     * Gets the cached user groups for the given combination of keys, or null if nothing is cached.
216     *
217     * @param userId the user id
218     * @param subKey a string that consists of the parameters/flags for the group reading operation
219     *
220     * @return the groups for the given combination of keys
221     */
222    public List<CmsGroup> getGroups(CmsUUID userId, String subKey) {
223
224        Entry userEntry = m_internalCache.getIfPresent(userId);
225        if (userEntry == null) {
226            return null;
227        }
228        List<CmsGroup> result = userEntry.getGroupCache().get(subKey);
229        if (result != null) {
230            result = Collections.unmodifiableList(result);
231        }
232        return result;
233
234    }
235
236    /**
237     * Gets the cached role membership for the given role key, or null if nothing is cached.
238     *
239     * @param userId the user id
240     * @param roleKey the role key
241     *
242     * @return the cached role membership
243     */
244    public Boolean getHasRole(CmsUUID userId, String roleKey) {
245
246        Entry userEntry = m_internalCache.getIfPresent(userId);
247        if (userEntry == null) {
248            return null;
249        }
250        return userEntry.getHasRoleCache().get(roleKey);
251    }
252
253    /**
254     * @see org.opencms.monitor.I_CmsMemoryMonitorable#getMemorySize()
255     */
256    public int getMemorySize() {
257
258        return (int)CmsMemoryMonitor.getValueSize(m_internalCache.asMap());
259    }
260
261    /**
262     * Sets the bare roles for a user (with no OU information).
263     *
264     * @param user the user
265     * @param bareRoles the list of bare roles
266     */
267    public void setBareRoles(CmsUser user, List<CmsRole> bareRoles) {
268
269        if (!user.isWebuser()) { // web users take away space from normal workplace users/editors
270            CmsUUID userId = user.getId();
271            m_internalCache.getUnchecked(userId).setBareRoles(bareRoles);
272        }
273    }
274
275    /**
276     * Caches a new value for the given combination of keys.
277     *
278     * @param user the user
279     * @param subKey a string that consists of the parameters/flags for the group reading operation
280     * @param groups the value to cache
281     */
282    public void setGroups(CmsUser user, String subKey, List<CmsGroup> groups) {
283
284        if (!user.isWebuser()) { // web users take away space from normal workplace users/editors
285            CmsUUID userId = user.getId();
286            Map<String, List<CmsGroup>> groupCache = m_internalCache.getUnchecked(userId).getGroupCache();
287            groupCache.put(subKey, new ArrayList<>(groups));
288        }
289    }
290
291    /**
292     * Caches the role membership for the given user id and role key.
293     *
294     * @param user the user
295     * @param roleKey the role key
296     * @param value the role membership value
297     */
298    public void setHasRole(CmsUser user, String roleKey, Boolean value) {
299
300        if (!user.isWebuser()) { // web users take away space from normal workplace users/editors
301            roleKey = m_interner.intern(roleKey);
302            CmsUUID userId = user.getId();
303            m_internalCache.getUnchecked(userId).getHasRoleCache().put(roleKey, value);
304        }
305    }
306
307    /**
308     * Returns the number of user ids for which group lists are cached.
309     *
310     * @return the number of user ids for which group lists are cached
311     */
312    public int size() {
313
314        return (int)m_internalCache.size();
315    }
316
317}