Merged HEAD-BUG-FIX (5.0/Cloud) to HEAD (5.0/Cloud)

80628: Merged WAT1 (5.0/Cloud) to HEAD-BUG-FIX (5.0/Cloud)
      76618: Initial commit of Java service changes for facet reordering. Part of ACE-1582.
      Facet persistence has not been changed, but a property has been added to the folder
      which contains the facet nodes. This property 'facetOrder' holds a sequence of strings
      which are the ordered facet IDs.
      Facets are now returned from the SolrFacetService in a sorted order, where this explicit
      'facetOrder' is the primary source for sort order.
      The sorting algorithm falls back to using indexes if no order is available.
      It then falls back to using alphabetic sorting if no index is available.
      These last two scenarios are likely to be corner cases.
      Still to do: some enhancements to the Java service, webscripts as REST API endpoints.
                   the REST API will provide for reordering of existing facets.


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@82922 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Will Abson
2014-09-03 16:12:05 +00:00
parent 3599152c64
commit 76e5df2cb7
9 changed files with 498 additions and 22 deletions

View File

@@ -151,10 +151,24 @@
</property> </property>
</properties> </properties>
</type> </type>
<!-- Facets Root Folder --> <!-- Facets Root Folder -->
<type name="srft:facets"> <type name="srft:facets">
<title>Facets</title> <title>Facets</title>
<parent>cm:folder</parent> <parent>cm:folder</parent>
<properties>
<property name="srft:facetOrder">
<title>Ordered sequence of Facet IDs</title>
<type>d:text</type>
<mandatory>false</mandatory>
<multiple>true</multiple>
<index enabled="false">
<atomic>false</atomic>
<stored>false</stored>
<tokenised>false</tokenised>
</index>
</property>
</properties>
</type> </type>
</types> </types>

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2005-2014 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* 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/>.
*/
package org.alfresco.repo.search.impl.solr.facet;
import org.alfresco.error.AlfrescoRuntimeException;
/** These exceptions are thrown by the {@link SolrFacetService}. */
public class Exceptions extends AlfrescoRuntimeException
{
private static final long serialVersionUID = 1L;
// Constructors for the basic SolrFacet Exception itself.
public Exceptions() { this("", null); }
public Exceptions(String message) { this(message, null); }
public Exceptions(Throwable cause) { this("", null); }
public Exceptions(String message, Throwable cause) { super(message, cause); }
/** This exception is used to signal a bad parameter. */
public static class IllegalArgument extends Exceptions
{
private static final long serialVersionUID = 1L;
public IllegalArgument() { super(); }
public IllegalArgument(String message) { super(message); }
}
public static class MissingFacetId extends IllegalArgument
{
private static final long serialVersionUID = 1L;
public MissingFacetId() { super(); }
public MissingFacetId(String message) { super(message); }
}
public static class DuplicateFacetId extends IllegalArgument
{
private static final long serialVersionUID = 1L;
private final String facetId;
public DuplicateFacetId(String facetId)
{
this("", facetId);
}
public DuplicateFacetId(String message, String facetId)
{
super(message);
this.facetId = facetId;
}
public String getFacetId() { return this.facetId; }
}
public static class UnrecognisedFacetId extends IllegalArgument
{
private static final long serialVersionUID = 1L;
private final String facetId;
public UnrecognisedFacetId(String facetId)
{
this("", facetId);
}
public UnrecognisedFacetId(String message, String facetId)
{
super(message);
this.facetId = facetId;
}
public String getFacetId() { return this.facetId; }
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright (C) 2005-2014 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* 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/>.
*/
package org.alfresco.repo.search.impl.solr.facet;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import org.alfresco.util.Pair;
/** This comparator defines the default sort order for facets. */
public class SolrFacetComparator implements Comparator<SolrFacetProperties>
{
/** A sequence of facet IDs which defines their order, as used in REST API &amp; UI. */
private final List<String> sortedIDs;
public SolrFacetComparator(List<String> sortedIDs)
{
this.sortedIDs = new ArrayList<>();
if (sortedIDs != null) { this.sortedIDs.addAll(sortedIDs); }
}
@Override public int compare(SolrFacetProperties facet1, SolrFacetProperties facet2)
{
Pair<Integer, Integer> facetIndicesInSortedList = find(facet1, facet2);
if (bothSorted(facetIndicesInSortedList))
{
// Sorting is by position in the sortedIDs list.
return facetIndicesInSortedList.getFirst() - facetIndicesInSortedList.getSecond();
}
else if (neitherSorted(facetIndicesInSortedList))
{
// Sorting is by the index value defined in the facet itself.
final int indexDifference = facet1.getIndex() - facet2.getIndex();
if (indexDifference == 0)
{
// This could happen if an end user defines/overrides indexes to be equal.
// We'll sort based on facet ID if it does happen.
return facet1.getFilterID().compareTo(facet2.getFilterID());
}
else
{
return indexDifference;
}
}
else
{
// One is in the sortedIDs list and one is not.
// All we want in this case is predictability. The order should be the same.
// We'll (arbitrarily) have facets without an explicit position go at the end.
return facetIndicesInSortedList.getSecond() == -1 ? -1 : 1;
}
}
/** Get the positional indices of the provided {@link SolrFacetProperties} in the {@link #sortedIDs}. */
private Pair<Integer, Integer> find(SolrFacetProperties facet1, SolrFacetProperties facet2)
{
return new Pair<>(sortedIDs.indexOf(facet1.getFilterID()),
sortedIDs.indexOf(facet2.getFilterID()));
}
/** Are both of the provided positional indexes in the {@link #sortedIDs}? */
private boolean bothSorted(Pair<Integer, Integer> indices)
{ return indices.getFirst() != -1 && indices.getSecond() != -1; }
/** Are neither of the provided positional indexes in the {@link #sortedIDs}? */
private boolean neitherSorted(Pair<Integer, Integer> indices)
{ return indices.getFirst() == -1 && indices.getSecond() == -1; }
}

View File

@@ -63,4 +63,9 @@ public interface SolrFacetModel
public static final QName PROP_IS_DEFAULT = QName.createQName(SOLR_FACET_MODEL_URL, "isDefault"); 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"); public static final QName PROP_EXTRA_INFORMATION = QName.createQName(SOLR_FACET_CUSTOM_PROPERTY_URL, "extraInformation");
/** The type of the facet container folder. */
public static final QName TYPE_FACETS = QName.createQName(SOLR_FACET_MODEL_URL, "facets");
public static final QName PROP_FACET_ORDER = QName.createQName(SOLR_FACET_MODEL_URL, "facetOrder");
} }

