diff --git a/config/alfresco/model/solrFacetModel.xml b/config/alfresco/model/solrFacetModel.xml index 7ed3d04200..663f0a1c01 100644 --- a/config/alfresco/model/solrFacetModel.xml +++ b/config/alfresco/model/solrFacetModel.xml @@ -18,6 +18,8 @@ + + @@ -155,4 +157,18 @@ cm:folder + + + + + Facet Custom Properties + + + Additional Facet Information + d:text + + + + + \ No newline at end of file diff --git a/config/alfresco/subsystems/Search/solr/facet/solr-facets-config.properties b/config/alfresco/subsystems/Search/solr/facet/solr-facets-config.properties index 4c630e6571..050a1dcd5e 100644 --- a/config/alfresco/subsystems/Search/solr/facet/solr-facets-config.properties +++ b/config/alfresco/subsystems/Search/solr/facet/solr-facets-config.properties @@ -12,7 +12,7 @@ default.cm\:content.mimetype.hitThreshold=1 default.cm\:content.mimetype.minFilterValueLength=4 default.cm\:content.mimetype.sortBy=DESCENDING default.cm\:content.mimetype.scope=SCOPED_SITES -default.cm\:content.mimetype.scopedSites=site1,site2,site3 +default.cm\:content.mimetype.scopedSites= default.cm\:content.mimetype.index=0 default.cm\:content.mimetype.isEnabled=true @@ -25,7 +25,7 @@ default.cm\:description.__.hitThreshold=1 default.cm\:description.__.minFilterValueLength=4 default.cm\:description.__.sortBy=DESCENDING default.cm\:description.__.scope=SCOPED_SITES -default.cm\:description.__.scopedSites=site1,site2,site3 +default.cm\:description.__.scopedSites= default.cm\:description.__.index=1 default.cm\:description.__.isEnabled=true @@ -38,7 +38,7 @@ default.cm\:creator.__.u.hitThreshold=1 default.cm\:creator.__.u.minFilterValueLength=4 default.cm\:creator.__.u.sortBy=ALPHABETICALLY default.cm\:creator.__.u.scope=SCOPED_SITES -default.cm\:creator.__.u.scopedSites=site1,site2,site3 +default.cm\:creator.__.u.scopedSites= default.cm\:creator.__.u.index=2 default.cm\:creator.__.u.isEnabled=true @@ -51,7 +51,7 @@ default.cm\:modifier.__.u.hitThreshold=1 default.cm\:modifier.__.u.minFilterValueLength=4 default.cm\:modifier.__.u.sortBy=ALPHABETICALLY default.cm\:modifier.__.u.scope=SCOPED_SITES -default.cm\:modifier.__.u.scopedSites=site1,site2,site3 +default.cm\:modifier.__.u.scopedSites= default.cm\:modifier.__.u.index=3 default.cm\:modifier.__.u.isEnabled=true @@ -59,40 +59,40 @@ default.cm\:modifier.__.u.isEnabled=true default.cm\:created.filterID=filter_created default.cm\:created.displayName=faceted-search.facet-menu.facet.created default.cm\:created.displayControl=alfresco/search/FacetFilters -default.cm\:created.blockIncludeFacetRequest=true default.cm\:created.maxFilters=5 default.cm\:created.hitThreshold=1 default.cm\:created.minFilterValueLength=4 default.cm\:created.sortBy=ALPHABETICALLY default.cm\:created.scope=SCOPED_SITES -default.cm\:created.scopedSites=site1,site2,site3 +default.cm\:created.scopedSites= default.cm\:created.index=4 default.cm\:created.isEnabled=true +default.cm\:created.EXTRA-PROP.blockIncludeFacetRequest=true # Field-Facet-Qname => cm:modified default.cm\:modified.filterID=filter_modified default.cm\:modified.displayName=faceted-search.facet-menu.facet.modified default.cm\:modified.displayControl=alfresco/search/FacetFilters -default.cm\:modified.blockIncludeFacetRequest=true default.cm\:modified.maxFilters=5 default.cm\:modified.hitThreshold=1 default.cm\:modified.minFilterValueLength=4 default.cm\:modified.sortBy=ALPHABETICALLY default.cm\:modified.scope=SCOPED_SITES -default.cm\:modified.scopedSites=site1,site2,site3 +default.cm\:modified.scopedSites= default.cm\:modified.index=5 default.cm\:modified.isEnabled=true +default.cm\:modified.EXTRA-PROP.blockIncludeFacetRequest=true # Field-Facet-Qname => cm:content.size default.cm\:content.size.filterID=filter_content_size default.cm\:content.size.displayName=faceted-search.facet-menu.facet.size default.cm\:content.size.displayControl=alfresco/search/FacetFilters -default.cm\:content.size.blockIncludeFacetRequest=true default.cm\:content.size.maxFilters=5 default.cm\:content.size.hitThreshold=1 default.cm\:content.size.minFilterValueLength=4 default.cm\:content.size.sortBy=ALPHABETICALLY default.cm\:content.size.scope=SCOPED_SITES -default.cm\:content.size.scopedSites=site1,site2,site3 +default.cm\:content.size.scopedSites= default.cm\:content.size.index=6 default.cm\:content.size.isEnabled=true +default.cm\:content.size.EXTRA-PROP.blockIncludeFacetRequest=true diff --git a/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetConfig.java b/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetConfig.java index 1c4f1a2a54..73ad83c73c 100644 --- a/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetConfig.java +++ b/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetConfig.java @@ -19,15 +19,19 @@ package org.alfresco.repo.search.impl.solr.facet; +import java.io.Serializable; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; +import org.alfresco.repo.search.impl.solr.facet.SolrFacetProperties.CustomProperties; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.PropertyCheck; @@ -47,6 +51,7 @@ import org.springframework.extensions.surf.util.AbstractLifecycleBean; * + * Also, if there is a need to add additional properties, the following needs to be + * put into a properties file: + * * The inheritance order is strictly defined using property:
* ${solr_facets.inheritanceHierarchy}
* The default inheritance orders are:
@@ -69,6 +80,9 @@ public class SolrFacetConfig extends AbstractLifecycleBean { private static final Log logger = LogFactory.getLog(SolrFacetConfig.class); + private static final String KEY_EXTRA_INFO = ".EXTRA-PROP."; + private static final int KEY_EXTRA_INFO_LENGTH = KEY_EXTRA_INFO.length(); + private final Properties rawProperties; private final Set propInheritanceOrder; @@ -163,15 +177,39 @@ public class SolrFacetConfig extends AbstractLifecycleBean } } - Set facetFields = new HashSet<>(); + Map> facetFields = new HashMap<>(); for(String key : propValues.keySet()) { - facetFields.add(key.substring(0, key.lastIndexOf('.'))); + String facetQName = null; + Set extraProp = null; + int index = key.indexOf(KEY_EXTRA_INFO); + if (index > 0) + { + String extraInfo = key.substring(index + KEY_EXTRA_INFO_LENGTH); + facetQName = key.substring(0, index); + + extraProp = facetFields.get(facetQName); + if (extraProp == null) + { + extraProp = new HashSet<>(); + } + if (extraInfo.length() > 0) + { + extraProp.add(extraInfo); + } + } + else + { + index = key.lastIndexOf('.'); + facetQName = key.substring(0, index); + extraProp = facetFields.get(facetQName); + } + facetFields.put(facetQName, extraProp); } // Build the facet config objects Map facetProperties = new HashMap<>(100); - for (String field : facetFields) + for (String field : facetFields.keySet()) { // FacetProperty attributes // Resolve facet field into QName @@ -186,7 +224,12 @@ public class SolrFacetConfig extends AbstractLifecycleBean String scope = propValues.get(ValueName.PROP_SCOPE.getPropValueName(field)); Set scopedSites = getScopedSites(propValues.get(ValueName.PROP_SCOPED_SITES.getPropValueName(field))); int index = getIntegerValue(propValues.get(ValueName.PROP_INDEX.getPropValueName(field))); + if(index < 0) + { + throw new SolrFacetConfigException("Index must be greater than or equal to 0"); + } boolean isEnabled = Boolean.valueOf(propValues.get(ValueName.PROP_IS_ENABLED.getPropValueName(field))); + Set customProps = getCustomProps(facetFields.get(field), field, propValues); // Construct the FacetProperty object SolrFacetProperties fp = new SolrFacetProperties.Builder() @@ -202,8 +245,9 @@ public class SolrFacetConfig extends AbstractLifecycleBean .index(index) .isEnabled(isEnabled) .isDefault(true) - .scopedSites(scopedSites).build(); - + .scopedSites(scopedSites) + .customProperties(customProps).build(); + facetProperties.put(filterID, fp); } @@ -283,6 +327,35 @@ public class SolrFacetConfig extends AbstractLifecycleBean } return set; } + + private static Set getCustomProps(Set additionalProps, String field, Map propValues) + { + if (additionalProps == null) + { + return Collections.emptySet(); + } + + Set customProps = new HashSet<>(); + for (String extraInfo : additionalProps) + { + String value = propValues.get(field + KEY_EXTRA_INFO + extraInfo); + if (value != null) + { + QName qName = QName.createQName(SolrFacetModel.SOLR_FACET_CUSTOM_PROPERTY_URL, extraInfo); + String[] extra = value.split(","); + if (extra.length == 1) + { + customProps.add(new CustomProperties(qName, null, null, extra[0])); + } + else + { + List list = Arrays.asList(extra); + customProps.add(new CustomProperties(qName, null, null, (Serializable) list)); + } + } + } + return customProps; + } private static int getIntegerValue(String propValue) { diff --git a/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetModel.java b/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetModel.java index 1e0d597262..2b3f27a4ff 100644 --- a/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetModel.java +++ b/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetModel.java @@ -31,8 +31,13 @@ public interface SolrFacetModel public static final String SOLR_FACET_MODEL_URL = "http://www.alfresco.org/model/solrfacet/1.0"; public static final String PREFIX = "srft"; + public static final String SOLR_FACET_CUSTOM_PROPERTY_URL = "http://www.alfresco.org/model/solrfacetcustomproperty/1.0"; + public static final String SOLR_FACET_CUSTOM_PROPERTY_PREFIX = "srftcustom"; + public static final QName TYPE_FACET_FIELD = QName.createQName(SOLR_FACET_MODEL_URL, "facetField"); + public static final QName ASPECT_CUSTOM_PROPERTIES = QName.createQName(SOLR_FACET_MODEL_URL, "customProperties"); + public static final QName PROP_FIELD_TYPE = QName.createQName(SOLR_FACET_MODEL_URL, "fieldType"); public static final QName PROP_FIELD_LABEL = QName.createQName(SOLR_FACET_MODEL_URL, "fieldLabel"); @@ -56,4 +61,6 @@ public interface SolrFacetModel public static final QName PROP_IS_ENABLED = QName.createQName(SOLR_FACET_MODEL_URL, "isEnabled"); public static final QName PROP_IS_DEFAULT = QName.createQName(SOLR_FACET_MODEL_URL, "isDefault"); + + public static final QName PROP_EXTRA_INFORMATION = QName.createQName(SOLR_FACET_CUSTOM_PROPERTY_URL, "extraInformation"); } diff --git a/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetProperties.java b/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetProperties.java index b2f03fc997..49938973c6 100644 --- a/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetProperties.java +++ b/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetProperties.java @@ -19,20 +19,23 @@ package org.alfresco.repo.search.impl.solr.facet; +import java.io.Serializable; import java.util.Collections; import java.util.HashSet; import java.util.Set; import org.alfresco.service.namespace.QName; +import org.alfresco.util.EqualsHelper; /** - * Domain-Specific Language (DSL) style builder class for encapsulating the - * facet properties. + * Domain-Specific Language (DSL) style builder class for encapsulating the facet properties. * * @author Jamal Kaabi-Mofrad */ -public class SolrFacetProperties implements Comparable +public class SolrFacetProperties implements Serializable { + private static final long serialVersionUID = 2991173095752087202L; + private final String filterID; private final QName facetQName; private final String displayName; @@ -46,6 +49,7 @@ public class SolrFacetProperties implements Comparable private final int index; private final boolean isEnabled; private final boolean isDefault; // is loaded from properties files? + private final Set customProperties; /** * Initialises a newly created SolrFacetProperty object @@ -66,7 +70,8 @@ public class SolrFacetProperties implements Comparable this.index = builder.index; this.isEnabled = builder.isEnabled; this.isDefault = builder.isDefault; - this.scopedSites = (builder.scopedSites == null) ? null :Collections.unmodifiableSet(new HashSet(builder.scopedSites)); + this.scopedSites = Collections.unmodifiableSet(new HashSet(builder.scopedSites)); + this.customProperties = Collections.unmodifiableSet(new HashSet(builder.customProperties)); } /** @@ -142,16 +147,12 @@ public class SolrFacetProperties implements Comparable } /** - * Returns an unmodifiable view of the Scoped Sites set or null + * Returns an unmodifiable view of the Scoped Sites set. Never null. * * @return the scopedSites */ public Set getScopedSites() { - if (this.scopedSites == null) - { - return null; - } return Collections.unmodifiableSet(new HashSet(this.scopedSites)); } @@ -181,6 +182,17 @@ public class SolrFacetProperties implements Comparable return this.isDefault; } + /** + * Returns an unmodifiable view of the custom properties set. Never null. + * + * @return the customProperties + */ + public Set getCustomProperties() + { + return Collections.unmodifiableSet(new HashSet(this.customProperties)); + } + + /* * @see java.lang.Object#hashCode() */ @@ -226,22 +238,13 @@ public class SolrFacetProperties implements Comparable return true; } - /* - * @see java.lang.Comparable#compareTo(T) - */ - @Override - public int compareTo(SolrFacetProperties that) - { - return Integer.compare(this.index, that.index); - } - /* * @see java.lang.Object#toString() */ @Override public String toString() { - StringBuilder sb = new StringBuilder(320); + StringBuilder sb = new StringBuilder(400); sb.append("FacetProperty [filterID=").append(this.filterID).append(", facetQName=") .append(this.facetQName).append(", displayName=").append(this.displayName) .append(", displayControl=").append(this.displayControl).append(", maxFilters=") @@ -249,7 +252,8 @@ public class SolrFacetProperties implements Comparable .append(", minFilterValueLength=").append(this.minFilterValueLength).append(", sortBy=") .append(this.sortBy).append(", scope=").append(this.scope).append(", scopedSites=") .append(this.scopedSites).append(", index=").append(this.index).append(", isEnabled=").append(this.isEnabled) - .append(", isDefault=").append(this.isDefault).append("]"); + .append(", isDefault=").append(this.isDefault).append(", customProperties=").append(this.customProperties) + .append("]"); return sb.toString(); } @@ -264,10 +268,38 @@ public class SolrFacetProperties implements Comparable private int minFilterValueLength; private String sortBy; private String scope; - private Set scopedSites; + private Set scopedSites = Collections.emptySet(); private int index; private boolean isEnabled; private boolean isDefault; + private Set customProperties = Collections.emptySet(); + + public Builder() + { + } + + /** + * Copy builder + * + * @param that existing {@code SolrFacetProperties} object + */ + public Builder(SolrFacetProperties that) + { + this.filterID = that.filterID; + this.facetQName = that.facetQName; + this.displayName = that.displayName; + this.displayControl = that.displayControl; + this.maxFilters = that.maxFilters; + this.hitThreshold = that.hitThreshold; + this.minFilterValueLength = that.minFilterValueLength; + this.sortBy = that.sortBy; + this.scope = that.scope; + this.scopedSites = that.scopedSites; + this.index = that.index; + this.isEnabled = that.isEnabled; + this.isDefault = that.isDefault; + this.customProperties = that.customProperties; + } public Builder filterID(String filterID) { @@ -325,7 +357,10 @@ public class SolrFacetProperties implements Comparable public Builder scopedSites(Set scopedSites) { - this.scopedSites = scopedSites; + if (scopedSites != null) + { + this.scopedSites = scopedSites; + } return this; } @@ -347,9 +382,106 @@ public class SolrFacetProperties implements Comparable return this; } + public Builder customProperties(Set customProperties) + { + if (customProperties != null) + { + this.customProperties = customProperties; + } + return this; + } + public SolrFacetProperties build() { return new SolrFacetProperties(this); } } + + public static class CustomProperties implements Serializable + { + private static final long serialVersionUID = 2250062300454166258L; + + private final QName name; + private final String title; + private final String type; + private final Serializable value; + + public CustomProperties(QName name, String title, String type, Serializable value) + { + this.name = name; + this.title = title; + this.type = type; + this.value = value; + } + + public QName getName() + { + return this.name; + } + + /** + * @return the title + */ + public String getTitle() + { + return this.title; + } + + /** + * @return the type + */ + public String getType() + { + return this.type; + } + + public Serializable getValue() + { + return this.value; + } + + /* + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); + return result; + } + + /* + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) + { + if (this == obj) + { + return true; + } + if (obj == null || !(obj instanceof CustomProperties)) + { + return false; + } + CustomProperties other = (CustomProperties) obj; + return EqualsHelper.nullSafeEquals(this.name, other.name); + } + + /* + * @see java.lang.Object#toString() + */ + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(100); + builder.append("CustomProperties [name=").append(this.name).append(", title=") + .append(this.title).append(", type=").append(this.type).append(", value=") + .append(this.value).append("]"); + return builder.toString(); + } + + } } diff --git a/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetService.java b/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetService.java index a751aab5e6..c26f23d943 100644 --- a/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetService.java +++ b/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetService.java @@ -19,7 +19,7 @@ package org.alfresco.repo.search.impl.solr.facet; -import java.util.Map; +import java.util.List; import org.alfresco.service.cmr.repository.NodeRef; @@ -34,10 +34,9 @@ public interface SolrFacetService /** * Gets all the available facets. * - * @return Map of {@code SolrFacetProperties} with the - * {@code SolrFacetProperties.filterID} as the key or an empty map if none exists + * @return List of {@code SolrFacetProperties} or an empty list if none exists */ - public Map getFacets(); + public List getFacets(); /** * Gets the facet by filter Id. @@ -86,4 +85,6 @@ public interface SolrFacetService * @param filterID the filter Id */ public void deleteFacet(String filterID); + + public int getNextIndex(); } diff --git a/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetServiceImpl.java b/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetServiceImpl.java index dfa1d514a7..366f84e6e6 100644 --- a/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetServiceImpl.java +++ b/source/java/org/alfresco/repo/search/impl/solr/facet/SolrFacetServiceImpl.java @@ -21,24 +21,30 @@ package org.alfresco.repo.search.impl.solr.facet; import java.io.Serializable; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.NavigableMap; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentSkipListMap; + import org.alfresco.model.ContentModel; import org.alfresco.repo.cache.SimpleCache; import org.alfresco.repo.node.NodeServicePolicies; import org.alfresco.repo.node.NodeServicePolicies.BeforeDeleteNodePolicy; +import org.alfresco.repo.node.NodeServicePolicies.BeforeUpdateNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.OnCreateNodePolicy; import org.alfresco.repo.node.NodeServicePolicies.OnUpdateNodePolicy; 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.SolrFacetProperties.CustomProperties; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.transaction.RetryingTransactionHelper; @@ -60,14 +66,13 @@ import org.springframework.context.ApplicationEvent; import org.springframework.extensions.surf.util.AbstractLifecycleBean; /** - * - * * @author Jamal Kaabi-Mofrad */ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrFacetService, NodeServicePolicies.OnCreateNodePolicy, NodeServicePolicies.OnUpdateNodePolicy, - NodeServicePolicies.BeforeDeleteNodePolicy + NodeServicePolicies.BeforeDeleteNodePolicy, + NodeServicePolicies.BeforeUpdateNodePolicy { private static final Log logger = LogFactory.getLog(SolrFacetServiceImpl.class); /** @@ -76,7 +81,7 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF private static final String ALFRESCO_SEARCH_ADMINISTRATORS_AUTHORITY = "ALFRESCO_SEARCH_ADMINISTRATORS"; private 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"); @@ -87,12 +92,13 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF private RetryingTransactionHelper retryingTransactionHelper; private BehaviourFilter behaviourFilter; private PolicyComponent policyComponent; - private SolrFacetConfig facetConfig; + private SolrFacetConfig facetConfig; private String facetsRootXPath; private SimpleCache singletonCache; // eg. for facetsHomeNodeRef private final String KEY_FACETS_HOME_NODEREF = "key.facetshome.noderef"; private SimpleCache facetNodeRefCache; // for filterID to nodeRef lookup - private ConcurrentMap facetsMap = new ConcurrentHashMap<>(); + private NavigableMap facetsMap = new ConcurrentSkipListMap<>(); + private int maxAllowedFilters = 100; /** * @param authorityService the authorityService to set @@ -182,6 +188,14 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF this.facetNodeRefCache = facetNodeRefCache; } + /** + * @param maxAllowedFilters the maxAllowedFilters to set + */ + public void setMaxAllowedFilters(int maxAllowedFilters) + { + this.maxAllowedFilters = maxAllowedFilters; + } + @Override public boolean isSearchAdmin(String userName) { @@ -194,16 +208,26 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF } @Override - public Map getFacets() + public List getFacets() { - Map sortedMap = CollectionUtils.sortMapByValue(facetsMap); - return sortedMap; + return new ArrayList<>(facetsMap.values()); } @Override public SolrFacetProperties getFacet(String filterID) { - return facetsMap.get(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) ? getDefaultLoadedFacet(filterID) : getFacetProperties(nodeRef); + } + + private SolrFacetProperties getDefaultLoadedFacet(String filterID) + { + return facetConfig.getDefaultFacets().get(filterID); } @Override @@ -268,6 +292,12 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF List scSites = (List) properties.get(SolrFacetModel.PROP_SCOPED_SITES); Set scopedSites = (scSites == null) ? null : new HashSet<>(scSites); + Map customProperties = getFacetCustomProperties(properties); + Set extraProps = new HashSet<>(customProperties.size()); + for(Entry cp : customProperties.entrySet()) + { + extraProps.add(new CustomProperties(cp.getKey(), (String) properties.get(ContentModel.PROP_TITLE), null, cp.getValue())); + } // Construct the FacetProperty object SolrFacetProperties fp = new SolrFacetProperties.Builder() .filterID(filterID) @@ -282,7 +312,8 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF .index(index) .isEnabled(isEnabled) .isDefault(isDefault) - .scopedSites(scopedSites).build(); + .scopedSites(scopedSites) + .customProperties(extraProps).build(); return fp; } @@ -298,9 +329,9 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF final String filterID = facetProperties.getFilterID(); NodeRef facetNodeRef = getFacetNodeRef(filterID); - // We need to check the bootstrapped Facet properties 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 && getFacet(filterID) != null)) + // 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 && getDefaultLoadedFacet(filterID) != null)) { throw new SolrFacetConfigException("Unable to create facet because the filterID [" + filterID + "] is already in use."); } @@ -312,7 +343,7 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF throw new SolrFacetConfigException("Facets root folder does not exist."); } - return facetNodeRef = AuthenticationUtil.runAs(new RunAsWork() + return facetNodeRef = AuthenticationUtil.runAs(new RunAsWork() { @Override public NodeRef doWork() throws Exception @@ -324,7 +355,7 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF behaviourFilter.disableBehaviour(facetRoot, ContentModel.ASPECT_AUDITABLE); try { - Map properties = createNodeProperties(facetProperties, true); + Map properties = createNodeProperties(facetProperties); // We don't want the node to be indexed properties.put(ContentModel.PROP_IS_INDEXED, false); NodeRef ref = nodeService.createNode(facetRoot, ContentModel.ASSOC_CONTAINS, @@ -355,12 +386,12 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF NodeRef facetNodeRef = getFacetNodeRef(filterID); if (facetNodeRef == null) { - SolrFacetProperties fp = getFacet(filterID); + SolrFacetProperties fp = getDefaultLoadedFacet(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. + // update the default properties for the first time. createFacetNodeImpl(facetProperties, false); } else @@ -370,7 +401,12 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF } else { - Map properties = createNodeProperties(facetProperties, false); + String name = (String) nodeService.getProperty(facetNodeRef, ContentModel.PROP_NAME); + if (!filterID.equals(name)) + { + throw new SolrFacetConfigException("The filterID cannot be renamed."); + } + Map properties = createNodeProperties(facetProperties); // Set the updated properties back onto the facet node reference this.nodeService.setProperties(facetNodeRef, properties); } @@ -384,17 +420,16 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF public void deleteFacet(String filterID) { NodeRef facetNodeRef = getFacetNodeRef(filterID); - SolrFacetProperties defaultFP = facetConfig.getDefaultFacets().get(filterID); - if(defaultFP != null) - { - throw new SolrFacetConfigException("The default [" + filterID + "] facet cannot be deleted. It can only be disabled."); - } - - if(facetNodeRef == null) + if (facetNodeRef == null) { throw new SolrFacetConfigException("The [" + filterID + "] facet cannot be found."); } - + + SolrFacetProperties defaultFP = getDefaultLoadedFacet(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()) { @@ -402,19 +437,17 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF } } - private Map createNodeProperties(SolrFacetProperties facetProperties, boolean withFilterId) + private Map createNodeProperties(SolrFacetProperties facetProperties) { if (facetProperties.getFilterID() == null) { throw new SolrFacetConfigException("Filter Id cannot be null."); } - Map properties = new HashMap(14); + Set customProperties = facetProperties.getCustomProperties(); + Map properties = new HashMap(14 + customProperties.size()); - if (withFilterId) - { - properties.put(ContentModel.PROP_NAME, facetProperties.getFilterID()); - } + properties.put(ContentModel.PROP_NAME, facetProperties.getFilterID()); properties.put(SolrFacetModel.PROP_FIELD_TYPE, facetProperties.getFacetQName()); properties.put(SolrFacetModel.PROP_FIELD_LABEL, facetProperties.getDisplayName()); properties.put(SolrFacetModel.PROP_DISPLAY_CONTROL, facetProperties.getDisplayControl()); @@ -427,9 +460,14 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF properties.put(SolrFacetModel.PROP_INDEX, facetProperties.getIndex()); properties.put(SolrFacetModel.PROP_IS_ENABLED, facetProperties.isEnabled()); - SolrFacetProperties fp = facetConfig.getDefaultFacets().get(facetProperties.getFilterID()); + SolrFacetProperties fp = getDefaultLoadedFacet(facetProperties.getFilterID()); properties.put(SolrFacetModel.PROP_IS_DEFAULT, (fp == null) ? false : fp.isDefault()); + for (CustomProperties cp : customProperties) + { + properties.put(cp.getName(), cp.getValue()); + } + return properties; } @@ -467,7 +505,6 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF } return facetHomeRef; } - @Override protected void onBootstrap(ApplicationEvent event) @@ -478,46 +515,89 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF SolrFacetModel.TYPE_FACET_FIELD, new JavaBehaviour(this, "onCreateNode")); + // Filter before update + this.policyComponent.bindClassBehaviour( + BeforeUpdateNodePolicy.QNAME, + SolrFacetModel.TYPE_FACET_FIELD, + new JavaBehaviour(this, "beforeUpdateNode")); + // Filter update this.policyComponent.bindClassBehaviour( OnUpdateNodePolicy.QNAME, SolrFacetModel.TYPE_FACET_FIELD, new JavaBehaviour(this, "onUpdateNode")); - // Filter deletion + // Filter before deletion this.policyComponent.bindClassBehaviour( BeforeDeleteNodePolicy.QNAME, SolrFacetModel.TYPE_FACET_FIELD, new JavaBehaviour(this, "beforeDeleteNode")); + Map mergedMap = new HashMap<>(100); + // Loaded facets Map defaultFP = facetConfig.getDefaultFacets(); - for(Entry fpEntry : defaultFP.entrySet()) - { - facetsMap.put(fpEntry.getKey(), fpEntry.getValue()); - } - - List persistedProperties = getPersistedFacetProperties(); + mergedMap.putAll(defaultFP); + + // Persisted facets + Map persistedProperties = getPersistedFacetProperties(); // The persisted facets will override the default facets - for(SolrFacetProperties fp : persistedProperties) + mergedMap.putAll(persistedProperties); + + // Sort the merged maps + Map sortedMap = CollectionUtils.sortMapByValue(mergedMap, getIndextComparator()); + LinkedList orderedFacets = new LinkedList<>(sortedMap.values()); + + // Get the last index, as the map is sorted by the FP's index value + int maxIndex = orderedFacets.getLast().getIndex(); + int previousIndex = -1; + SolrFacetProperties previousFP = null; + for (SolrFacetProperties facet : orderedFacets) { - facetsMap.put(fp.getFilterID(), fp); + String filterID = facet.getFilterID(); + int index = facet.getIndex(); + if (index == previousIndex) + { + // we can be sure that previousFP is never null, as we don't + // allow the index to be -1; + if (defaultFP.get(previousFP.getFilterID()) != null && persistedProperties.get(filterID) != null) + { + SolrFacetProperties updatedPreviousFacet = new SolrFacetProperties.Builder(previousFP).index(++maxIndex).build(); + mergedMap.put(previousFP.getFilterID(), updatedPreviousFacet); + mergedMap.put(filterID, facet); + } + else + { + SolrFacetProperties updatedCurrentFacet = new SolrFacetProperties.Builder(facet).index(++maxIndex).build(); + mergedMap.put(updatedCurrentFacet.getFilterID(), updatedCurrentFacet); + } + } + else + { + mergedMap.put(filterID, facet); + } + previousIndex = index; + previousFP = facet; } + for (SolrFacetProperties fp : mergedMap.values()) + { + facetsMap.put(fp.getIndex(), fp); + } if (logger.isDebugEnabled() && persistedProperties.size() > 0) { logger.debug("The facets [" + persistedProperties + "] have overridden their matched default facets."); } } - - private List getPersistedFacetProperties() + + private Map getPersistedFacetProperties() { List list = nodeService.getChildAssocs(getFacetsRoot()); - List facets = new ArrayList<>(list.size()); + Map facets = new HashMap<>(list.size()); for (ChildAssociationRef associationRef : list) { SolrFacetProperties fp = getFacetProperties(associationRef.getChildRef()); - facets.add(fp); + facets.put(fp.getFilterID(), fp); } return facets; } @@ -528,18 +608,26 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF // nothing to do } + @Override + public void beforeUpdateNode(NodeRef nodeRef) + { + // Remove the facet, in order to not end up with duplicate facets but different index + SolrFacetProperties fp = getFacetProperties(nodeRef); + this.facetsMap.remove(fp.getIndex()); + } + @Override public void onUpdateNode(NodeRef nodeRef) { SolrFacetProperties fp = getFacetProperties(nodeRef); - this.facetsMap.put(fp.getFilterID(), fp); + this.facetsMap.put(fp.getIndex(), fp); } @Override public void onCreateNode(ChildAssociationRef childAssocRef) { SolrFacetProperties fp = getFacetProperties(childAssocRef.getChildRef()); - this.facetsMap.put(fp.getFilterID(), fp); + this.facetsMap.put(fp.getIndex(), fp); this.facetNodeRefCache.put(fp.getFilterID(), childAssocRef.getChildRef()); } @@ -547,7 +635,106 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF public void beforeDeleteNode(NodeRef nodeRef) { String filterID = (String) nodeService.getProperty(nodeRef, ContentModel.PROP_NAME); - this.facetsMap.remove(filterID); + int index = (Integer) nodeService.getProperty(nodeRef, SolrFacetModel.PROP_INDEX); + + this.facetsMap.remove(index); this.facetNodeRefCache.remove(filterID); } + + /** + * Note: this comparator imposes orderings that are inconsistent with equals + * method of the {@link SolrFacetProperties}." + * + * @return + */ + private Comparator> getIndextComparator() + { + return new Comparator>() + { + public int compare(Entry facet1, + Entry facet2) + { + return Integer.compare(facet1.getValue().getIndex(), facet2.getValue().getIndex()); + } + }; + } + + @Override + public int getNextIndex() + { + synchronized (facetsMap) + { + if (facetsMap.size() >= maxAllowedFilters) + { + throw new SolrFacetConfigException("You have reached the maximum number of allowed filters. Please delete an existing filter in order to make a new one!"); + } + int max = facetsMap.lastKey(); + if (max >= Integer.MAX_VALUE) + { + reorder(); + max = facetsMap.lastKey(); + } + + return max + 1; + } + } + + /** + * Gets a map containing the facet's custom properties + * + * @return Map map containing the custom properties of the facet + */ + private Map getFacetCustomProperties(Map properties) + { + Map customProperties = new HashMap(5); + + for (Map.Entry entry : properties.entrySet()) + { + if (SolrFacetModel.SOLR_FACET_CUSTOM_PROPERTY_URL.equals(entry.getKey().getNamespaceURI())) + { + customProperties.put(entry.getKey(), entry.getValue()); + } + } + return customProperties; + } + + /** + * This will reorder the facetsMap, hence, the invoker needs to use an + * appropriate locking mechanism + */ + private void reorder() + { + boolean order = false; + int previous = 0; + for (int i : facetsMap.keySet()) + { + if (i != previous) + { + order = true; + break; + } + previous++; + } + + if (order) + { + Map tempMap = new LinkedHashMap<>(); + int index = 0; + for (SolrFacetProperties fp : facetsMap.values()) + { + if (fp.getIndex() != index) + { + fp = new SolrFacetProperties.Builder(fp).index(index).build(); + } + tempMap.put(index, fp); + index++; + } + facetsMap.clear(); + + for (SolrFacetProperties fp : tempMap.values()) + { + updateFacet(fp); + } + } + } }