/* * BeanContext.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-2007 Per Cederberg. All rights reserved. */ package org.liquidsite.app.template; import java.util.ArrayList; import java.util.HashMap; import org.liquidsite.core.content.Content; import org.liquidsite.core.content.ContentDocument; import org.liquidsite.core.content.ContentException; import org.liquidsite.core.content.ContentFile; import org.liquidsite.core.content.ContentFolder; import org.liquidsite.core.content.ContentForum; import org.liquidsite.core.content.ContentManager; import org.liquidsite.core.content.ContentPost; import org.liquidsite.core.content.ContentSection; import org.liquidsite.core.content.ContentSecurityException; import org.liquidsite.core.content.ContentSelector; import org.liquidsite.core.content.ContentSite; import org.liquidsite.core.content.ContentTopic; import org.liquidsite.core.content.Domain; import org.liquidsite.core.content.Group; import org.liquidsite.core.content.User; import org.liquidsite.core.web.Request; import org.liquidsite.util.log.Log; import org.liquidsite.util.mail.MailMessage; import org.liquidsite.util.mail.MailMessageException; import org.liquidsite.util.mail.MailQueue; import org.liquidsite.util.mail.SimpleMailMessage; /** * A template bean context. This class holds contains references to * common objects used by all beans. It also contains several utility * methods to simplify the code in the various beans. * * @author Per Cederberg, <per at percederberg dot net> * @version 1.0 */ public class BeanContext { /** * The class logger. */ private static final Log LOG = new Log(BeanContext.class); /** * The request being processed. */ private Request request; /** * The content manager to use. */ private ContentManager manager; /** * The relative path to the site root directory. The path is * relative to the request URL and may be empty if the request * refers to an object in the site root directory. Otherwise the * path ends with an "/" character. */ private String sitePath = null; /** * The cache of user beans. Each user is indexed by the user name. * The special user name "" (i.e. an empty string) is used for the * current user. */ private HashMap usersCache = new HashMap(); /** * The output content MIME type. */ private String mimeType = "text/html"; /** * Creates a new bean context. * * @param request the request object * @param manager the content manager to use */ BeanContext(Request request, ContentManager manager) { UserBean user; this.request = request; this.manager = manager; user = new UserBean(this, request.getUser()); usersCache.put(user.getLogin(), user); if (!user.getLogin().equals("")) { usersCache.put("", user); } } /** * Returns the response content MIME type. By default the content * type is set to "text/html". * * @return the response content MIME type */ public String getMimeType() { return mimeType; } /** * Sets the response content MIME type. * * @param mimeType the new MIME type */ public void setMimeType(String mimeType) { this.mimeType = mimeType; } /** * Returns the request object. * * @return the request object */ public Request getRequest() { return request; } /** * Returns the relative path to the site root directory. The path * is relative to the request URL and may be empty if the request * refers to an object in the site root directory. Otherwise the * path ends with an "/" character. The result of this method * will be stored in an instance variable to avoid calculations * on further requests. * * @return the relative path to the site root directory */ public String getSitePath() { ContentSite site; if (sitePath == null) { site = request.getEnvironment().getSite(); sitePath = getRelativePath(site.getDirectory()); } return sitePath; } /** * Returns the relative path to a specified base path. The * specified path is assumed to be a part of the complete request * path. This method will return an empty string if the request * refers to a object in the specified directory. * * @param basePath the absolute base path * * @return the relative path to the base path for the request */ public String getRelativePath(String basePath) { StringBuffer buffer = new StringBuffer(); String path; int pos; path = request.getPath(); path = path.substring(basePath.length()); while ((pos = path.indexOf('/')) >= 0) { buffer.append("../"); path = path.substring(pos + 1); } return buffer.toString(); } /** * Returns the absolute path to a content object. * * @param content the content object * * @return the absolute path to a content object */ public String getContentPath(Content content) { try { if (content instanceof ContentSite) { return request.getEnvironment().getSite().getDirectory(); } else if (content instanceof ContentFolder || content instanceof ContentSection || content instanceof ContentDocument || content instanceof ContentForum || content instanceof ContentTopic) { return getContentPath(content.getParent()) + content.getName() + "/"; } else if (content != null) { return getContentPath(content.getParent()) + content.getName(); } else { return "/"; } } catch (ContentException e) { LOG.error(e.getMessage()); return ""; } } /** * Counts the number of content objects matching a content * selector. * * @param selector the content selector * * @return the number of matching content objects * * @throws ContentException if the database couldn't be accessed * properly */ public int countContent(ContentSelector selector) throws ContentException { return manager.getContentCount(selector); } /** * Counts the number of documents under the specified absolute * path. * * @param path the section path * * @return the number of documents found */ public int countDocuments(String path) { Content content; try { content = findContent(path); if (content instanceof ContentSection) { return countDocuments((ContentSection) content); } else { LOG.info("failed to find section: " + path); } } catch (ContentException e) { LOG.error(e.getMessage()); } catch (ContentSecurityException e) { LOG.warning(e.getMessage()); } return 0; } /** * Counts the number of documents under the specified section (and * subsections). * * @param section the parent section * * @return the number of documents found */ public int countDocuments(ContentSection section) { ContentSelector selector; try { selector = new ContentSelector(section.getDomain()); selector.requireCategory(Content.DOCUMENT_CATEGORY); setSelectorParents(selector, section); return countContent(selector); } catch (ContentException e) { LOG.error(e.getMessage()); } return 0; } /** * Creates a content bean from a content object. The default bean * constructors will be used in most cases. * * @param content the content object * * @return the content bean created, or * null if the content type was unsupported * * @throws ContentException if the database couldn't be accessed * properly (for document parent retrieval) */ public ContentBean createContentBean(Content content) throws ContentException { if (content instanceof ContentDocument) { return new DocumentBean(this, (ContentDocument) content, (ContentSection) content.getParent()); } else if (content instanceof ContentFile) { return new DocumentFileBean(this, (ContentFile) content); } else if (content instanceof ContentSection) { return new SectionBean(this, (ContentSection) content); } else if (content instanceof ContentForum) { return new ForumBean(this, (ContentForum) content); } else if (content instanceof ContentTopic) { return new TopicBean(this, (ContentTopic) content); } else if (content instanceof ContentPost) { return new PostBean(this, (ContentPost) content); } else if (content instanceof ContentSite) { return new SiteBean(this, (ContentSite) content); } else { return null; } } /** * Finds a content object from a specified content id. * * @param id the content identifier * * @return the content object found, or * null for none * * @throws ContentException if the database couldn't be accessed * properly * @throws ContentSecurityException if the content couldn't be * read by the current user */ public Content findContent(int id) throws ContentException, ContentSecurityException { return manager.getContent(request.getUser(), id); } /** * Finds a content object from an absolute path. The object found * can be any object present in a section, that is either a * section, a document or a forum. * * @param path the content path (from the domain root) * * @return the content object found, or * null if not found * * @throws ContentException if the database couldn't be accessed * properly * @throws ContentSecurityException if the content couldn't be * read by the current user */ public Content findContent(String path) throws ContentException, ContentSecurityException { Domain domain; domain = request.getEnvironment().getDomain(); return findContent(domain, path); } /** * Finds a content object from a relative path. The object found * can be any object present in a section, that is either a * section, a document or a forum. * * @param parent the parent domain or section * @param path the content path (relative to the parent) * * @return the content object found, or * null if not found * * @throws ContentException if the database couldn't be accessed * properly * @throws ContentSecurityException if the content couldn't be * read by the current user */ public Content findContent(Object parent, String path) throws ContentException, ContentSecurityException { String name; int pos; // Check for empty path or null parent if (path == null || path.equals("") || parent == null) { if (parent instanceof Content) { return (Content) parent; } else { return null; } } // Find child name if (path.startsWith("/")) { path = path.substring(1); } pos = path.indexOf("/"); if (pos > 0) { name = path.substring(0, pos); path = path.substring(pos + 1); } else { name = path; path = ""; } // Find child content return findContent(findContentChild(parent, name), path); } /** * Finds a named child object. The parent may be either a domain * or a content section. The child returned can be either a * content section or some content object present directly in the * section. * * @param parent the parent domain or section * @param name the child name * * @return the child content object found, or * null if not found * * @throws ContentException if the database couldn't be accessed * properly * @throws ContentSecurityException if the section couldn't be * read by the current user */ private Content findContentChild(Object parent, String name) throws ContentException, ContentSecurityException { Content[] children; if (manager == null) { return null; } else if (parent instanceof Domain) { // TODO: implement this more efficiently children = manager.getContentChildren(request.getUser(), (Domain) parent, Content.SECTION_CATEGORY); for (int i = 0; i < children.length; i++) { if (children[i].getName().equals(name)) { return children[i]; } } } else { return manager.getContentChild(request.getUser(), (Content) parent, name); } return null; } /** * Finds a set of content objects with a content selector. * * @param selector the content selector * * @return the array of content objects found * * @throws ContentException if the database couldn't be accessed * properly */ public Content[] findContent(ContentSelector selector) throws ContentException { return manager.getContentObjects(request.getUser(), selector); } /** * Finds a document corresponding to the specified absolute path. * * @param path the document (and section) path * * @return the document found, or * an empty document if not found */ public DocumentBean findDocument(String path) { Domain domain; domain = request.getEnvironment().getDomain(); return findDocument(domain, path); } /** * Finds a document corresponding to the specified relative path. * * @param parent the parent domain or section * @param path the document (and section) path * * @return the document found, or * an empty document if not found */ public DocumentBean findDocument(Object parent, String path) { Content content; try { content = findContent(parent, path); if (content instanceof ContentDocument) { return (DocumentBean) createContentBean(content); } else { LOG.info("failed to find document: " + path); } } catch (ContentException e) { LOG.error(e.getMessage()); } catch (ContentSecurityException e) { LOG.warning(e.getMessage()); } return new DocumentBean(); } /** * Finds all documents under the specified absolute path. The * documents will be returned (as document beans) in a list. * * @param path the section path * @param sorting the sorting information * @param offset the number of documents to skip * @param count the maximum number of documents * * @return a list of the documents found (as document beans) */ public ArrayList findDocuments(String path, String sorting, int offset, int count) { Content content; try { content = findContent(path); if (content instanceof ContentSection) { return findDocuments((ContentSection) content, sorting, offset, count); } else { LOG.info("failed to find section: " + path); } } catch (ContentException e) { LOG.error(e.getMessage()); } catch (ContentSecurityException e) { LOG.warning(e.getMessage()); } return new ArrayList(0); } /** * Finds all documents under the specified section (and * subsections). The documents will be returned (as document * beans) in a list. * * @param section the parent section * @param sorting the sorting information * @param offset the number of documents to skip * @param count the maximum number of documents * * @return a list of the documents found (as document beans) */ public ArrayList findDocuments(ContentSection section, String sorting, int offset, int count) { ArrayList results = new ArrayList(); ContentSelector selector; Content[] children; DocumentBean doc; try { selector = new ContentSelector(section.getDomain()); selector.requireCategory(Content.DOCUMENT_CATEGORY); setSelectorParents(selector, section); setSelectorSorting(selector, sorting.trim()); selector.limitResults(offset, count); children = findContent(selector); for (int i = 0; i < children.length; i++) { doc = new DocumentBean(this, (ContentDocument) children[i], section); results.add(doc); } } catch (ContentException e) { LOG.error(e.getMessage()); } return results; } /** * Finds a named forum in a section. * * @param section the parent section * @param name the forum name * * @return the forum found, or * an empty forum if not found */ public ForumBean findForum(ContentSection section, String name) { Content[] content; try { // TODO: implement this more efficiently content = manager.getContentChildren(request.getUser(), section, Content.FORUM_CATEGORY); for (int i = 0; i < content.length; i++) { if (content[i].getName().equals(name)) { return (ForumBean) createContentBean(content[i]); } } } catch (ContentException e) { LOG.error(e.getMessage()); } return new ForumBean(); } /** * Finds a section corresponding to the specified absolute path. * * @param path the section path * * @return the section found, or * an empty section if not found */ public SectionBean findSection(String path) { Domain domain; domain = request.getEnvironment().getDomain(); return findSection(domain, path); } /** * Finds a section corresponding to the specified relative path. * * @param parent the parent domain or section * @param path the section path * * @return the section found, or * an empty section if not found */ public SectionBean findSection(Object parent, String path) { Content content; try { content = findContent(parent, path); if (content instanceof ContentSection) { return (SectionBean) createContentBean(content); } else { LOG.info("failed to find section: " + path); } } catch (ContentException e) { LOG.error(e.getMessage()); } catch (ContentSecurityException e) { LOG.warning(e.getMessage()); } return new SectionBean(); } /** * Finds a specified user. All users found will be added to an * internal cache, making repetitive queries efficient. * * @param name the user name, or "" for the current user * * @return the user bean for the specified user, or * an empty bean if no such user could be found */ public UserBean findUser(String name) { Domain domain; User user = null; if (!usersCache.containsKey(name)) { try { domain = request.getEnvironment().getDomain(); user = manager.getUser(domain, name); } catch (ContentException e) { LOG.error(e.getMessage()); } usersCache.put(name, new UserBean(this, user)); } return (UserBean) usersCache.get(name); } /** * Finds a specified user by email address. All users found will * be added to an internal cache, making queries for user name * efficient. * * @param email the user email address * * @return the user bean for the specified user, or * an empty bean if no such user could be found */ public UserBean findUserByEmail(String email) { Domain domain; User user = null; try { domain = request.getEnvironment().getDomain(); user = manager.getUserByEmail(domain, email); } catch (ContentException e) { LOG.error(e.getMessage()); } if (user == null) { return new UserBean(this, null); } else { if (!usersCache.containsKey(user.getName())) { usersCache.put(user.getName(), new UserBean(this, user)); } return (UserBean) usersCache.get(user.getName()); } } /** * Finds a specified group. * * @param name the group name * * @return the group found, or * null if not found */ public Group findGroup(String name) { Domain domain; Group group = null; try { domain = request.getEnvironment().getDomain(); group = manager.getGroup(domain, name); } catch (ContentException e) { LOG.error(e.getMessage()); } return group; } /** * Creates a new user with the specified name in the current * environment domain. The user object will not be saved to the * database, only instantiated with the correct parameters. * * @param name the login name * * @return the user object create, or * null if no such user could be created */ public User createUser(String name) { return new User(manager, request.getEnvironment().getDomain(), name); } /** * Sends an email to the specified receiver. The email will not be * sent immediately, but rather queued in the outgoing mail queue. * * @param receiver the message recipient address * @param subject the message subject line * @param text the message text * * @return true if the mail could be queued correctly, or * false otherwise */ public boolean sendMail(String receiver, String subject, String text) { SimpleMailMessage msg = new SimpleMailMessage(); Domain domain; try { domain = request.getEnvironment().getDomain(); msg.setFrom(domain.getMailFrom()); msg.setRecipients(receiver); msg.setSubject(subject); msg.setText(text); setMailAttributes(msg); MailQueue.getInstance().add(msg); return true; } catch (MailMessageException e) { LOG.info("couldn't send mail", e); return false; } } /** * Sends an email to all the members in a group. The email will * not be sent immediately, but rather queued in the outgoing * mail queue. * * @param receiver the message recipient group * @param subject the message subject line * @param text the message text * * @return true if the mail could be queued correctly, or * false otherwise */ public boolean sendMail(Group receiver, String subject, String text) { GroupMailMessage msg = new GroupMailMessage(); Domain domain; try { domain = request.getEnvironment().getDomain(); msg.setFrom(domain.getMailFrom()); msg.setRecipient(receiver); msg.setSubject(subject); msg.setText(text); setMailAttributes(msg); MailQueue.getInstance().add(msg); return true; } catch (MailMessageException e) { LOG.info("couldn't send mail", e); return false; } } /** * Sets the default attributes on a mail message. These attributes * are added for security reasons, to be able to track any page or * user sending spam email through this service. * * @param msg the mail message */ private void setMailAttributes(MailMessage msg) { if (request.getUser() == null) { msg.setAttribute("URL", request.getUrl() ); msg.setAttribute("IP", request.getRemoteAddr()); } else { msg.setAttribute("Domain", request.getUser().getDomainName()); msg.setAttribute("User", request.getUser().getName()); } } /** * Sets the selector parent requirement from the specified section * and all its subsections. * * @param selector the content selector * @param section the content section * * @throws ContentException if the database couldn't be accessed * properly */ private void setSelectorParents(ContentSelector selector, ContentSection section) throws ContentException { Content[] children; selector.requireParent(section); children = manager.getContentChildren(request.getUser(), section, Content.SECTION_CATEGORY); for (int i = 0; i < children.length; i++) { setSelectorParents(selector, (ContentSection) children[i]); } } /** * Sets the selector sorting from the specified sorting * information. * * @param selector the content selector * @param sorting the sorting information */ private void setSelectorSorting(ContentSelector selector, String sorting) { String str; boolean ascending; int pos; if (sorting.equals("")) { selector.sortByModified(false); } while (sorting.length() > 0) { pos = sorting.indexOf(","); if (pos > 0) { str = sorting.substring(0, pos).trim(); sorting = sorting.substring(pos + 1).trim(); } else { str = sorting; sorting = ""; } ascending = true; if (str.startsWith("+") || str.startsWith("-")) { ascending = str.startsWith("+"); str = str.substring(1); } if (str.equals("id")) { selector.sortById(ascending); } else if (str.equals("name")) { selector.sortByName(ascending); } else if (str.equals("path")) { selector.sortByParent(ascending); selector.sortByName(ascending); } else if (str.equals("parent")) { selector.sortByParent(ascending); } else if (str.equals("revision")) { selector.sortByRevision(ascending); } else if (str.equals("date")) { selector.sortByModified(ascending); } else if (str.equals("user")) { selector.sortByAuthor(ascending); } else if (str.equals("online")) { selector.sortByOnline(ascending); } else if (str.startsWith("data.")) { selector.sortByDocumentProperty(str.substring(5), ascending); } else { LOG.warning("invalid sorting column: " + sorting + " in page " + request.getUrl()); } } } }