View File

@@ -21,6 +21,10 @@ package org.alfresco.repo.search.impl.solr.facet;
import java.util.List; import java.util.List;
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.Exceptions.UnrecognisedFacetId;
import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeRef;
/** /**
@@ -87,4 +91,15 @@ public interface SolrFacetService
public void deleteFacet(String filterID); public void deleteFacet(String filterID);
public int getNextIndex(); public int getNextIndex();
/**
* Reorders existing facets to the provided order.
*
* @param filterIds an ordered sequence of filter IDs.
* @throws NullPointerException if filterIds is {@code null}.
* @throws MissingFacetId if the list is empty.
* @throws UnrecognisedFacetId if any of the provided filter IDs are not recognised.
* @throws DuplicateFacetId if there is a duplicate filter ID in the list.
*/
public void reorderFacets(List<String> filterIds);
} }

View File

@@ -31,9 +31,10 @@ import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.NavigableMap; import java.util.NavigableMap;
import java.util.Set; import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ConcurrentSkipListMap;
import org.alfresco.model.ContentModel; import org.alfresco.model.ContentModel;
import org.alfresco.repo.cache.SimpleCache; import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.node.NodeServicePolicies; import org.alfresco.repo.node.NodeServicePolicies;
@@ -44,6 +45,10 @@ import org.alfresco.repo.node.NodeServicePolicies.OnUpdateNodePolicy;
import org.alfresco.repo.policy.BehaviourFilter; import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.policy.JavaBehaviour; import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent; 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.Exceptions.UnrecognisedFacetId;
import org.alfresco.repo.search.impl.solr.facet.SolrFacetProperties.CustomProperties; import org.alfresco.repo.search.impl.solr.facet.SolrFacetProperties.CustomProperties;
import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
@@ -210,7 +215,22 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF
@Override @Override
public List<SolrFacetProperties> getFacets() public List<SolrFacetProperties> getFacets()
{ {
return new ArrayList<>(facetsMap.values()); // Sort the facets into display order
final SolrFacetComparator comparator = new SolrFacetComparator(getFacetOrder());
SortedSet<SolrFacetProperties> result = new TreeSet<>(comparator);
result.addAll(facetsMap.values());
return new ArrayList<>(result);
}
public List<String> getFacetOrder()
{
final NodeRef facetContainer = getFacetsRoot();
@SuppressWarnings("unchecked")
final List<String> facetOrder = (List<String>) nodeService.getProperty(facetContainer, SolrFacetModel.PROP_FACET_ORDER);
return facetOrder;
} }
@Override @Override
@@ -435,6 +455,8 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF
{ {
logger.debug("Deleted [" + filterID + "] facet."); logger.debug("Deleted [" + filterID + "] facet.");
} }
// TODO Remove the matching filterID from the property list on the container.
} }
private Map<QName, Serializable> createNodeProperties(SolrFacetProperties facetProperties) private Map<QName, Serializable> createNodeProperties(SolrFacetProperties facetProperties)
@@ -544,7 +566,8 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF
mergedMap.putAll(persistedProperties); mergedMap.putAll(persistedProperties);
// Sort the merged maps // Sort the merged maps
Map<String, SolrFacetProperties> sortedMap = CollectionUtils.sortMapByValue(mergedMap, getIndextComparator()); Comparator<Entry<String, SolrFacetProperties>> entryComparator = CollectionUtils.toEntryComparator(new SolrFacetComparator(getFacetOrder()));
Map<String, SolrFacetProperties> sortedMap = CollectionUtils.sortMapByValue(mergedMap, entryComparator);
LinkedList<SolrFacetProperties> orderedFacets = new LinkedList<>(sortedMap.values()); LinkedList<SolrFacetProperties> orderedFacets = new LinkedList<>(sortedMap.values());
// Get the last index, as the map is sorted by the FP's index value // Get the last index, as the map is sorted by the FP's index value
@@ -641,24 +664,6 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF
this.facetNodeRefCache.remove(filterID); this.facetNodeRefCache.remove(filterID);
} }
/**
* Note: this comparator imposes orderings that are inconsistent with equals
* method of the {@link SolrFacetProperties}."
*
* @return
*/
private Comparator<Entry<String, SolrFacetProperties>> getIndextComparator()
{
return new Comparator<Entry<String, SolrFacetProperties>>()
{
public int compare(Entry<String, SolrFacetProperties> facet1,
Entry<String, SolrFacetProperties> facet2)
{
return Integer.compare(facet1.getValue().getIndex(), facet2.getValue().getIndex());
}
};
}
@Override @Override
public int getNextIndex() public int getNextIndex()
{ {
@@ -737,4 +742,47 @@ public class SolrFacetServiceImpl extends AbstractLifecycleBean implements SolrF
} }
} }
} }
@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
for (String facetId : facetIds)
{
SolrFacetProperties facet = getFacet(facetId);
if (facet == null)
{
throw new UnrecognisedFacetId("Cannot reorder facets as ID not recognised:", 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);
nodeService.setProperty(getFacetsRoot(), SolrFacetModel.PROP_FACET_ORDER, serializableProp);
}
}
} }

