diff --git a/source/java/org/alfresco/web/app/servlet/BaseServlet.java b/source/java/org/alfresco/web/app/servlet/BaseServlet.java index 16b8c4fb43..2e8568a056 100644 --- a/source/java/org/alfresco/web/app/servlet/BaseServlet.java +++ b/source/java/org/alfresco/web/app/servlet/BaseServlet.java @@ -25,7 +25,6 @@ import java.util.List; import java.util.Set; import javax.faces.context.FacesContext; -import javax.faces.el.ValueBinding; import javax.servlet.ServletContext; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -159,20 +158,6 @@ public abstract class BaseServlet extends HttpServlet } } - /** - * Return a JSF managed bean reference. - * - * @param fc FacesContext - * @param name Name of the managed bean to return - * - * @return the managed bean or null if not found - */ - public static Object getManagedBean(FacesContext fc, String name) - { - ValueBinding vb = fc.getApplication().createValueBinding("#{" + name + "}"); - return vb.getValue(fc); - } - /** * Returns true if the specified JSP file is valid for a redirect after login. * Only a specific sub-set of the available JSPs are valid to jump directly too after a diff --git a/source/java/org/alfresco/web/app/servlet/ExternalAccessServlet.java b/source/java/org/alfresco/web/app/servlet/ExternalAccessServlet.java index 1060637ae8..3c3cd76222 100644 --- a/source/java/org/alfresco/web/app/servlet/ExternalAccessServlet.java +++ b/source/java/org/alfresco/web/app/servlet/ExternalAccessServlet.java @@ -34,7 +34,6 @@ import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.web.app.Application; import org.alfresco.web.bean.BrowseBean; -import org.alfresco.web.bean.LoginBean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -109,7 +108,7 @@ public class ExternalAccessServlet extends BaseServlet // we almost always need this bean reference FacesContext fc = FacesHelper.getFacesContext(req, res, getServletContext()); - BrowseBean browseBean = (BrowseBean)getManagedBean(fc, "BrowseBean"); + BrowseBean browseBean = (BrowseBean)FacesHelper.getManagedBean(fc, "BrowseBean"); // get services we need ServiceRegistry serviceRegistry = getServiceRegistry(getServletContext()); diff --git a/source/java/org/alfresco/web/app/servlet/FacesHelper.java b/source/java/org/alfresco/web/app/servlet/FacesHelper.java index 8e7dd9d703..5ea60d4f13 100644 --- a/source/java/org/alfresco/web/app/servlet/FacesHelper.java +++ b/source/java/org/alfresco/web/app/servlet/FacesHelper.java @@ -20,6 +20,7 @@ import javax.faces.FactoryFinder; import javax.faces.component.UIViewRoot; import javax.faces.context.FacesContext; import javax.faces.context.FacesContextFactory; +import javax.faces.el.ValueBinding; import javax.faces.lifecycle.Lifecycle; import javax.faces.lifecycle.LifecycleFactory; import javax.portlet.PortletContext; @@ -103,6 +104,20 @@ public final class FacesHelper return facesContext; } + /** + * Return a JSF managed bean reference. + * + * @param fc FacesContext + * @param name Name of the managed bean to return + * + * @return the managed bean or null if not found + */ + public static Object getManagedBean(FacesContext fc, String name) + { + ValueBinding vb = fc.getApplication().createValueBinding("#{" + name + "}"); + return vb.getValue(fc); + } + /** * We need an inner class to be able to call FacesContext.setCurrentInstance * since it's a protected method diff --git a/source/java/org/alfresco/web/bean/BrowseBean.java b/source/java/org/alfresco/web/bean/BrowseBean.java index d62ebaf60d..5a6a5babfd 100644 --- a/source/java/org/alfresco/web/bean/BrowseBean.java +++ b/source/java/org/alfresco/web/bean/BrowseBean.java @@ -19,16 +19,20 @@ package org.alfresco.web.bean; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import javax.faces.application.FacesMessage; import javax.faces.context.FacesContext; import javax.faces.event.ActionEvent; import javax.transaction.UserTransaction; +import org.alfresco.config.Config; +import org.alfresco.config.ConfigElement; +import org.alfresco.config.ConfigService; import org.alfresco.model.ContentModel; -import org.alfresco.model.ForumModel; import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.dictionary.TypeDefinition; import org.alfresco.service.cmr.lock.LockService; @@ -50,6 +54,7 @@ import org.alfresco.web.app.Application; import org.alfresco.web.app.context.IContextListener; import org.alfresco.web.app.context.UIContextService; import org.alfresco.web.app.servlet.DownloadContentServlet; +import org.alfresco.web.app.servlet.FacesHelper; import org.alfresco.web.bean.repository.MapNode; import org.alfresco.web.bean.repository.Node; import org.alfresco.web.bean.repository.NodePropertyResolver; @@ -71,7 +76,6 @@ import org.alfresco.web.ui.repo.component.UINodePath; import org.alfresco.web.ui.repo.component.UISimpleSearch; import org.apache.log4j.Logger; import org.apache.log4j.Priority; -import org.springframework.util.StringUtils; /** * Bean providing properties and behaviour for the main folder/document browse screen and @@ -391,17 +395,17 @@ public class BrowseBean implements IContextListener } /** - * Setup the additional properties required at data-binding time. + * Setup the common properties required at data-binding time. *

* These are properties used by components on the page when iterating over the nodes. * Information such as whether the node is locked, a working copy, download URL etc. *

* We use a set of anonymous inner classes to provide the implemention for the property - * getters. The interfaces are only called when the properties are first required. + * getters. The interfaces are only called when the properties are first requested. * - * @param node MapNode to add the properties too + * @param node Node to add the properties too */ - public void setupDataBindingProperties(MapNode node) + public void setupCommonBindingProperties(Node node) { // special properties to be used by the value binding components on the page node.addPropertyResolver("locked", this.resolverlocked); @@ -413,7 +417,6 @@ public class BrowseBean implements IContextListener node.addPropertyResolver("size", this.resolverSize); node.addPropertyResolver("cancelCheckOut", this.resolverCancelCheckOut); node.addPropertyResolver("checkIn", this.resolverCheckIn); - node.addPropertyResolver("beingDiscussed", this.resolverBeingDiscussed); node.addPropertyResolver("editLinkType", this.resolverEditLinkType); node.addPropertyResolver("webdavUrl", this.resolverWebdavUrl); node.addPropertyResolver("cifsPath", this.resolverCifsPath); @@ -432,6 +435,26 @@ public class BrowseBean implements IContextListener } + // ------------------------------------------------------------------------------ + // NodeEventListener listeners + + /** + * Add a listener to those called by the BrowseBean when nodes are created + */ + public void addNodeEventListener(NodeEventListener listener) + { + getNodeEventListeners().add(listener); + } + + /** + * Remove a listener from the list of those called by BrowseBean + */ + public void removeNodeEventListener(NodeEventListener listener) + { + getNodeEventListeners().remove(listener); + } + + // ------------------------------------------------------------------------------ // Navigation action event handlers @@ -533,7 +556,11 @@ public class BrowseBean implements IContextListener MapNode node = new MapNode(nodeRef, this.nodeService, true); node.addPropertyResolver("icon", this.resolverSpaceIcon); node.addPropertyResolver("smallIcon", this.resolverSmallIcon); - node.addPropertyResolver("beingDiscussed", this.resolverBeingDiscussed); + + for (NodeEventListener listener : getNodeEventListeners()) + { + listener.created(node, type); + } this.containerNodes.add(node); } @@ -542,7 +569,12 @@ public class BrowseBean implements IContextListener // create our Node representation MapNode node = new MapNode(nodeRef, this.nodeService, true); - setupDataBindingProperties(node); + setupCommonBindingProperties(node); + + for (NodeEventListener listener : getNodeEventListeners()) + { + listener.created(node, type); + } this.contentNodes.add(node); } @@ -646,12 +678,15 @@ public class BrowseBean implements IContextListener // create our Node representation MapNode node = new MapNode(nodeRef, this.nodeService, true); - // construct the path to this Node node.addPropertyResolver("path", this.resolverPath); node.addPropertyResolver("displayPath", this.resolverDisplayPath); node.addPropertyResolver("icon", this.resolverSpaceIcon); node.addPropertyResolver("smallIcon", this.resolverSmallIcon); - node.addPropertyResolver("beingDiscussed", this.resolverBeingDiscussed); + + for (NodeEventListener listener : getNodeEventListeners()) + { + listener.created(node, type); + } this.containerNodes.add(node); } @@ -660,12 +695,16 @@ public class BrowseBean implements IContextListener // create our Node representation MapNode node = new MapNode(nodeRef, this.nodeService, true); - setupDataBindingProperties(node); + setupCommonBindingProperties(node); - // construct the path to this Node node.addPropertyResolver("path", this.resolverPath); node.addPropertyResolver("displayPath", this.resolverDisplayPath); + for (NodeEventListener listener : getNodeEventListeners()) + { + listener.created(node, type); + } + this.contentNodes.add(node); } } @@ -741,34 +780,8 @@ public class BrowseBean implements IContextListener }; public NodePropertyResolver resolverCheckIn = new NodePropertyResolver() { - public Object get(Node node) - { - boolean canCheckin = false; - - // if the working copy has a discussion the user will also need to have - // contributor permission on the locked node - if (node.hasAspect(ContentModel.ASPECT_WORKING_COPY)) - { - if (node.hasAspect(ForumModel.ASPECT_DISCUSSABLE)) - { - // get the original locked node (via the copiedfrom aspect) - NodeRef lockedNodeRef = (NodeRef)nodeService.getProperty(node.getNodeRef(), ContentModel.PROP_COPY_REFERENCE); - if (lockedNodeRef != null) - { - Node lockedNode = new Node(lockedNodeRef); - canCheckin = node.hasPermission(PermissionService.CHECK_IN) && - lockedNode.hasPermission(PermissionService.CONTRIBUTOR); - } - } - else - { - // there is no discussion so just check they have checkin permission - // for the node - canCheckin = node.hasPermission(PermissionService.CHECK_IN); - } - } - - return canCheckin; + public Object get(Node node) { + return node.hasAspect(ContentModel.ASPECT_WORKING_COPY) && node.hasPermission(PermissionService.CHECK_IN); } }; @@ -778,12 +791,6 @@ public class BrowseBean implements IContextListener } }; - public NodePropertyResolver resolverBeingDiscussed = new NodePropertyResolver() { - public Object get(Node node) { - return node.hasAspect(ForumModel.ASPECT_DISCUSSABLE); - } - }; - public NodePropertyResolver resolverDownload = new NodePropertyResolver() { public Object get(Node node) { return DownloadContentServlet.generateDownloadURL(node.getNodeRef(), node.getName()); @@ -845,24 +852,8 @@ public class BrowseBean implements IContextListener public NodePropertyResolver resolverSmallIcon = new NodePropertyResolver() { public Object get(Node node) { - QNameNodeMap props = (QNameNodeMap)node.getProperties(); - - String icon = "space_small"; - - // we know we have small versions of the forum space types so use them! - QName nodeType = node.getType(); - if (nodeType.equals(ForumModel.TYPE_FORUMS) || nodeType.equals(ForumModel.TYPE_FORUM) || - nodeType.equals(ForumModel.TYPE_TOPIC)) - { - String storedIcon = (String)props.getRaw("app:icon"); - - if (storedIcon != null) - { - icon = StringUtils.replace(storedIcon, "_large", ""); - } - } - - return icon; + // TODO: add support for different small space icons + return SPACE_SMALL_DEFAULT; } }; @@ -1155,6 +1146,11 @@ public class BrowseBean implements IContextListener node.addPropertyResolver("cancelCheckOut", this.resolverCancelCheckOut); node.addPropertyResolver("checkIn", this.resolverCheckIn); + for (NodeEventListener listener : getNodeEventListeners()) + { + listener.created(node, node.getType()); + } + // get hold of the DocumentDetailsBean and reset it DocumentDetailsBean docDetails = (DocumentDetailsBean)FacesContext.getCurrentInstance(). getExternalContext().getSessionMap().get("DocumentDetailsBean"); @@ -1321,8 +1317,9 @@ public class BrowseBean implements IContextListener */ private void initFromClientConfig() { - this.viewsConfig = (ViewsConfigElement)Application.getConfigService( - FacesContext.getCurrentInstance()).getConfig("Views"). + ConfigService config = Application.getConfigService(FacesContext.getCurrentInstance()); + + this.viewsConfig = (ViewsConfigElement)config.getConfig("Views"). getConfigElement(ViewsConfigElement.CONFIG_ELEMENT_ID); this.browseViewMode = this.viewsConfig.getDefaultView(PAGE_NAME_BROWSE); @@ -1330,6 +1327,42 @@ public class BrowseBean implements IContextListener this.browseViewMode); } + /** + * @return the Set of NodeEventListeners registered against this bean + */ + private Set getNodeEventListeners() + { + if (this.nodeEventListeners == null) + { + this.nodeEventListeners = new HashSet(); + + FacesContext fc = FacesContext.getCurrentInstance(); + + Config listenerConfig = Application.getConfigService(fc).getConfig("Node Event Listeners"); + if (listenerConfig != null) + { + ConfigElement listenerElement = listenerConfig.getConfigElement("node-event-listeners"); + if (listenerElement != null) + { + for (ConfigElement child : listenerElement.getChildren()) + { + if (child.getName().equals("listener")) + { + // retrieved the JSF Managed Bean identified in the config + String listenerName = child.getValue().trim(); + Object bean = FacesHelper.getManagedBean(fc, listenerName); + if (bean instanceof NodeEventListener) + { + addNodeEventListener((NodeEventListener)bean); + } + } + } + } + } + } + return this.nodeEventListeners; + } + /** * Refresh the UI after a Space selection change. Adds the selected space to the breadcrumb * location path and also updates the list components in the UI. @@ -1515,8 +1548,12 @@ public class BrowseBean implements IContextListener // ------------------------------------------------------------------------------ // Private data + /** Browse screen view ID */ public static final String BROWSE_VIEW_ID = "/jsp/browse/browse.jsp"; + /** Small icon default name */ + public static final String SPACE_SMALL_DEFAULT = "space_small"; + private static final String VIEWMODE_DASHBOARD = "dashboard"; private static final String PAGE_NAME_BROWSE = "browse"; @@ -1549,6 +1586,9 @@ public class BrowseBean implements IContextListener /** Views configuration object */ private ViewsConfigElement viewsConfig = null; + /** Listeners for Node events */ + private Set nodeEventListeners = null; + /** Component references */ private UIRichList spacesRichList; private UIRichList contentRichList; diff --git a/source/java/org/alfresco/web/bean/ForumsBean.java b/source/java/org/alfresco/web/bean/ForumsBean.java index 162427c2e3..f24834b903 100644 --- a/source/java/org/alfresco/web/bean/ForumsBean.java +++ b/source/java/org/alfresco/web/bean/ForumsBean.java @@ -47,6 +47,7 @@ import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.search.QueryParameterDefinition; import org.alfresco.service.cmr.search.SearchService; +import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; @@ -75,7 +76,7 @@ import org.springframework.util.StringUtils; * * @author gavinc */ -public class ForumsBean implements IContextListener +public class ForumsBean implements IContextListener, NodeEventListener { private static Log logger = LogFactory.getLog(ForumsBean.class); private static final String PAGE_NAME_FORUMS = "forums"; @@ -134,6 +135,7 @@ public class ForumsBean implements IContextListener /** The current topic view page size */ private int topicPageSize; + // ------------------------------------------------------------------------------ // Construction @@ -147,6 +149,7 @@ public class ForumsBean implements IContextListener initFromClientConfig(); } + // ------------------------------------------------------------------------------ // Bean property getters and setters @@ -488,7 +491,7 @@ public class ForumsBean implements IContextListener // create our Node representation MapNode node = new MapNode(nodeRef, this.nodeService, true); - this.browseBean.setupDataBindingProperties(node); + this.browseBean.setupCommonBindingProperties(node); node.addPropertyResolver("smallIcon", this.resolverSmallIcon); node.addPropertyResolver("message", this.resolverContent); node.addPropertyResolver("replyTo", this.resolverReplyTo); @@ -592,9 +595,38 @@ public class ForumsBean implements IContextListener this.posts = null; } + + // ------------------------------------------------------------------------------ + // NodeEventListener implementation + + /** + * @see org.alfresco.web.bean.NodeEventListener#created(org.alfresco.web.bean.repository.Node, org.alfresco.service.namespace.QName) + */ + public void created(Node node, QName type) + { + // override the checkin resolver if appropriate + if (node.containsPropertyResolver("checkIn") == true) + { + node.addPropertyResolver("checkIn", this.resolverCheckIn); + } + + // add the forums specific action resolver + node.addPropertyResolver("beingDiscussed", this.resolverBeingDiscussed); + + // override the small icon resolver if it's a forum model type + if (type.equals(ForumModel.TYPE_FORUMS) || + type.equals(ForumModel.TYPE_FORUM) || + type.equals(ForumModel.TYPE_TOPIC)) + { + // override icon handling for forum objects - as we have a specific small icon set + node.addPropertyResolver("smallIcon", resolverSmallIcon); + } + } + + // ------------------------------------------------------------------------------ // Navigation action event handlers - + /** * Change the current forums view mode based on user selection * @@ -847,9 +879,42 @@ public class ForumsBean implements IContextListener return outcome; } + // ------------------------------------------------------------------------------ // Property Resolvers + public NodePropertyResolver resolverCheckIn = new NodePropertyResolver() { + public Object get(Node node) + { + boolean canCheckin = false; + + // if the working copy has a discussion the user will also need to have + // contributor permission on the locked node + if (node.hasAspect(ContentModel.ASPECT_WORKING_COPY)) + { + if (node.hasAspect(ForumModel.ASPECT_DISCUSSABLE)) + { + // get the original locked node (via the copiedfrom aspect) + NodeRef lockedNodeRef = (NodeRef)nodeService.getProperty(node.getNodeRef(), ContentModel.PROP_COPY_REFERENCE); + if (lockedNodeRef != null) + { + Node lockedNode = new Node(lockedNodeRef); + canCheckin = node.hasPermission(PermissionService.CHECK_IN) && + lockedNode.hasPermission(PermissionService.CONTRIBUTOR); + } + } + else + { + // there is no discussion so just check they have checkin permission + // for the node + canCheckin = node.hasPermission(PermissionService.CHECK_IN); + } + } + + return canCheckin; + } + }; + public NodePropertyResolver resolverSmallIcon = new NodePropertyResolver() { public Object get(Node node) { QNameNodeMap props = (QNameNodeMap)node.getProperties(); @@ -861,13 +926,19 @@ public class ForumsBean implements IContextListener } else { - icon = "space_small"; + icon = BrowseBean.SPACE_SMALL_DEFAULT; } return icon; } }; + public NodePropertyResolver resolverBeingDiscussed = new NodePropertyResolver() { + public Object get(Node node) { + return node.hasAspect(ForumModel.ASPECT_DISCUSSABLE); + } + }; + public NodePropertyResolver resolverReplies = new NodePropertyResolver() { public Object get(Node node) { @@ -957,6 +1028,7 @@ public class ForumsBean implements IContextListener return name.toString(); } + // ------------------------------------------------------------------------------ // Private helpers diff --git a/source/java/org/alfresco/web/bean/NodeEventListener.java b/source/java/org/alfresco/web/bean/NodeEventListener.java new file mode 100644 index 0000000000..eee7cffd32 --- /dev/null +++ b/source/java/org/alfresco/web/bean/NodeEventListener.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2005 Alfresco, Inc. + * + * Licensed under the Mozilla Public License version 1.1 + * with a permitted attribution clause. You may obtain a + * copy of the License at + * + * http://www.alfresco.org/legal/license.txt + * + * 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.alfresco.web.bean; + +import org.alfresco.service.namespace.QName; +import org.alfresco.web.bean.repository.Node; + +/** + * @author Kevin Roast + */ +public interface NodeEventListener +{ + /** + * Callback executed when a Node wrapped object is created. This is generally used + * to add additional property resolvers to the Node for a specific model type. + * + * @param node The Node wrapper that has been created + * @param type Type of the Node that has been created + */ + public void created(Node node, QName type); +} diff --git a/source/java/org/alfresco/web/bean/repository/Node.java b/source/java/org/alfresco/web/bean/repository/Node.java index f257be38c4..c85756e0e6 100644 --- a/source/java/org/alfresco/web/bean/repository/Node.java +++ b/source/java/org/alfresco/web/bean/repository/Node.java @@ -242,6 +242,18 @@ public class Node implements Serializable this.properties.addPropertyResolver(name, resolver); } + /** + * Returns if a property resolver with a specific name has been applied to the Node + * + * @param name of property resolver to look for + * + * @return true if a resolver with the name is found, false otherwise + */ + public final boolean containsPropertyResolver(String name) + { + return this.properties.containsPropertyResolver(name); + } + /** * Determines whether the given property name is held by this node * diff --git a/source/java/org/alfresco/web/bean/repository/QNameNodeMap.java b/source/java/org/alfresco/web/bean/repository/QNameNodeMap.java index b8d9bb53ad..f1023ad8b9 100644 --- a/source/java/org/alfresco/web/bean/repository/QNameNodeMap.java +++ b/source/java/org/alfresco/web/bean/repository/QNameNodeMap.java @@ -34,7 +34,7 @@ import org.apache.commons.logging.LogFactory; public final class QNameNodeMap extends QNameMap implements Map, Cloneable { private Node parent = null; - private Map resolvers = new HashMap(11, 1.0f); + private Map resolvers = new HashMap(16, 1.0f); /** * Constructor @@ -61,6 +61,18 @@ public final class QNameNodeMap extends QNameMap implements Map, Cloneable { this.resolvers.put(name, resolver); } + + /** + * Returns if a property resolver with a specific name has been applied to the map + * + * @param name of property resolver to look for + * + * @return true if a resolver with the name is found, false otherwise + */ + public boolean containsPropertyResolver(String name) + { + return this.resolvers.containsKey(name); + } /** * @see java.util.Map#containsKey(java.lang.Object) diff --git a/source/java/org/alfresco/web/ui/common/renderer/DatePickerRenderer.java b/source/java/org/alfresco/web/ui/common/renderer/DatePickerRenderer.java index aa1edffa5a..4f85d1404f 100644 --- a/source/java/org/alfresco/web/ui/common/renderer/DatePickerRenderer.java +++ b/source/java/org/alfresco/web/ui/common/renderer/DatePickerRenderer.java @@ -149,7 +149,7 @@ public class DatePickerRenderer extends BaseRenderer } else { - nStartYear = new Date().getYear() + 1900; + nStartYear = new Date().getYear() + 1900 + 2; // for "effectivity date" searches } int nYearCount = 25;