diff --git a/source/java/org/alfresco/web/bean/ajax/JSONWriter.java b/source/java/org/alfresco/web/bean/ajax/JSONWriter.java index c8e8f36f08..68333d5772 100644 --- a/source/java/org/alfresco/web/bean/ajax/JSONWriter.java +++ b/source/java/org/alfresco/web/bean/ajax/JSONWriter.java @@ -29,7 +29,8 @@ import java.io.Writer; import java.util.Stack; /** - * Very simple JSON writer. Wraps a Writer to output a JSON stream. + * Fast and simple JSON stream writer. Wraps a Writer to output a JSON object stream. + * No intermediate objects are created - writes are immediate to the underlying stream. * * @author Kevin Roast */ diff --git a/source/java/org/alfresco/web/bean/ajax/PickerBean.java b/source/java/org/alfresco/web/bean/ajax/PickerBean.java index 0430883dd6..8a905d6244 100644 --- a/source/java/org/alfresco/web/bean/ajax/PickerBean.java +++ b/source/java/org/alfresco/web/bean/ajax/PickerBean.java @@ -25,8 +25,11 @@ package org.alfresco.web.bean.ajax; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; import javax.faces.context.FacesContext; import javax.transaction.UserTransaction; @@ -38,6 +41,8 @@ import org.alfresco.service.cmr.dictionary.DictionaryService; import org.alfresco.service.cmr.model.FileFolderService; import org.alfresco.service.cmr.model.FileInfo; import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentReader; +import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.FileTypeImageSize; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; @@ -58,6 +63,7 @@ import org.apache.commons.logging.LogFactory; */ public class PickerBean { + private static final String MSG_CATEGORIES = "categories"; private static final String ID_URL = "url"; private static final String ID_ICON = "icon"; private static final String ID_CHILDREN = "children"; @@ -66,6 +72,8 @@ public class PickerBean private static final String ID_NAME = "name"; private static final String ID_ID = "id"; private static final String ID_PARENT = "parent"; + private static final String PARAM_PARENT = "parent"; + private static final String PARAM_MIMETYPES = "mimetypes"; private static final String FOLDER_IMAGE_PREFIX = "/images/icons/"; @@ -110,6 +118,13 @@ public class PickerBean } + /** + * Return the JSON objects representing a list of categories. + * + * IN: "parent" - null for root categories, else the parent noderef of the categories to retrieve. + * + * The pseudo root node 'Categories' is not selectable. + */ @InvokeCommand.ResponseMimetype(value=MimetypeMap.MIMETYPE_HTML) public void getCategoryNodes() throws Exception { @@ -124,7 +139,7 @@ public class PickerBean Collection childRefs; NodeRef parentRef = null; Map params = fc.getExternalContext().getRequestParameterMap(); - String strParentRef = (String)params.get(ID_PARENT); + String strParentRef = (String)params.get(PARAM_PARENT); if (strParentRef == null || strParentRef.length() == 0) { childRefs = this.categoryService.getRootCategories( @@ -147,7 +162,7 @@ public class PickerBean if (parentRef == null) { out.writeNullValue(ID_ID); - out.writeValue(ID_NAME, "Categories"); + out.writeValue(ID_NAME, Application.getMessage(fc, MSG_CATEGORIES)); out.writeValue(ID_ISROOT, true); out.writeValue(ID_SELECTABLE, false); } @@ -182,6 +197,13 @@ public class PickerBean } } + /** + * Return the JSON objects representing a list of cm:folder nodes. + * + * IN: "parent" - null for Company Home child folders, else the parent noderef of the folders to retrieve. + * + * The 16x16 pixel folder icon path is output as the 'icon' property for each child folder. + */ @InvokeCommand.ResponseMimetype(value=MimetypeMap.MIMETYPE_HTML) public void getFolderNodes() throws Exception { @@ -197,7 +219,7 @@ public class PickerBean NodeRef companyHomeRef = new NodeRef(Repository.getStoreRef(), Application.getCompanyRootId(fc)); NodeRef parentRef = null; Map params = fc.getExternalContext().getRequestParameterMap(); - String strParentRef = (String)params.get(ID_PARENT); + String strParentRef = (String)params.get(PARAM_PARENT); if (strParentRef == null || strParentRef.length() == 0) { parentRef = companyHomeRef; @@ -249,6 +271,18 @@ public class PickerBean } } + /** + * Return the JSON objects representing a list of cm:folder and cm:content nodes. + * + * IN: "parent" - null for Company Home child nodes, else the parent noderef of the folders to retrieve. + * IN: "mimetypes" (optional) - if set, a comma separated list of mimetypes to restrict the file list. + * + * It is assumed that only files should be selectable, all cm:folder nodes will be marked with the + * 'selectable:false' property. Therefore the parent (which is a folder) is not selectable. + * + * The 16x16 pixel node icon path is output as the 'icon' property for each child, in addition each + * cm:content node has an property of 'url' for content download. + */ @InvokeCommand.ResponseMimetype(value=MimetypeMap.MIMETYPE_HTML) public void getFileFolderNodes() throws Exception { @@ -261,12 +295,13 @@ public class PickerBean tx.begin(); DictionaryService dd = Repository.getServiceRegistry(fc).getDictionaryService(); + ContentService cs = Repository.getServiceRegistry(fc).getContentService(); List childRefs; NodeRef companyHomeRef = new NodeRef(Repository.getStoreRef(), Application.getCompanyRootId(fc)); NodeRef parentRef = null; Map params = fc.getExternalContext().getRequestParameterMap(); - String strParentRef = (String)params.get(ID_PARENT); + String strParentRef = (String)params.get(PARAM_PARENT); if (strParentRef == null || strParentRef.length() == 0) { parentRef = companyHomeRef; @@ -276,6 +311,20 @@ public class PickerBean { parentRef = new NodeRef(strParentRef); } + + // look for mimetype restriction parameter + Set mimetypes = null; + String mimetypeParam = (String)params.get(PARAM_MIMETYPES); + if (mimetypeParam != null && mimetypeParam.length() != 0) + { + // convert to a set of mimetypes to test each file against + mimetypes = new HashSet(); + for (StringTokenizer t = new StringTokenizer(mimetypeParam, ","); t.hasMoreTokens(); /**/) + { + mimetypes.add(t.nextToken()); + } + } + List items = this.fileFolderService.list(parentRef); JSONWriter out = new JSONWriter(fc.getResponseWriter()); @@ -294,28 +343,46 @@ public class PickerBean out.startValue(ID_CHILDREN); out.startArray(); - // filter out those children that are not spaces for (FileInfo item : items) { - out.startObject(); - out.writeValue(ID_ID, item.getNodeRef().toString()); - String name = (String)item.getProperties().get(ContentModel.PROP_NAME); - out.writeValue(ID_NAME, name); if (dd.isSubClass(this.internalNodeService.getType(item.getNodeRef()), ContentModel.TYPE_FOLDER)) { // found a folder + out.startObject(); + out.writeValue(ID_ID, item.getNodeRef().toString()); + String name = (String)item.getProperties().get(ContentModel.PROP_NAME); + out.writeValue(ID_NAME, name); String icon = (String)item.getProperties().get(ApplicationModel.PROP_ICON); out.writeValue(ID_ICON, FOLDER_IMAGE_PREFIX + (icon != null ? icon + "-16.gif" : BrowseBean.SPACE_SMALL_DEFAULT + ".gif")); out.writeValue(ID_SELECTABLE, false); + out.endObject(); } else { // must be a file - String icon = Utils.getFileTypeImage(fc, name, FileTypeImageSize.Small); - out.writeValue(ID_ICON, icon); - out.writeValue(ID_URL, DownloadContentServlet.generateBrowserURL(item.getNodeRef(), name)); + boolean validFile = true; + if (mimetypes != null) + { + validFile = false; + ContentReader reader = cs.getReader(item.getNodeRef(), ContentModel.PROP_CONTENT); + if (reader != null) + { + String mimetype = reader.getMimetype(); + validFile = (mimetype != null && mimetypes.contains(mimetype)); + } + } + if (validFile) + { + out.startObject(); + out.writeValue(ID_ID, item.getNodeRef().toString()); + String name = (String)item.getProperties().get(ContentModel.PROP_NAME); + out.writeValue(ID_NAME, name); + String icon = Utils.getFileTypeImage(fc, name, FileTypeImageSize.Small); + out.writeValue(ID_ICON, icon); + out.writeValue(ID_URL, DownloadContentServlet.generateBrowserURL(item.getNodeRef(), name)); + out.endObject(); + } } - out.endObject(); } out.endArray(); diff --git a/source/java/org/alfresco/web/ui/repo/component/BaseAjaxItemPicker.java b/source/java/org/alfresco/web/ui/repo/component/BaseAjaxItemPicker.java index b16f67e340..3717e72ed1 100644 --- a/source/java/org/alfresco/web/ui/repo/component/BaseAjaxItemPicker.java +++ b/source/java/org/alfresco/web/ui/repo/component/BaseAjaxItemPicker.java @@ -49,6 +49,14 @@ import org.alfresco.web.ui.common.Utils; import org.springframework.web.jsf.FacesContextUtils; /** + * Base class for the JSP components representing Ajax object pickers. + * + * Handles the JSF lifecycle for the Ajax component. The Ajax calls themselves + * are processed via the class org.alfresco.web.bean.ajax.PickerBean. + * + * The derived components are only responsible for specifing the ajax service call + * to make, plus any defaults for icons etc. + * * @author Kevin Roast */ public abstract class BaseAjaxItemPicker extends UIInput @@ -121,7 +129,7 @@ public abstract class BaseAjaxItemPicker extends UIInput this.initialSelectionId, this.disabled, this.height}; - return (values); + return values; } /** @@ -268,6 +276,12 @@ public abstract class BaseAjaxItemPicker extends UIInput { out.write(" window." + objId + ".setSelectedItems('" + selectedItems + "');"); } + // write any addition custom request attributes required by specific picker implementations + String requestProps = getRequestAttributes(); + if (requestProps != null) + { + out.write(" window." + objId + ".setRequestAttributes('" + requestProps + "');"); + } out.write("}"); out.write("window.addEvent('domready', init" + divId + ");"); out.write(""); @@ -371,6 +385,14 @@ public abstract class BaseAjaxItemPicker extends UIInput */ protected abstract String getDefaultIcon(); + /** + * @return custom request properties optional for some specific picker implementations + */ + protected String getRequestAttributes() + { + return null; + } + // ------------------------------------------------------------------------------ // Strongly typed component property accessors diff --git a/source/java/org/alfresco/web/ui/repo/component/UIAjaxCategoryPicker.java b/source/java/org/alfresco/web/ui/repo/component/UIAjaxCategoryPicker.java index 7488ae762b..e248289948 100644 --- a/source/java/org/alfresco/web/ui/repo/component/UIAjaxCategoryPicker.java +++ b/source/java/org/alfresco/web/ui/repo/component/UIAjaxCategoryPicker.java @@ -25,6 +25,8 @@ package org.alfresco.web.ui.repo.component; /** + * JSF Ajax object picker for navigating through and selecting categories. + * * @author Kevin Roast */ public class UIAjaxCategoryPicker extends BaseAjaxItemPicker diff --git a/source/java/org/alfresco/web/ui/repo/component/UIAjaxFilePicker.java b/source/java/org/alfresco/web/ui/repo/component/UIAjaxFilePicker.java index dad7a5992c..e7fe1ef46c 100644 --- a/source/java/org/alfresco/web/ui/repo/component/UIAjaxFilePicker.java +++ b/source/java/org/alfresco/web/ui/repo/component/UIAjaxFilePicker.java @@ -24,16 +24,51 @@ */ package org.alfresco.web.ui.repo.component; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import javax.faces.context.FacesContext; +import javax.faces.el.ValueBinding; + +import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.web.ui.common.Utils; + /** + * JSF Ajax object picker for navigating through folders and selecting a file. + * * @author Kevin Roast */ public class UIAjaxFilePicker extends BaseAjaxItemPicker { + /** list of mimetypes to restrict the available file list */ + private String mimetypes = null; + @Override public String getFamily() { return "org.alfresco.faces.AjaxFilePicker"; } + + /** + * @see javax.faces.component.StateHolder#restoreState(javax.faces.context.FacesContext, java.lang.Object) + */ + public void restoreState(FacesContext context, Object state) + { + Object values[] = (Object[])state; + super.restoreState(context, values[0]); + this.mimetypes = (String)values[1]; + } + + /** + * @see javax.faces.component.StateHolder#saveState(javax.faces.context.FacesContext) + */ + public Object saveState(FacesContext context) + { + Object values[] = new Object[] { + super.saveState(context), + this.mimetypes}; + return values; + } @Override protected String getServiceCall() @@ -47,4 +82,51 @@ public class UIAjaxFilePicker extends BaseAjaxItemPicker // none required - we always return an icon name in the service call return null; } + + @Override + protected String getRequestAttributes() + { + String mimetypes = getMimetypes(); + if (mimetypes != null) + { + try + { + return "mimetypes=" + URLEncoder.encode(mimetypes, "UTF-8"); + } + catch (UnsupportedEncodingException e) + { + throw new AlfrescoRuntimeException("Unsupported encoding.", e); + } + } + else + { + return null; + } + } + + + // ------------------------------------------------------------------------------ + // Strongly typed component property accessors + + /** + * @return Returns the mimetypes to restrict the file list. + */ + public String getMimetypes() + { + ValueBinding vb = getValueBinding("mimetypes"); + if (vb != null) + { + this.mimetypes = (String)vb.getValue(getFacesContext()); + } + + return this.mimetypes; + } + + /** + * @param mimetypes The mimetypes restriction list to set. + */ + public void setMimetypes(String mimetypes) + { + this.mimetypes = mimetypes; + } } diff --git a/source/java/org/alfresco/web/ui/repo/component/UIAjaxFolderPicker.java b/source/java/org/alfresco/web/ui/repo/component/UIAjaxFolderPicker.java index debcf4f916..621cb82eff 100644 --- a/source/java/org/alfresco/web/ui/repo/component/UIAjaxFolderPicker.java +++ b/source/java/org/alfresco/web/ui/repo/component/UIAjaxFolderPicker.java @@ -25,6 +25,8 @@ package org.alfresco.web.ui.repo.component; /** + * JSF Ajax object picker for navigating through and selecting folders. + * * @author Kevin Roast */ public class UIAjaxFolderPicker extends BaseAjaxItemPicker diff --git a/source/java/org/alfresco/web/ui/repo/tag/AjaxFileSelectorTag.java b/source/java/org/alfresco/web/ui/repo/tag/AjaxFileSelectorTag.java index 596d0e1602..6fb0b798f1 100644 --- a/source/java/org/alfresco/web/ui/repo/tag/AjaxFileSelectorTag.java +++ b/source/java/org/alfresco/web/ui/repo/tag/AjaxFileSelectorTag.java @@ -27,6 +27,8 @@ */ package org.alfresco.web.ui.repo.tag; +import javax.faces.component.UIComponent; + /** * @author Kevin Roast @@ -40,4 +42,36 @@ public class AjaxFileSelectorTag extends AjaxItemSelectorTag { return "org.alfresco.faces.AjaxFilePicker"; } + + /** + * @see javax.faces.webapp.UIComponentTag#setProperties(javax.faces.component.UIComponent) + */ + protected void setProperties(UIComponent component) + { + super.setProperties(component); + setStringProperty(component, "mimetypes", this.mimetypes); + } + + /** + * @see org.alfresco.web.ui.common.tag.HtmlComponentTag#release() + */ + public void release() + { + super.release(); + this.mimetypes = null; + } + + /** + * Set the mimetypes + * + * @param mimetypes the mimetypes + */ + public void setMimetypes(String mimetypes) + { + this.mimetypes = mimetypes; + } + + + /** the mimetypes */ + private String mimetypes; } diff --git a/source/web/WEB-INF/repo.tld b/source/web/WEB-INF/repo.tld index 763adeb748..01a7344668 100644 --- a/source/web/WEB-INF/repo.tld +++ b/source/web/WEB-INF/repo.tld @@ -2418,6 +2418,12 @@ false true + + + mimetypes + false + true + diff --git a/source/web/jsp/users/edit-user-details.jsp b/source/web/jsp/users/edit-user-details.jsp index 6f62aeaa4c..67decdf5cb 100644 --- a/source/web/jsp/users/edit-user-details.jsp +++ b/source/web/jsp/users/edit-user-details.jsp @@ -107,6 +107,7 @@ function updateButtonState() value="#{DialogManager.bean.personPhotoRef}" label="#{msg.select_avatar_prompt}" initialSelection="#{DialogManager.bean.personProperties.homeFolder}" + mimetypes="image/gif,image/jpeg,image/png" styleClass="selector" /> diff --git a/source/web/scripts/ajax/picker.js b/source/web/scripts/ajax/picker.js index 70714e7d6c..261cf9c5f5 100644 --- a/source/web/scripts/ajax/picker.js +++ b/source/web/scripts/ajax/picker.js @@ -66,6 +66,9 @@ var AlfPicker = new Class( /* initial display style of the outer div */ initialDisplayStyle: null, + /* addition service request attributes if any */ + requestAttributes: null, + initialize: function(id, varName, service, formClientId, singleSelect) { this.id = id; @@ -95,6 +98,11 @@ var AlfPicker = new Class( this.preselected = Json.evaluate(jsonString); }, + setRequestAttributes: function(attrs) + { + this.requestAttributes = attrs; + }, + showSelector: function() { // init selector state @@ -135,7 +143,11 @@ var AlfPicker = new Class( this.hidePanels(); // pop the parent off - peek for the grandparent var parent = this.stack.pop(); - var grandParent = this.stack[this.stack.length-1]; + var grandParent = null; + if (this.stack.length != 0) + { + grandParent = this.stack[this.stack.length-1]; + } this.getChildData(grandParent != null ? grandParent.id : null, this.populateChildren, parent.scrollpos); }, @@ -344,7 +356,7 @@ var AlfPicker = new Class( else { upLink.setStyle("display", "block"); - upLink.setProperty("href", "javascript:" + picker.varName + ".upClicked('" + picker.parent.id + "');"); + upLink.setProperty("href", "javascript:" + picker.varName + ".upClicked();"); } // show what the parent next to the breadcrumb drop-down @@ -472,7 +484,9 @@ var AlfPicker = new Class( var picker = this; // execute ajax service call to retrieve list of child nodes as JSON response - new Ajax(getContextPath() + "/ajax/invoke/" + this.service + "?parent=" + (parent!=null ? parent : ""), + new Ajax(getContextPath() + "/ajax/invoke/" + this.service + + "?parent=" + (parent!=null ? parent : "") + + (this.requestAttributes!=null ? ("&" + this.requestAttributes) : ""), { method: 'get', async: false, @@ -489,6 +503,15 @@ var AlfPicker = new Class( $(picker.id + '-results-list').setStyle('visibility', 'visible'); $(picker.id + '-ajax-wait').setStyle('display', 'none'); } + else + { + // display results list again and hide ajax wait panel + $(picker.id + '-results-list').setStyle('visibility', 'visible'); + $(picker.id + '-ajax-wait').setStyle('display', 'none'); + + // display the error + alert(r); + } }, onFailure: function (r) { @@ -498,6 +521,13 @@ var AlfPicker = new Class( sortByName: function(a, b) { - return ((a.name < b.name) ? -1 : ((a.name > b.name) ? 1 : 0)); + if (a.selectable == b.selectable) + { + return ((a.name < b.name) ? -1 : ((a.name > b.name) ? 1 : 0)); + } + else + { + return (a.selectable == false) ? -1 : 1; + } } }); \ No newline at end of file