Java tutorial
/* * #%L * Alfresco Repository * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * #L% */ package org.alfresco.repo.search.impl.solr.facet; import static org.alfresco.repo.security.authentication.AuthenticationUtil.getSystemUserName; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.alfresco.model.ContentModel; import org.alfresco.repo.cache.SimpleCache; import org.alfresco.repo.dictionary.Facetable; import org.alfresco.repo.importer.ImporterBootstrap; import org.alfresco.repo.model.Repository; import org.alfresco.repo.node.NodeServicePolicies; import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy; import org.alfresco.repo.policy.BehaviourFilter; import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.PolicyComponent; import org.alfresco.repo.search.impl.solr.facet.Exceptions.DuplicateFacetId; import org.alfresco.repo.search.impl.solr.facet.Exceptions.IllegalArgument; import org.alfresco.repo.search.impl.solr.facet.Exceptions.MissingFacetId; import org.alfresco.repo.search.impl.solr.facet.SolrFacetProperties.CustomProperties; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.transaction.RetryingTransactionHelper; import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; 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.repository.ChildAssociationRef; 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.search.SearchService; import org.alfresco.service.cmr.security.AuthorityService; 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.ParameterCheck; import org.alfresco.util.collections.CollectionUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationEvent; import org.springframework.extensions.surf.util.AbstractLifecycleBean; /** * Solr Facet Service Implementation. * * @author Jamal Kaabi-Mofrad * @since 5.0 */ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrFacetService, NodeServicePolicies.OnCreateNodePolicy, NodeServicePolicies.BeforeDeleteNodePolicy { private static final Log logger = LogFactory.getLog(SolrFacetServiceImpl.class); /** * The authority that needs to contain the users allowed to administer the faceted-search config. */ public static final String ALFRESCO_SEARCH_ADMINISTRATORS_AUTHORITY = "ALFRESCO_SEARCH_ADMINISTRATORS"; public static final String GROUP_ALFRESCO_SEARCH_ADMINISTRATORS_AUTHORITY = PermissionService.GROUP_PREFIX + ALFRESCO_SEARCH_ADMINISTRATORS_AUTHORITY; /** The store where facets are kept */ private static final StoreRef FACET_STORE = new StoreRef("workspace://SpacesStore"); private AuthorityService authorityService; private DictionaryService dictionaryService; protected NodeService nodeService; private NamespaceService namespaceService; private SearchService searchService; private RetryingTransactionHelper retryingTransactionHelper; private BehaviourFilter behaviourFilter; private PolicyComponent policyComponent; private SolrFacetConfig facetConfig; private Repository repositoryHelper; private String facetsRootXPath; private String facetsRootChildName; private ImporterBootstrap importerBootstrap; private Properties bootstrapView; private SimpleCache<String, Object> singletonCache; // eg. for facetsHomeNodeRef private final String KEY_FACETS_HOME_NODEREF = "key.facetshome.noderef"; private SimpleCache<String, NodeRef> facetNodeRefCache; // for filterID to nodeRef lookup private ConcurrentMap<String, SolrFacetProperties> defaultFacetsMap = new ConcurrentHashMap<>(10); /** * @param authorityService the authorityService to set */ public void setAuthorityService(AuthorityService authorityService) { this.authorityService = authorityService; } /** * @param dictionaryService the dictionaryService to set */ public void setDictionaryService(DictionaryService dictionaryService) { this.dictionaryService = dictionaryService; } /** * @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 searchService to set */ public void setSearchService(SearchService searchService) { this.searchService = searchService; } /** * @param retryingTransactionHelper the retryingTransactionHelper to set */ public void setRetryingTransactionHelper(RetryingTransactionHelper retryingTransactionHelper) { this.retryingTransactionHelper = retryingTransactionHelper; } /** * @param behaviourFilter the behaviourFilter to set */ public void setBehaviourFilter(BehaviourFilter behaviourFilter) { this.behaviourFilter = behaviourFilter; } /** * @param policyComponent the policyComponent to set */ public void setPolicyComponent(PolicyComponent policyComponent) { this.policyComponent = policyComponent; } public void setRepositoryHelper(Repository repository) { this.repositoryHelper = repository; } /** * @param facetConfig the facetConfig to set */ public void setFacetConfig(SolrFacetConfig facetConfig) { this.facetConfig = facetConfig; } /** * @param facetsRootXPath the facetsRootXPath to set */ public void setFacetsRootXPath(String facetsRootXPath) { this.facetsRootXPath = facetsRootXPath; } public void setFacetsRootChildName(String facetsRootChildName) { this.facetsRootChildName = facetsRootChildName; } public void setImporterBootstrap(ImporterBootstrap importer) { this.importerBootstrap = importer; } public void setBootstrapView(Properties bootstrapView) { this.bootstrapView = bootstrapView; } /** * @param singletonCache the singletonCache to set */ public void setSingletonCache(SimpleCache<String, Object> singletonCache) { this.singletonCache = singletonCache; } /** * @param facetNodeRefCache the facetNodeRefCache to set */ public void setFacetNodeRefCache(SimpleCache<String, NodeRef> facetNodeRefCache) { this.facetNodeRefCache = facetNodeRefCache; } @Override public boolean isSearchAdmin(String userName) { if (userName == null) { return false; } return this.authorityService.isAdminAuthority(userName) || this.authorityService .getAuthoritiesForUser(userName).contains(GROUP_ALFRESCO_SEARCH_ADMINISTRATORS_AUTHORITY); } @Override public List<SolrFacetProperties> getFacets() { // Sort the facets into display order final SolrFacetComparator comparator = new SolrFacetComparator(getFacetOrder()); SortedSet<SolrFacetProperties> result = new TreeSet<>(comparator); final NodeRef facetsRoot = getFacetsRoot(); if (facetsRoot != null) { for (ChildAssociationRef ref : nodeService.getChildAssocs(facetsRoot)) { // MNT-13812 Check that child has facetField type if (nodeService.getType(ref.getChildRef()).equals(SolrFacetModel.TYPE_FACET_FIELD)) { result.add(getFacetProperties(ref.getChildRef())); } } } // add the default filters result.addAll(defaultFacetsMap.values()); return new ArrayList<>(result); } /** Gets the filter IDs in display order. Will not return {@code null}. */ public List<String> getFacetOrder() { final NodeRef facetsRoot = getFacetsRoot(); return facetsRoot == null ? new ArrayList<>(facetConfig.getDefaultFacets().keySet()) : (List<String>) nodeService.getProperty(facetsRoot, SolrFacetModel.PROP_FACET_ORDER); } @Override public SolrFacetProperties getFacet(String filterID) { /* * Note: There is no need to worry about the state of the SolrFacetProperties returned from * facetConfig (getDefaultLoadedFacet), as if the FP has been modified, then we'll get it from * the nodeService. */ NodeRef nodeRef = getFacetNodeRef(filterID); return (nodeRef == null) ? defaultFacetsMap.get(filterID) : getFacetProperties(nodeRef); } @Override public NodeRef getFacetNodeRef(final String filterID) { ParameterCheck.mandatory("filterID", filterID); NodeRef facetNodeRef = facetNodeRefCache.get(filterID); if (facetNodeRef != null) { // test for existence - and remove from cache if no longer exists if (!this.nodeService.exists(facetNodeRef)) { facetNodeRefCache.remove(filterID); facetNodeRef = null; } } else { // not in cache - find and store final NodeRef facetRoot = getFacetsRoot(); if (facetRoot != null) { facetNodeRef = AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork<NodeRef>() { public NodeRef doWork() throws Exception { // the filterID directly maps to the cm:name property NodeRef nodeRef = nodeService.getChildByName(facetRoot, ContentModel.ASSOC_CONTAINS, filterID); // cache the result if found if (nodeRef != null) { facetNodeRefCache.put(filterID, nodeRef); } return nodeRef; } }, AuthenticationUtil.getSystemUserName()); } } return facetNodeRef; } /** * Gets the {@link SolrFacetProperties} stored on the specified {@link NodeRef}. * @throws org.alfresco.service.cmr.repository.InvalidNodeRefException if the nodeRef does not exist. */ private SolrFacetProperties getFacetProperties(NodeRef nodeRef) { Map<QName, Serializable> properties = nodeService.getProperties(nodeRef); if (properties.isEmpty()) { return new SolrFacetProperties.Builder().build(); } String filterID = (String) properties.get(ContentModel.PROP_NAME); boolean isDefault = (Boolean) properties.get(SolrFacetModel.PROP_IS_DEFAULT); SolrFacetProperties defaultFacet = defaultFacetsMap.get(filterID); if (defaultFacet == null) { defaultFacet = new SolrFacetProperties.Builder().build(); } QName fieldQName = getDefaultIfNull(defaultFacet.getFacetQName(), (QName) properties.get(SolrFacetModel.PROP_FIELD_TYPE)); String displayName = getDefaultIfNull(defaultFacet.getDisplayName(), (String) properties.get(SolrFacetModel.PROP_FIELD_LABEL)); String displayControl = getDefaultIfNull(defaultFacet.getDisplayControl(), (String) properties.get(SolrFacetModel.PROP_DISPLAY_CONTROL)); int maxFilters = getDefaultIfNull(defaultFacet.getMaxFilters(), (Integer) properties.get(SolrFacetModel.PROP_MAX_FILTERS)); int hitThreshold = getDefaultIfNull(defaultFacet.getHitThreshold(), (Integer) properties.get(SolrFacetModel.PROP_HIT_THRESHOLD)); int minFilterValueLength = getDefaultIfNull(defaultFacet.getMinFilterValueLength(), (Integer) properties.get(SolrFacetModel.PROP_MIN_FILTER_VALUE_LENGTH)); String sortBy = getDefaultIfNull(defaultFacet.getSortBy(), (String) properties.get(SolrFacetModel.PROP_SORT_BY)); String scope = getDefaultIfNull(defaultFacet.getScope(), (String) properties.get(SolrFacetModel.PROP_SCOPE)); Boolean isEnabled = getDefaultIfNull(defaultFacet.isEnabled(), (Boolean) properties.get(SolrFacetModel.PROP_IS_ENABLED)); @SuppressWarnings("unchecked") List<String> scSites = (List<String>) properties.get(SolrFacetModel.PROP_SCOPED_SITES); Set<String> scopedSites = getDefaultIfNull(defaultFacet.getScopedSites(), (scSites == null) ? null : new HashSet<>(scSites)); Set<CustomProperties> extraProps = null; Map<QName, Serializable> customProperties = getFacetCustomProperties(properties); boolean hasAspect = nodeService.hasAspect(nodeRef, SolrFacetModel.ASPECT_CUSTOM_PROPERTIES); if (!hasAspect && customProperties.isEmpty()) { extraProps = defaultFacet.getCustomProperties(); } else { extraProps = new HashSet<>(customProperties.size()); for (Entry<QName, Serializable> cp : customProperties.entrySet()) { extraProps.add(new CustomProperties(cp.getKey(), cp.getValue())); } } // Construct the FacetProperty object SolrFacetProperties fp = new SolrFacetProperties.Builder().filterID(filterID).facetQName(fieldQName) .displayName(displayName).displayControl(displayControl).maxFilters(maxFilters) .hitThreshold(hitThreshold).minFilterValueLength(minFilterValueLength).sortBy(sortBy).scope(scope) .isEnabled(isEnabled).isDefault(isDefault).scopedSites(scopedSites).customProperties(extraProps) .build(); return fp; } private <T> T getDefaultIfNull(T defaultValue, T newValue) { return (newValue == null) ? defaultValue : newValue; } @Override public NodeRef createFacetNode(SolrFacetProperties facetProperties) { return createFacetNodeImpl(facetProperties, true); } private NodeRef createFacetNodeImpl(final SolrFacetProperties facetProperties, boolean checkDefaultFP) { final String filterID = facetProperties.getFilterID(); NodeRef facetNodeRef = getFacetNodeRef(filterID); // We need to check the bootstrapped Facet properties (i.e loaded from properties file(s)) as well, // in order to not allow the user to create a new facet with the same filterID as the bootstrapped FP. if (facetNodeRef != null || (checkDefaultFP && defaultFacetsMap.get(filterID) != null)) { throw new SolrFacetConfigException( "Unable to create facet because the filterID [" + filterID + "] is already in use."); } // Get the facet root node reference NodeRef facetRoot = getFacetsRoot(); if (facetRoot == null) { facetRoot = createFacetsRootFolder(); } final NodeRef finalFacetRoot = facetRoot; return AuthenticationUtil.runAs(new RunAsWork<NodeRef>() { @Override public NodeRef doWork() throws Exception { return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback<NodeRef>() { public NodeRef execute() throws Exception { behaviourFilter.disableBehaviour(finalFacetRoot, ContentModel.ASPECT_AUDITABLE); try { Map<QName, Serializable> properties = createNodeProperties(facetProperties); // We don't want the node to be indexed properties.put(ContentModel.PROP_IS_INDEXED, false); NodeRef ref = nodeService.createNode(finalFacetRoot, ContentModel.ASSOC_CONTAINS, QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, filterID), SolrFacetModel.TYPE_FACET_FIELD, properties).getChildRef(); if (logger.isDebugEnabled()) { logger.debug("Created [" + filterID + "] facet node with properties: [" + properties + "]"); } return ref; } finally { behaviourFilter.enableBehaviour(finalFacetRoot, ContentModel.ASPECT_AUDITABLE); } } }, false); } }, AuthenticationUtil.getSystemUserName()); } @Override public void updateFacet(SolrFacetProperties facetProperties) { final String filterID = facetProperties.getFilterID(); NodeRef facetNodeRef = getFacetNodeRef(filterID); if (facetNodeRef == null) { SolrFacetProperties fp = defaultFacetsMap.get(filterID); if (fp != null) { // As we don't create nodes for the bootstrapped FP on server // startup, we need to create a node here, when a user tries to // update the default properties for the first time. createFacetNodeImpl(facetProperties, false); } else { throw new SolrFacetConfigException("Cannot update facet [" + filterID + "] as it does not exist."); } } else { Map<QName, Serializable> properties = createNodeProperties(facetProperties); // Set the updated properties back onto the facet node reference for (Entry<QName, Serializable> prop : properties.entrySet()) { this.nodeService.setProperty(facetNodeRef, prop.getKey(), prop.getValue()); } } if (logger.isDebugEnabled()) { logger.debug("Updated [" + filterID + "] facet node. Properties: [" + facetProperties + "]"); } } @Override public void deleteFacet(String filterID) { NodeRef facetNodeRef = getFacetNodeRef(filterID); if (facetNodeRef == null) { throw new SolrFacetConfigException("The [" + filterID + "] facet cannot be found."); } SolrFacetProperties defaultFP = defaultFacetsMap.get(filterID); if (defaultFP != null) { throw new SolrFacetConfigException( "The default [" + filterID + "] facet cannot be deleted. It can only be disabled."); } nodeService.deleteNode(facetNodeRef); if (logger.isDebugEnabled()) { logger.debug("Deleted [" + filterID + "] facet."); } } private Map<QName, Serializable> createNodeProperties(SolrFacetProperties facetProperties) { if (facetProperties.getFilterID() == null) { throw new SolrFacetConfigException("Filter Id cannot be null."); } boolean isDefaultFP = defaultFacetsMap.containsKey(facetProperties.getFilterID()); Map<QName, Serializable> properties = new HashMap<>(15); properties.put(ContentModel.PROP_NAME, facetProperties.getFilterID()); properties.put(SolrFacetModel.PROP_IS_DEFAULT, isDefaultFP); addNodeProperty(properties, SolrFacetModel.PROP_FIELD_TYPE, facetProperties.getFacetQName()); addNodeProperty(properties, SolrFacetModel.PROP_FIELD_LABEL, facetProperties.getDisplayName()); addNodeProperty(properties, SolrFacetModel.PROP_DISPLAY_CONTROL, facetProperties.getDisplayControl()); addNodeProperty(properties, SolrFacetModel.PROP_MAX_FILTERS, facetProperties.getMaxFilters()); addNodeProperty(properties, SolrFacetModel.PROP_HIT_THRESHOLD, facetProperties.getHitThreshold()); addNodeProperty(properties, SolrFacetModel.PROP_MIN_FILTER_VALUE_LENGTH, facetProperties.getMinFilterValueLength()); addNodeProperty(properties, SolrFacetModel.PROP_SCOPE, facetProperties.getScope()); addNodeProperty(properties, SolrFacetModel.PROP_SORT_BY, facetProperties.getSortBy()); addNodeProperty(properties, SolrFacetModel.PROP_SCOPED_SITES, (Serializable) facetProperties.getScopedSites()); addNodeProperty(properties, SolrFacetModel.PROP_IS_ENABLED, facetProperties.isEnabled()); Set<CustomProperties> customProperties = facetProperties.getCustomProperties(); if (customProperties != null) { properties.put(SolrFacetModel.PROP_EXTRA_INFORMATION, new ArrayList<>(customProperties)); } return properties; } private void addNodeProperty(Map<QName, Serializable> properties, QName qname, Serializable propValue) { if (propValue == null) { return; } if (propValue instanceof Integer && ((Integer) propValue) < 0) { return; } if (propValue instanceof Collection<?> && ((Collection<?>) propValue).isEmpty()) { return; } properties.put(qname, propValue); } /** * Gets the {@link NodeRef} of the {@code srft:facets} folder, if it exists. * @return the {@link NodeRef} if it exists, else {@code null}. */ public NodeRef getFacetsRoot() { NodeRef facetHomeRef = (NodeRef) singletonCache.get(KEY_FACETS_HOME_NODEREF); if (facetHomeRef == null) { facetHomeRef = AuthenticationUtil.runAs(new RunAsWork<NodeRef>() { public NodeRef doWork() throws Exception { return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback<NodeRef>() { public NodeRef execute() throws Exception { NodeRef result = null; // Get the root 'facets' folder NodeRef rootNodeRef = nodeService.getRootNode(FACET_STORE); List<NodeRef> results = searchService.selectNodes(rootNodeRef, facetsRootXPath, null, namespaceService, false, SearchService.LANGUAGE_XPATH); if (results.size() != 0) { result = results.get(0); } return result; } }, true); } }, AuthenticationUtil.getSystemUserName()); if (facetHomeRef != null) { singletonCache.put(KEY_FACETS_HOME_NODEREF, facetHomeRef); } } return facetHomeRef; } @Override protected void onBootstrap(ApplicationEvent event) { // Filter creation this.policyComponent.bindClassBehaviour(OnCreateNodePolicy.QNAME, SolrFacetModel.TYPE_FACET_FIELD, new JavaBehaviour(this, "onCreateNode")); // Filter before deletion this.policyComponent.bindClassBehaviour(BeforeDeleteNodePolicy.QNAME, SolrFacetModel.TYPE_FACET_FIELD, new JavaBehaviour(this, "beforeDeleteNode")); Map<String, SolrFacetProperties> mergedMap = new HashMap<>(100); // Loaded facets Map<String, SolrFacetProperties> defaultFP = facetConfig.getDefaultFacets(); defaultFacetsMap.putAll(defaultFP); // add the default facets to a ConcurrentHashMap for performance reasons mergedMap.putAll(defaultFP); // Persisted facets Map<String, SolrFacetProperties> persistedProperties = getPersistedFacetProperties(); for (Entry<String, SolrFacetProperties> entry : persistedProperties.entrySet()) { final String facetId = entry.getKey(); /* * If the default facet has been removed from the config file and * the facet was persisted as its property was modified, then, the * persisted node needs to be deleted. This should be done to avoid * errors when loading the facets. Also, as all the properties of * the facet may not have been persisted and the default facet * doesn't exist anymore, there is no way of merging the * non-persisted properties. */ if (entry.getValue().isDefault() && !defaultFP.containsKey(facetId)) { AuthenticationUtil.runAs(new RunAsWork<Void>() { @Override public Void doWork() throws Exception { return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback<Void>() { public Void execute() throws Exception { deleteFacet(facetId); logger.info("Deleted [" + facetId + "] node, as the filter has been removed from the config file!"); return null; } }, false); } }, AuthenticationUtil.getSystemUserName()); } else { // The persisted facets will override the default facets mergedMap.put(facetId, entry.getValue()); } } final List<String> facetOrder = getFacetOrder(); // Sort the merged maps Comparator<Entry<String, SolrFacetProperties>> entryComparator = CollectionUtils .toEntryComparator(new SolrFacetComparator(facetOrder)); Map<String, SolrFacetProperties> sortedMap = CollectionUtils.sortMapByValue(mergedMap, entryComparator); if (logger.isDebugEnabled() && persistedProperties.size() > 0) { logger.debug("The facets [" + persistedProperties + "] have overridden their matched default facets."); } final Set<String> newFacetOrder = (facetOrder == null) ? new LinkedHashSet<String>(sortedMap.size()) : new LinkedHashSet<>(facetOrder); for (SolrFacetProperties fp : sortedMap.values()) { newFacetOrder.add(fp.getFilterID()); } AuthenticationUtil.runAs(new RunAsWork<Void>() { @Override public Void doWork() throws Exception { return retryingTransactionHelper.doInTransaction(new RetryingTransactionCallback<Void>() { public Void execute() throws Exception { reorderFacets(new ArrayList<>(newFacetOrder)); return null; } }, false); } }, AuthenticationUtil.getSystemUserName()); if (logger.isDebugEnabled()) { logger.debug("The facets order [" + newFacetOrder + "] have been persisted."); } } /** Gets the persisted {@link SolrFacetProperties} if there are any, else an empty map. */ private Map<String, SolrFacetProperties> getPersistedFacetProperties() { final NodeRef facetsRoot = getFacetsRoot(); Map<String, SolrFacetProperties> facets = new HashMap<>(); final List<ChildAssociationRef> list = facetsRoot == null ? new ArrayList<ChildAssociationRef>() : nodeService.getChildAssocs(facetsRoot); for (ChildAssociationRef associationRef : list) { // MNT-13812 Check that child has facetField type if (nodeService.getType(associationRef.getChildRef()).equals(SolrFacetModel.TYPE_FACET_FIELD)) { SolrFacetProperties fp = getFacetProperties(associationRef.getChildRef()); facets.put(fp.getFilterID(), fp); } } return facets; } @Override protected void onShutdown(ApplicationEvent event) { // nothing to do } @Override public void onCreateNode(ChildAssociationRef childAssocRef) { SolrFacetProperties fp = getFacetProperties(childAssocRef.getChildRef()); this.facetNodeRefCache.put(fp.getFilterID(), childAssocRef.getChildRef()); // We must also add the new filterID to the facetOrder property. final NodeRef facetsRoot = getFacetsRoot(); @SuppressWarnings("unchecked") ArrayList<String> facetOrder = (ArrayList<String>) nodeService.getProperty(facetsRoot, SolrFacetModel.PROP_FACET_ORDER); if (facetOrder == null) { List<SolrFacetProperties> facets = getFacets(); facetOrder = new ArrayList<String>(facets.size()); for (SolrFacetProperties facet : facets) { facetOrder.add(facet.getFilterID()); } } // We'll put it at the end (arbitrarily). facetOrder.add(fp.getFilterID()); nodeService.setProperty(facetsRoot, SolrFacetModel.PROP_FACET_ORDER, facetOrder); } @Override public void beforeDeleteNode(NodeRef nodeRef) { String filterID = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); this.facetNodeRefCache.remove(filterID); // We must also remove the filterID from the facetOrder property. final NodeRef facetsRoot = getFacetsRoot(); @SuppressWarnings("unchecked") ArrayList<String> facetOrder = (ArrayList<String>) nodeService.getProperty(facetsRoot, SolrFacetModel.PROP_FACET_ORDER); if (facetOrder.remove(filterID)) { nodeService.setProperty(facetsRoot, SolrFacetModel.PROP_FACET_ORDER, facetOrder); } } /** * Gets a map containing the facet's custom properties * * @return map containing the custom properties of the facet */ private Map<QName, Serializable> getFacetCustomProperties(Map<QName, Serializable> properties) { Map<QName, Serializable> customProperties = new HashMap<>(5); for (Map.Entry<QName, Serializable> entry : properties.entrySet()) { if (SolrFacetModel.SOLR_FACET_CUSTOM_PROPERTY_URL.equals(entry.getKey().getNamespaceURI())) { Serializable values = entry.getValue(); if (SolrFacetModel.PROP_EXTRA_INFORMATION.equals(entry.getKey()) && values instanceof List) { @SuppressWarnings("unchecked") List<CustomProperties> list = (List<CustomProperties>) values; for (CustomProperties cp : list) { customProperties.put(cp.getName(), cp.getValue()); } } else { customProperties.put(entry.getKey(), entry.getValue()); } } } return customProperties; } @Override public void reorderFacets(List<String> facetIds) { // We need to validate the provided facet IDs if (facetIds == null) { throw new NullPointerException("Illegal null facetIds"); } else if (facetIds.isEmpty()) { throw new MissingFacetId("Illegal empty facetIds"); } else { final List<SolrFacetProperties> existingFacets = getFacets(); final Map<String, SolrFacetProperties> sortedFacets = new LinkedHashMap<>(); // maintains insertion order final List<String> removedFacetIds = new ArrayList<>(); for (String facetId : facetIds) { final SolrFacetProperties facet = getFacet(facetId); if (facet == null) { // ACE-3083 logger.warn("Facet with [" + facetId + "] ID does not exist. Removing it from the facets' ordering list"); removedFacetIds.add(facetId); } else if (sortedFacets.containsKey(facetId)) { throw new DuplicateFacetId("Cannot reorder facets as sequence contains duplicate entry for ID:", facetId); } else { sortedFacets.put(facetId, facet); } } if (existingFacets.size() != sortedFacets.size()) { throw new IllegalArgument("Cannot reorder facets. Expected " + existingFacets.size() + " IDs but only received " + sortedFacets.size()); } // We can now safely apply the updates to the facet ID sequence. // // Put them in an ArrayList to ensure the collection is Serializable. // The alternative is changing the service API to look like <T extends Serializable & List<String>> // which is a bit verbose for an API. ArrayList<String> serializableProp = new ArrayList<>(facetIds); if (removedFacetIds.size() > 0) { boolean result = serializableProp.removeAll(removedFacetIds); if (result) { logger.info("Removed " + removedFacetIds + " from the facets' ordering list."); } } NodeRef facetsRoot = getFacetsRoot(); if (facetsRoot == null) { facetsRoot = createFacetsRootFolder(); } nodeService.setProperty(facetsRoot, SolrFacetModel.PROP_FACET_ORDER, serializableProp); } } private NodeRef createFacetsRootFolder() { return AuthenticationUtil.runAs(new RunAsWork<NodeRef>() { @Override public NodeRef doWork() throws Exception { final NodeRef companyHome = repositoryHelper.getCompanyHome(); final QName appModel = QName.createQName("http://www.alfresco.org/model/application/1.0", "dictionary"); final NodeRef dataDict = getSingleChildNodeRef(companyHome, appModel); // The name of the child-assoc to the facets root folder. final QName facetsRootAssocQName = QName.createQName(facetsRootChildName, namespaceService); NodeRef result = getSingleChildNodeRef(dataDict, facetsRootAssocQName); if (result == null) { List<Properties> singletonList = new ArrayList<>(); singletonList.add(bootstrapView); importerBootstrap.setBootstrapViews(singletonList); importerBootstrap.setUseExistingStore(true); importerBootstrap.bootstrap(); // Now to get the NodeRef we just imported. (Not using SOLR to avoid consistency effects.) result = getSingleChildNodeRef(dataDict, facetsRootAssocQName); } if (logger.isDebugEnabled()) { logger.debug("Created Facets Root Folder: " + result); } return result; } }, getSystemUserName()); } /** * Gets a child NodeRef under the specified parent NodeRef linked by a child-assoc of the specified name. * * @param parent the parent whose child is sought. * @param assocName the name of the child-association. * @return the NodeRef of the requested child, if it exists. null if there is no match. */ private NodeRef getSingleChildNodeRef(NodeRef parent, QName assocName) { final List<ChildAssociationRef> assocs = nodeService.getChildAssocs(parent, RegexQNamePattern.MATCH_ALL, assocName, true); final NodeRef result; if (assocs == null || assocs.isEmpty()) { result = null; } else if (assocs.size() > 1) { final StringBuilder msg = new StringBuilder(); msg.append("Expected exactly one child node at: ").append(parent).append("/").append(assocName) .append(" but found ").append(assocs == null ? "<null assocs>" : assocs.size()); if (logger.isErrorEnabled()) { logger.error(msg.toString()); } result = assocs.get(0).getChildRef(); } else { result = assocs.get(0).getChildRef(); } return result; } @Override public List<PropertyDefinition> getFacetableProperties() { final List<PropertyDefinition> result = new ArrayList<>(); final List<QName> allContentClasses = CollectionUtils.flatten(dictionaryService.getAllAspects(), dictionaryService.getAllTypes()); for (QName contentClass : allContentClasses) { result.addAll(getFacetableProperties(contentClass)); } return result; } @Override public List<PropertyDefinition> getFacetableProperties(QName contentClass) { final List<PropertyDefinition> result = new ArrayList<>(); final Map<QName, PropertyDefinition> propertyDefs = dictionaryService.getPropertyDefs(contentClass); if (propertyDefs != null) { for (final Map.Entry<QName, PropertyDefinition> prop : propertyDefs.entrySet()) { final PropertyDefinition propDef = prop.getValue(); if (propDef.isIndexed()) //SHA-1308 { final Facetable propIsFacetable = propDef.getFacetable(); switch (propIsFacetable) { case TRUE: result.add(propDef); break; case FALSE: // The value is not facetable. Do nothing. break; case UNSET: // These values may be facetable. final DataTypeDefinition datatype = propDef.getDataType(); if (isNumeric(datatype) || isDateLike(datatype) || isFacetableText(datatype)) { result.add(propDef); break; } break; default: // This should never happen. If it does, it's a programming error. throw new IllegalStateException("Failed to handle " + Facetable.class.getSimpleName() + " type: " + propIsFacetable); } } } } return result; } @Override public List<SyntheticPropertyDefinition> getFacetableSyntheticProperties() { final List<SyntheticPropertyDefinition> result = new ArrayList<>(); final List<QName> allContentClasses = CollectionUtils.flatten(dictionaryService.getAllAspects(), dictionaryService.getAllTypes()); for (QName contentClass : allContentClasses) { result.addAll(getFacetableSyntheticProperties(contentClass)); } return result; } @Override public List<SyntheticPropertyDefinition> getFacetableSyntheticProperties(QName contentClass) { final List<SyntheticPropertyDefinition> result = new ArrayList<>(); final Map<QName, PropertyDefinition> propertyDefs = dictionaryService.getPropertyDefs(contentClass); if (propertyDefs != null) { for (final Map.Entry<QName, PropertyDefinition> prop : propertyDefs.entrySet()) { final PropertyDefinition propDef = prop.getValue(); // Only properties of type cm:content can expand to synthetic properties. if (DataTypeDefinition.CONTENT.equals(propDef.getDataType().getName())) { // We do not want to treat the cm:content property itself as facetable. // It is a content URL whose value is not suitable for facetting. // e.g. 2010/1/22/13/14/6e228904-d5d2-4a99-b7b1-8fe7c03c71f3.bin|mimetype=application/octet-stream|size=728|encoding=UTF-8|locale=en_GB_ // // However there are elements within that content URL which *are* facetable and are correctly treated as such by SOLR. // As these are not actually Alfresco content properties, we must return artificial PropertyDefinition objects: result.add(new SyntheticPropertyDefinition(propDef, "size", DataTypeDefinition.LONG)); result.add(new SyntheticPropertyDefinition(propDef, "mimetype", DataTypeDefinition.TEXT)); } else { // Intentionally empty. Only cm:content's size and mimetype are currently supported. } } } return result; } private boolean isNumeric(DataTypeDefinition datatype) { boolean result; try { Class<?> clazz = Class.forName(datatype.getJavaClassName()); result = Number.class.isAssignableFrom(clazz); } catch (ClassNotFoundException e) { result = false; } return result; } private boolean isDateLike(DataTypeDefinition datatype) { return DataTypeDefinition.DATE.equals(datatype.getName()) || DataTypeDefinition.DATETIME.equals(datatype.getName()); } private boolean isFacetableText(DataTypeDefinition datatype) { // For now at least, we're excluding MLTEXT return DataTypeDefinition.TEXT.equals(datatype.getName()); } }