/*
* Copyright (C) 2005-2007 Alfresco Software Limited.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* As a special exception to the terms and conditions of version 2.0 of
* the GPL, you may redistribute this Program in connection with Free/Libre
* and Open Source Software ("FLOSS") applications as described in Alfresco's
* FLOSS exception. You should have recieved a copy of the text describing
* the FLOSS exception, and it is also available here:
* http://www.alfresco.com/legal/licensing"
*/
package org.alfresco.web.bean;
import java.io.Serializable;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.faces.component.UISelectBoolean;
import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import javax.faces.model.DataModel;
import javax.faces.model.ListDataModel;
import javax.faces.model.SelectItem;
import org.alfresco.model.ContentModel;
import org.alfresco.repo.cache.ExpiringValueCache;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.ServiceRegistry;
import org.alfresco.service.cmr.dictionary.AspectDefinition;
import org.alfresco.service.cmr.dictionary.DataTypeDefinition;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.service.cmr.dictionary.TypeDefinition;
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.ContentWriter;
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.search.SearchService;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.service.namespace.RegexQNamePattern;
import org.alfresco.util.CachingDateFormat;
import org.alfresco.util.ISO9075;
import org.alfresco.web.app.Application;
import org.alfresco.web.bean.SearchContext.RangeProperties;
import org.alfresco.web.bean.repository.MapNode;
import org.alfresco.web.bean.repository.Node;
import org.alfresco.web.bean.repository.Repository;
import org.alfresco.web.bean.repository.User;
import org.alfresco.web.config.AdvancedSearchConfigElement;
import org.alfresco.web.config.AdvancedSearchConfigElement.CustomProperty;
import org.alfresco.web.data.IDataContainer;
import org.alfresco.web.data.QuickSort;
import org.alfresco.web.ui.common.Utils;
import org.alfresco.web.ui.common.component.UIModeList;
import org.alfresco.web.ui.common.component.UIPanel.ExpandedEvent;
import org.alfresco.web.ui.repo.component.UICategorySelector;
import org.alfresco.web.ui.repo.component.UISearchCustomProperties;
/**
* Provides the form state and action event handling for the Advanced Search UI.
*
* Integrates with the web-client ConfigService to retrieve configuration of custom
* meta-data searching fields. Custom fields can be configured to appear in the UI
* and they are they automatically added to the search query by this bean.
*
* @author Kevin Roast
*/
public class AdvancedSearchBean
{
/**
* Default constructor
*/
public AdvancedSearchBean()
{
// initial state of progressive panels that don't use the default
panels.put(PANEL_CATEGORIES, false);
panels.put(PANEL_ATTRS, false);
panels.put(PANEL_CUSTOM, false);
}
// ------------------------------------------------------------------------------
// Bean property getters and setters
/**
* @param navigator The NavigationBean to set.
*/
public void setNavigator(NavigationBean navigator)
{
this.navigator = navigator;
}
/**
* @param nodeService The NodeService to set.
*/
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
/**
* @param namespaceService The NamespaceService to set.
*/
public void setNamespaceService(NamespaceService namespaceService)
{
this.namespaceService = namespaceService;
}
/**
* @param searchService the search service
*/
public void setSearchService(SearchService searchService)
{
this.searchService = searchService;
}
/**
* @param permissionService The PermissionService to set.
*/
public void setPermissionService(PermissionService permissionService)
{
this.permissionService = permissionService;
}
/**
* @return Returns the progressive panels expanded state map.
*/
public Map getPanels()
{
return this.panels;
}
/**
* @param panels The progressive panels expanded state map.
*/
public void setPanels(Map panels)
{
this.panels = panels;
}
/**
* @return Returns the saved search Description.
*/
public String getSearchDescription()
{
return this.searchDescription;
}
/**
* @param searchDescription The saved search Description to set.
*/
public void setSearchDescription(String searchDescription)
{
this.searchDescription = searchDescription;
}
/**
* @return Returns the saved search Name.
*/
public String getSearchName()
{
return this.searchName;
}
/**
* @param searchName The saved search Name to set.
*/
public void setSearchName(String searchName)
{
this.searchName = searchName;
}
/**
* @return ID of the last saved search selected by the user
*/
public String getSavedSearch()
{
return this.savedSearch;
}
/**
* @param savedSearch ID of the saved search selected by the user
*/
public void setSavedSearch(String savedSearch)
{
this.savedSearch = savedSearch;
}
/**
* @return name of the saved search to edit
*/
public String getEditSearchName()
{
return this.editSearchName;
}
/**
* @param editSearchName name of the saved search to edit
*/
public void setEditSearchName(String editSearchName)
{
this.editSearchName = editSearchName;
}
/**
* @return Returns the searchSaveGlobal.
*/
public boolean isSearchSaveGlobal()
{
return this.searchSaveGlobal;
}
/**
* @param searchSaveGlobal The searchSaveGlobal to set.
*/
public void setSearchSaveGlobal(boolean searchSaveGlobal)
{
this.searchSaveGlobal = searchSaveGlobal;
}
/**
* @return Returns the folder to search, null for all.
*/
public String getLookin()
{
return this.lookin;
}
/**
* @param lookIn The folder to search in or null for all.
*/
public void setLookin(String lookIn)
{
this.lookin = lookIn;
}
/**
* @return Returns the location.
*/
public NodeRef getLocation()
{
return this.location;
}
/**
* @param location The location to set.
*/
public void setLocation(NodeRef location)
{
this.location = location;
}
/**
* @return Returns the search mode.
*/
public String getMode()
{
return this.mode;
}
/**
* @param mode The search mode to set.
*/
public void setMode(String mode)
{
this.mode = mode;
}
/**
* @return Returns the savedSearchMode.
*/
public String getSavedSearchMode()
{
return this.savedSearchMode;
}
/**
* @param savedSearchMode The savedSearchMode to set.
*/
public void setSavedSearchMode(String savedSearchMode)
{
this.savedSearchMode = savedSearchMode;
}
/**
* @return Returns the allow Edit mode.
*/
public boolean isAllowEdit()
{
boolean allow = (this.savedSearch != null && NO_SELECTION.equals(this.savedSearch) == false);
if (allow)
{
NodeRef savedSearchRef = new NodeRef(Repository.getStoreRef(), this.savedSearch);
allow = (permissionService.hasPermission(savedSearchRef, PermissionService.WRITE) == AccessStatus.ALLOWED);
}
return allow;
}
/**
* @param allowEdit The allow Edit mode to set.
*/
public void setAllowEdit(boolean allowEdit)
{
// dummy method for Bean interface compliance
}
/**
* @return Returns the text to search for.
*/
public String getText()
{
return this.text;
}
/**
* @param text The text to set.
*/
public void setText(String text)
{
this.text = text;
}
/**
* Returns the properties for current categories JSF DataModel
*
* @return JSF DataModel representing the current categories to search against
*/
public DataModel getCategoriesDataModel()
{
if (this.categoriesDataModel == null)
{
this.categoriesDataModel = new ListDataModel();
}
this.categoriesDataModel.setWrappedData(this.categories);
return this.categoriesDataModel;
}
/**
* @return Returns true to search location children, false for just the specified location.
*/
public boolean getLocationChildren()
{
return this.locationChildren;
}
/**
* @param locationChildren True to search location children, false for just the specified location.
*/
public void setLocationChildren(boolean locationChildren)
{
this.locationChildren = locationChildren;
}
/**
* @return Returns the createdDateFrom.
*/
public Date getCreatedDateFrom()
{
return this.createdDateFrom;
}
/**
* @param createdDate The createdDateFrom to set.
*/
public void setCreatedDateFrom(Date createdDate)
{
this.createdDateFrom = createdDate;
}
/**
* @return Returns the description.
*/
public String getDescription()
{
return this.description;
}
/**
* @param description The description to set.
*/
public void setDescription(String description)
{
this.description = description;
}
/**
* @return Returns the modifiedDateFrom.
*/
public Date getModifiedDateFrom()
{
return this.modifiedDateFrom;
}
/**
* @param modifiedDate The modifiedDate to set.
*/
public void setModifiedDateFrom(Date modifiedDate)
{
this.modifiedDateFrom = modifiedDate;
}
/**
* @return Returns the createdDateTo.
*/
public Date getCreatedDateTo()
{
return this.createdDateTo;
}
/**
* @param createdDateTo The createdDateTo to set.
*/
public void setCreatedDateTo(Date createdDateTo)
{
this.createdDateTo = createdDateTo;
}
/**
* @return Returns the modifiedDateTo.
*/
public Date getModifiedDateTo()
{
return this.modifiedDateTo;
}
/**
* @param modifiedDateTo The modifiedDateTo to set.
*/
public void setModifiedDateTo(Date modifiedDateTo)
{
this.modifiedDateTo = modifiedDateTo;
}
/**
* @return Returns the title.
*/
public String getTitle()
{
return this.title;
}
/**
* @param title The title to set.
*/
public void setTitle(String title)
{
this.title = title;
}
/**
* @return Returns the author.
*/
public String getAuthor()
{
return this.author;
}
/**
* @param author The author to set.
*/
public void setAuthor(String author)
{
this.author = author;
}
/**
* @return Returns the modifiedDateChecked.
*/
public boolean isModifiedDateChecked()
{
return this.modifiedDateChecked;
}
/**
* @param modifiedDateChecked The modifiedDateChecked to set.
*/
public void setModifiedDateChecked(boolean modifiedDateChecked)
{
this.modifiedDateChecked = modifiedDateChecked;
}
/**
* @return Returns the createdDateChecked.
*/
public boolean isCreatedDateChecked()
{
return this.createdDateChecked;
}
/**
* @return Returns the content type currenty selected
*/
public String getContentType()
{
return this.contentType;
}
/**
* @param contentType Sets the currently selected content type
*/
public void setContentType(String contentType)
{
this.contentType = contentType;
}
/**
* @return Returns the folder type currenty selected
*/
public String getFolderType()
{
return this.folderType;
}
/**
* @param folderType Sets the currently selected folder type
*/
public void setFolderType(String folderType)
{
this.folderType = folderType;
}
/**
* @return Returns the contentFormat.
*/
public String getContentFormat()
{
return this.contentFormat;
}
/**
* @param contentFormat The contentFormat to set.
*/
public void setContentFormat(String contentFormat)
{
this.contentFormat = contentFormat;
}
/**
* @return Returns the custom properties Map.
*/
public Map getCustomProperties()
{
return this.customProperties;
}
/**
* @param customProperties The custom properties Map to set.
*/
public void setCustomProperties(Map customProperties)
{
this.customProperties = customProperties;
}
/**
* @param createdDateChecked The createdDateChecked to set.
*/
public void setCreatedDateChecked(boolean createdDateChecked)
{
this.createdDateChecked = createdDateChecked;
}
/**
* @return Returns a list of content object types to allow the user to select from
*/
public List getContentTypes()
{
if (this.contentTypes == null)
{
FacesContext context = FacesContext.getCurrentInstance();
DictionaryService dictionaryService = Repository.getServiceRegistry(context).getDictionaryService();
// add the well known cm:content object type by default
this.contentTypes = new ArrayList(5);
this.contentTypes.add(new SelectItem(ContentModel.TYPE_CONTENT.toString(),
dictionaryService.getType(ContentModel.TYPE_CONTENT).getTitle()));
// add any configured content sub-types to the list
List types = getSearchConfig().getContentTypes();
if (types != null)
{
for (String type : types)
{
QName idQName = Repository.resolveToQName(type);
if (idQName != null)
{
TypeDefinition typeDef = dictionaryService.getType(idQName);
if (typeDef != null && dictionaryService.isSubClass(typeDef.getName(), ContentModel.TYPE_CONTENT))
{
// try and get label from the dictionary
String label = typeDef.getTitle();
// else just use the localname
if (label == null)
{
label = idQName.getLocalName();
}
this.contentTypes.add(new SelectItem(idQName.toString(), label));
}
}
}
}
}
return this.contentTypes;
}
/**
* @return Returns a list of folder object types to allow the user to select from
*/
public List getFolderTypes()
{
if (this.folderTypes == null)
{
FacesContext context = FacesContext.getCurrentInstance();
DictionaryService dictionaryService = Repository.getServiceRegistry(context).getDictionaryService();
// add the well known cm:folder object type by default
this.folderTypes = new ArrayList(5);
this.folderTypes.add(new SelectItem(ContentModel.TYPE_FOLDER.toString(),
dictionaryService.getType(ContentModel.TYPE_FOLDER).getTitle()));
// add any configured folder sub-types to the list
List types = getSearchConfig().getFolderTypes();
if (types != null)
{
for (String type : types)
{
QName idQName = Repository.resolveToQName(type);
if (idQName != null)
{
TypeDefinition typeDef = dictionaryService.getType(idQName);
if (typeDef != null && dictionaryService.isSubClass(typeDef.getName(), ContentModel.TYPE_FOLDER))
{
// try and get label from the dictionary
String label = typeDef.getTitle();
// else just use the localname
if (label == null)
{
label = idQName.getLocalName();
}
this.folderTypes.add(new SelectItem(idQName.toString(), label));
}
}
}
}
}
return this.folderTypes;
}
/**
* @return Returns a list of content formats to allow the user to select from
*/
public List getContentFormats()
{
if (this.contentFormats == null)
{
this.contentFormats = new ArrayList(80);
ServiceRegistry registry = Repository.getServiceRegistry(FacesContext.getCurrentInstance());
MimetypeService mimetypeService = registry.getMimetypeService();
// get the mime type display names
Map mimeTypes = mimetypeService.getDisplaysByMimetype();
for (String mimeType : mimeTypes.keySet())
{
this.contentFormats.add(new SelectItem(mimeType, mimeTypes.get(mimeType)));
}
// make sure the list is sorted by the values
QuickSort sorter = new QuickSort(this.contentFormats, "label", true, IDataContainer.SORT_CASEINSENSITIVE);
sorter.sort();
// add the "All Formats" constant marker at the top of the list (default selection)
this.contentFormats.add(0, new SelectItem("", Application.getMessage(FacesContext.getCurrentInstance(), MSG_ALL_FORMATS)));
}
return this.contentFormats;
}
// ------------------------------------------------------------------------------
// Action event handlers
/**
* Handler to clear the advanced search screen form details
*/
public void reset(ActionEvent event)
{
resetFields();
this.savedSearch = null;
}
private void resetFields()
{
this.text = "";
this.mode = MODE_ALL;
this.lookin = LOOKIN_ALL;
this.contentType = null;
this.contentFormat = null;
this.folderType = null;
this.location = null;
this.locationChildren = true;
this.categories = new ArrayList(2);
this.title = null;
this.description = null;
this.author = null;
this.createdDateFrom = null;
this.createdDateTo = null;
this.modifiedDateFrom = null;
this.modifiedDateTo = null;
this.createdDateChecked = false;
this.modifiedDateChecked = false;
this.customProperties.clear();
}
/**
* Handler to perform a search based on the current criteria
*/
public String search()
{
// construct the Search Context and set on the navigation bean
// then simply navigating to the browse screen will cause it pickup the Search Context
SearchContext search = new SearchContext();
// set the full-text/name field value
search.setText(this.text);
// set whether to force AND operation on text terms
search.setForceAndTerms(Application.getClientConfig(FacesContext.getCurrentInstance()).getForceAndTerms());
if (this.mode.equals(MODE_ALL))
{
search.setMode(SearchContext.SEARCH_ALL);
}
else if (this.mode.equals(MODE_FILES_TEXT))
{
search.setMode(SearchContext.SEARCH_FILE_NAMES_CONTENTS);
}
else if (this.mode.equals(MODE_FILES))
{
search.setMode(SearchContext.SEARCH_FILE_NAMES);
}
else if (this.mode.equals(MODE_FOLDERS))
{
search.setMode(SearchContext.SEARCH_SPACE_NAMES);
}
// additional attributes search
if (this.description != null && this.description.length() != 0)
{
search.addAttributeQuery(ContentModel.PROP_DESCRIPTION, this.description);
}
if (this.title != null && this.title.length() != 0)
{
search.addAttributeQuery(ContentModel.PROP_TITLE, this.title);
}
if (this.author != null && this.author.length() != 0)
{
search.addAttributeQuery(ContentModel.PROP_AUTHOR, this.author);
}
if (this.contentFormat != null && this.contentFormat.length() != 0)
{
search.setMimeType(this.contentFormat);
}
if (this.createdDateChecked == true)
{
SimpleDateFormat df = CachingDateFormat.getDateFormat();
String strCreatedDate = df.format(this.createdDateFrom);
String strCreatedDateTo = df.format(this.createdDateTo);
search.addRangeQuery(ContentModel.PROP_CREATED, strCreatedDate, strCreatedDateTo, true);
}
if (this.modifiedDateChecked == true)
{
SimpleDateFormat df = CachingDateFormat.getDateFormat();
String strModifiedDate = df.format(this.modifiedDateFrom);
String strModifiedDateTo = df.format(this.modifiedDateTo);
search.addRangeQuery(ContentModel.PROP_MODIFIED, strModifiedDate, strModifiedDateTo, true);
}
// walk each of the custom properties add add them as additional attributes
for (String qname : this.customProperties.keySet())
{
Object value = this.customProperties.get(qname);
DataTypeDefinition typeDef = getCustomPropertyLookup().get(qname);
if (typeDef != null)
{
QName typeName = typeDef.getName();
if (DataTypeDefinition.DATE.equals(typeName) || DataTypeDefinition.DATETIME.equals(typeName))
{
// only apply date to search if the user has checked the enable checkbox
if (value != null && Boolean.valueOf(value.toString()) == true)
{
SimpleDateFormat df = CachingDateFormat.getDateFormat();
String strDateFrom = df.format(this.customProperties.get(
UISearchCustomProperties.PREFIX_DATE_FROM + qname));
String strDateTo = df.format(this.customProperties.get(
UISearchCustomProperties.PREFIX_DATE_TO + qname));
search.addRangeQuery(QName.createQName(qname), strDateFrom, strDateTo, true);
}
}
else if (DataTypeDefinition.BOOLEAN.equals(typeName))
{
if (((Boolean)value) == true)
{
search.addFixedValueQuery(QName.createQName(qname), value.toString());
}
}
else if (DataTypeDefinition.NODE_REF.equals(typeName) || DataTypeDefinition.CATEGORY.equals(typeName))
{
if (value != null)
{
search.addFixedValueQuery(QName.createQName(qname), value.toString());
}
}
else if (DataTypeDefinition.INT.equals(typeName) || DataTypeDefinition.LONG.equals(typeName) ||
DataTypeDefinition.FLOAT.equals(typeName) || DataTypeDefinition.DOUBLE.equals(typeName))
{
String strVal = value.toString();
if (strVal != null && strVal.length() != 0)
{
search.addFixedValueQuery(QName.createQName(qname), strVal);
}
}
else if (value != null)
{
Object item = this.customProperties.get(
UISearchCustomProperties.PREFIX_LOV_ITEM + qname);
if (item != null)
{
// ListOfValues
if (((Boolean)value) == true)
{
search.addFixedValueQuery(QName.createQName(qname), item.toString());
}
}
else
{
// by default use toString() value - this is for text fields and unknown types
String strVal = value.toString();
if (strVal != null && strVal.length() != 0)
{
search.addAttributeQuery(QName.createQName(qname), strVal);
}
}
}
}
}
// location path search
if (this.lookin.equals(LOOKIN_OTHER) && this.location != null)
{
search.setLocation(SearchContext.getPathFromSpaceRef(this.location, this.locationChildren));
}
// category path search
if (this.categories.size() != 0)
{
String[] paths = new String[this.categories.size()];
for (int i=0; i callback = new RetryingTransactionCallback