/* * ContentManager.java * * This work is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * * This work is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA * * Copyright (c) 2004-2006 Per Cederberg. All rights reserved. */ package org.liquidsite.core.content; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import org.liquidsite.util.db.DatabaseConnector; import org.liquidsite.util.log.Log; /** * The content manager. This class provides an interface for * retrieving objects from the database. The content manager * guarantees that security considerations are taken into account * before returning object (if a method comment does not explicitly * say otherwise). The content manager also makes sure not to return * unpublished content if it's policy does not allow that (only used * by the web administration). * * @author Per Cederberg, <per at percederberg dot net> * @version 1.0 */ public class ContentManager { /** * The class logger. */ private static final Log LOG = new Log(ContentManager.class); /** * The database connector. */ private DatabaseConnector db; /** * The content file base directory. */ private File baseDir; /** * The admin flag. When this flag is set, the content manager may * return content objects being offline or having work revisions. * Also, when this flag is set caching will be turned off. */ private boolean admin; /** * Creates a new content manager. If the admin content manager * flag is set, the content manager will NOT cache content * objects and the latest revision returned will always be a work * revision if one is available. * * @param db the database connector * @param baseDir the content base directory * @param admin the admin content manager flag */ public ContentManager(DatabaseConnector db, File baseDir, boolean admin) { this.db = db; this.baseDir = baseDir; this.admin = admin; } /** * Creates a new content manager. If the admin content manager * flag is set, the content manager will NOT cache content * objects and the latest revision returned will always be a work * revision if one is available. * * @param manager the content manager to modify * @param admin the admin content manager flag */ public ContentManager(ContentManager manager, boolean admin) { this(manager.getDatabase(), manager.getBaseDir(), admin); } /** * Checks if the admin flag is set. * * @return true if the admin flag is set, or * false otherwise */ public boolean isAdmin() { return admin; } /** * Checks if a content object is online. If the admin flag is set * all objects are considered online. * * @param content the content object * * @return true if the object is online, or * false otherwise */ private boolean isOnline(Content content) { return admin || content.isOnline(); } /** * Checks if a domain object is visible by a user. This method * will check if the user belongs to the same domain and has * read permissions on the domain object. * * @param user the user * @param domain the domain object * * @return true if the domain is visible, or * false otherwise * * @throws ContentException if the database couldn't be accessed * properly */ private boolean isVisible(User user, Domain domain) throws ContentException { boolean sameDomain; // Compare to user domain name if (user == null) { sameDomain = false; } else if (user.isSuperUser()) { sameDomain = true; } else { sameDomain = user.getDomainName().equals(domain.getName()); } // Skip access check if not same domain if (!sameDomain) { return false; } else { return domain.hasReadAccess(user); } } /** * Returns the database connector for this content manager. * * @return the database connector for this content manager. */ public DatabaseConnector getDatabase() { return db; } /** * Returns the content base directory. * * @return the content base directory. */ public File getBaseDir() { return baseDir; } /** * Returns an array of all domains visible by a user. Note that * this list will not contain all domains allowing read access * for the user, but only those domains that the user belongs * to (or all domains for a super user). This method should only * be used from the admin application. * * @param user the user requesting the list * * @return an array of all user readable domains * * @throws ContentException if the database couldn't be accessed * properly */ public Domain[] getDomains(User user) throws ContentException { CacheManager cache = CacheManager.getInstance(); Collection domains; Iterator iter; ArrayList list = new ArrayList(); Domain[] res; Domain domain; // Retrieve domain collection domains = cache.getAllDomains(); if (domains.isEmpty()) { getDomain("ROOT"); domains = cache.getAllDomains(); } // Find all readable domains iter = domains.iterator(); for (int i = 0; iter.hasNext(); i++) { domain = (Domain) iter.next(); if (isVisible(user, domain)) { list.add(domain); } } // Create domain array Collections.sort(list); res = new Domain[list.size()]; for (int i = 0; i < list.size(); i++) { res[i] = (Domain) list.get(i); } return res; } /** * Returns a domain with the specified name. * * @param name the domain name * * @return the domain found, or * null if no such domain exists * * @throws ContentException if the database couldn't be accessed * properly */ Domain getDomain(String name) throws ContentException { Domain domain; domain = CacheManager.getInstance().getDomain(name); if (domain == null) { CacheManager.getInstance().addAll(Domain.findAll(this)); domain = CacheManager.getInstance().getDomain(name); } return domain; } /** * Returns a domain with the specified name readable by a user. * * @param user the user requesting the domain * @param name the domain name * * @return the domain found, or * null if no such domain exists * * @throws ContentException if the database couldn't be accessed * properly * @throws ContentSecurityException if the specified domain * wasn't readable by the user */ public Domain getDomain(User user, String name) throws ContentException, ContentSecurityException { Domain domain = getDomain(name); if (domain != null && !domain.hasReadAccess(user)) { throw new ContentSecurityException(user, "read", domain); } return domain; } /** * Returns a domain having the specified host name. * * @param hostname the host name * * @return the domain having the host name, or * null if the host wasn't found */ Domain getHostDomain(String hostname) { return CacheManager.getInstance().getHostDomain(hostname); } /** * Returns all sites in a domain. * * @param domain the domain * * @return the array of sites in the domain * * @throws ContentException if the database couldn't be accessed * properly */ ContentSite[] getSites(Domain domain) throws ContentException { ContentSite[] res; res = CacheManager.getInstance().getSites(domain); if (res == null) { res = ContentSite.findByDomain(this, domain); CacheManager.getInstance().addSites(domain, res); } return res; } /** * Returns the content object with the specified identifier and * highest revision. * * @param id the content identifier * * @return the content object found, or * null if no matching content existed * * @throws ContentException if the database couldn't be accessed * properly */ Content getContent(int id) throws ContentException { CacheManager cache = CacheManager.getInstance(); Content content; content = cache.getContent(id); if (content == null) { content = InternalContent.findByMaxRevision(this, id); if (!cache.isCached(content)) { cache.add(content); } } return content; } /** * Returns the content object with the specified identifier and * highest revision readable by the user. * * @param user the user requesting the content * @param id the content identifier * * @return the content object found, or * null if no matching content existed * * @throws ContentException if the database couldn't be accessed * properly * @throws ContentSecurityException if the specified content * object wasn't readable by the user */ public Content getContent(User user, int id) throws ContentException, ContentSecurityException { return postProcess(user, getContent(id)); } /** * Returns the domain root content object with the specified name * and highest revision readable by the user. * * @param user the user requesting the content * @param domain the domain * @param name the child name * * @return the content object found, or * null if no matching content existed * * @throws ContentException if the database couldn't be accessed * properly * @throws ContentSecurityException if the specified content * object wasn't readable by the user */ public Content getContentChild(User user, Domain domain, String name) throws ContentException, ContentSecurityException { CacheManager cache = CacheManager.getInstance(); Content content; content = InternalContent.findByName(this, domain, name); if (!cache.isCached(content)) { cache.add(content); } return postProcess(user, content); } /** * Returns the child content object with the specified name and * highest revision readable by the user. * * @param user the user requesting the content * @param parent the content parent * @param name the child name * * @return the content object found, or * null if no matching content existed * * @throws ContentException if the database couldn't be accessed * properly * @throws ContentSecurityException if the specified content * object wasn't readable by the user */ public Content getContentChild(User user, Content parent, String name) throws ContentException, ContentSecurityException { CacheManager cache = CacheManager.getInstance(); Content content; content = InternalContent.findByName(this, parent, name); if (!cache.isCached(content)) { cache.add(content); } return postProcess(user, content); } /** * Returns the user readable domain root content objects. Only * the highest revision of each object will be returned. * * @param user the user requesting the content * @param domain the domain * * @return the user readable domain root content objects * * @throws ContentException if the database couldn't be accessed * properly */ public Content[] getContentChildren(User user, Domain domain) throws ContentException { CacheManager cache = CacheManager.getInstance(); Content[] children; children = InternalContent.findByParent(this, domain); cache.addAll(children); return postProcess(user, children); } /** * Returns the user readable domain root content objects in the * specified category. Only the highest revision of each object * will be returned. * * @param user the user requesting the content * @param domain the domain * @param category the content category * * @return the user readable domain root content objects * * @throws ContentException if the database couldn't be accessed * properly */ public Content[] getContentChildren(User user, Domain domain, int category) throws ContentException { CacheManager cache = CacheManager.getInstance(); Content[] children; children = InternalContent.findByCategory(this, domain, category); cache.addAll(children); return postProcess(user, children); } /** * Returns the user readable child content objects. Only the * highest revision of each object will be returned. * * @param user the user requesting the content * @param parent the content parent * * @return the user readable child content objects * * @throws ContentException if the database couldn't be accessed * properly */ public Content[] getContentChildren(User user, Content parent) throws ContentException { CacheManager cache = CacheManager.getInstance(); Content[] children; children = InternalContent.findByParent(this, parent); cache.addAll(children); return postProcess(user, children); } /** * Returns the user readable child content objects in the * specified category. Only the highest revision of each object * will be returned. * * @param user the user requesting the content * @param parent the content parent * @param category the content category * * @return the user readable child content objects * * @throws ContentException if the database couldn't be accessed * properly */ public Content[] getContentChildren(User user, Content parent, int category) throws ContentException { CacheManager cache = CacheManager.getInstance(); Content[] children; children = InternalContent.findByCategory(this, parent, category); cache.addAll(children); return postProcess(user, children); } /** * Returns the number of content objects matching the selector. * Only the highest revision of each object will be returned. Note * that the number returned by this method may in some cases be * higher than the number of content object actually visible by * the user, due to lacking read permissions. * * @param selector the content selector * * @return the number of matching content objects * * @throws ContentException if the database couldn't be accessed * properly */ public int getContentCount(ContentSelector selector) throws ContentException { return InternalContent.countBySelector(this, selector); } /** * Returns the user readable content objects matching the * selector. Only the highest revision of each object will be * returned. * * @param user the user requesting the content * @param selector the content selector * * @return the user readable content objects * * @throws ContentException if the database couldn't be accessed * properly */ public Content[] getContentObjects(User user, ContentSelector selector) throws ContentException { CacheManager cache = CacheManager.getInstance(); Content[] content; content = InternalContent.findBySelector(this, selector); cache.addAll(content); return postProcess(user, content); } /** * Returns the permission list applicable to a domain object. If * the object has no permissions an empty permission list will be * returned. * * @param domain the domain object * * @return the permission list for this object * * @throws ContentException if the database couldn't be accessed * properly */ public PermissionList getPermissions(Domain domain) throws ContentException { CacheManager cache = CacheManager.getInstance(); PermissionList permissions; permissions = cache.getPermissions(domain); if (permissions == null) { permissions = PermissionList.findByDomain(this, domain); cache.add(permissions); } return permissions; } /** * Returns the permission list applicable to a content object. If * the object has no permissions either an empty list or the * inherited permission list will be returned. * * @param content the content object * @param inherit the search inherited permissions flag * * @return the permission list for this object * * @throws ContentException if the database couldn't be accessed * properly */ public PermissionList getPermissions(Content content, boolean inherit) throws ContentException { CacheManager cache = CacheManager.getInstance(); PermissionList permissions; permissions = cache.getPermissions(content, inherit); if (permissions == null) { permissions = PermissionList.findByContent(this, content); cache.add(permissions); while (permissions.isEmpty() && inherit) { permissions = cache.getPermissions(content, true); if (permissions != null) { break; } if (content.getParentId() <= 0) { return getPermissions(content.getDomain()); } content = content.getParent(); if (!cache.isCached(content)) { cache.add(content); } permissions = cache.getPermissions(content, true); if (permissions == null) { permissions = PermissionList.findByContent(this, content); cache.add(permissions); } } } return permissions; } /** * Returns a user with a specified name. If the user couldn't be * found in the specified domain, this method also checks for * superusers with the specified name. * * @param domain the domain * @param name the user name * * @return the user found, or * null if no matching user existed * * @throws ContentException if the database couldn't be accessed * properly */ public User getUser(Domain domain, String name) throws ContentException { User user; user = User.findByName(this, domain, name); if (user == null) { user = User.findByName(this, null, name); } return user; } /** * Returns a user with a specified email address. If the user * couldn't be found in the specified domain, this method also * checks for superusers with the specified email address. * * @param domain the domain * @param email the user email address * * @return the user found, or * null if no matching user existed * * @throws ContentException if the database couldn't be accessed * properly */ public User getUserByEmail(Domain domain, String email) throws ContentException { User user; user = User.findByEmail(this, domain, email); if (user == null) { user = User.findByEmail(this, null, email); } return user; } /** * Returns the number of users in a specified domain. Only users * with matching names will be counted. * * @param domain the domain, or null for superusers * @param filter the user search filter (empty for all) * * @return the number of matching users in the domain * * @throws ContentException if the database couldn't be accessed * properly */ public int getUserCount(Domain domain, String filter) throws ContentException { return User.countByDomain(this, domain, filter); } /** * Returns an array of users in a specified domain. Only users * with matching names will be returned. Also, only a limited * interval of the matching users will be returned. * * @param domain the domain, or null for superusers * @param filter the user search filter (empty for all) * @param startPos the list interval start position * @param maxLength the list interval maximum length * * @return an array of matching users in the domain * * @throws ContentException if the database couldn't be accessed * properly */ public User[] getUsers(Domain domain, String filter, int startPos, int maxLength) throws ContentException { return User.findByDomain(this, domain, filter, startPos, maxLength); } /** * Returns a group with a specified name. * * @param domain the domain * @param name the group name * * @return the group found, or * null if no matching group existed * * @throws ContentException if the database couldn't be accessed * properly */ public Group getGroup(Domain domain, String name) throws ContentException { return Group.findByName(this, domain, name); } /** * Returns an array of groups in a specified domain. Only groups * with matching names will be returned. * * @param domain the domain * @param filter the search filter (empty for all) * * @return an array of matching groups in the domain * * @throws ContentException if the database couldn't be accessed * properly */ public Group[] getGroups(Domain domain, String filter) throws ContentException { if (domain == null) { return new Group[0]; } else { return Group.findByDomain(this, domain, filter); } } /** * Finds the site corresponding to a web request. This method * does NOT control access permissions and should thus ONLY be * used internally in the request processing. * * @param protocol the request protocol (i.e. "http") * @param hostname the request host name * @param port the request port number * @param path the full request path * * @return the site corresponding to the request, or * null if no matching site was found * * @throws ContentException if the database couldn't be accessed * properly */ public ContentSite findSite(String protocol, String hostname, int port, String path) throws ContentException { Domain domain; ContentSite[] sites; ContentSite res = null; int max = 0; int match; domain = getHostDomain(hostname); if (domain == null) { domain = getDomain("ROOT"); } sites = getSites(domain); LOG.trace("evaluating " + sites.length + " sites"); for (int i = 0; i < sites.length; i++) { match = sites[i].match(protocol, hostname, port, path); LOG.trace("site " + sites[i] + " match value: " + match + ", online: " + sites[i].isOnline() + ", revision: " + sites[i].getRevisionNumber()); if (sites[i].isOnline() && sites[i].getRevisionNumber() > 0 && match > max) { res = sites[i]; max = match; } } return res; } /** * Resets this content manager and frees all cached resources. * This method should be called in order to garbage collect the * resources used by this manager. It may have a negative effect * on performance and should therefore be avoided if possible. */ public void reset() { CacheManager.getInstance().removeAll(); LOG.trace("reset content manager"); } /** * Post-processes a retrieved content object. This method checks * that the object is safe to return to the specified user. * * @param user the user requesting the object * @param content the content object found, or null * * @return the content object found, or * null if the object isn't visible for the user * * @throws ContentException if the database couldn't be accessed * properly * @throws ContentSecurityException if the specified content * object wasn't readable by the user */ private Content postProcess(User user, Content content) throws ContentException, ContentSecurityException { if (content != null && !content.hasReadAccess(user)) { throw new ContentSecurityException(user, "read", content); } else if (content != null && !isOnline(content)) { return null; } else { return content; } } /** * Post-processes an array of retrieved content objects. This * method checks that the objects are safe to return to the * specified user, filtering out the ones that are not. * * @param user the user requesting the object * @param content the content objects found * * @return the readable and sorted content objects * * @throws ContentException if the database couldn't be accessed * properly */ private Content[] postProcess(User user, Content[] content) throws ContentException { ArrayList list = new ArrayList(content.length); Content[] res; for (int i = 0; i < content.length; i++) { if (content[i].hasReadAccess(user)) { list.add(content[i]); } } if (content.length == list.size()) { res = content; } else { res = new Content[list.size()]; list.toArray(res); } return res; } }