diff --git a/config/alfresco/messages/webclient.properties b/config/alfresco/messages/webclient.properties index 9429e47a81..30372cb8aa 100644 --- a/config/alfresco/messages/webclient.properties +++ b/config/alfresco/messages/webclient.properties @@ -996,10 +996,22 @@ reassign_select_user=Select the user to assign the task to, then press OK. error_reassign_task=Unable to reassign the task due to system error: part_of_workflow=Part of Workflow initiated_by=Initiated by -start_date=Start date +started_on=Started on add_resource=Add Resource view_properties=View Content Properties edit_properties=Edit Content Properties +save_changes=Save Changes +no_tasks=No tasks found. +no_resources=No resources found. +in_progress=In Progress + +# Workflow Definitions +wf_review_due_date=Review Due Date +wf_review_priority=Review Priority +wf_reviewer=Reviewer +wf_adhoc_due_date=Due Date +wf_adhoc_priority=Priority +wf_adhoc_assignee=Assign To # Admin Console messages title_admin_console=Administration Console diff --git a/config/alfresco/web-client-config-properties.xml b/config/alfresco/web-client-config-properties.xml index a59d27d5f7..6df11f4d08 100644 --- a/config/alfresco/web-client-config-properties.xml +++ b/config/alfresco/web-client-config-properties.xml @@ -230,9 +230,21 @@ + + + - - + + + + + + + + + + + @@ -242,19 +254,21 @@ + + + - - - - + + + - + @@ -263,12 +277,12 @@ - - - + + + - - + + @@ -276,10 +290,10 @@ - + + + - - @@ -287,9 +301,9 @@ - - - + + + diff --git a/config/alfresco/web-client-config-workflow-actions.xml b/config/alfresco/web-client-config-workflow-actions.xml index 5f1fb1f61a..d8064a9fd4 100644 --- a/config/alfresco/web-client-config-workflow-actions.xml +++ b/config/alfresco/web-client-config-workflow-actions.xml @@ -13,19 +13,40 @@ + + manage_task + /images/icons/manage_workflow_task.gif + dialog:manageTask + #{DialogManager.setupParameters} + + #{actionContext.id} + + + + + view_completed_task_title + /images/icons/view_workflow_task.gif + dialog:viewCompletedTask + #{DialogManager.setupParameters} + + #{actionContext.id} + + + reassign /images/icons/reassign_task.gif dialog:reassignTask #{DialogManager.setupParameters} - #{actionContext.id} + #{actionContext.id} cancel_workflow /images/icons/cancel_workflow.gif + org.alfresco.web.action.evaluator.CancelWorkflowEvaluator dialog:cancelWorkflow #{DialogManager.setupParameters} @@ -51,7 +72,7 @@ view_properties - /images/icons/View_details.gif + /images/icons/view_properties.gif dialog:viewContentProperties #{BrowseBean.setupContentAction} @@ -64,7 +85,7 @@ Write edit_properties - /images/icons/Change_details.gif + /images/icons/edit_properties.gif dialog:editContentProperties #{BrowseBean.setupContentAction} @@ -72,6 +93,45 @@ + + + org.alfresco.web.action.evaluator.CheckinDocEvaluator + checkin + /images/icons/CheckIn_icon.gif + #{CheckinCheckoutBean.setupWorkflowContentAction} + dialog:checkinFile + + #{actionContext.id} + #{actionContext.taskId} + + + + + + org.alfresco.web.action.evaluator.CheckoutDocEvaluator + checkout + /images/icons/CheckOut_icon.gif + #{CheckinCheckoutBean.setupWorkflowContentAction} + dialog:checkoutFile + + #{actionContext.id} + #{actionContext.taskId} + + + + + + org.alfresco.web.action.evaluator.CancelCheckoutDocEvaluator + undocheckout + /images/icons/undo_checkout.gif + #{CheckinCheckoutBean.setupWorkflowContentAction} + dialog:undoCheckoutFile + + #{actionContext.id} + #{actionContext.taskId} + + + @@ -81,10 +141,12 @@ + + @@ -104,9 +166,9 @@ - - - + + + diff --git a/source/java/org/alfresco/web/action/evaluator/CancelWorkflowEvaluator.java b/source/java/org/alfresco/web/action/evaluator/CancelWorkflowEvaluator.java new file mode 100644 index 0000000000..cdefed2495 --- /dev/null +++ b/source/java/org/alfresco/web/action/evaluator/CancelWorkflowEvaluator.java @@ -0,0 +1,85 @@ +/* + * 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.action.evaluator; + +import javax.faces.context.FacesContext; + +import org.alfresco.model.ContentModel; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.workflow.WorkflowService; +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.alfresco.util.ISO9075; +import org.alfresco.web.action.ActionEvaluator; +import org.alfresco.web.app.Application; +import org.alfresco.web.bean.repository.Node; +import org.alfresco.web.bean.repository.Repository; +import org.alfresco.web.bean.repository.User; + +/** + * UI Action Evaluator for cancel workflow action. The action + * is only allowed if the workflow the task belongs to was + * started by the current user. + * + * @author gavinc + */ +public class CancelWorkflowEvaluator implements ActionEvaluator +{ + /** + * @see org.alfresco.web.action.ActionEvaluator#evaluate(org.alfresco.web.bean.repository.Node) + */ + public boolean evaluate(Node node) + { + boolean result = false; + + // get the id of the task + String taskId = (String)node.getProperties().get("id"); + if (taskId != null) + { + FacesContext context = FacesContext.getCurrentInstance(); + + // get the initiator of the workflow the task belongs to + WorkflowService workflowSvc = Repository.getServiceRegistry( + context).getWorkflowService(); + + WorkflowTask task = workflowSvc.getTaskById(taskId); + if (task != null) + { + NodeRef initiator = task.path.instance.initiator; + if (initiator != null) + { + // find the current username + User user = Application.getCurrentUser(context); + String currentUserName = ISO9075.encode(user.getUserName()); + + // get the username of the initiator + NodeService nodeSvc = Repository.getServiceRegistry( + context).getNodeService(); + String userName = (String)nodeSvc.getProperty(initiator, ContentModel.PROP_USERNAME); + + // if the current user started the workflow allow the cancel action + if (currentUserName.equals(userName)) + { + result = true; + } + } + } + } + + return result; + } +} diff --git a/source/java/org/alfresco/web/app/servlet/AdminAuthenticationFilter.java b/source/java/org/alfresco/web/app/servlet/AdminAuthenticationFilter.java new file mode 100644 index 0000000000..9b9a7f69f1 --- /dev/null +++ b/source/java/org/alfresco/web/app/servlet/AdminAuthenticationFilter.java @@ -0,0 +1,110 @@ +/* + * 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.app.servlet; + +import java.io.IOException; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.web.bean.repository.User; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This servlet filter is used to restrict direct URL access to administration + * resource in the web client, for example the admin and jBPM consoles. + * + * @author gavinc + */ +public class AdminAuthenticationFilter implements Filter +{ + private static final Log logger = LogFactory.getLog(AdminAuthenticationFilter.class); + + /** + * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) + */ + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException + { + HttpServletRequest httpRequest = (HttpServletRequest)req; + HttpServletResponse httpResponse = (HttpServletResponse)res; + + // The fact that this filter is being called means a request for a protected + // resource has taken place, check that the current user is in fact an + // administrator. + + if (logger.isDebugEnabled()) + logger.debug("Authorising request for protected resource: " + httpRequest.getRequestURI()); + + // there should be a user at this point so retrieve it + User user = AuthenticationHelper.getUser(httpRequest, httpResponse); + + // if the user is present check to see whether it is an admin user + boolean isAdmin = (user != null && user.isAdmin()); + + if (isAdmin) + { + if (logger.isDebugEnabled()) + logger.debug("Current user has admin authority, allowing access."); + + // continue filter chaining if current user is admin user + chain.doFilter(req, res); + } + else + { + // return the 401 Forbidden error as the current user is not an administrator + // if the response has already been committed there's nothing we can do but + // print out a warning + if (httpResponse.isCommitted() == false) + { + if (logger.isDebugEnabled()) + logger.debug("Current user does not have admin authority, returning 401 Forbidden error..."); + + httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN); + } + else + { + if (logger.isWarnEnabled()) + logger.warn("Access denied to '" + httpRequest.getRequestURI() + + "'. The response has already been committed so a 401 Forbidden error could not be sent!"); + } + } + } + + /** + * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) + */ + public void init(FilterConfig config) throws ServletException + { + // nothing to do + } + + /** + * @see javax.servlet.Filter#destroy() + */ + public void destroy() + { + // nothing to do + } +} diff --git a/source/java/org/alfresco/web/app/servlet/AuthenticationHelper.java b/source/java/org/alfresco/web/app/servlet/AuthenticationHelper.java index 7238435bf0..611e0dd28d 100644 --- a/source/java/org/alfresco/web/app/servlet/AuthenticationHelper.java +++ b/source/java/org/alfresco/web/app/servlet/AuthenticationHelper.java @@ -100,39 +100,15 @@ public final class AuthenticationHelper { HttpSession session = httpRequest.getSession(); - // examine the appropriate session for our User object - User user = null; + // retrieve the User object + User user = getUser(httpRequest, httpResponse); + + // get the login bean if we're not in the portal LoginBean loginBean = null; if (Application.inPortalServer() == false) { - user = (User)session.getAttribute(AUTHENTICATION_USER); loginBean = (LoginBean)session.getAttribute(LOGIN_BEAN); } - else - { - // naff solution as we need to enumerate all session keys until we find the one that - // should match our User objects - this is weak but we don't know how the underlying - // Portal vendor has decided to encode the objects in the session - if (portalUserKeyName.get() == null) - { - String userKeyPostfix = "?" + AUTHENTICATION_USER; - Enumeration enumNames = session.getAttributeNames(); - while (enumNames.hasMoreElements()) - { - String name = (String)enumNames.nextElement(); - if (name.endsWith(userKeyPostfix)) - { - // cache the key value once found! - portalUserKeyName.set(name); - break; - } - } - } - if (portalUserKeyName.get() != null) - { - user = (User)session.getAttribute(portalUserKeyName.get()); - } - } // setup the authentication context WebApplicationContext wc = WebApplicationContextUtils.getRequiredWebApplicationContext(context); @@ -388,6 +364,52 @@ public final class AuthenticationHelper return AuthenticationStatus.Failure; } + /** + * Attempts to retrieve the User object stored in the current session. + * + * @param httpRequest The HTTP request + * @param httpResponse The HTTP response + * @return The User object representing the current user or null if it could not be found + */ + public static User getUser(HttpServletRequest httpRequest, HttpServletResponse httpResponse) + { + HttpSession session = httpRequest.getSession(); + User user = null; + + // examine the appropriate session to try and find the User object + if (Application.inPortalServer() == false) + { + user = (User)session.getAttribute(AUTHENTICATION_USER); + } + else + { + // naff solution as we need to enumerate all session keys until we find the one that + // should match our User objects - this is weak but we don't know how the underlying + // Portal vendor has decided to encode the objects in the session + if (portalUserKeyName.get() == null) + { + String userKeyPostfix = "?" + AUTHENTICATION_USER; + Enumeration enumNames = session.getAttributeNames(); + while (enumNames.hasMoreElements()) + { + String name = (String)enumNames.nextElement(); + if (name.endsWith(userKeyPostfix)) + { + // cache the key value once found! + portalUserKeyName.set(name); + break; + } + } + } + if (portalUserKeyName.get() != null) + { + user = (User)session.getAttribute(portalUserKeyName.get()); + } + } + + return user; + } + /** * Setup the Alfresco auth cookie value. * diff --git a/source/java/org/alfresco/web/app/servlet/BaseDownloadContentServlet.java b/source/java/org/alfresco/web/app/servlet/BaseDownloadContentServlet.java new file mode 100644 index 0000000000..11a965866f --- /dev/null +++ b/source/java/org/alfresco/web/app/servlet/BaseDownloadContentServlet.java @@ -0,0 +1,301 @@ +/* + * 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.app.servlet; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.SocketException; +import java.net.URLEncoder; +import java.text.MessageFormat; +import java.util.Date; +import java.util.StringTokenizer; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.filestore.FileContentReader; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.MimetypeService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.security.AccessStatus; +import org.alfresco.service.cmr.security.PermissionService; +import org.alfresco.service.namespace.QName; +import org.alfresco.web.app.Application; +import org.alfresco.web.ui.common.Utils; +import org.apache.commons.logging.Log; + +/** + * Base class for the download content servlets. Provides common + * processing for the request. + * + * @see org.alfresco.web.app.servlet.DownloadContentServlet + * @see org.alfresco.web.app.servlet.GuestDownloadContentServlet + * + * @author Kevin Roast + * @author gavinc + */ +public abstract class BaseDownloadContentServlet extends BaseServlet +{ + private static final long serialVersionUID = -4558907921887235966L; + + protected static final String MIMETYPE_OCTET_STREAM = "application/octet-stream"; + + protected static final String MSG_ERROR_CONTENT_MISSING = "error_content_missing"; + + protected static final String ARG_PROPERTY = "property"; + protected static final String ARG_ATTACH = "attach"; + protected static final String ARG_PATH = "path"; + + /** + * Gets the logger to use for this request. + *

