From a1200deadb2fe6333961cf7d550f9d1acdf0e0f7 Mon Sep 17 00:00:00 2001 From: Gavin Cornwell Date: Wed, 19 Jul 2006 11:50:32 +0000 Subject: [PATCH] - More AJAX support (couple of bug fixes, added timing for AJAX calls, added support for adding AJAX scripts to a page once and once only) git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@3352 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../web/app/servlet/ajax/AjaxServlet.java | 16 ++ .../web/app/servlet/ajax/GetCommand.java | 5 +- .../alfresco/web/bean/ajax/BaseAjaxBean.java | 51 ------- .../alfresco/web/bean/ajax/NodeInfoBean.java | 128 ++++++++++++++++ .../web/config/ClientConfigElement.java | 19 +++ .../web/config/ClientElementReader.java | 8 + .../org/alfresco/web/ui/common/Utils.java | 35 +++++ .../web/ui/repo/component/UINodeInfo.java | 137 ++++++++++++++++++ .../alfresco/web/ui/repo/tag/NodeInfoTag.java | 77 ++++++++++ source/web/WEB-INF/faces-config-beans.xml | 19 +++ source/web/WEB-INF/faces-config-repo.xml | 5 + source/web/WEB-INF/repo.tld | 24 +++ source/web/css/main.css | 8 + source/web/jsp/browse/browse.jsp | 8 +- .../web/scripts/{ajax.js => ajax/common.js} | 0 source/web/scripts/{ => ajax}/dojo.js | 0 source/web/scripts/ajax/node-info.js | 73 ++++++++++ 17 files changed, 558 insertions(+), 55 deletions(-) delete mode 100644 source/java/org/alfresco/web/bean/ajax/BaseAjaxBean.java create mode 100644 source/java/org/alfresco/web/bean/ajax/NodeInfoBean.java create mode 100644 source/java/org/alfresco/web/ui/repo/component/UINodeInfo.java create mode 100644 source/java/org/alfresco/web/ui/repo/tag/NodeInfoTag.java rename source/web/scripts/{ajax.js => ajax/common.js} (100%) rename source/web/scripts/{ => ajax}/dojo.js (100%) create mode 100644 source/web/scripts/ajax/node-info.js diff --git a/source/java/org/alfresco/web/app/servlet/ajax/AjaxServlet.java b/source/java/org/alfresco/web/app/servlet/ajax/AjaxServlet.java index 68896aefe0..20d5d07938 100644 --- a/source/java/org/alfresco/web/app/servlet/ajax/AjaxServlet.java +++ b/source/java/org/alfresco/web/app/servlet/ajax/AjaxServlet.java @@ -38,6 +38,7 @@ public class AjaxServlet extends BaseServlet private static final long serialVersionUID = -7654769105419391840L; private static Log logger = LogFactory.getLog(AJAX_LOG_KEY); private static Log headersLogger = LogFactory.getLog(AJAX_LOG_KEY + ".headers"); + private static Log perfLogger = LogFactory.getLog(AJAX_LOG_KEY + ".performance"); /** * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) @@ -45,6 +46,8 @@ public class AjaxServlet extends BaseServlet protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + long startTime = 0; + try { String uri = request.getRequestURI(); @@ -95,6 +98,10 @@ public class AjaxServlet extends BaseServlet // setup the faces context FacesContext facesContext = FacesHelper.getFacesContext(request, response, getServletContext()); + // start a timer + if (perfLogger.isDebugEnabled()) + startTime = System.currentTimeMillis(); + // instantiate the relevant command AjaxCommand command = null; if (Command.invoke.toString().equals(commandName)) @@ -117,6 +124,15 @@ public class AjaxServlet extends BaseServlet { handleError(response, error); } + finally + { + // measure the time taken + if (perfLogger.isDebugEnabled()) + { + long endTime = System.currentTimeMillis(); + perfLogger.debug("Time to execute command: " + (endTime - startTime) + "ms"); + } + } } /** diff --git a/source/java/org/alfresco/web/app/servlet/ajax/GetCommand.java b/source/java/org/alfresco/web/app/servlet/ajax/GetCommand.java index be3363f41f..b513e27154 100644 --- a/source/java/org/alfresco/web/app/servlet/ajax/GetCommand.java +++ b/source/java/org/alfresco/web/app/servlet/ajax/GetCommand.java @@ -49,7 +49,10 @@ public class GetCommand extends BaseAjaxCommand // get the value from the value binding Object value = binding.getValue(facesContext); - response.getWriter().write(value.toString()); + if (value != null) + { + response.getWriter().write(value.toString()); + } // commit tx.commit(); diff --git a/source/java/org/alfresco/web/bean/ajax/BaseAjaxBean.java b/source/java/org/alfresco/web/bean/ajax/BaseAjaxBean.java deleted file mode 100644 index 6dbd25f18d..0000000000 --- a/source/java/org/alfresco/web/bean/ajax/BaseAjaxBean.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.alfresco.web.bean.ajax; - -import java.io.IOException; - -import javax.faces.context.FacesContext; -import javax.faces.context.ResponseWriter; - -/** - * Base class for all Ajax managed beans. - * - * It is not necessary to extend this bean but it provides - * helper methods. - * - * @author gavinc - */ -public abstract class BaseAjaxBean -{ - private ResponseWriter writer; - - /** - * Writes the given string to the response writer for this bean - * - * @param str The string to send back to the client - */ - public void write(String str) - { - try - { - getWriter().write(str); - } - catch (IOException ioe) - { - // not much we can do here, ignore - } - } - - /** - * Returns the ResponseWriter for this bean - * - * @return The JSF ResponseWriter - */ - public ResponseWriter getWriter() - { - if (this.writer == null) - { - this.writer = FacesContext.getCurrentInstance().getResponseWriter(); - } - - return this.writer; - } -} diff --git a/source/java/org/alfresco/web/bean/ajax/NodeInfoBean.java b/source/java/org/alfresco/web/bean/ajax/NodeInfoBean.java new file mode 100644 index 0000000000..d01e995801 --- /dev/null +++ b/source/java/org/alfresco/web/bean/ajax/NodeInfoBean.java @@ -0,0 +1,128 @@ +package org.alfresco.web.bean.ajax; + +import java.io.IOException; +import java.util.Map; + +import javax.faces.context.FacesContext; +import javax.faces.context.ResponseWriter; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.web.bean.repository.Node; +import org.alfresco.web.bean.repository.Repository; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Bean used by an AJAX control to send information back on the + * requested node. + * + * @author gavinc + */ +public class NodeInfoBean +{ + private NodeService nodeService; + private ContentService contentService; + + private static final Log logger = LogFactory.getLog(NodeInfoBean.class); + + /** + * Returns information on the node identified by the 'noderef' + * parameter found in the ExternalContext. + *

+ * The result is the formatted HTML to show on the client. + */ + public void sendNodeInfo() throws IOException + { + FacesContext context = FacesContext.getCurrentInstance(); + ResponseWriter out = context.getResponseWriter(); + + String nodeRef = (String)context.getExternalContext().getRequestParameterMap().get("noderef"); + + if (nodeRef == null || nodeRef.length() == 0) + { + throw new IllegalArgumentException("'noderef' parameter is missing"); + } + + NodeRef repoNode = new NodeRef(nodeRef); + + if (this.nodeService.exists(repoNode)) + { + // get the client side node representation and its properties + Node clientNode = new Node(repoNode); + Map props = clientNode.getProperties(); + + // get the content size + Object content = props.get(ContentModel.PROP_CONTENT); + + // start the containing table + out.write(""); + + // write out information about the node as table rows + out.write(""); + + // add debug information to the summary if debug is enabled + if (logger.isDebugEnabled()) + { + writeRow(out, "Id:", clientNode.getId()); + writeRow(out, "Type:", clientNode.getType().toPrefixString()); + } + + writeRow(out, "Description:", (String)props.get(ContentModel.PROP_DESCRIPTION)); + writeRow(out, "Title:", (String)props.get(ContentModel.PROP_TITLE)); + writeRow(out, "Created:", props.get("created").toString()); + writeRow(out, "Modified:", props.get("modified").toString()); + + // close the
Summary
and
tags + out.write("
"); + } + else + { + out.write("Node could not be found in the repository!"); + } + } + + // ------------------------------------------------------------------------------ + // Bean getters and setters + + /** + * @param nodeService The NodeService to set. + */ + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public void setContentService(ContentService contentService) + { + this.contentService = contentService; + } + + // ------------------------------------------------------------------------------ + // Helper methods + + /** + * Writes a table row with the given data + * + * @param nameColumn The name of the data item + * @param dataColumn The data + */ + protected void writeRow(ResponseWriter out, String nameColumn, String dataColumn) + throws IOException + { + out.write(""); + } +} diff --git a/source/java/org/alfresco/web/config/ClientConfigElement.java b/source/java/org/alfresco/web/config/ClientConfigElement.java index fc7488ceea..4c75bfcc8d 100644 --- a/source/java/org/alfresco/web/config/ClientConfigElement.java +++ b/source/java/org/alfresco/web/config/ClientConfigElement.java @@ -39,6 +39,7 @@ public class ClientConfigElement extends ConfigElementAdapter private String helpUrl = null; private String editLinkType = "http"; private String homeSpacePermission = null; + private boolean ajaxEnabled = false; /** * Default Constructor @@ -329,4 +330,22 @@ public class ClientConfigElement extends ConfigElementAdapter { this.homeSpacePermission = homeSpacePermission; } + + /** + * @return Returns whether AJAX support is enabled in the client + */ + public boolean isAjaxEnabled() + { + return this.ajaxEnabled; + } + + /** + * Sets whether AJAX support is enabled in the client + * + * @param ajaxEnabled + */ + /*package*/ void setAjaxEnabled(boolean ajaxEnabled) + { + this.ajaxEnabled = ajaxEnabled; + } } diff --git a/source/java/org/alfresco/web/config/ClientElementReader.java b/source/java/org/alfresco/web/config/ClientElementReader.java index 73cf1ecc5c..00feb8377f 100644 --- a/source/java/org/alfresco/web/config/ClientElementReader.java +++ b/source/java/org/alfresco/web/config/ClientElementReader.java @@ -39,6 +39,7 @@ public class ClientElementReader implements ConfigElementReader public static final String ELEMENT_HOMESPACEPERMISSION = "home-space-permission"; public static final String ELEMENT_FROMEMAILADDRESS = "from-email-address"; public static final String ELEMENT_SHELFVISIBLE = "shelf-visible"; + public static final String ELEMENT_AJAX_ENABLED = "ajax-enabled"; /** * @see org.alfresco.config.xml.elementreader.ConfigElementReader#parse(org.dom4j.Element) @@ -136,6 +137,13 @@ public class ClientElementReader implements ConfigElementReader { configElement.setLoginPage(loginPage.getTextTrim()); } + + // get the ajax enabled flag + Element ajaxEnabled = element.element(ELEMENT_AJAX_ENABLED); + if (ajaxEnabled != null) + { + configElement.setAjaxEnabled(Boolean.parseBoolean(ajaxEnabled.getTextTrim())); + } } return configElement; diff --git a/source/java/org/alfresco/web/ui/common/Utils.java b/source/java/org/alfresco/web/ui/common/Utils.java index 7f7ee84716..182fd34003 100644 --- a/source/java/org/alfresco/web/ui/common/Utils.java +++ b/source/java/org/alfresco/web/ui/common/Utils.java @@ -88,6 +88,8 @@ public final class Utils private static final String DEFAULT_FILE_IMAGE16 = IMAGE_PREFIX16 + "_default" + IMAGE_POSTFIX; private static final String DEFAULT_FILE_IMAGE32 = IMAGE_PREFIX32 + "_default" + IMAGE_POSTFIX; + private static final String AJAX_SCRIPTS_WRITTEN = "_alfAjaxScriptsWritten"; + private static final Map s_fileExtensionMap = new HashMap(89, 1.0f); private static Log logger = LogFactory.getLog(Utils.class); @@ -1238,4 +1240,37 @@ public final class Utils return description; } + + /** + * Writes the script tags for including AJAX support, ensuring they + * only get written once per page render. + * + * @param context Faces context + * @param out The response writer + */ + @SuppressWarnings("unchecked") + public static void writeAjaxScripts(FacesContext context, ResponseWriter out) + throws IOException + { + Object present = context.getExternalContext().getRequestMap().get(AJAX_SCRIPTS_WRITTEN); + + if (present == null) + { + // write out the scripts + out.write("\n\n"); + out.write("\n"); + + // write out a global variable to hold the webapp context path + out.write("\n"); + + // add marker to request + context.getExternalContext().getRequestMap().put(AJAX_SCRIPTS_WRITTEN, Boolean.TRUE); + } + } } diff --git a/source/java/org/alfresco/web/ui/repo/component/UINodeInfo.java b/source/java/org/alfresco/web/ui/repo/component/UINodeInfo.java new file mode 100644 index 0000000000..e894e5bf19 --- /dev/null +++ b/source/java/org/alfresco/web/ui/repo/component/UINodeInfo.java @@ -0,0 +1,137 @@ +package org.alfresco.web.ui.repo.component; + +import java.io.IOException; + +import javax.faces.context.FacesContext; +import javax.faces.context.ResponseWriter; +import javax.faces.el.ValueBinding; + +import org.alfresco.web.app.Application; +import org.alfresco.web.bean.repository.Repository; +import org.alfresco.web.ui.common.Utils; +import org.alfresco.web.ui.common.component.SelfRenderingComponent; + +/** + * JSF component that displays information about a node. + *

+ * The node to show information on + * + * @author gavinc + */ +public class UINodeInfo extends SelfRenderingComponent +{ + protected final static String NODE_INFO_SCRIPTS_WRITTEN = "_alfNodeInfoScripts"; + + protected Object value = null; + + // ------------------------------------------------------------------------------ + // Component Impl + + @Override + public String getFamily() + { + return "org.alfresco.faces.NodeInfo"; + } + + @Override + public void restoreState(FacesContext context, Object state) + { + Object values[] = (Object[])state; + // standard component attributes are restored by the super class + super.restoreState(context, values[0]); + this.value = values[1]; + } + + @Override + public Object saveState(FacesContext context) + { + Object values[] = new Object[8]; + // standard component attributes are saved by the super class + values[0] = super.saveState(context); + values[1] = this.value; + return values; + } + + @Override + @SuppressWarnings("unchecked") + public void encodeBegin(FacesContext context) throws IOException + { + if (!isRendered()) return; + + // if AJAX is disabled don't render anything + if (Application.getClientConfig(context).isAjaxEnabled()) + { + ResponseWriter out = context.getResponseWriter(); + + // output the scripts required by the component (checks are + // made to make sure the scripts are only written once) + Utils.writeAjaxScripts(context, out); + + // write out the JavaScript specific to the NodeInfo component, + // again, make sure it's only done once + Object present = context.getExternalContext().getRequestMap(). + get(NODE_INFO_SCRIPTS_WRITTEN); + if (present == null) + { + out.write("\n"); + + context.getExternalContext().getRequestMap().put( + NODE_INFO_SCRIPTS_WRITTEN, Boolean.TRUE); + } + + // wrap the child components in a that has the onmouseover + // event which kicks off the request for node information + String id = (String)this.getValue(); + out.write(""); + } + } + + @Override + public void encodeEnd(FacesContext context) throws IOException + { + if (!isRendered()) return; + + // if AJAX is disabled don't render anything + if (Application.getClientConfig(context).isAjaxEnabled()) + { + context.getResponseWriter().write(""); + } + } + + // ------------------------------------------------------------------------------ + // Strongly typed component property accessors + + /** + * Get the value - the value is used in a equals() match against the current value in the + * parent ModeList component to set the selected item. + * + * @return the value + */ + public Object getValue() + { + ValueBinding vb = getValueBinding("value"); + if (vb != null) + { + this.value = vb.getValue(getFacesContext()); + } + + return this.value; + } + + /** + * Set the value - the value is used in a equals() match against the current value in the + * parent ModeList component to set the selected item. + * + * @param value the value + */ + public void setValue(Object value) + { + this.value = value; + } +} diff --git a/source/java/org/alfresco/web/ui/repo/tag/NodeInfoTag.java b/source/java/org/alfresco/web/ui/repo/tag/NodeInfoTag.java new file mode 100644 index 0000000000..5796edbe0a --- /dev/null +++ b/source/java/org/alfresco/web/ui/repo/tag/NodeInfoTag.java @@ -0,0 +1,77 @@ +/* + * 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.ui.repo.tag; + +import javax.faces.component.UIComponent; + +import org.alfresco.web.ui.common.tag.HtmlComponentTag; + +/** + * Tag class for the UINodeInfo component + * + * @author gavinc + */ +public class NodeInfoTag extends HtmlComponentTag +{ + private String value; + + /** + * @see javax.faces.webapp.UIComponentTag#getComponentType() + */ + public String getComponentType() + { + return "org.alfresco.faces.NodeInfo"; + } + + /** + * @see javax.faces.webapp.UIComponentTag#getRendererType() + */ + public String getRendererType() + { + return null; + } + + /** + * @see javax.faces.webapp.UIComponentTag#setProperties(javax.faces.component.UIComponent) + */ + protected void setProperties(UIComponent component) + { + super.setProperties(component); + + setStringBindingProperty(component, "value", this.value); + } + + /** + * @see org.alfresco.web.ui.common.tag.HtmlComponentTag#release() + */ + public void release() + { + super.release(); + + this.value = null; + } + + /** + * Set the value + * + * @param value the value + */ + public void setValue(String value) + { + this.value = value; + } +} diff --git a/source/web/WEB-INF/faces-config-beans.xml b/source/web/WEB-INF/faces-config-beans.xml index cff60d1f04..2dcdcba704 100644 --- a/source/web/WEB-INF/faces-config-beans.xml +++ b/source/web/WEB-INF/faces-config-beans.xml @@ -1536,4 +1536,23 @@ request + + + + + Bean that returns information on a node + + NodeInfoBean + org.alfresco.web.bean.ajax.NodeInfoBean + request + + nodeService + #{NodeService} + + + contentService + #{ContentService} + + + diff --git a/source/web/WEB-INF/faces-config-repo.xml b/source/web/WEB-INF/faces-config-repo.xml index 1fd9dece11..e4d3bd38ab 100644 --- a/source/web/WEB-INF/faces-config-repo.xml +++ b/source/web/WEB-INF/faces-config-repo.xml @@ -124,6 +124,11 @@ org.alfresco.web.ui.repo.component.evaluator.ActionInstanceEvaluator + + org.alfresco.faces.NodeInfo + org.alfresco.web.ui.repo.component.UINodeInfo + + diff --git a/source/web/WEB-INF/repo.tld b/source/web/WEB-INF/repo.tld index ac35d51978..02c03607b6 100644 --- a/source/web/WEB-INF/repo.tld +++ b/source/web/WEB-INF/repo.tld @@ -1514,4 +1514,28 @@ + + nodeInfo + org.alfresco.web.ui.repo.tag.NodeInfoTag + JSP + + + The nodeInfo component wraps another component, typically an + action link, to provide a floating pop up panel containing + information on a particular node. + + + + id + false + true + + + + value + true + true + + + diff --git a/source/web/css/main.css b/source/web/css/main.css index 79c9f8d46e..49419161fc 100644 --- a/source/web/css/main.css +++ b/source/web/css/main.css @@ -503,3 +503,11 @@ a.topToolbarLinkHighlight, a.topToolbarLinkHighlight:link, a.topToolbarLinkHighl { width: 70%; } + +.summaryPopupPanel +{ + background-color: #e9f0f4; + border: 1px solid #999999; + padding: 4px; + -moz-border-radius: 4px; +} diff --git a/source/web/jsp/browse/browse.jsp b/source/web/jsp/browse/browse.jsp index 9427e5a3bc..2bc57ab8d3 100644 --- a/source/web/jsp/browse/browse.jsp +++ b/source/web/jsp/browse/browse.jsp @@ -263,9 +263,11 @@ - - - + + + + + <%-- Description column for all view modes --%> diff --git a/source/web/scripts/ajax.js b/source/web/scripts/ajax/common.js similarity index 100% rename from source/web/scripts/ajax.js rename to source/web/scripts/ajax/common.js diff --git a/source/web/scripts/dojo.js b/source/web/scripts/ajax/dojo.js similarity index 100% rename from source/web/scripts/dojo.js rename to source/web/scripts/ajax/dojo.js diff --git a/source/web/scripts/ajax/node-info.js b/source/web/scripts/ajax/node-info.js new file mode 100644 index 0000000000..cf32ccd503 --- /dev/null +++ b/source/web/scripts/ajax/node-info.js @@ -0,0 +1,73 @@ +// +// Supporting JavaScript for the NodeInfo component +// Gavin Cornwell 17-07-2006 +// + +var _launchElement = null; +var _popupElement = null; + +/** + * Makes the AJAX request back to the server to get the node info. + * + * @param nodeRef The node reference to get information for + * @param launchElement The element that requested the summary panel + */ +function showNodeInfo(nodeRef, launchElement) +{ + _launchElement = launchElement; + + dojo.io.bind({ + method: 'post', + url: WEBAPP_CONTEXT + '/ajax/invoke/NodeInfoBean.sendNodeInfo', + content: { noderef: nodeRef }, + load: showNodeInfoHandler, + error: handleErrorDojo, + mimetype: 'text/html' + }); +} + +/** + * Fades in the summary panel containing the node information. + * This function is called back via the dojo bind call above. + */ +function showNodeInfoHandler(type, data, evt) +{ + // create a 'div' to hold the summary table + var div = document.createElement("div"); + + // get the position of the element we are showing info for + var pos = dojo.style.getAbsolutePosition(_launchElement, false); + + // setup the div with the correct appearance + div.innerHTML = data; + div.setAttribute("class", "summaryPopupPanel"); + // NOTE: use className for IE + div.setAttribute("className", "summaryPopupPanel"); + div.style.position = "absolute"; + div.style.left = pos[0]; + div.style.top = pos[1] + 16; + div.style.zIndex = 99; + + // is there a better way of doing this, dojo.dom.insertBefore?? + var body = document.getElementsByTagName("body")[0]; + dojo.style.setOpacity(div, 0); + _popupElement = div; + body.appendChild(div); + + dojo.lfx.html.fadeIn(div, 300).play(); +} + +/** + * Fades out the summary panel with the node info + * and then removes it from the DOM + */ +function hideNodeInfo() +{ + // remove the node from the DOM and reset variables + dojo.lfx.html.fadeOut(_popupElement, 300, dojo.lfx.easeOut, function(nodes) + { + dojo.lang.forEach(nodes, dojo.dom.removeNode); + _popupElement = null; + _launchElement = null; + }).play(); +}

"); + out.write(nameColumn); + out.write(""); + if (dataColumn != null) + { + out.write(dataColumn); + } + else + { + out.write(" "); + } + out.write("