/* * Copyright (c) JForum Team * All rights reserved. * * Redistribution and use in source and binary forms, * with or without modification, are permitted provided * that the following conditions are met: * * 1) Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * 2) Redistributions in binary form must reproduce the * above copyright notice, this list of conditions and * the following disclaimer in the documentation and/or * other materials provided with the distribution. * 3) Neither the name of "Rafael Steil" nor * the names of its contributors may be used to endorse * or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT * HOLDERS AND CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER * IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE * * This file creation date: Apr 23, 2003 / 10:46:05 PM * The JForum Project * http://www.jforum.net */ package net.jforum.repository; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import org.apache.log4j.Logger; import net.jforum.SessionFacade; import net.jforum.cache.CacheEngine; import net.jforum.cache.Cacheable; import net.jforum.dao.CategoryDAO; import net.jforum.dao.ConfigDAO; import net.jforum.dao.DataAccessDriver; import net.jforum.dao.ForumDAO; import net.jforum.dao.UserDAO; import net.jforum.entities.Category; import net.jforum.entities.Config; import net.jforum.entities.Forum; import net.jforum.entities.LastPostInfo; import net.jforum.entities.MostUsersEverOnline; import net.jforum.entities.Post; import net.jforum.entities.Topic; import net.jforum.entities.User; import net.jforum.exceptions.CategoryNotFoundException; import net.jforum.exceptions.DatabaseException; import net.jforum.security.PermissionControl; import net.jforum.security.SecurityConstants; import net.jforum.util.CategoryOrderComparator; import net.jforum.util.preferences.ConfigKeys; import net.jforum.util.preferences.SystemGlobals; /** * Repository for the forums of the System. * This repository acts like a cache system, to avoid repetitive and unnecessary SQL queries * every time we need some info about the forums. * To start the repository, call the method <code>start(ForumModel, CategoryModel)</code> * * @author Rafael Steil * @version $Id: ForumRepository.java,v 1.60 2007/09/10 23:06:59 rafaelsteil Exp $ */ public class ForumRepository implements Cacheable { private static CacheEngine cache; private static ForumRepository instance; private static Logger logger = Logger.getLogger(ForumRepository.class); private static final String FQN = "forumRepository"; private static final String CATEGORIES_SET = "categoriesSet"; private static final String RELATION = "relationForums"; private static final String FQN_MODERATORS = FQN + "/moderators"; private static final String TOTAL_MESSAGES = "totalMessages"; private static final String MOST_USERS_ONLINE = "mostUsersEverOnline"; private static final String LOADED = "loaded"; private static final String LAST_USER = "lastUser"; private static final String TOTAL_USERS = "totalUsers"; /** * @see net.jforum.cache.Cacheable#setCacheEngine(net.jforum.cache.CacheEngine) */ public void setCacheEngine(CacheEngine engine) { cache = engine; } /** * Starts the repository. * * @param fm The <code>ForumModel</code> instance which will be * used to retrieve information about the forums. * @param cm The <code>CategoryModel</code> instance which will * be used to retrieve information about the categories. * @param configModel ConfigDAO */ public synchronized static void start(ForumDAO fm, CategoryDAO cm, ConfigDAO configModel) { instance = new ForumRepository(); if (cache.get(FQN, LOADED) == null) { instance.loadCategories(cm); instance.loadForums(fm); instance.loadMostUsersEverOnline(configModel); instance.loadUsersInfo(); Integer i = (Integer)cache.get(FQN, TOTAL_MESSAGES); if (i == null) { cache.add(FQN, TOTAL_MESSAGES, new Integer(0)); } cache.add(FQN, LOADED, "1"); } } /** * Gets a category by its id. * A call to @link #getCategory(int, int) is made, using the * return of <code>SessionFacade.getUserSession().getUserId()</code> * as argument for the "userId" parameter. * * @param categoryId The id of the category to check * @return <code>null</code> if the category is either not * found or access is denied. * @see #getCategory(int, int) */ public static Category getCategory(int categoryId) { return getCategory(SessionFacade.getUserSession().getUserId(), categoryId); } /** * Gets a category by its id. * * @param userId The user id who is requesting the category * @param categoryId The id of the category to get * @return <code>null</code> if the category is either not * found or access is denied. * @see #getCategory(int) */ public static Category getCategory(int userId, int categoryId) { if (!isCategoryAccessible(userId, categoryId)) { return null; } return (Category)cache.get(FQN, Integer.toString(categoryId)); } public static Category getCategory(PermissionControl pc, int categoryId) { if (!isCategoryAccessible(pc, categoryId)) { return null; } return (Category)cache.get(FQN, Integer.toString(categoryId)); } public static Category retrieveCategory(int categoryId) { return (Category)cache.get(FQN, Integer.toString(categoryId)); } /** * Check is some category is accessible. * * @param userId The user's id who is trying to get the category * @param categoryId The category's id to check for access rights * @return <code>true</code> if access to the category is allowed. */ public static boolean isCategoryAccessible(int userId, int categoryId) { return isCategoryAccessible(SecurityRepository.get(userId), categoryId); } /** * Check if some category is accessible. * * @param categoryId The category id to check for access rights * @return <code>true</code> if access to the category is allowed. */ public static boolean isCategoryAccessible(int categoryId) { return isCategoryAccessible(SessionFacade.getUserSession().getUserId(), categoryId); } /** * Check is some category is accessible. * * @param pc The <code>PermissionControl</code> instance containing * all security info related to the user. * @param categoryId the category's id to check for access rights * @return <code>true</code> if access to the category is allowed. */ public static boolean isCategoryAccessible(PermissionControl pc, int categoryId) { return pc.canAccess(SecurityConstants.PERM_CATEGORY, Integer.toString(categoryId)); } /** * Gets all categories from the cache. * * @param userId int * @return <code>List</code> with the categories. Each entry is a <code>Category</code> object. */ public static List getAllCategories(int userId) { PermissionControl pc = SecurityRepository.get(userId); List l = new ArrayList(); Set categoriesSet = (Set)cache.get(FQN, CATEGORIES_SET); if (categoriesSet == null) { synchronized (ForumRepository.instance) { if (categoriesSet == null) { logger.warn("Categories set returned null from the cache. Trying to reload"); try { ForumRepository.instance.loadCategories(DataAccessDriver.getInstance().newCategoryDAO()); ForumRepository.instance.loadForums(DataAccessDriver.getInstance().newForumDAO()); } catch (Exception e) { throw new CategoryNotFoundException("Failed to get the category", e); } categoriesSet = (Set)cache.get(FQN, CATEGORIES_SET); if (categoriesSet == null) { throw new CategoryNotFoundException("Could not find all categories. There must be a problem with the cache"); } } } } for (Iterator iter = categoriesSet.iterator(); iter.hasNext(); ) { Category c = getCategory(pc, ((Category)iter.next()).getId()); if (c != null) { l.add(c); } } return l; } /** * Get all categories. * A call to @link #getAllCategories(int) is made, passing * the return of <code>SessionFacade.getUserSession().getUserId()</code> * as the value for the "userId" argument. * * @return <code>List</code> with the categories. Each entry is a <code>Category</code> object. * @see #getAllCategories(int) */ public static List getAllCategories() { return getAllCategories(SessionFacade.getUserSession().getUserId()); } private static Category findCategoryByOrder(int order) { for (Iterator iter = ((Set)cache.get(FQN, CATEGORIES_SET)).iterator(); iter.hasNext(); ) { Category c = (Category)iter.next(); if (c.getOrder() == order) { return c; } } return null; } /** * Updates some category. * This method only updated the "name" and "order" fields. * * @param c The category to update. The method will search for a category * with the same id and update its data. */ public synchronized static void reloadCategory(Category c) { Category current = (Category)cache.get(FQN, Integer.toString(c.getId())); Category currentAtOrder = findCategoryByOrder(c.getOrder()); Set tmpSet = new TreeSet(new CategoryOrderComparator()); tmpSet.addAll((Set)cache.get(FQN, CATEGORIES_SET)); if (currentAtOrder != null) { tmpSet.remove(currentAtOrder); cache.remove(FQN, Integer.toString(currentAtOrder.getId())); } tmpSet.add(c); cache.add(FQN, Integer.toString(c.getId()), c); if (currentAtOrder != null && c.getId() != currentAtOrder.getId()) { tmpSet.remove(current); currentAtOrder.setOrder(current.getOrder()); tmpSet.add(currentAtOrder); cache.add(FQN, Integer.toString(currentAtOrder.getId()), currentAtOrder); } cache.add(FQN, CATEGORIES_SET, tmpSet); } /** * Refreshes a category entry in the cache. * * @param c The category to refresh */ public synchronized static void refreshCategory(Category c) { cache.add(FQN, Integer.toString(c.getId()), c); Set s = (Set)cache.get(FQN, CATEGORIES_SET); s.remove(c); s.add(c); cache.add(FQN, CATEGORIES_SET, s); } public synchronized static void refreshForum(Forum forum) { Category c = retrieveCategory(forum.getCategoryId()); c.addForum(forum); refreshCategory(c); } /** * Remove a category from the cache * @param c The category to remove. The instance should have the * category id at least */ public synchronized static void removeCategory(Category c) { cache.remove(FQN, Integer.toString(c.getId())); Set s = (Set)cache.get(FQN, CATEGORIES_SET); s.remove(c); cache.add(FQN, CATEGORIES_SET, s); Map m = (Map)cache.get(FQN, RELATION); for (Iterator iter = m.values().iterator(); iter.hasNext(); ) { if (Integer.parseInt((String)iter.next()) == c.getId()) { iter.remove(); } } cache.add(FQN, RELATION, m); } /** * Adds a new category to the cache. * @param c The category instance to insert in the cache. */ public synchronized static void addCategory(Category c) { String categoryId = Integer.toString(c.getId()); cache.add(FQN, categoryId, c); Set s = (Set)cache.get(FQN, CATEGORIES_SET); if (s == null) { s = new TreeSet(new CategoryOrderComparator()); } s.add(c); cache.add(FQN, CATEGORIES_SET, s); Map relation = (Map)cache.get(FQN, RELATION); if (relation == null) { relation = new HashMap(); } for (Iterator iter = c.getForums().iterator(); iter.hasNext(); ) { Forum f = (Forum)iter.next(); relation.put(Integer.toString(f.getId()), categoryId); } cache.add(FQN, RELATION, relation); } /** * Gets a specific forum from the cache. * * @param forumId The forum's ID to get * @return <code>net.jforum.Forum</code> object instance or <code>null</code> * if the forum was not found or is not accessible to the user. */ public static Forum getForum(int forumId) { String categoryId = (String)((Map)cache.get(FQN, RELATION)).get(Integer.toString(forumId)); if (categoryId != null) { Category category = (Category)cache.get(FQN, categoryId); if (isCategoryAccessible(category.getId())) { return category.getForum(forumId); } } return null; } public static boolean isForumAccessible(int forumId) { return isForumAccessible(SessionFacade.getUserSession().getUserId(), forumId); } public static boolean isForumAccessible(int userId, int forumId) { int categoryId = Integer.parseInt((String)((Map)cache.get(FQN, RELATION)).get(Integer.toString(forumId))); return isForumAccessible(userId, categoryId, forumId); } public static boolean isForumAccessible(int userId, int categoryId, int forumId) { return ((Category)cache.get(FQN, Integer.toString(categoryId))).getForum(userId, forumId) != null; } /** * Adds a new forum to the cache repository. * * @param forum The forum to add */ public synchronized static void addForum(Forum forum) { String categoryId = Integer.toString(forum.getCategoryId()); Category c = (Category)cache.get(FQN, categoryId); c.addForum(forum); cache.add(FQN, categoryId, c); Map m = (Map)cache.get(FQN, RELATION); m.put(Integer.toString(forum.getId()), categoryId); cache.add(FQN, RELATION, m); Set s = (Set)cache.get(FQN, CATEGORIES_SET); cache.add(FQN, CATEGORIES_SET, s); } /** * Removes a forum from the cache. * * @param forum The forum instance to remove. */ public synchronized static void removeForum(Forum forum) { String id = Integer.toString(forum.getId()); Map m = (Map)cache.get(FQN, RELATION); m.remove(id); cache.add(FQN, RELATION, m); id = Integer.toString(forum.getCategoryId()); Category c = (Category)cache.get(FQN, id); c.removeForum(forum.getId()); cache.add(FQN, id, c); Set s = (Set)cache.get(FQN, CATEGORIES_SET); cache.add(FQN, CATEGORIES_SET, s); } /** * Reloads a forum. * The forum should already be in the cache and <b>SHOULD NOT</b> * have its order changed. If the forum's order was changed, * then you <b>MUST CALL</b> @link Category#changeForumOrder(Forum) <b>BEFORE</b> * calling this method. * * @param forumId int The forum to reload its information */ public static synchronized void reloadForum(int forumId) { Forum f = DataAccessDriver.getInstance().newForumDAO().selectById(forumId); if (((Map)cache.get(FQN, RELATION)).containsKey(Integer.toString(forumId))) { String id = Integer.toString(f.getCategoryId()); Category c = (Category)cache.get(FQN, id); f.setLastPostInfo(null); f.setLastPostInfo(ForumRepository.getLastPostInfo(f)); c.reloadForum(f); cache.add(FQN, id, c); Set s = (Set)cache.get(FQN, CATEGORIES_SET); cache.add(FQN, CATEGORIES_SET, s); } getTotalMessages(true); } public static synchronized void updateForumStats(Topic t, User u, Post p) { String f = Integer.toString(t.getForumId()); if (((Map)cache.get(FQN, RELATION)).containsKey(f)) { Forum forum = getForum(t.getForumId()); SimpleDateFormat df = new SimpleDateFormat(SystemGlobals.getValue(ConfigKeys.DATE_TIME_FORMAT)); LastPostInfo lpi = forum.getLastPostInfo(); if (lpi == null) { lpi = new LastPostInfo(); } lpi.setPostId(p.getId()); lpi.setPostDate(df.format(p.getTime())); lpi.setPostTimeMillis(p.getTime().getTime()); lpi.setTopicId(t.getId()); lpi.setTopicReplies(t.getTotalReplies()); lpi.setUserId(u.getId()); lpi.setUsername(u.getUsername()); forum.setLastPostInfo(lpi); if (t.getTotalReplies() == 0) { forum.setTotalTopics(forum.getTotalTopics() + 1); } forum.setTotalPosts(forum.getTotalPosts() + 1); Category c = retrieveCategory(forum.getCategoryId()); c.reloadForum(forum); refreshCategory(c); } } /** * Gets information about the last message posted in some forum. * @param forum The forum to retrieve information * @return LastPostInfo */ public static LastPostInfo getLastPostInfo(Forum forum) { LastPostInfo lpi = forum.getLastPostInfo(); if (lpi == null || !forum.getLastPostInfo().hasInfo()) { lpi = DataAccessDriver.getInstance().newForumDAO().getLastPostInfo(forum.getId()); forum.setLastPostInfo(lpi); } return lpi; } /** * Gets information about the last message posted in some forum. * * @param forumId The forum's id to retrieve information * @return LastPostInfo */ public static LastPostInfo getLastPostInfo(int forumId) { return getLastPostInfo(getForum(forumId)); } /** * Gets information about the moderators of some forum. * @param forumId The forum to retrieve information * @return List */ public static List getModeratorList(int forumId) { List l = (List)cache.get(FQN_MODERATORS, Integer.toString(forumId)); if (l == null) { synchronized (FQN_MODERATORS) { try { l = DataAccessDriver.getInstance().newForumDAO().getModeratorList(forumId); cache.add(FQN_MODERATORS, Integer.toString(forumId), l); } catch (Exception e) { throw new DatabaseException(e); } } } return l; } public static void clearModeratorList() { cache.remove(FQN_MODERATORS); } public static User lastRegisteredUser() { return (User)cache.get(FQN, LAST_USER); } public static void setLastRegisteredUser(User user) { cache.add(FQN, LAST_USER, user); } public static Integer totalUsers() { return (Integer)cache.get(FQN, TOTAL_USERS); } public static void incrementTotalUsers() { Integer i = (Integer)cache.get(FQN, TOTAL_USERS); if (i == null) { i = new Integer(0); } cache.add(FQN,TOTAL_USERS, new Integer(i.intValue() + 1)); } /** * Gets the number of messages in the entire board. * @return int * @see #getTotalMessages(boolean) */ public static int getTotalMessages() { return getTotalMessages(false); } /** * Gets the number of messags in the entire board. * * @param fromDb If <code>true</code>, a query to the database will * be made, to retrieve the desired information. If <code>false</code>, the * data will be fetched from the cache. * @return The number of messages posted in the board. * @see #getTotalMessages() */ public static int getTotalMessages(boolean fromDb) { Integer i = (Integer)cache.get(FQN, TOTAL_MESSAGES); int total = i != null ? i.intValue() : 0; if (fromDb || total == 0) { total = DataAccessDriver.getInstance().newForumDAO().getTotalMessages(); cache.add(FQN, TOTAL_MESSAGES, new Integer(total)); } return total; } public static synchronized void incrementTotalMessages() { int total = ((Integer)cache.get(FQN, TOTAL_MESSAGES)).intValue(); cache.add(FQN, TOTAL_MESSAGES, new Integer(total + 1)); } /** * Gets the number of most online users ever * @return MostUsersEverOnline */ public static MostUsersEverOnline getMostUsersEverOnline() { MostUsersEverOnline online = (MostUsersEverOnline)cache.get(FQN, MOST_USERS_ONLINE); if (online == null) { synchronized (MOST_USERS_ONLINE) { online = (MostUsersEverOnline)cache.get(FQN, MOST_USERS_ONLINE); if (online == null) { online = instance.loadMostUsersEverOnline( DataAccessDriver.getInstance().newConfigDAO()); } } } return online; } /** * Update the value of most online users ever. * * @param m MostUsersEverOnline The new value to store. Generally it * will be a bigger one. */ public static void updateMostUsersEverOnline(MostUsersEverOnline m) { ConfigDAO cm = DataAccessDriver.getInstance().newConfigDAO(); Config config = cm.selectByName(ConfigKeys.MOST_USERS_EVER_ONLINE); if (config == null) { // Total config = new Config(); config.setName(ConfigKeys.MOST_USERS_EVER_ONLINE); config.setValue(Integer.toString(m.getTotal())); cm.insert(config); // Date config.setName(ConfigKeys.MOST_USER_EVER_ONLINE_DATE); config.setValue(Long.toString(m.getTimeInMillis())); cm.insert(config); } else { // Total config.setValue(Integer.toString(m.getTotal())); cm.update(config); // Date config.setName(ConfigKeys.MOST_USER_EVER_ONLINE_DATE); config.setValue(Long.toString(m.getTimeInMillis())); cm.update(config); } cache.add(FQN, MOST_USERS_ONLINE, m); } /** * Loads all forums. * @param fm ForumDAO */ private void loadForums(ForumDAO fm) { List l = fm.selectAll(); Map m = (Map)cache.get(FQN, RELATION); if (m == null) { m = new HashMap(); } int lastId = 0; Category c = null; String catId = null; for (Iterator iter = l.iterator(); iter.hasNext(); ) { Forum f = (Forum)iter.next(); if (f.getCategoryId() != lastId) { if (c != null) { cache.add(FQN, catId, c); } lastId = f.getCategoryId(); catId = Integer.toString(f.getCategoryId()); c = (Category)cache.get(FQN, catId); } if (c == null) { throw new CategoryNotFoundException("Category for forum #" + f.getId() + " not found"); } String forumId = Integer.toString(f.getId()); c.addForum(f); m.put(forumId, catId); } if (c != null) { cache.add(FQN, catId, c); } cache.add(FQN, RELATION, m); } private void loadUsersInfo() { UserDAO udao = DataAccessDriver.getInstance().newUserDAO(); cache.add(FQN, LAST_USER, udao.getLastUserInfo()); cache.add(FQN, TOTAL_USERS, new Integer(udao.getTotalUsers())); } /** * Loads all categories. * @param cm CategoryDAO */ private void loadCategories(CategoryDAO cm) { List categories = cm.selectAll(); Set categoriesSet = new TreeSet(new CategoryOrderComparator()); for (Iterator iter = categories.iterator(); iter.hasNext(); ) { Category c = (Category)iter.next(); cache.add(FQN, Integer.toString(c.getId()), c); categoriesSet.add(c); } cache.add(FQN, CATEGORIES_SET, categoriesSet); } private MostUsersEverOnline loadMostUsersEverOnline(ConfigDAO cm) { Config config = cm.selectByName(ConfigKeys.MOST_USERS_EVER_ONLINE); MostUsersEverOnline mostUsersEverOnline = new MostUsersEverOnline(); if (config != null) { mostUsersEverOnline.setTotal(Integer.parseInt(config.getValue())); // We're assuming that, if we have one key, the another one // will always exist config = cm.selectByName(ConfigKeys.MOST_USER_EVER_ONLINE_DATE); mostUsersEverOnline.setTimeInMillis(Long.parseLong(config.getValue())); } cache.add(FQN, MOST_USERS_ONLINE, mostUsersEverOnline); return mostUsersEverOnline; } public static String getListAllowedForums() { int n = 0; StringBuffer buf = new StringBuffer(); List allCategories = ForumRepository.getAllCategories(); for (Iterator iter = allCategories.iterator(); iter.hasNext(); ) { Collection forums = ((Category)iter.next()).getForums(); for (Iterator tmpIterator = forums.iterator(); tmpIterator.hasNext(); ) { Forum f = (Forum)tmpIterator.next(); if (ForumRepository.isForumAccessible(f.getId())) { if(n++ > 0) { buf.append(','); } buf.append(f.getId()); } } } if (n <= 0) { return "-1"; } return buf.toString(); } }