View File

@@ -96,5 +96,6 @@ public class AllUnitTestsSuite extends TestSuite
suite.addTest(new JUnit4TestAdapter(org.alfresco.util.test.junitrules.TemporaryMockOverrideTest.class)); suite.addTest(new JUnit4TestAdapter(org.alfresco.util.test.junitrules.TemporaryMockOverrideTest.class));
suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.search.impl.solr.SolrQueryHTTPClientTest.class)); suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.search.impl.solr.SolrQueryHTTPClientTest.class));
suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.search.impl.solr.SolrStatsResultTest.class)); suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.search.impl.solr.SolrStatsResultTest.class));
suite.addTest(new JUnit4TestAdapter(org.alfresco.repo.search.impl.solr.facet.SolrFacetComparatorTest.class));
} }
} }

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2005-2014 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* 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/>.
*/
package org.alfresco.repo.search.impl.solr.facet;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.alfresco.util.collections.CollectionUtils;
import org.alfresco.util.collections.Function;
import org.junit.Test;
/**Some Unit tests for {@link SolrFacetComparator}. */
public class SolrFacetComparatorTest
{
@Test public void simpleSortOfSortedFacets() throws Exception
{
List<String> expectedIds = Arrays.asList(new String[] { "a", "b", "c"});
SolrFacetProperties.Builder builder = new SolrFacetProperties.Builder();
List<SolrFacetProperties> facets = Arrays.asList(new SolrFacetProperties[]
{
builder.filterID("c").index(1).build(),
builder.filterID("b").index(2).build(),
builder.filterID("a").index(3).build(),
});
Collections.sort(facets, new SolrFacetComparator(expectedIds));
assertEquals(expectedIds, toFacetIds(facets));
}
private List<String> toFacetIds(List<SolrFacetProperties> facets)
{
return CollectionUtils.transform(facets, new Function<SolrFacetProperties, String>()
{
@Override public String apply(SolrFacetProperties value)
{
return value.getFilterID();
}
});
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright (C) 2005-2014 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* 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/>.
*/
package org.alfresco.repo.search.impl.solr.facet;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.alfresco.repo.search.impl.solr.facet.Exceptions.DuplicateFacetId;
import org.alfresco.repo.search.impl.solr.facet.Exceptions.MissingFacetId;
import org.alfresco.repo.search.impl.solr.facet.Exceptions.UnrecognisedFacetId;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.util.collections.CollectionUtils;
import org.alfresco.util.collections.Function;
import org.alfresco.util.test.junitrules.ApplicationContextInit;
import org.alfresco.util.test.junitrules.RunAsFullyAuthenticatedRule;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
/**
* Integration tests for {@link SolrFacetServiceImpl}.
*/
public class SolrFacetServiceImplTest
{
// Rule to initialise the default Alfresco spring configuration
@ClassRule public static ApplicationContextInit APP_CONTEXT_INIT = new ApplicationContextInit();
@Rule public RunAsFullyAuthenticatedRule runAsRule = new RunAsFullyAuthenticatedRule(AuthenticationUtil.getAdminUserName());
// Various services
private static SolrFacetService SOLR_FACET_SERVICE;
private static RetryingTransactionHelper TRANSACTION_HELPER;
@BeforeClass public static void initStaticData() throws Exception
{
SOLR_FACET_SERVICE = APP_CONTEXT_INIT.getApplicationContext().getBean("solrFacetService", SolrFacetService.class);
TRANSACTION_HELPER = APP_CONTEXT_INIT.getApplicationContext().getBean("retryingTransactionHelper", RetryingTransactionHelper.class);
}
// TODO Ensure non-admin, non-search-admin user cannot access SolrFacetService
@Test public void getFacetsAndReorderThem() throws Exception
{
TRANSACTION_HELPER.doInTransaction(new RetryingTransactionCallback<Void>()
{
@Override public Void execute() throws Throwable
{
final List<String> facetIds = getExistingFacetIds();
final List<String> reorderedFacetIds = new ArrayList<>(facetIds);
Collections.reverse(reorderedFacetIds);
SOLR_FACET_SERVICE.reorderFacets(reorderedFacetIds);
final List<String> newfacetIds = getExistingFacetIds();
assertEquals(reorderedFacetIds, newfacetIds);
return null;
}
});
}
@Test(expected=NullPointerException.class)
public void reorderNullFacetIdsShouldFail() throws Exception
{
TRANSACTION_HELPER.doInTransaction(new RetryingTransactionCallback<Void>()
{
@Override public Void execute() throws Throwable
{
SOLR_FACET_SERVICE.reorderFacets(null);
return null;
}
});
}
@Test(expected=MissingFacetId.class)
public void reorderEmptyFacetIdsShouldFail() throws Exception
{
TRANSACTION_HELPER.doInTransaction(new RetryingTransactionCallback<Void>()
{
@Override public Void execute() throws Throwable
{
SOLR_FACET_SERVICE.reorderFacets(Collections.<String>emptyList());
return null;
}
});
}
@Test(expected=DuplicateFacetId.class)
public void reorderDuplicateFacetIdsShouldFail() throws Exception
{
TRANSACTION_HELPER.doInTransaction(new RetryingTransactionCallback<Void>()
{
@Override public Void execute() throws Throwable
{
final List<String> facetIds = getExistingFacetIds();
facetIds.add(facetIds.get(0));
SOLR_FACET_SERVICE.reorderFacets(facetIds);
return null;
}
});
}
@Test(expected=UnrecognisedFacetId.class)
public void reorderUnrecognisedFacetIdsShouldFail() throws Exception
{
TRANSACTION_HELPER.doInTransaction(new RetryingTransactionCallback<Void>()
{
@Override public Void execute() throws Throwable
{
final List<String> facetIds = getExistingFacetIds();
facetIds.add("unrecognisedID");
SOLR_FACET_SERVICE.reorderFacets(facetIds);
return null;
}
});
}
private List<String> getExistingFacetIds()
{
final List<SolrFacetProperties> facetProps = SOLR_FACET_SERVICE.getFacets();
final List<String> facetIds = CollectionUtils.transform(facetProps,
new Function<SolrFacetProperties, String>()
{
@Override public String apply(SolrFacetProperties value)
{
return value.getFilterID();
}
});
return facetIds;
}
}