/* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.activiti.explorer.cache; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.activiti.engine.IdentityService; import org.activiti.engine.identity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * Simple cache of user information, to avoid hitting the database too often for * information that doesn't change much over time. * * Based on a Trie datastructure (http://en.wikipedia.org/wiki/Trie), see {@link RadixTree}, * for fast 'telephonebook'-like retrieval based on the first and last name of the users. * Note that we are using the Trie such that we can have multiple results for a given key, by * giving each key a list of matching values: * eg. key='kermit' has a list of values {Kermit The Frog, Kermit The Evil Overlord, ...} * * TODO: In a clustered/cloud environment, this cache must be refreshed each xx minutes, * in case updates have been done on other machines. Alternatively, a solution * such as memcached could replace this implementation later on. * * @author Joram Barrez */ @Component public class TrieBasedUserCache implements UserCache { private static final Logger LOGGER = Logger.getLogger(TrieBasedUserCache.class.getName()); protected IdentityService identityService; protected RadixTree<List<User>> userTrie = new RadixTreeImpl<List<User>>(); protected Map<String, List<String>> keyCache = new HashMap<String, List<String>>(); protected Map<String, User> userCache = new HashMap<String, User>(); public void refresh() { userTrie = new RadixTreeImpl<List<User>>(); loadUsers(); } public synchronized void loadUsers() { long nrOfUsers = identityService.createUserQuery().count(); long usersAdded = 0; userTrie = new RadixTreeImpl<List<User>>(); userCache = new HashMap<String, User>(); keyCache = new HashMap<String, List<String>>(); while (usersAdded < nrOfUsers) { if (LOGGER.isLoggable(Level.INFO)) { LOGGER.info("Caching users " + usersAdded + " to " + (usersAdded+25)); } List<User> users = identityService.createUserQuery().listPage((int) usersAdded, 25); for (User user : users) { addTrieItem(user); addUserCacheItem(user); usersAdded++; } } } protected void addTrieItem(User user) { for (String key : getKeys(user)) { addTrieCacheItem(key, user); } } protected String[] getKeys(User user) { String fullname = ""; if (user.getFirstName() != null) { fullname += user.getFirstName(); } if (user.getLastName() != null) { fullname += " " + user.getLastName(); } return fullname.split(" "); } protected void addTrieCacheItem(String key, User user) { key = key.toLowerCase(); // Trie update List<User> value = null; if (!userTrie.contains(key)) { value = new ArrayList<User>(); } else { value = userTrie.find(key); } value.add(user); userTrie.delete(key); userTrie.insert(key, value); // Key map update if (!keyCache.containsKey(user.getId())) { keyCache.put(user.getId(), new ArrayList<String>()); } keyCache.get(user.getId()).add(key); } protected void addUserCacheItem(User user) { userCache.put(user.getId(), user); } public User findUser(String userId) { if (userCache.isEmpty()) { loadUsers(); } return userCache.get(userId); } public List<User> findMatchingUsers(String prefix) { if (userTrie.getSize() == 0) { refresh(); } List<User> returnValue = new ArrayList<User>(); List<List<User>> results = userTrie.searchPrefix(prefix.toLowerCase(), 100); // 100 should be enough for any name for (List<User> result : results) { for (User userDetail : result) { returnValue.add(userDetail); } } return returnValue; } public void notifyUserDataChanged(String userId) { User newData = identityService.createUserQuery().userId(userId).singleResult(); // Update user trie: first remove old values if (keyCache.containsKey(userId)) { for (String key : keyCache.get(userId)) { List<User> users = userTrie.find(key); if (users != null && !users.isEmpty()) { Iterator<User> userIterator = users.iterator(); while (userIterator.hasNext()) { User next = userIterator.next(); if (next.getId().equals(userId)) { userIterator.remove(); } } } } } // Update key cache keyCache.remove(userId); if (newData != null) { // Update user trie: add new value addTrieItem(newData); // Update user cache userCache.put(newData.getId(), newData); } } @Autowired public void setIdentityService(IdentityService identityService) { this.identityService = identityService; loadUsers(); } }