+ * This will show all debug entries from this class as though they + * came from the subclass. + * + * @return The logger + */ + protected abstract Log getLogger(); + + /** + * Processes the download request using the current context i.e. no + * authentication checks are made, it is presumed they have already + * been done. + * + * @param req The HTTP request + * @param res The HTTP response + * @param redirectToLogin Flag to determine whether to redirect to the login + * page if the user does not have the correct permissions + */ + protected void processDownloadRequest(HttpServletRequest req, HttpServletResponse res, + boolean redirectToLogin) + throws ServletException, IOException + { + Log logger = getLogger(); + String uri = req.getRequestURI(); + + if (logger.isDebugEnabled()) + { + String queryString = req.getQueryString(); + logger.debug("Processing URL: " + uri + + ((queryString != null && queryString.length() > 0) ? ("?" + queryString) : "")); + } + + // TODO: add compression here? + // see http://servlets.com/jservlet2/examples/ch06/ViewResourceCompress.java for example + // only really needed if we don't use the built in compression of the servlet container + uri = uri.substring(req.getContextPath().length()); + StringTokenizer t = new StringTokenizer(uri, "/"); + int tokenCount = t.countTokens(); + + t.nextToken(); // skip servlet name + + // attachment mode (either 'attach' or 'direct') + String attachToken = t.nextToken(); + boolean attachment = attachToken.equals(ARG_ATTACH); + + // get or calculate the noderef and filename to download as + NodeRef nodeRef; + String filename; + + // do we have a path parameter instead of a NodeRef? + String path = req.getParameter(ARG_PATH); + if (path != null && path.length() != 0) + { + // process the name based path to resolve the NodeRef and the Filename element + PathRefInfo pathInfo = resolveNamePath(getServletContext(), path); + + nodeRef = pathInfo.NodeRef; + filename = pathInfo.Filename; + } + else + { + // a NodeRef must have been specified if no path has been found + if (tokenCount < 6) + { + throw new IllegalArgumentException("Download URL did not contain all required args: " + uri); + } + + // assume 'workspace' or other NodeRef based protocol for remaining URL elements + StoreRef storeRef = new StoreRef(t.nextToken(), t.nextToken()); + String id = t.nextToken(); + // build noderef from the appropriate URL elements + nodeRef = new NodeRef(storeRef, id); + + // filename is last remaining token + filename = t.nextToken(); + } + + // get qualified of the property to get content from - default to ContentModel.PROP_CONTENT + QName propertyQName = ContentModel.PROP_CONTENT; + String property = req.getParameter(ARG_PROPERTY); + if (property != null && property.length() != 0) + { + propertyQName = QName.createQName(property); + } + + if (logger.isDebugEnabled()) + { + logger.debug("Found NodeRef: " + nodeRef.toString()); + logger.debug("Will use filename: " + filename); + logger.debug("For property: " + propertyQName); + logger.debug("With attachment mode: " + attachment); + } + + // get the services we need to retrieve the content + ServiceRegistry serviceRegistry = getServiceRegistry(getServletContext()); + NodeService nodeService = serviceRegistry.getNodeService(); + ContentService contentService = serviceRegistry.getContentService(); + PermissionService permissionService = serviceRegistry.getPermissionService(); + + try + { + // check that the user has at least READ_CONTENT access - else redirect to the login page + if (permissionService.hasPermission(nodeRef, PermissionService.READ_CONTENT) == AccessStatus.DENIED) + { + if (logger.isDebugEnabled()) + logger.debug("User does not have permissions to read content for NodeRef: " + nodeRef.toString()); + + if (redirectToLogin) + { + if (logger.isDebugEnabled()) + logger.debug("Redirecting to login page..."); + + redirectToLoginPage(req, res, getServletContext()); + } + else + { + if (logger.isDebugEnabled()) + logger.debug("Returning 403 Forbidden error..."); + + res.sendError(HttpServletResponse.SC_FORBIDDEN); + } + return; + } + + // check If-Modified-Since header and set Last-Modified header as appropriate + Date modified = (Date)nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED); + long modifiedSince = req.getDateHeader("If-Modified-Since"); + if (modifiedSince > 0L) + { + // round the date to the ignore millisecond value which is not supplied by header + long modDate = (modified.getTime() / 1000L) * 1000L; + if (modDate <= modifiedSince) + { + res.setStatus(304); + return; + } + } + res.setDateHeader("Last-Modified", modified.getTime()); + + if (attachment == true) + { + // set header based on filename - will force a Save As from the browse if it doesn't recognise it + // this is better than the default response of the browser trying to display the contents + res.setHeader("Content-Disposition", "attachment"); + } + + // get the content reader + ContentReader reader = contentService.getReader(nodeRef, propertyQName); + // ensure that it is safe to use + reader = FileContentReader.getSafeContentReader( + reader, + Application.getMessage(req.getSession(), MSG_ERROR_CONTENT_MISSING), + nodeRef, reader); + + String mimetype = reader.getMimetype(); + // fall back if unable to resolve mimetype property + if (mimetype == null || mimetype.length() == 0) + { + MimetypeService mimetypeMap = serviceRegistry.getMimetypeService(); + mimetype = MIMETYPE_OCTET_STREAM; + int extIndex = filename.lastIndexOf('.'); + if (extIndex != -1) + { + String ext = filename.substring(extIndex + 1); + String mt = mimetypeMap.getMimetypesByExtension().get(ext); + if (mt != null) + { + mimetype = mt; + } + } + } + // set mimetype for the content and the character encoding for the stream + res.setContentType(mimetype); + res.setCharacterEncoding(reader.getEncoding()); + + // get the content and stream directly to the response output stream + // assuming the repo is capable of streaming in chunks, this should allow large files + // to be streamed directly to the browser response stream. + try + { + reader.getContent( res.getOutputStream() ); + } + catch (SocketException e) + { + if (e.getMessage().contains("ClientAbortException")) + { + // the client cut the connection - our mission was accomplished apart from a little error message + logger.error("Client aborted stream read:\n node: " + nodeRef + "\n content: " + reader); + } + else + { + throw e; + } + } + } + catch (Throwable err) + { + throw new AlfrescoRuntimeException("Error during download content servlet processing: " + err.getMessage(), err); + } + } + + /** + * Helper to generate a URL to a content node for downloading content from the server. + * + * @param pattern The pattern to use for the URL + * @param ref NodeRef of the content node to generate URL for (cannot be null) + * @param name File name to return in the URL (cannot be null) + * + * @return URL to download the content from the specified node + */ + protected final static String generateUrl(String pattern, NodeRef ref, String name) + { + String url = null; + + try + { + url = MessageFormat.format(pattern, new Object[] { + ref.getStoreRef().getProtocol(), + ref.getStoreRef().getIdentifier(), + ref.getId(), + Utils.replace(URLEncoder.encode(name, "UTF-8"), "+", "%20") } ); + } + catch (UnsupportedEncodingException uee) + { + throw new AlfrescoRuntimeException("Failed to encode content URL for node: " + ref, uee); + } + + return url; + } +} diff --git a/source/java/org/alfresco/web/app/servlet/CommandServlet.java b/source/java/org/alfresco/web/app/servlet/CommandServlet.java index 0beb509ddc..69923e6026 100644 --- a/source/java/org/alfresco/web/app/servlet/CommandServlet.java +++ b/source/java/org/alfresco/web/app/servlet/CommandServlet.java @@ -73,7 +73,7 @@ public class CommandServlet extends BaseServlet /** * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ - protected void doGet(HttpServletRequest req, HttpServletResponse res) + protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String uri = req.getRequestURI(); diff --git a/source/java/org/alfresco/web/app/servlet/DownloadContentServlet.java b/source/java/org/alfresco/web/app/servlet/DownloadContentServlet.java index 951f1c2b3f..afcd7b9bf4 100644 --- a/source/java/org/alfresco/web/app/servlet/DownloadContentServlet.java +++ b/source/java/org/alfresco/web/app/servlet/DownloadContentServlet.java @@ -17,32 +17,12 @@ package org.alfresco.web.app.servlet; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.SocketException; -import java.net.URLEncoder; -import java.text.MessageFormat; -import java.util.Date; -import java.util.StringTokenizer; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.alfresco.error.AlfrescoRuntimeException; -import org.alfresco.model.ContentModel; -import org.alfresco.repo.content.filestore.FileContentReader; -import org.alfresco.service.ServiceRegistry; -import org.alfresco.service.cmr.repository.ContentReader; -import org.alfresco.service.cmr.repository.ContentService; -import org.alfresco.service.cmr.repository.MimetypeService; import org.alfresco.service.cmr.repository.NodeRef; -import org.alfresco.service.cmr.repository.NodeService; -import org.alfresco.service.cmr.repository.StoreRef; -import org.alfresco.service.cmr.security.AccessStatus; -import org.alfresco.service.cmr.security.PermissionService; -import org.alfresco.service.namespace.QName; -import org.alfresco.web.app.Application; -import org.alfresco.web.ui.common.Utils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -71,26 +51,30 @@ import org.apache.commons.logging.LogFactory; * Like most Alfresco servlets, the URL may be followed by a valid 'ticket' argument for authentication: * ?ticket=1234567890 *

- * And/or also followed by the "?guest=true" argument to force guest access login for the URL. + * And/or also followed by the "?guest=true" argument to force guest access login for the URL. If the + * guest=true parameter is used the current session will be logged out and the guest user logged in. + * Therefore upon completion of this request the current user will be "guest". + *

+ * If the user attempting the request is not authorised to access the requested node the login page + * will be redirected to. * * @author Kevin Roast + * @author gavinc */ -public class DownloadContentServlet extends BaseServlet +public class DownloadContentServlet extends BaseDownloadContentServlet { - private static final long serialVersionUID = -4558907921887235966L; - + private static final long serialVersionUID = -576405943603122206L; + private static Log logger = LogFactory.getLog(DownloadContentServlet.class); private static final String DOWNLOAD_URL = "/download/attach/{0}/{1}/{2}/{3}"; private static final String BROWSER_URL = "/download/direct/{0}/{1}/{2}/{3}"; - private static final String MIMETYPE_OCTET_STREAM = "application/octet-stream"; - - private static final String MSG_ERROR_CONTENT_MISSING = "error_content_missing"; - - private static final String ARG_PROPERTY = "property"; - private static final String ARG_ATTACH = "attach"; - private static final String ARG_PATH = "path"; + @Override + protected Log getLogger() + { + return logger; + } /** * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) @@ -98,10 +82,12 @@ public class DownloadContentServlet extends BaseServlet protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { - String uri = req.getRequestURI(); - if (logger.isDebugEnabled()) - logger.debug("Processing URL: " + uri + (req.getQueryString() != null ? ("?" + req.getQueryString()) : "")); + { + String queryString = req.getQueryString(); + logger.debug("Authenticating request to URL: " + req.getRequestURI() + + ((queryString != null && queryString.length() > 0) ? ("?" + queryString) : "")); + } AuthenticationStatus status = servletAuthenticate(req, res); if (status == AuthenticationStatus.Failure) @@ -109,159 +95,7 @@ public class DownloadContentServlet extends BaseServlet return; } - // TODO: add compression here? - // see http://servlets.com/jservlet2/examples/ch06/ViewResourceCompress.java for example - // only really needed if we don't use the built in compression of the servlet container - uri = uri.substring(req.getContextPath().length()); - StringTokenizer t = new StringTokenizer(uri, "/"); - int tokenCount = t.countTokens(); - - t.nextToken(); // skip servlet name - - // attachment mode (either 'attach' or 'direct') - String attachToken = t.nextToken(); - boolean attachment = attachToken.equals(ARG_ATTACH); - - // get or calculate the noderef and filename to download as - NodeRef nodeRef; - String filename; - - // do we have a path parameter instead of a NodeRef? - String path = req.getParameter(ARG_PATH); - if (path != null && path.length() != 0) - { - // process the name based path to resolve the NodeRef and the Filename element - PathRefInfo pathInfo = resolveNamePath(getServletContext(), path); - - nodeRef = pathInfo.NodeRef; - filename = pathInfo.Filename; - } - else - { - // a NodeRef must have been specified if no path has been found - if (tokenCount < 6) - { - throw new IllegalArgumentException("Download URL did not contain all required args: " + uri); - } - - // assume 'workspace' or other NodeRef based protocol for remaining URL elements - StoreRef storeRef = new StoreRef(t.nextToken(), t.nextToken()); - String id = t.nextToken(); - // build noderef from the appropriate URL elements - nodeRef = new NodeRef(storeRef, id); - - // filename is last remaining token - filename = t.nextToken(); - } - - // get qualified of the property to get content from - default to ContentModel.PROP_CONTENT - QName propertyQName = ContentModel.PROP_CONTENT; - String property = req.getParameter(ARG_PROPERTY); - if (property != null && property.length() != 0) - { - propertyQName = QName.createQName(property); - } - - if (logger.isDebugEnabled()) - { - logger.debug("Found NodeRef: " + nodeRef.toString()); - logger.debug("Will use filename: " + filename); - logger.debug("For property: " + propertyQName); - logger.debug("With attachment mode: " + attachment); - } - - // get the services we need to retrieve the content - ServiceRegistry serviceRegistry = getServiceRegistry(getServletContext()); - NodeService nodeService = serviceRegistry.getNodeService(); - ContentService contentService = serviceRegistry.getContentService(); - PermissionService permissionService = serviceRegistry.getPermissionService(); - - try - { - // check that the user has at least READ_CONTENT access - else redirect to the login page - if (permissionService.hasPermission(nodeRef, PermissionService.READ_CONTENT) == AccessStatus.DENIED) - { - if (logger.isDebugEnabled()) - logger.debug("User does not have permissions to read content for NodeRef: " + nodeRef.toString()); - redirectToLoginPage(req, res, getServletContext()); - return; - } - - // check If-Modified-Since header and set Last-Modified header as appropriate - Date modified = (Date)nodeService.getProperty(nodeRef, ContentModel.PROP_MODIFIED); - long modifiedSince = req.getDateHeader("If-Modified-Since"); - if (modifiedSince > 0L) - { - // round the date to the ignore millisecond value which is not supplied by header - long modDate = (modified.getTime() / 1000L) * 1000L; - if (modDate <= modifiedSince) - { - res.setStatus(304); - return; - } - } - res.setDateHeader("Last-Modified", modified.getTime()); - - if (attachment == true) - { - // set header based on filename - will force a Save As from the browse if it doesn't recognise it - // this is better than the default response of the browser trying to display the contents - res.setHeader("Content-Disposition", "attachment"); - } - - // get the content reader - ContentReader reader = contentService.getReader(nodeRef, propertyQName); - // ensure that it is safe to use - reader = FileContentReader.getSafeContentReader( - reader, - Application.getMessage(req.getSession(), MSG_ERROR_CONTENT_MISSING), - nodeRef, reader); - - String mimetype = reader.getMimetype(); - // fall back if unable to resolve mimetype property - if (mimetype == null || mimetype.length() == 0) - { - MimetypeService mimetypeMap = serviceRegistry.getMimetypeService(); - mimetype = MIMETYPE_OCTET_STREAM; - int extIndex = filename.lastIndexOf('.'); - if (extIndex != -1) - { - String ext = filename.substring(extIndex + 1); - String mt = mimetypeMap.getMimetypesByExtension().get(ext); - if (mt != null) - { - mimetype = mt; - } - } - } - // set mimetype for the content and the character encoding for the stream - res.setContentType(mimetype); - res.setCharacterEncoding(reader.getEncoding()); - - // get the content and stream directly to the response output stream - // assuming the repo is capable of streaming in chunks, this should allow large files - // to be streamed directly to the browser response stream. - try - { - reader.getContent( res.getOutputStream() ); - } - catch (SocketException e) - { - if (e.getMessage().contains("ClientAbortException")) - { - // the client cut the connection - our mission was accomplished apart from a little error message - logger.error("Client aborted stream read:\n node: " + nodeRef + "\n content: " + reader); - } - else - { - throw e; - } - } - } - catch (Throwable err) - { - throw new AlfrescoRuntimeException("Error during download content servlet processing: " + err.getMessage(), err); - } + processDownloadRequest(req, res, true); } /** @@ -276,22 +110,7 @@ public class DownloadContentServlet extends BaseServlet */ public final static String generateDownloadURL(NodeRef ref, String name) { - String url = null; - - try - { - url = MessageFormat.format(DOWNLOAD_URL, new Object[] { - ref.getStoreRef().getProtocol(), - ref.getStoreRef().getIdentifier(), - ref.getId(), - Utils.replace(URLEncoder.encode(name, "UTF-8"), "+", "%20") } ); - } - catch (UnsupportedEncodingException uee) - { - throw new AlfrescoRuntimeException("Failed to encode content URL for node: " + ref, uee); - } - - return url; + return generateUrl(DOWNLOAD_URL, ref, name); } /** @@ -306,21 +125,6 @@ public class DownloadContentServlet extends BaseServlet */ public final static String generateBrowserURL(NodeRef ref, String name) { - String url = null; - - try - { - url = MessageFormat.format(BROWSER_URL, new Object[] { - ref.getStoreRef().getProtocol(), - ref.getStoreRef().getIdentifier(), - ref.getId(), - Utils.replace(URLEncoder.encode(name, "UTF-8"), "+", "%20") } ); - } - catch (UnsupportedEncodingException uee) - { - throw new AlfrescoRuntimeException("Failed to encode content URL for node: " + ref, uee); - } - - return url; + return generateUrl(BROWSER_URL, ref, name); } } diff --git a/source/java/org/alfresco/web/app/servlet/FacesHelper.java b/source/java/org/alfresco/web/app/servlet/FacesHelper.java index 7b8a008a0b..701ac2b0f2 100644 --- a/source/java/org/alfresco/web/app/servlet/FacesHelper.java +++ b/source/java/org/alfresco/web/app/servlet/FacesHelper.java @@ -145,12 +145,31 @@ public final class FacesHelper else { // make sure we do not have illegal characters in the id + id = makeLegalId(id); + } + + component.setId(id); + } + + /** + * Makes the given id a legal JSF component id by replacing illegal + * characters with underscores. + * + * @param id The id to make legal + * @return The legalised id + */ + public static String makeLegalId(String id) + { + if (id != null) + { + // replace illegal ID characters with an underscore id = id.replace(':', '_'); + id = id.replace(' ', '_'); // TODO: check all other illegal characters - only allowed dash and underscore } - component.setId(id); + return id; } /** diff --git a/source/java/org/alfresco/web/app/servlet/GuestDownloadContentServlet.java b/source/java/org/alfresco/web/app/servlet/GuestDownloadContentServlet.java new file mode 100644 index 0000000000..5a61b3a868 --- /dev/null +++ b/source/java/org/alfresco/web/app/servlet/GuestDownloadContentServlet.java @@ -0,0 +1,147 @@ +/* + * 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.app.servlet; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PermissionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Servlet responsible for streaming node content from the repo directly to the response stream. + * The appropriate mimetype is calculated based on filename extension. + *

+ * The URL to the servlet should be generated thus: + *

/alfresco/guestDownload/attach/workspace/SpacesStore/0000-0000-0000-0000/myfile.pdf
+ * or + *
/alfresco/guestDownload/direct/workspace/SpacesStore/0000-0000-0000-0000/myfile.pdf
+ * or + *
/alfresco/guestDownload/[direct|attach]?path=/Company%20Home/MyFolder/myfile.pdf
+ * The protocol, followed by either the store and Id (NodeRef) or instead specify a name based + * encoded Path to the content, note that the filename element is used for mimetype lookup and + * as the returning filename for the response stream. + *

+ * The 'attach' or 'direct' element is used to indicate whether to display the stream directly + * in the browser or download it as a file attachment. + *

+ * By default, the download assumes that the content is on the + * {@link org.alfresco.model.ContentModel#PROP_CONTENT content property}.
+ * To retrieve the content of a specific model property, use a 'property' arg, providing the workspace, + * node ID AND the qualified name of the property. + *

+ * This servlet only accesses content available to the guest user. If the guest user does not + * have access to the requested a 401 Forbidden response is returned to the caller. + *

+ * This servlet does not effect the current session, therefore if guest access is required to a + * resource this servlet can be used without logging out the current user. + * + * @author gavinc + */ +public class GuestDownloadContentServlet extends BaseDownloadContentServlet +{ + private static final long serialVersionUID = -5258137503339817457L; + + private static Log logger = LogFactory.getLog(GuestDownloadContentServlet.class); + + private static final String DOWNLOAD_URL = "/guestDownload/attach/{0}/{1}/{2}/{3}"; + private static final String BROWSER_URL = "/guestDownload/direct/{0}/{1}/{2}/{3}"; + + @Override + protected Log getLogger() + { + return logger; + } + + /** + * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + protected void doGet(HttpServletRequest req, HttpServletResponse res) + throws ServletException, IOException + { + if (logger.isDebugEnabled()) + { + String queryString = req.getQueryString(); + logger.debug("Setting up guest access to URL: " + req.getRequestURI() + + ((queryString != null && queryString.length() > 0) ? ("?" + queryString) : "")); + } + + DownloadContentWork dcw = new DownloadContentWork(req, res); + AuthenticationUtil.runAs(dcw, PermissionService.GUEST_AUTHORITY); + } + + /** + * Helper to generate a URL to a content node for downloading content from the server. + * The content is supplied as an HTTP1.1 attachment to the response. This generally means + * a browser should prompt the user to save the content to specified location. + * + * @param ref NodeRef of the content node to generate URL for (cannot be null) + * @param name File name to return in the URL (cannot be null) + * + * @return URL to download the content from the specified node + */ + public final static String generateDownloadURL(NodeRef ref, String name) + { + return generateUrl(DOWNLOAD_URL, ref, name); + } + + /** + * Helper to generate a URL to a content node for downloading content from the server. + * The content is supplied directly in the reponse. This generally means a browser will + * attempt to open the content directly if possible, else it will prompt to save the file. + * + * @param ref NodeRef of the content node to generate URL for (cannot be null) + * @param name File name to return in the URL (cannot be null) + * + * @return URL to download the content from the specified node + */ + public final static String generateBrowserURL(NodeRef ref, String name) + { + return generateUrl(BROWSER_URL, ref, name); + } + + /** + * Class to wrap the call to processDownloadRequest. + * + * @author gavinc + */ + public class DownloadContentWork implements RunAsWork + { + private HttpServletRequest req = null; + private HttpServletResponse res = null; + + public DownloadContentWork(HttpServletRequest req, HttpServletResponse res) + { + this.req = req; + this.res = res; + } + + public Object doWork() throws Exception + { + processDownloadRequest(this.req, this.res, false); + + return null; + } + } +} diff --git a/source/java/org/alfresco/web/app/servlet/TemplateContentServlet.java b/source/java/org/alfresco/web/app/servlet/TemplateContentServlet.java index 99fdce8af5..a728e7bc93 100644 --- a/source/java/org/alfresco/web/app/servlet/TemplateContentServlet.java +++ b/source/java/org/alfresco/web/app/servlet/TemplateContentServlet.java @@ -86,8 +86,6 @@ public class TemplateContentServlet extends BaseServlet private static final String DEFAULT_URL = "/template/{0}/{1}/{2}"; private static final String TEMPLATE_URL = "/template/{0}/{1}/{2}/{3}/{4}/{5}"; - private static final String MSG_ERROR_CONTENT_MISSING = "error_content_missing"; - private static final String ARG_MIMETYPE = "mimetype"; private static final String ARG_TEMPLATE_PATH = "templatePath"; private static final String ARG_CONTEXT_PATH = "contextPath"; @@ -95,13 +93,17 @@ public class TemplateContentServlet extends BaseServlet /** * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ - protected void doGet(HttpServletRequest req, HttpServletResponse res) + protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String uri = req.getRequestURI(); if (logger.isDebugEnabled()) - logger.debug("Processing URL: " + uri + (req.getQueryString() != null ? ("?" + req.getQueryString()) : "")); + { + String queryString = req.getQueryString(); + logger.debug("Processing URL: " + uri + + ((queryString != null && queryString.length() > 0) ? ("?" + queryString) : "")); + } AuthenticationStatus status = servletAuthenticate(req, res); if (status == AuthenticationStatus.Failure) @@ -258,6 +260,7 @@ public class TemplateContentServlet extends BaseServlet * * @return an object model ready for executing template against */ + @SuppressWarnings("unchecked") private Object getModel(ServiceRegistry services, HttpServletRequest req, NodeRef templateRef, NodeRef nodeRef) { // build FreeMarker default model and merge diff --git a/source/java/org/alfresco/web/app/servlet/ajax/AjaxCommand.java b/source/java/org/alfresco/web/app/servlet/ajax/AjaxCommand.java index 17eba223be..c61a623e8c 100644 --- a/source/java/org/alfresco/web/app/servlet/ajax/AjaxCommand.java +++ b/source/java/org/alfresco/web/app/servlet/ajax/AjaxCommand.java @@ -22,7 +22,7 @@ public interface AjaxCommand * expression. Parameters required to call the method can be retrieved * from the request. * - * Currently the content type of the response will always be text/html, in the + * Currently the content type of the response will always be text/xml, in the * future sublcasses may provide a mechanism to allow the content type to be set * dynamically. * diff --git a/source/java/org/alfresco/web/app/servlet/ajax/InvokeCommand.java b/source/java/org/alfresco/web/app/servlet/ajax/InvokeCommand.java index 3c5e905804..1509d75a44 100644 --- a/source/java/org/alfresco/web/app/servlet/ajax/InvokeCommand.java +++ b/source/java/org/alfresco/web/app/servlet/ajax/InvokeCommand.java @@ -42,7 +42,7 @@ public class InvokeCommand extends BaseAjaxCommand // NOTE: it doesn't seem to matter what the content type of the response is (at least with Dojo), // it determines it's behaviour from the mimetype specified in the AJAX call on the client, - // therefore, for now we will always return a content type of text/html. + // therefore, for now we will always return a content type of text/xml. // In the future we may use annotations on the method to be called to specify what content // type should be used for the response. @@ -53,7 +53,7 @@ public class InvokeCommand extends BaseAjaxCommand RenderKit renderKit = renderFactory.getRenderKit(facesContext, viewRoot.getRenderKitId()); ResponseWriter writer = renderKit.createResponseWriter( - new OutputStreamWriter(os), MimetypeMap.MIMETYPE_HTML, "UTF-8"); + new OutputStreamWriter(os), MimetypeMap.MIMETYPE_XML, "UTF-8"); facesContext.setResponseWriter(writer); response.setContentType(writer.getContentType()); diff --git a/source/java/org/alfresco/web/bean/AdvancedSearchBean.java b/source/java/org/alfresco/web/bean/AdvancedSearchBean.java index 9ee2208ae1..7f47ed15a6 100644 --- a/source/java/org/alfresco/web/bean/AdvancedSearchBean.java +++ b/source/java/org/alfresco/web/bean/AdvancedSearchBean.java @@ -848,7 +848,7 @@ public class AdvancedSearchBean search.addFixedValueQuery(QName.createQName(qname), strVal); } } - else + else if (value != null) { // by default use toString() value - this is for text fields and unknown types String strVal = value.toString(); diff --git a/source/java/org/alfresco/web/bean/CheckinCheckoutBean.java b/source/java/org/alfresco/web/bean/CheckinCheckoutBean.java index 3359683f70..0d0e8fa77e 100644 --- a/source/java/org/alfresco/web/bean/CheckinCheckoutBean.java +++ b/source/java/org/alfresco/web/bean/CheckinCheckoutBean.java @@ -29,6 +29,7 @@ import javax.transaction.UserTransaction; import org.alfresco.model.ContentModel; import org.alfresco.repo.content.MimetypeMap; import org.alfresco.repo.version.VersionModel; +import org.alfresco.repo.workflow.WorkflowModel; import org.alfresco.service.cmr.coci.CheckOutCheckInService; import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentData; @@ -40,6 +41,10 @@ import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.version.Version; import org.alfresco.service.cmr.version.VersionType; +import org.alfresco.service.cmr.workflow.WorkflowService; +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; import org.alfresco.web.app.AlfrescoNavigationHandler; import org.alfresco.web.app.Application; import org.alfresco.web.app.context.UIContextService; @@ -131,6 +136,14 @@ public class CheckinCheckoutBean this.contentService = contentService; } + /** + * @param workflowService The WorkflowService to set. + */ + public void setWorkflowService(WorkflowService workflowService) + { + this.workflowService = workflowService; + } + /** * @return The document node being used for the current operation */ @@ -357,13 +370,29 @@ public class CheckinCheckoutBean if (id != null && id.length() != 0) { setupContentDocument(id); - } + } else { setDocument(null); } - clearUpload(); + resetState(); + } + + public void setupWorkflowContentAction(ActionEvent event) + { + // do the common processing + setupContentAction(event); + + // retrieve the id of the task + UIActionLink link = (UIActionLink)event.getComponent(); + Map params = link.getParameterMap(); + this.workflowTaskId = params.get("taskId"); + + this.isWorkflowAction = true; + + if (logger.isDebugEnabled()) + logger.debug("Setup for workflow package action for task id: " + this.workflowTaskId); } /** @@ -435,7 +464,7 @@ public class CheckinCheckoutBean logger.debug("Checkout copy location: " + getCopyLocation()); logger.debug("Selected Space Id: " + this.selectedSpaceId); } - NodeRef workingCopyRef; + NodeRef workingCopyRef = null; if (getCopyLocation().equals(COPYLOCATION_OTHER) && this.selectedSpaceId != null) { // checkout to a arbituary parent Space @@ -447,7 +476,31 @@ public class CheckinCheckoutBean } else { + // checkout the content to the current space workingCopyRef = this.versionOperationsService.checkout(node.getNodeRef()); + + // if this is a workflow action and there is a task id present we need + // to also link the working copy to the workflow package so it appears + // in the resources panel in the manage task dialog + if (this.isWorkflowAction && this.workflowTaskId != null) + { + WorkflowTask task = this.workflowService.getTaskById(this.workflowTaskId); + if (task != null) + { + NodeRef workflowPackage = (NodeRef)task.properties.get(WorkflowModel.ASSOC_PACKAGE); + if (workflowPackage != null) + { + this.nodeService.addChild(workflowPackage, workingCopyRef, + ContentModel.ASSOC_CONTAINS, QName.createQName( + NamespaceService.CONTENT_MODEL_1_0_URI, + QName.createValidLocalName((String)this.nodeService.getProperty( + workingCopyRef, ContentModel.PROP_NAME)))); + + if (logger.isDebugEnabled()) + logger.debug("Added working copy to workflow package: " + workflowPackage); + } + } + } } // set the working copy Node instance @@ -500,7 +553,7 @@ public class CheckinCheckoutBean } // clean up and clear action context - clearUpload(); + resetState(); setDocument(null); setWorkingDocument(null); @@ -525,7 +578,7 @@ public class CheckinCheckoutBean if (node != null) { // clean up and clear action context - clearUpload(); + resetState(); setDocument(null); setWorkingDocument(null); @@ -631,7 +684,7 @@ public class CheckinCheckoutBean tx.commit(); // clean up and clear action context - clearUpload(); + resetState(); setDocument(null); setDocumentContent(null); setEditorOutput(null); @@ -669,7 +722,7 @@ public class CheckinCheckoutBean // try to cancel checkout of the working copy this.versionOperationsService.cancelCheckout(node.getNodeRef()); - clearUpload(); + resetState(); outcome = AlfrescoNavigationHandler.CLOSE_DIALOG_OUTCOME; } @@ -718,10 +771,15 @@ public class CheckinCheckoutBean { throw new IllegalStateException("Node supplied for undo checkout has neither Working Copy or Locked aspect!"); } - - clearUpload(); - outcome = AlfrescoNavigationHandler.CLOSE_DIALOG_OUTCOME + AlfrescoNavigationHandler.OUTCOME_SEPARATOR + "browse"; + outcome = AlfrescoNavigationHandler.CLOSE_DIALOG_OUTCOME; + + if (this.isWorkflowAction == false) + { + outcome = outcome + AlfrescoNavigationHandler.OUTCOME_SEPARATOR + "browse"; + } + + resetState(); } catch (Throwable err) { @@ -804,12 +862,16 @@ public class CheckinCheckoutBean // commit the transaction tx.commit(); + outcome = AlfrescoNavigationHandler.CLOSE_DIALOG_OUTCOME; + + if (this.isWorkflowAction == false) + { + outcome = outcome + AlfrescoNavigationHandler.OUTCOME_SEPARATOR + "browse"; + } + // clear action context setDocument(null); - clearUpload(); - - outcome = AlfrescoNavigationHandler.CLOSE_DIALOG_OUTCOME + - AlfrescoNavigationHandler.OUTCOME_SEPARATOR + "browse"; + resetState(); } catch (Throwable err) { @@ -863,7 +925,7 @@ public class CheckinCheckoutBean // clear action context setDocument(null); - clearUpload(); + resetState(); outcome = AlfrescoNavigationHandler.CLOSE_DIALOG_OUTCOME; } @@ -889,7 +951,7 @@ public class CheckinCheckoutBean public String cancel() { // reset the state - clearUpload(); + resetState(); return AlfrescoNavigationHandler.CLOSE_DIALOG_OUTCOME; } @@ -897,7 +959,7 @@ public class CheckinCheckoutBean /** * Clear form state and upload file bean */ - private void clearUpload() + private void resetState() { // delete the temporary file we uploaded earlier if (this.file != null) @@ -912,6 +974,8 @@ public class CheckinCheckoutBean this.copyLocation = COPYLOCATION_CURRENT; this.versionNotes = ""; this.selectedSpaceId = null; + this.isWorkflowAction = false; + this.workflowTaskId = null; // remove the file upload bean from the session FacesContext ctx = FacesContext.getCurrentInstance(); @@ -951,6 +1015,8 @@ public class CheckinCheckoutBean private String fileName; private boolean keepCheckedOut = false; private boolean minorChange = true; + private boolean isWorkflowAction = false; + private String workflowTaskId; private String copyLocation = COPYLOCATION_CURRENT; private String versionNotes = ""; private NodeRef selectedSpaceId = null; @@ -969,4 +1035,7 @@ public class CheckinCheckoutBean /** The ContentService to be used by the bean */ protected ContentService contentService; + + /** The WorkflowService to be used by the bean */ + protected WorkflowService workflowService; } diff --git a/source/java/org/alfresco/web/bean/ErrorBean.java b/source/java/org/alfresco/web/bean/ErrorBean.java index 8cf9b29244..0ddeeee0cc 100644 --- a/source/java/org/alfresco/web/bean/ErrorBean.java +++ b/source/java/org/alfresco/web/bean/ErrorBean.java @@ -121,15 +121,20 @@ public class ErrorBean */ public String getStackTrace() { - StringWriter stringWriter = new StringWriter(); - PrintWriter writer = new PrintWriter(stringWriter); - this.lastError.printStackTrace(writer); + String trace = "No stack trace available"; - // format the message for HTML display - String trace = stringWriter.toString(); - trace = trace.replaceAll("<", "<"); - trace = trace.replaceAll(">", ">"); - trace = trace.replaceAll("\n", "
"); + if (this.lastError != null) + { + StringWriter stringWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(stringWriter); + this.lastError.printStackTrace(writer); + + // format the message for HTML display + trace = stringWriter.toString(); + trace = trace.replaceAll("<", "<"); + trace = trace.replaceAll(">", ">"); + trace = trace.replaceAll("\n", "
"); + } return trace; } diff --git a/source/java/org/alfresco/web/bean/SpaceDetailsBean.java b/source/java/org/alfresco/web/bean/SpaceDetailsBean.java index 234a37ecdb..2f5f053b00 100644 --- a/source/java/org/alfresco/web/bean/SpaceDetailsBean.java +++ b/source/java/org/alfresco/web/bean/SpaceDetailsBean.java @@ -73,6 +73,7 @@ public class SpaceDetailsBean extends BaseDetailsBean { // initial state of some panels that don't use the default panels.put("rules-panel", false); + panels.put("dashboard-panel", false); } diff --git a/source/java/org/alfresco/web/bean/dialog/BaseDialogBean.java b/source/java/org/alfresco/web/bean/dialog/BaseDialogBean.java index f2f48e6981..4f60060878 100644 --- a/source/java/org/alfresco/web/bean/dialog/BaseDialogBean.java +++ b/source/java/org/alfresco/web/bean/dialog/BaseDialogBean.java @@ -130,12 +130,12 @@ public abstract class BaseDialogBean implements IDialogBean return true; } - public String getTitle() + public String getContainerTitle() { return null; } - public String getDescription() + public String getContainerDescription() { return null; } diff --git a/source/java/org/alfresco/web/bean/dialog/DialogManager.java b/source/java/org/alfresco/web/bean/dialog/DialogManager.java index c7998ab253..abeff99b4d 100644 --- a/source/java/org/alfresco/web/bean/dialog/DialogManager.java +++ b/source/java/org/alfresco/web/bean/dialog/DialogManager.java @@ -141,7 +141,7 @@ public final class DialogManager public String getTitle() { // try and get the title directly from the dialog - String title = this.currentDialogState.getDialog().getTitle(); + String title = this.currentDialogState.getDialog().getContainerTitle(); if (title == null) { @@ -170,7 +170,7 @@ public final class DialogManager public String getDescription() { // try and get the description directly from the dialog - String desc = this.currentDialogState.getDialog().getDescription(); + String desc = this.currentDialogState.getDialog().getContainerDescription(); if (desc == null) { diff --git a/source/java/org/alfresco/web/bean/dialog/IDialogBean.java b/source/java/org/alfresco/web/bean/dialog/IDialogBean.java index becb29932b..b2a228f83f 100644 --- a/source/java/org/alfresco/web/bean/dialog/IDialogBean.java +++ b/source/java/org/alfresco/web/bean/dialog/IDialogBean.java @@ -73,7 +73,7 @@ public interface IDialogBean * * @return The title or null if the title is to be acquired via configuration */ - public String getTitle(); + public String getContainerTitle(); /** * Returns the description to be used for the dialog @@ -82,5 +82,5 @@ public interface IDialogBean * * @return The title or null if the title is to be acquired via configuration */ - public String getDescription(); + public String getContainerDescription(); } diff --git a/source/java/org/alfresco/web/bean/generator/DatePickerGenerator.java b/source/java/org/alfresco/web/bean/generator/DatePickerGenerator.java index a7f0463ddb..7138673285 100644 --- a/source/java/org/alfresco/web/bean/generator/DatePickerGenerator.java +++ b/source/java/org/alfresco/web/bean/generator/DatePickerGenerator.java @@ -4,7 +4,6 @@ import java.util.Date; import javax.faces.component.UIComponent; import javax.faces.component.UIOutput; -import javax.faces.component.UISelectOne; import javax.faces.context.FacesContext; import javax.faces.convert.Converter; @@ -25,6 +24,7 @@ import org.alfresco.web.ui.repo.component.property.UIPropertySheet; */ public class DatePickerGenerator extends BaseComponentGenerator { + private boolean initialiseIfNull = false; private int yearCount = 30; private int startYear = new Date().getYear() + 1900 + 2; @@ -61,6 +61,26 @@ public class DatePickerGenerator extends BaseComponentGenerator { this.yearCount = yearCount; } + + /** + * @return Determines whether the control should initially show + * today's date if the model value is null + */ + public boolean isInitialiseIfNull() + { + return initialiseIfNull; + } + + /** + * @param initialiseIfNull Determines whether the control should + * initially show today's date if the model value is null. + * This will also hide the None button thus disallowing + * the user to set the date back to null. + */ + public void setInitialiseIfNull(boolean initialiseIfNull) + { + this.initialiseIfNull = initialiseIfNull; + } @SuppressWarnings("unchecked") public UIComponent generate(FacesContext context, String id) @@ -71,6 +91,7 @@ public class DatePickerGenerator extends BaseComponentGenerator FacesHelper.setupComponentId(context, component, id); component.getAttributes().put("startYear", this.startYear); component.getAttributes().put("yearCount", this.yearCount); + component.getAttributes().put("initialiseIfNull", new Boolean(this.initialiseIfNull)); component.getAttributes().put("style", "margin-right: 7px;"); return component; diff --git a/source/java/org/alfresco/web/bean/generator/HtmlSeparatorGenerator.java b/source/java/org/alfresco/web/bean/generator/HtmlSeparatorGenerator.java index 3e8153750b..8fe3079b3d 100644 --- a/source/java/org/alfresco/web/bean/generator/HtmlSeparatorGenerator.java +++ b/source/java/org/alfresco/web/bean/generator/HtmlSeparatorGenerator.java @@ -15,7 +15,7 @@ import org.alfresco.web.ui.repo.component.property.UIPropertySheet; */ public class HtmlSeparatorGenerator extends BaseComponentGenerator { - protected String html = "default"; + protected String html = ""; /** * Returns the HTML configured to be used for this separator diff --git a/source/java/org/alfresco/web/bean/wizard/WizardManager.java b/source/java/org/alfresco/web/bean/wizard/WizardManager.java index c70115e53b..333f236118 100644 --- a/source/java/org/alfresco/web/bean/wizard/WizardManager.java +++ b/source/java/org/alfresco/web/bean/wizard/WizardManager.java @@ -152,7 +152,7 @@ public final class WizardManager public String getTitle() { // try and get the title directly from the wizard - String title = this.currentWizardState.getWizard().getTitle(); + String title = this.currentWizardState.getWizard().getContainerTitle(); if (title == null) { @@ -181,7 +181,7 @@ public final class WizardManager public String getDescription() { // try and get the description directly from the dialog - String desc = this.currentWizardState.getWizard().getDescription(); + String desc = this.currentWizardState.getWizard().getContainerDescription(); if (desc == null) { diff --git a/source/java/org/alfresco/web/bean/workflow/ManageTaskDialog.java b/source/java/org/alfresco/web/bean/workflow/ManageTaskDialog.java index 936199c415..2e63eb4630 100644 --- a/source/java/org/alfresco/web/bean/workflow/ManageTaskDialog.java +++ b/source/java/org/alfresco/web/bean/workflow/ManageTaskDialog.java @@ -26,6 +26,7 @@ import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; import org.alfresco.web.app.AlfrescoNavigationHandler; import org.alfresco.web.app.Application; +import org.alfresco.web.app.servlet.FacesHelper; import org.alfresco.web.bean.dialog.BaseDialogBean; import org.alfresco.web.bean.repository.MapNode; import org.alfresco.web.bean.repository.Node; @@ -105,16 +106,7 @@ public class ManageTaskDialog extends BaseDialogBean this.workflowInstance = this.task.path.instance; // setup the workflow package for the task - Serializable obj = this.task.properties.get(WorkflowModel.ASSOC_PACKAGE); - // TODO: remove this workaroud where JBPM may return a String and not the NodeRef - if (obj instanceof NodeRef) - { - this.workflowPackage = (NodeRef)obj; - } - else if (obj instanceof String) - { - this.workflowPackage = new NodeRef((String)obj); - } + this.workflowPackage = (NodeRef)this.task.properties.get(WorkflowModel.ASSOC_PACKAGE); if (logger.isDebugEnabled()) { @@ -208,7 +200,7 @@ public class ManageTaskDialog extends BaseDialogBean @Override public String getFinishButtonLabel() { - return Application.getMessage(FacesContext.getCurrentInstance(), "save"); + return Application.getMessage(FacesContext.getCurrentInstance(), "save_changes"); } @Override @@ -218,7 +210,7 @@ public class ManageTaskDialog extends BaseDialogBean } @Override - public String getTitle() + public String getContainerTitle() { String titleStart = Application.getMessage(FacesContext.getCurrentInstance(), "manage_task_title"); @@ -226,7 +218,7 @@ public class ManageTaskDialog extends BaseDialogBean } @Override - public String getDescription() + public String getContainerDescription() { return this.task.description; } @@ -251,7 +243,7 @@ public class ManageTaskDialog extends BaseDialogBean String selectedTransition = null; for (WorkflowTransition trans : this.transitions) { - Object result = reqParams.get(CLIENT_ID_PREFIX + trans.title); + Object result = reqParams.get(CLIENT_ID_PREFIX + FacesHelper.makeLegalId(trans.title)); if (result != null) { // this was the button that was pressed @@ -635,7 +627,10 @@ public class ManageTaskDialog extends BaseDialogBean node.addPropertyResolver("displayPath", this.browseBean.resolverDisplayPath); // add a property resolver to indicate whether the item has been completed or not -// node.addPropertyResolver("completed", this.completeResolver); +// node.addPropertyResolver("completed", this.completeResolver); + + // add the id of the task being managed + node.getProperties().put("taskId", this.task.id); this.resources.add(node); } diff --git a/source/java/org/alfresco/web/bean/workflow/ReassignTaskDialog.java b/source/java/org/alfresco/web/bean/workflow/ReassignTaskDialog.java index f543730687..b3077cfd57 100644 --- a/source/java/org/alfresco/web/bean/workflow/ReassignTaskDialog.java +++ b/source/java/org/alfresco/web/bean/workflow/ReassignTaskDialog.java @@ -50,7 +50,7 @@ public class ReassignTaskDialog extends BaseDialogBean { super.init(parameters); - this.taskId = this.parameters.get("task-id"); + this.taskId = this.parameters.get("id"); if (this.taskId == null || this.taskId.length() == 0) { throw new IllegalArgumentException("Reassign task dialog called without task id"); diff --git a/source/java/org/alfresco/web/bean/workflow/StartWorkflowWizard.java b/source/java/org/alfresco/web/bean/workflow/StartWorkflowWizard.java index c8e5d0a086..8922ebd0f1 100644 --- a/source/java/org/alfresco/web/bean/workflow/StartWorkflowWizard.java +++ b/source/java/org/alfresco/web/bean/workflow/StartWorkflowWizard.java @@ -46,6 +46,7 @@ import org.apache.commons.logging.LogFactory; public class StartWorkflowWizard extends BaseWizardBean { protected String selectedWorkflow; + protected String previouslySelectedWorkflow; protected List availableWorkflows; protected Map workflows; protected WorkflowService workflowService; @@ -77,18 +78,13 @@ public class StartWorkflowWizard extends BaseWizardBean this.selectedWorkflow = null; } + this.previouslySelectedWorkflow = null; this.startTaskNode = null; this.resources = null; this.itemsToAdd = null; this.packageItemsToAdd = new ArrayList(); this.isItemBeingAdded = false; - if (this.packageItemsRichList != null) - { - this.packageItemsRichList.setValue(null); - this.packageItemsRichList = null; - } - - // TODO: Does this need to be in a read-only transaction?? + resetRichList(); // add the item the workflow wizard was started on to the list of resources String itemToWorkflowId = this.parameters.get("item-to-workflow"); @@ -110,11 +106,7 @@ public class StartWorkflowWizard extends BaseWizardBean public void restored() { // reset the workflow package rich list so everything gets re-evaluated - if (this.packageItemsRichList != null) - { - this.packageItemsRichList.setValue(null); - this.packageItemsRichList = null; - } + resetRichList(); } @Override @@ -190,13 +182,14 @@ public class StartWorkflowWizard extends BaseWizardBean { String stepName = Application.getWizardManager().getCurrentStepName(); - if ("options".equals(stepName) && this.startTaskNode == null) + if ("options".equals(stepName) && + (this.selectedWorkflow.equals(this.previouslySelectedWorkflow) == false)) { // retrieve the start task for the selected workflow, get the task // definition and create a transient node to allow the property // sheet to collect the required data. - WorkflowDefinition flowDef = this.workflows.get(this.selectedWorkflow); + WorkflowDefinition flowDef = this.workflows.get(this.selectedWorkflow); if (logger.isDebugEnabled()) logger.debug("Selected workflow: "+ flowDef); @@ -211,11 +204,29 @@ public class StartWorkflowWizard extends BaseWizardBean this.startTaskNode = new TransientNode(taskDef.metadata.getName(), "task_" + System.currentTimeMillis(), null); } + + // we also need to reset the resources list so that the actions get re-evaluated + resetRichList(); } return null; } + @Override + public String back() + { + String stepName = Application.getWizardManager().getCurrentStepName(); + + // if we have come back to the "choose-workflow" step remember + // the current workflow selection + if ("choose-workflow".equals(stepName)) + { + this.previouslySelectedWorkflow = this.selectedWorkflow; + } + + return null; + } + @Override public boolean getNextButtonDisabled() { @@ -293,7 +304,7 @@ public class StartWorkflowWizard extends BaseWizardBean // reset the rich list so it re-renders this.packageItemsRichList.setValue(null); } - + // ------------------------------------------------------------------------------ // Bean Getters and Setters @@ -465,33 +476,34 @@ public class StartWorkflowWizard extends BaseWizardBean */ public List getStartableWorkflows() { - if (this.availableWorkflows == null) + // NOTE: we don't cache the list of startable workflows as they could get + // updated, in which case we need the latest instance id, they could + // theoretically also get removed. + + this.availableWorkflows = new ArrayList(4); + this.workflows = new HashMap(4); + + List workflowDefs = this.workflowService.getDefinitions(); + for (WorkflowDefinition workflowDef : workflowDefs) { - this.availableWorkflows = new ArrayList(4); - this.workflows = new HashMap(4); - - List workflowDefs = this.workflowService.getDefinitions(); - for (WorkflowDefinition workflowDef : workflowDefs) + String label = workflowDef.title; + if (workflowDef.description != null && workflowDef.description.length() > 0) { - String label = workflowDef.title; - if (workflowDef.description != null && workflowDef.description.length() > 0) - { - label = label + " (" + workflowDef.description + ")"; - } - this.availableWorkflows.add(new SelectItem(workflowDef.id, label)); - this.workflows.put(workflowDef.id, workflowDef); - } - - // set the initial selected workflow to the first in the list, unless there are no - // workflows, in which disable the next button - if (this.availableWorkflows.size() > 0) - { - this.selectedWorkflow = (String)this.availableWorkflows.get(0).getValue(); - } - else - { - this.nextButtonDisabled = true; + label = label + " (" + workflowDef.description + ")"; } + this.availableWorkflows.add(new SelectItem(workflowDef.id, label)); + this.workflows.put(workflowDef.id, workflowDef); + } + + // set the initial selected workflow to the first in the list, unless there are no + // workflows, in which disable the next button + if (this.availableWorkflows.size() > 0) + { + this.selectedWorkflow = (String)this.availableWorkflows.get(0).getValue(); + } + else + { + this.nextButtonDisabled = true; } return availableWorkflows; @@ -559,4 +571,19 @@ public class StartWorkflowWizard extends BaseWizardBean { this.workflowService = workflowService; } + + // ------------------------------------------------------------------------------ + // Helper methods + + /** + * Resets the rich list + */ + protected void resetRichList() + { + if (this.packageItemsRichList != null) + { + this.packageItemsRichList.setValue(null); + this.packageItemsRichList = null; + } + } } diff --git a/source/java/org/alfresco/web/bean/workflow/ViewCompletedTaskDialog.java b/source/java/org/alfresco/web/bean/workflow/ViewCompletedTaskDialog.java index b037ad74ce..e8b2a45cf5 100644 --- a/source/java/org/alfresco/web/bean/workflow/ViewCompletedTaskDialog.java +++ b/source/java/org/alfresco/web/bean/workflow/ViewCompletedTaskDialog.java @@ -39,7 +39,7 @@ public class ViewCompletedTaskDialog extends ManageTaskDialog } @Override - public String getTitle() + public String getContainerTitle() { String titleStart = Application.getMessage(FacesContext.getCurrentInstance(), "view_completed_task_title"); diff --git a/source/java/org/alfresco/web/bean/workflow/WorkflowBean.java b/source/java/org/alfresco/web/bean/workflow/WorkflowBean.java index 9800da87b5..f90d4b369b 100644 --- a/source/java/org/alfresco/web/bean/workflow/WorkflowBean.java +++ b/source/java/org/alfresco/web/bean/workflow/WorkflowBean.java @@ -196,7 +196,10 @@ public class WorkflowBean } // add the targets for this particular association - params.put(assocQName, (Serializable)targets); + if (targets.size() > 0) + { + params.put(assocQName, (Serializable)targets); + } } return params; @@ -223,19 +226,8 @@ public class WorkflowBean node.getProperties().put("id", task.id); // add the name of the source space (if there is one) - // TODO: remove this workaroud where JBPM may return a String and not the NodeRef - Serializable obj = task.properties.get(WorkflowModel.PROP_CONTEXT); - NodeRef context = null; - if (obj instanceof NodeRef) - { - context = (NodeRef)obj; - } - else if (obj instanceof String) - { - context = new NodeRef((String)obj); - } - - if (context != null) + NodeRef context = (NodeRef)task.properties.get(WorkflowModel.PROP_CONTEXT); + if (context != null && this.nodeService.exists(context)) { String name = Repository.getNameForNode(this.nodeService, context); node.getProperties().put("sourceSpaceName", name); diff --git a/source/java/org/alfresco/web/data/Sort.java b/source/java/org/alfresco/web/data/Sort.java index fce5bcaca2..c561e87d86 100644 --- a/source/java/org/alfresco/web/data/Sort.java +++ b/source/java/org/alfresco/web/data/Sort.java @@ -17,6 +17,7 @@ package org.alfresco.web.data; import java.lang.reflect.Method; +import java.sql.Timestamp; import java.text.CollationKey; import java.text.Collator; import java.util.ArrayList; @@ -175,6 +176,10 @@ public abstract class Sort { this.comparator = new FloatComparator(); } + else if (returnType.equals(Timestamp.class)) + { + this.comparator = new TimestampComparator(); + } else { s_logger.warn("Unsupported sort data type: " + returnType + " defaulting to .toString()"); @@ -408,6 +413,20 @@ public abstract class Sort } } + private static class TimestampComparator implements Comparator + { + /** + * @see org.alfresco.web.data.IDataComparator#compare(java.lang.Object, java.lang.Object) + */ + public int compare(final Object obj1, final Object obj2) + { + if (obj1 == null && obj2 == null) return 0; + if (obj1 == null) return -1; + if (obj2 == null) return 1; + return ((Timestamp)obj1).compareTo((Timestamp)obj2); + } + } + // ------------------------------------------------------------------------------ // Private Data diff --git a/source/java/org/alfresco/web/ui/common/Utils.java b/source/java/org/alfresco/web/ui/common/Utils.java index 182fd34003..05cbfe303b 100644 --- a/source/java/org/alfresco/web/ui/common/Utils.java +++ b/source/java/org/alfresco/web/ui/common/Utils.java @@ -55,6 +55,7 @@ import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.model.FileNotFoundException; import org.alfresco.service.cmr.repository.InvalidNodeRefException; +import org.alfresco.service.cmr.repository.NoTransformerException; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.repository.Path; @@ -968,7 +969,8 @@ public final class Utils if (err != null) { if ((err instanceof InvalidNodeRefException == false && - err instanceof AccessDeniedException == false) || logger.isDebugEnabled()) + err instanceof AccessDeniedException == false && + err instanceof NoTransformerException == false) || logger.isDebugEnabled()) { logger.error(msg, err); } diff --git a/source/java/org/alfresco/web/ui/common/renderer/ActionLinkRenderer.java b/source/java/org/alfresco/web/ui/common/renderer/ActionLinkRenderer.java index 0d0acb2fc2..78b0f0b863 100644 --- a/source/java/org/alfresco/web/ui/common/renderer/ActionLinkRenderer.java +++ b/source/java/org/alfresco/web/ui/common/renderer/ActionLinkRenderer.java @@ -111,165 +111,175 @@ public class ActionLinkRenderer extends BaseRenderer */ private String renderActionLink(FacesContext context, UIActionLink link) { - Map attrs = link.getAttributes(); - StringBuilder linkBuf = new StringBuilder(256); + // if there is no value for the link there will be no visible output + // on the page so don't bother rendering anything + String linkHtml = ""; + Object linkValue = link.getValue(); - if (link.getHref() == null) + if (linkValue != null) { - linkBuf.append("'); - - StringBuilder buf = new StringBuilder(350); - if (link.getImage() != null) - { - int padding = link.getPadding(); - if (padding != 0) - { - // TODO: make this width value a property! - buf.append("
"); - } - - if (link.getShowLink() == false) - { - buf.append(linkBuf.toString()); - } - - // TODO: allow configuring of alignment attribute - buf.append(Utils.buildImageTag(context, link.getImage(), (String)link.getValue(), "absmiddle")); - - if (link.getShowLink() == false) - { - buf.append(""); - } - else - { - if (padding != 0) - { - buf.append(""); + linkBuf.append(link.getOnclick()); } else { - // TODO: add horizontal spacing as component property - buf.append(""); + // generate JavaScript to set a hidden form field and submit + // a form which request attributes that we can decode + linkBuf.append(Utils.generateFormSubmit(context, link, Utils.getActionHiddenFieldName(context, link), link.getClientId(context), getParameterComponents(link))); } - buf.append(linkBuf.toString()); - buf.append(Utils.encode(link.getValue().toString())); - buf.append(""); + linkBuf.append('"'); + } + else + { + String href = link.getHref(); - if (padding == 0) + // prefix the web context path if required + linkBuf.append(""); + linkBuf.append(context.getExternalContext().getRequestContextPath()); + } + linkBuf.append(href); + + // append arguments if specified + Map actionParams = getParameterComponents(link); + if (actionParams != null) + { + boolean first = (href.indexOf('?') == -1); + for (String name : actionParams.keySet()) + { + String paramValue = actionParams.get(name); + if (first) + { + linkBuf.append('?'); + first = false; + } + else + { + linkBuf.append('&'); + } + try + { + linkBuf.append(name).append("=").append(URLEncoder.encode(paramValue, "UTF-8")); + } + catch (UnsupportedEncodingException err) + { + // if this happens we have bigger problems than a missing URL parameter...! + } + } + } + + linkBuf.append('"'); + + // output href 'target' attribute if supplied + if (link.getTarget() != null) + { + linkBuf.append(" target=\"") + .append(link.getTarget()) + .append("\""); } } - if (padding != 0) + if (attrs.get("style") != null) { - buf.append("
"); + linkBuf.append(" style=\"") + .append(attrs.get("style")) + .append('"'); } - } - else - { - buf.append(linkBuf.toString()); - buf.append(Utils.encode(link.getValue().toString())); - buf.append("
"); + if (attrs.get("styleClass") != null) + { + linkBuf.append(" class=") + .append(attrs.get("styleClass")); + } + if (link.getTooltip() != null) + { + linkBuf.append(" title=\"") + .append(Utils.encode(link.getTooltip())) + .append('"'); + } + linkBuf.append('>'); + + StringBuilder buf = new StringBuilder(350); + if (link.getImage() != null) + { + int padding = link.getPadding(); + if (padding != 0) + { + // TODO: make this width value a property! + buf.append("
"); + } + + if (link.getShowLink() == false) + { + buf.append(linkBuf.toString()); + } + + // TODO: allow configuring of alignment attribute + buf.append(Utils.buildImageTag(context, link.getImage(), (String)link.getValue(), "absmiddle")); + + if (link.getShowLink() == false) + { + buf.append(""); + } + else + { + if (padding != 0) + { + buf.append(""); + } + else + { + // TODO: add horizontal spacing as component property + buf.append(""); + } + + buf.append(linkBuf.toString()); + buf.append(Utils.encode(link.getValue().toString())); + buf.append(""); + + if (padding == 0) + { + buf.append(""); + } + } + + if (padding != 0) + { + buf.append("
"); + } + } + else + { + buf.append(linkBuf.toString()); + buf.append(Utils.encode(link.getValue().toString())); + buf.append(""); + } + + linkHtml = buf.toString(); } - return buf.toString(); + return linkHtml; } /** @@ -282,71 +292,81 @@ public class ActionLinkRenderer extends BaseRenderer */ private String renderMenuAction(FacesContext context, UIActionLink link, int padding) { - StringBuilder buf = new StringBuilder(256); + // if there is no value for the link there will be no visible output + // on the page so don't bother rendering anything + String linkHtml = ""; + Object linkValue = link.getValue(); - buf.append(""); - - // render image cell first for a menu - if (link.getImage() != null) + if (linkValue != null) { - buf.append(Utils.buildImageTag(context, link.getImage(), (String)link.getValue())); - } - - buf.append(""); - - // render text link cell for the menu - if (link.getHref() == null) - { - buf.append(""); + + // render image cell first for a menu + if (link.getImage() != null) { - buf.append(" target=\"") - .append(link.getTarget()) - .append("\""); + buf.append(Utils.buildImageTag(context, link.getImage(), (String)link.getValue())); } + + buf.append(""); + + // render text link cell for the menu + if (link.getHref() == null) + { + buf.append("'); + buf.append(Utils.encode(link.getValue().toString())); + buf.append(""); + + buf.append(""); + + linkHtml = buf.toString(); } - Map attrs = link.getAttributes(); - if (attrs.get("style") != null) - { - buf.append(" style=\"") - .append(attrs.get("style")) - .append('"'); - } - if (attrs.get("styleClass") != null) - { - buf.append(" class=") - .append(attrs.get("styleClass")); - } - buf.append('>'); - buf.append(Utils.encode(link.getValue().toString())); - buf.append(""); - - buf.append(""); - - return buf.toString(); + return linkHtml; } 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 6181461f69..26196fb2c0 100644 --- a/source/java/org/alfresco/web/ui/common/renderer/DatePickerRenderer.java +++ b/source/java/org/alfresco/web/ui/common/renderer/DatePickerRenderer.java @@ -37,8 +37,6 @@ import javax.faces.model.SelectItem; import org.alfresco.web.app.Application; import org.alfresco.web.ui.common.Utils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; /** * @author kevinr @@ -58,8 +56,6 @@ public class DatePickerRenderer extends BaseRenderer private static final int CMD_SET = 1; private static final int CMD_RESET = 2; private static final int CMD_TODAY = 3; - - private static final Log logger = LogFactory.getLog(DatePickerRenderer.class); /** * @see javax.faces.render.Renderer#decode(javax.faces.context.FacesContext, javax.faces.component.UIComponent) @@ -168,6 +164,7 @@ public class DatePickerRenderer extends BaseRenderer * input component must render the submitted value if it's set, and use the local * value only if there is no submitted value. */ + @SuppressWarnings("deprecation") public void encodeBegin(FacesContext context, UIComponent component) throws IOException { @@ -178,6 +175,7 @@ public class DatePickerRenderer extends BaseRenderer String clientId = component.getClientId(context); ResponseWriter out = context.getResponseWriter(); String cmdFieldName = clientId + FIELD_CMD; + Boolean initIfNull = (Boolean)component.getAttributes().get("initialiseIfNull"); // this is part of the spec: // first you attempt to build the date from the submitted value @@ -188,12 +186,19 @@ public class DatePickerRenderer extends BaseRenderer } else { - // second if no submitted value is found, default to the current value + // second - if no submitted value is found, default to the current value Object value = ((ValueHolder)component).getValue(); if (value instanceof Date) { date = (Date)value; } + + // third - if no date is present and the initialiseIfNull attribute + // is set to true set the date to today's date + if (date == null && initIfNull != null && initIfNull.booleanValue()) + { + date = new Date(); + } } // create a flag to show if the component is disabled @@ -253,18 +258,23 @@ public class DatePickerRenderer extends BaseRenderer out.write(" "); // render 2 links (if the component is not disabled) to allow the user to reset the - // date back to null or to select today's date + // date back to null (if initialiseIfNull is false) or to select today's date if (disabled.booleanValue() == false) { out.write(" "); + out.write("\"> "); + + if (initIfNull != null && initIfNull.booleanValue() == false) + { + out.write(""); + } } } else @@ -412,7 +422,7 @@ public class DatePickerRenderer extends BaseRenderer Locale locale = Application.getLanguage(FacesContext.getCurrentInstance()); if (locale == null) { - locale = locale.getDefault(); + locale = Locale.getDefault(); } DateFormatSymbols dfs = new DateFormatSymbols(locale); String[] names = dfs.getMonths(); diff --git a/source/java/org/alfresco/web/ui/common/tag/InputDatePickerTag.java b/source/java/org/alfresco/web/ui/common/tag/InputDatePickerTag.java index 15e6cb5ffa..ab7b1e41d2 100644 --- a/source/java/org/alfresco/web/ui/common/tag/InputDatePickerTag.java +++ b/source/java/org/alfresco/web/ui/common/tag/InputDatePickerTag.java @@ -60,6 +60,7 @@ public class InputDatePickerTag extends HtmlComponentTag this.value = null; this.showTime = null; this.disabled = null; + this.initIfNull = null; } /** @@ -75,6 +76,7 @@ public class InputDatePickerTag extends HtmlComponentTag setStringProperty(component, "value", this.value); setBooleanProperty(component, "showTime", this.showTime); setBooleanProperty(component, "disabled", this.disabled); + setBooleanProperty(component, "initialiseIfNull", this.initIfNull); } /** @@ -127,9 +129,22 @@ public class InputDatePickerTag extends HtmlComponentTag this.disabled = disabled; } + /** + * Sets whether today's date should be shown initially if the underlying + * model value is null. This will also hide the None button thus disallowing + * the user to set the date back to null. + * + * @param initialiseIfNull true to show today's date instead of 'None' + */ + public void setInitialiseIfNull(String initialiseIfNull) + { + this.initIfNull = initialiseIfNull; + } + private String startYear = null; private String yearCount = null; private String value = null; private String showTime = null; private String disabled = null; + private String initIfNull = null; } diff --git a/source/java/org/alfresco/web/ui/repo/component/UIDialogButtons.java b/source/java/org/alfresco/web/ui/repo/component/UIDialogButtons.java index e9f19c9927..826644455d 100644 --- a/source/java/org/alfresco/web/ui/repo/component/UIDialogButtons.java +++ b/source/java/org/alfresco/web/ui/repo/component/UIDialogButtons.java @@ -186,6 +186,9 @@ public class UIDialogButtons extends SelfRenderingComponent if (logger.isDebugEnabled()) logger.debug("Adding " + buttons.size() + " additional buttons: " + buttons); + // add a spacing row to separate the additional buttons from the OK button + addSpacingRow(context); + for (DialogButtonConfig buttonCfg : buttons) { UICommand button = (UICommand)context.getApplication(). diff --git a/source/java/org/alfresco/web/ui/repo/component/UISearchCustomProperties.java b/source/java/org/alfresco/web/ui/repo/component/UISearchCustomProperties.java index fc085b9029..5d461669d8 100644 --- a/source/java/org/alfresco/web/ui/repo/component/UISearchCustomProperties.java +++ b/source/java/org/alfresco/web/ui/repo/component/UISearchCustomProperties.java @@ -319,6 +319,7 @@ public class UISearchCustomProperties extends SelfRenderingComponent implements inputFromDate.setRendererType(RepoConstants.ALFRESCO_FACES_DATE_PICKER_RENDERER); inputFromDate.setValueBinding("startYear", startYearBind); inputFromDate.setValueBinding("yearCount", yearCountBind); + inputFromDate.getAttributes().put("initialiseIfNull", Boolean.TRUE); inputFromDate.getAttributes().put("showTime", showTime); ValueBinding vbFromDate = facesApp.createValueBinding( "#{" + beanBinding + "[\"" + PREFIX_DATE_FROM + propDef.getName().toString() + "\"]}"); @@ -338,6 +339,7 @@ public class UISearchCustomProperties extends SelfRenderingComponent implements inputToDate.setRendererType(RepoConstants.ALFRESCO_FACES_DATE_PICKER_RENDERER); inputToDate.setValueBinding("startYear", startYearBind); inputToDate.setValueBinding("yearCount", yearCountBind); + inputToDate.getAttributes().put("initialiseIfNull", Boolean.TRUE); inputToDate.getAttributes().put("showTime", showTime); ValueBinding vbToDate = facesApp.createValueBinding( "#{" + beanBinding + "[\"" + PREFIX_DATE_TO + propDef.getName().toString() + "\"]}"); diff --git a/source/java/org/alfresco/web/ui/repo/component/UIWorkflowSummary.java b/source/java/org/alfresco/web/ui/repo/component/UIWorkflowSummary.java index 40676f0844..955a8fac13 100644 --- a/source/java/org/alfresco/web/ui/repo/component/UIWorkflowSummary.java +++ b/source/java/org/alfresco/web/ui/repo/component/UIWorkflowSummary.java @@ -103,19 +103,25 @@ public class UIWorkflowSummary extends SelfRenderingComponent out.write(userName); } out.write(""); - out.write(bundle.getString("start_date")); + out.write(bundle.getString("started_on")); out.write(":"); if (wi.startDate != null) { out.write(format.format(wi.startDate)); } out.write(""); - out.write(bundle.getString("due_date")); + out.write(bundle.getString("completed_on")); out.write(":"); if (wi.endDate != null) { out.write(format.format(wi.endDate)); } + else + { + out.write("<"); + out.write(bundle.getString("in_progress")); + out.write(">"); + } out.write(""); } } diff --git a/source/java/org/alfresco/web/ui/repo/component/property/UIPropertySheet.java b/source/java/org/alfresco/web/ui/repo/component/property/UIPropertySheet.java index d60b492400..97fb700929 100644 --- a/source/java/org/alfresco/web/ui/repo/component/property/UIPropertySheet.java +++ b/source/java/org/alfresco/web/ui/repo/component/property/UIPropertySheet.java @@ -75,6 +75,7 @@ public class UIPropertySheet extends UIPanel implements NamingContainer private Boolean validationEnabled; private String mode; private String configArea; + private String nextButtonId; private String finishButtonId; /** @@ -219,6 +220,7 @@ public class UIPropertySheet extends UIPanel implements NamingContainer this.validationEnabled = (Boolean)values[7]; this.validations = (List)values[8]; this.finishButtonId = (String)values[9]; + this.nextButtonId = (String)values[10]; } /** @@ -226,7 +228,7 @@ public class UIPropertySheet extends UIPanel implements NamingContainer */ public Object saveState(FacesContext context) { - Object values[] = new Object[10]; + Object values[] = new Object[11]; // standard component attributes are saved by the super class values[0] = super.saveState(context); values[1] = this.nodeRef; @@ -238,6 +240,7 @@ public class UIPropertySheet extends UIPanel implements NamingContainer values[7] = this.validationEnabled; values[8] = this.validations; values[9] = this.finishButtonId; + values[10] = this.nextButtonId; return (values); } @@ -391,6 +394,26 @@ public class UIPropertySheet extends UIPanel implements NamingContainer { this.finishButtonId = finishButtonId; } + + /** + * Returns the id of the next button + * + * @return The id of the next button on the page + */ + public String getNextButtonId() + { + return this.nextButtonId; + } + + /** + * Sets the id of the next button being used on the page + * + * @param nextButtonId The id of the next button + */ + public void setNextButtonId(String nextButtonId) + { + this.nextButtonId = nextButtonId; + } /** * @return Returns the mode @@ -487,8 +510,12 @@ public class UIPropertySheet extends UIPanel implements NamingContainer ResponseWriter out = context.getResponseWriter(); UIForm form = Utils.getParentForm(context, this); + // TODO: We need to encode all the JavaScript functions here + // with the client id of the property sheet so that we + // can potentially add more than one property sheet to + // page and have validation function correctly. + // output the validation.js script - // TODO: make sure its only included once per page!! out.write("\n