From ad217b69acf121f4c2deedc179527c1b55bb0aca Mon Sep 17 00:00:00 2001 From: Simon Urli <simon.urli@xwiki.com> Date: Fri, 20 Nov 2020 10:56:21 +0100 Subject: [PATCH] XWIKI-18078: Likes should be kept on a page if it is moved * Add a method for updating ratings and average ratings information when an entity reference has been moved * Add a new listener for updating ratings information in case of page rename * Improve existing listener for Likes for cleaning cache in case of page rename * Add a new utility method in SolrUtils * Small refactoring to fix a typo in RatingsManager signature --- xwiki-platform-core/pom.xml | 15 ++ .../internal/DeletedEntityLikeListener.java | 9 +- .../DeletedEntityLikeListenerTest.java | 14 +- .../org/xwiki/ratings/RatingsManager.java | 16 +- .../DefaultRatingsManagerFactory.java | 2 +- .../internal/RatingDeletedEntityListener.java | 11 +- .../internal/RatingMovedEntityListener.java | 92 +++++++++ .../ratings/internal/SolrRatingsManager.java | 86 ++++++++- .../averagerating/AverageRatingManager.java | 13 ++ .../SolrAverageRatingManager.java | 72 ++++++- .../XObjectAverageRatingManager.java | 8 + .../main/resources/META-INF/components.txt | 3 +- .../RatingDeletedEntityListenerTest.java | 23 +++ .../RatingMovedEntityListenerTest.java | 72 +++++++ .../internal/SolrRatingsManagerTest.java | 182 ++++++++++++++++-- .../AbstractAverageRatingManagerTest.java | 7 + .../SolrAverageRatingManagerTest.java | 166 ++++++++++++++++ .../java/org/xwiki/search/solr/SolrUtils.java | 19 ++ .../solr/internal/DefaultSolrUtils.java | 7 + 19 files changed, 793 insertions(+), 24 deletions(-) create mode 100644 xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingMovedEntityListener.java create mode 100644 xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingMovedEntityListenerTest.java diff --git a/xwiki-platform-core/pom.xml b/xwiki-platform-core/pom.xml index bb6800f639b..6d6b6f6e6ad 100644 --- a/xwiki-platform-core/pom.xml +++ b/xwiki-platform-core/pom.xml @@ -192,6 +192,21 @@ <new>field org.xwiki.search.solr.AbstractSolrCoreInitializer.SCHEMA_BASE_VERSION</new> <justification>Not a breackage</justification> </item> + <item> + <code>java.method.addedToInterface</code> + <new>method long org.xwiki.ratings.RatingsManager::moveRatings(org.xwiki.model.reference.EntityReference, org.xwiki.model.reference.EntityReference) throws org.xwiki.ratings.RatingsException</new> + <justification>Young API.</justification> + </item> + <item> + <code>java.method.removed</code> + <old>method void org.xwiki.ratings.RatingsManager::setIdentifer(java.lang.String)</old> + <justification>Young API: fix typo.</justification> + </item> + <item> + <code>java.method.addedToInterface</code> + <new>method void org.xwiki.ratings.RatingsManager::setIdentifier(java.lang.String)</new> + <justification>Young API: fix typo.</justification> + </item> </revapi.ignore> </analysisConfiguration> </configuration> diff --git a/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/java/org/xwiki/like/internal/DeletedEntityLikeListener.java b/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/java/org/xwiki/like/internal/DeletedEntityLikeListener.java index 9026d641efb..ec01226626a 100644 --- a/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/java/org/xwiki/like/internal/DeletedEntityLikeListener.java +++ b/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/java/org/xwiki/like/internal/DeletedEntityLikeListener.java @@ -34,6 +34,7 @@ import org.xwiki.model.reference.EntityReference; import org.xwiki.observation.AbstractEventListener; import org.xwiki.observation.event.Event; +import org.xwiki.refactoring.event.DocumentRenamedEvent; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.internal.event.XObjectDeletedEvent; @@ -48,6 +49,8 @@ * <li>deletion of entire wiki</li> * <li>deletion of users</li> * </ul> + * + * Note that this component also listen to rename of documents. * The deletion of a documents will perform a partial clean of cache, to remove the information related to that * document (see {@link LikeManager#clearCache(EntityReference)}), while the other kinds lead to full clean of the * cache (see {@link LikeManager#clearCache()}). @@ -64,7 +67,8 @@ public class DeletedEntityLikeListener extends AbstractEventListener private static final List<Event> EVENT_LIST = Arrays.asList( new DocumentDeletedEvent(), new WikiDeletedEvent(), - new XObjectDeletedEvent(BaseObjectReference.any(XWikiUsersDocumentInitializer.CLASS_REFERENCE_STRING)) + new XObjectDeletedEvent(BaseObjectReference.any(XWikiUsersDocumentInitializer.CLASS_REFERENCE_STRING)), + new DocumentRenamedEvent() ); @Inject @@ -84,6 +88,9 @@ public void onEvent(Event event, Object source, Object data) if (event instanceof DocumentDeletedEvent) { XWikiDocument sourceDoc = (XWikiDocument) source; this.likeManagerProvider.get().clearCache(sourceDoc.getDocumentReference()); + } else if (event instanceof DocumentRenamedEvent) { + DocumentRenamedEvent documentRenamedEvent = (DocumentRenamedEvent) event; + this.likeManagerProvider.get().clearCache(documentRenamedEvent.getSourceReference()); } else { this.likeManagerProvider.get().clearCache(); } diff --git a/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/test/java/org/xwiki/like/internal/DeletedEntityLikeListenerTest.java b/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/test/java/org/xwiki/like/internal/DeletedEntityLikeListenerTest.java index 794ccd33e6b..cde994a9fc2 100644 --- a/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/test/java/org/xwiki/like/internal/DeletedEntityLikeListenerTest.java +++ b/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/test/java/org/xwiki/like/internal/DeletedEntityLikeListenerTest.java @@ -28,6 +28,7 @@ import org.xwiki.bridge.event.WikiDeletedEvent; import org.xwiki.like.LikeManager; import org.xwiki.model.reference.DocumentReference; +import org.xwiki.refactoring.event.DocumentRenamedEvent; import org.xwiki.test.junit5.mockito.ComponentTest; import org.xwiki.test.junit5.mockito.InjectMockComponents; import org.xwiki.test.junit5.mockito.MockComponent; @@ -63,7 +64,7 @@ void setup() } @Test - void onEventDocument() + void onEventDeletedDocument() { XWikiDocument sourceDocument = mock(XWikiDocument.class); DocumentReference documentReference = mock(DocumentReference.class); @@ -73,6 +74,17 @@ void onEventDocument() verify(this.likeManager).clearCache(documentReference); } + @Test + void onEventRenamedDocument() + { + DocumentRenamedEvent documentRenamedEvent = mock(DocumentRenamedEvent.class); + DocumentReference documentReference = mock(DocumentReference.class); + when(documentRenamedEvent.getSourceReference()).thenReturn(documentReference); + + this.listener.onEvent(documentRenamedEvent, null, null); + verify(this.likeManager).clearCache(documentReference); + } + @Test void onEventWiki() { diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/RatingsManager.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/RatingsManager.java index 28c0d789ba3..0d3e0c66205 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/RatingsManager.java +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/RatingsManager.java @@ -78,7 +78,7 @@ public String getFieldName() * * @param identifier the identifier to be set. */ - void setIdentifer(String identifier); + void setIdentifier(String identifier); /** * @return the upper bound of the scale used by this manager for rating. @@ -172,6 +172,20 @@ List<Rating> getRatings(Map<RatingQueryField, Object> queryParameters, */ long removeRatings(EntityReference entityReference) throws RatingsException; + /** + * Update all ratings concerning the given reference to point to the new reference. + * This update is performed for both ratings targeting directly the given reference, but also for those having the + * reference as ancestor. + * + * @param oldReference the old reference to be updated. + * @param newReference the new reference. + * @return the total number of updated ratings. + * @throws RatingsException in case of problem during the query. + * @since 12.10 + */ + @Unstable + long moveRatings(EntityReference oldReference, EntityReference newReference) throws RatingsException; + /** * Retrieve the average rating information of the given reference. * diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/DefaultRatingsManagerFactory.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/DefaultRatingsManagerFactory.java index e409c9cf1d6..0baa4582657 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/DefaultRatingsManagerFactory.java +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/DefaultRatingsManagerFactory.java @@ -98,7 +98,7 @@ public RatingsManager getRatingsManager(String managerName) throws RatingsExcept // step 4: set the information of the RatingManager result.setRatingConfiguration(ratingsConfiguration); - result.setIdentifer(managerName); + result.setIdentifier(managerName); // step 5: copy the descriptor and modifies the hint and the instantiation strategy DefaultComponentDescriptor<RatingsManager> componentDescriptorCopy = diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingDeletedEntityListener.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingDeletedEntityListener.java index bc1dbdf42f9..0593989caea 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingDeletedEntityListener.java +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingDeletedEntityListener.java @@ -34,10 +34,12 @@ import org.xwiki.model.reference.EntityReference; import org.xwiki.model.reference.WikiReference; import org.xwiki.observation.AbstractEventListener; +import org.xwiki.observation.ObservationContext; import org.xwiki.observation.event.Event; import org.xwiki.ratings.RatingsException; import org.xwiki.ratings.RatingsManager; import org.xwiki.ratings.RatingsManagerFactory; +import org.xwiki.refactoring.event.DocumentRenamingEvent; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.internal.event.XObjectDeletedEvent; @@ -62,12 +64,17 @@ public class RatingDeletedEntityListener extends AbstractEventListener new DocumentDeletedEvent(), new WikiDeletedEvent()); + private static final DocumentRenamingEvent RENAMING_EVENT = new DocumentRenamingEvent(); + @Inject private Logger logger; @Inject private RatingsManagerFactory ratingsManagerFactory; + @Inject + private ObservationContext observationContext; + /** * Default constructor. */ @@ -79,7 +86,9 @@ public RatingDeletedEntityListener() @Override public void onEvent(Event event, Object source, Object data) { - if (event instanceof DocumentDeletedEvent) { + // if the event is sent because of a rename, we ignore it: in that case RatingMovedEntityListener will be called + // and will handle properly the changes to be done. + if (event instanceof DocumentDeletedEvent && !this.observationContext.isIn(RENAMING_EVENT)) { XWikiDocument sourceDoc = (XWikiDocument) source; this.handleDeletedReference(sourceDoc.getDocumentReference()); } else if (event instanceof XObjectDeletedEvent) { diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingMovedEntityListener.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingMovedEntityListener.java new file mode 100644 index 00000000000..4d550a66ffc --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingMovedEntityListener.java @@ -0,0 +1,92 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This 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 2.1 of + * the License, or (at your option) any later version. + * + * This software 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 this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.ratings.internal; + +import java.util.Collections; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.xwiki.component.annotation.Component; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.model.reference.EntityReference; +import org.xwiki.observation.AbstractEventListener; +import org.xwiki.observation.event.Event; +import org.xwiki.ratings.RatingsException; +import org.xwiki.ratings.RatingsManager; +import org.xwiki.ratings.RatingsManagerFactory; +import org.xwiki.refactoring.event.DocumentRenamedEvent; + +/** + * This listener aims at updating any ratings related to the moved entities. + * This component listens on {@link DocumentRenamedEvent} and calls + * {@link RatingsManager#moveRatings(EntityReference, EntityReference)} with the appropriate references + * on all instantiated ratings managers. + * + * @version $Id$ + * @since 12.10 + */ +@Component +@Singleton +@Named(RatingMovedEntityListener.NAME) +public class RatingMovedEntityListener extends AbstractEventListener +{ + static final String NAME = "RatingMovedEntityListener"; + private static final List<Event> EVENT_LIST = Collections.singletonList( + new DocumentRenamedEvent() + ); + + @Inject + private Logger logger; + + @Inject + private RatingsManagerFactory ratingsManagerFactory; + + /** + * Default constructor. + */ + public RatingMovedEntityListener() + { + super(NAME, EVENT_LIST); + } + + @Override + public void onEvent(Event event, Object source, Object data) + { + if (event instanceof DocumentRenamedEvent) { + DocumentRenamedEvent renamedEvent = (DocumentRenamedEvent) event; + DocumentReference oldReference = renamedEvent.getSourceReference(); + DocumentReference newReference = renamedEvent.getTargetReference(); + + try { + for (RatingsManager manager : this.ratingsManagerFactory.getInstantiatedManagers()) { + manager.moveRatings(oldReference, newReference); + } + } catch (RatingsException e) { + logger.error("Error while updating ratings related to old reference [{}] from ratings: [{}]", + oldReference, ExceptionUtils.getRootCause(e)); + } + } + } +} diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/SolrRatingsManager.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/SolrRatingsManager.java index cb47aa3e89c..446c4ad336f 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/SolrRatingsManager.java +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/SolrRatingsManager.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; @@ -74,7 +75,9 @@ @InstantiationStrategy(ComponentInstantiationStrategy.PER_LOOKUP) public class SolrRatingsManager implements RatingsManager { - private static final int AVERAGE_COMPUTATION_BATCH_SIZE = 100; + private static final int BULK_OPERATIONS_BATCH_SIZE = 100; + + private static final String FILTER_REFERENCE_OR_PARENTS = "filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))"; private static final String AVERAGE_RATING_NOT_ENABLED_ERROR_MESSAGE = "This rating manager is not configured to store average rating."; @@ -138,7 +141,7 @@ public String getIdentifier() } @Override - public void setIdentifer(String identifier) + public void setIdentifier(String identifier) { this.identifier = identifier; } @@ -355,6 +358,13 @@ public Rating saveRating(EntityReference reference, UserReference user, int vote @Override public List<Rating> getRatings(Map<RatingQueryField, Object> queryParameters, int offset, int limit, RatingQueryField orderBy, boolean asc) throws RatingsException + { + SolrDocumentList rawRatings = getRawRatings(queryParameters, offset, limit, orderBy, asc); + return this.getRatingsFromQueryResult(rawRatings); + } + + private SolrDocumentList getRawRatings(Map<RatingQueryField, Object> queryParameters, int offset, int limit, + RatingQueryField orderBy, boolean asc) throws RatingsException { SolrQuery solrQuery = new SolrQuery() .addFilterQuery(this.mapToQuery(queryParameters)) @@ -364,7 +374,7 @@ public List<Rating> getRatings(Map<RatingQueryField, Object> queryParameters, in try { QueryResponse query = this.getRatingSolrClient().query(solrQuery); - return this.getRatingsFromQueryResult(query.getResults()); + return query.getResults(); } catch (SolrServerException | IOException | SolrException e) { throw new RatingsException("Error while trying to get ratings", e); } @@ -415,7 +425,7 @@ public boolean removeRating(String ratingIdentifier) throws RatingsException public long removeRatings(EntityReference entityReference) throws RatingsException { String escapedEntityReference = this.solrUtils.toFilterQueryString(entityReference, EntityReference.class); - String filterQuery = String.format("filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))", + String filterQuery = String.format(FILTER_REFERENCE_OR_PARENTS, RatingQueryField.MANAGER_ID.getFieldName(), solrUtils.toFilterQueryString(this.getIdentifier()), RatingQueryField.ENTITY_REFERENCE.getFieldName(), escapedEntityReference, RatingQueryField.PARENTS_REFERENCE.getFieldName(), escapedEntityReference); @@ -439,6 +449,70 @@ public long removeRatings(EntityReference entityReference) throws RatingsExcepti return result; } + @Override + public long moveRatings(EntityReference oldReference, EntityReference newReference) + throws RatingsException + { + String escapedEntityReference = this.solrUtils.toFilterQueryString(oldReference, EntityReference.class); + String filterQuery = String.format(FILTER_REFERENCE_OR_PARENTS, + RatingQueryField.MANAGER_ID.getFieldName(), solrUtils.toFilterQueryString(this.getIdentifier()), + RatingQueryField.ENTITY_REFERENCE.getFieldName(), escapedEntityReference, + RatingQueryField.PARENTS_REFERENCE.getFieldName(), escapedEntityReference); + int offset = 0; + SolrDocumentList rawRatings; + long result = 0; + do { + SolrQuery solrQuery = new SolrQuery() + .addFilterQuery(filterQuery) + .setStart(offset) + .setRows(BULK_OPERATIONS_BATCH_SIZE) + .setSort(RatingQueryField.CREATED_DATE.getFieldName(), this.getOrder(true)); + + try { + QueryResponse queryResponse = this.getRatingSolrClient().query(solrQuery); + rawRatings = queryResponse.getResults(); + + offset += BULK_OPERATIONS_BATCH_SIZE; + for (SolrDocument rawRating : rawRatings) { + SolrInputDocument solrInputDocument = new SolrInputDocument(); + this.solrUtils.setId(this.solrUtils.getId(rawRating), solrInputDocument); + + EntityReference ratingReference = this.solrUtils.get( + RatingQueryField.ENTITY_REFERENCE.getFieldName(), rawRating, EntityReference.class); + + if (oldReference.equals(ratingReference)) { + this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, + RatingQueryField.ENTITY_REFERENCE.getFieldName(), newReference, + EntityReference.class, solrInputDocument); + } + + Collection<EntityReference> parentReferences = this.solrUtils + .getCollection(RatingQueryField.PARENTS_REFERENCE.getFieldName(), rawRating, + EntityReference.class); + if (parentReferences.contains(oldReference)) { + this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, + RatingQueryField.PARENTS_REFERENCE + .getFieldName(), oldReference, EntityReference.class, solrInputDocument); + this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, + RatingQueryField.PARENTS_REFERENCE + .getFieldName(), newReference, EntityReference.class, solrInputDocument); + } + this.getRatingSolrClient().add(solrInputDocument); + result++; + } + if (!rawRatings.isEmpty()) { + this.getRatingSolrClient().commit(); + } + } catch (SolrException | IOException | SolrServerException e) { + throw new RatingsException("Error while trying to update rating reference", e); + } + } while (!rawRatings.isEmpty()); + if (this.getRatingConfiguration().isAverageStored()) { + this.getAverageRatingManager().moveAverageRatings(oldReference, newReference); + } + return result; + } + @Override public AverageRating getAverageRating(EntityReference entityReference) throws RatingsException { @@ -473,11 +547,11 @@ public AverageRating recomputeAverageRating(EntityReference entityReference) thr List<Rating> ratings; int offsetIndex = 0; do { - ratings = this.getRatings(queryMap, offsetIndex, AVERAGE_COMPUTATION_BATCH_SIZE, + ratings = this.getRatings(queryMap, offsetIndex, BULK_OPERATIONS_BATCH_SIZE, RatingQueryField.CREATED_DATE, true); sumOfVotes += ratings.stream().map(Rating::getVote).map(Long::valueOf).reduce(0L, Long::sum); numberOfVotes += ratings.size(); - offsetIndex += AVERAGE_COMPUTATION_BATCH_SIZE; + offsetIndex += BULK_OPERATIONS_BATCH_SIZE; } while (!ratings.isEmpty()); float newAverage = Float.valueOf(sumOfVotes) / numberOfVotes; diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/AverageRatingManager.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/AverageRatingManager.java index 0f6269c4c8c..99c639ded64 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/AverageRatingManager.java +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/AverageRatingManager.java @@ -24,6 +24,7 @@ import org.xwiki.ratings.AverageRating; import org.xwiki.ratings.RatingsException; import org.xwiki.ratings.RatingsManager; +import org.xwiki.stability.Unstable; /** * Generic interface to manage {@link AverageRating}. @@ -173,4 +174,16 @@ AverageRating resetAverageRating(EntityReference entityReference, float averageV * @throws RatingsException in case of problem when removing average ratings. */ long removeAverageRatings(EntityReference entityReference) throws RatingsException; + + /** + * Update reference data in case of a move of the given reference. + * + * @param oldReference the old reference that has been moved. + * @param newReference the new reference to store. + * @return the total number of average ratings updated. + * @throws RatingsException in case of problem during the update. + * @since 12.10 + */ + @Unstable + long moveAverageRatings(EntityReference oldReference, EntityReference newReference) throws RatingsException; } diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManager.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManager.java index 6283e3f0872..2dc82f49ed3 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManager.java +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManager.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.UUID; @@ -32,6 +33,7 @@ import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocument; +import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrInputDocument; import org.xwiki.component.annotation.Component; import org.xwiki.component.annotation.InstantiationStrategy; @@ -39,6 +41,7 @@ import org.xwiki.model.reference.EntityReference; import org.xwiki.ratings.AverageRating; import org.xwiki.ratings.RatingsException; +import org.xwiki.ratings.RatingsManager; import org.xwiki.search.solr.Solr; import org.xwiki.search.solr.SolrException; import org.xwiki.search.solr.SolrUtils; @@ -54,6 +57,10 @@ @InstantiationStrategy(ComponentInstantiationStrategy.PER_LOOKUP) public class SolrAverageRatingManager extends AbstractAverageRatingManager { + private static final int BULK_OPERATIONS_BATCH_SIZE = 100; + + private static final String FILTER_REFERENCE_OR_PARENTS = "filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))"; + @Inject private SolrUtils solrUtils; @@ -132,7 +139,7 @@ public AverageRating getAverageRating(EntityReference entityReference) throws Ra public long removeAverageRatings(EntityReference entityReference) throws RatingsException { String escapedEntityReference = this.solrUtils.toFilterQueryString(entityReference, EntityReference.class); - String filterQuery = String.format("filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))", + String filterQuery = String.format(FILTER_REFERENCE_OR_PARENTS, AverageRatingQueryField.MANAGER_ID.getFieldName(), solrUtils.toFilterQueryString(this.getIdentifier()), AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), escapedEntityReference, AverageRatingQueryField.PARENTS.getFieldName(), escapedEntityReference); @@ -153,6 +160,69 @@ public long removeAverageRatings(EntityReference entityReference) throws Ratings return result; } + @Override + public long moveAverageRatings(EntityReference oldReference, EntityReference newReference) + throws RatingsException + { + String escapedEntityReference = this.solrUtils.toFilterQueryString(oldReference, EntityReference.class); + String filterQuery = String.format(FILTER_REFERENCE_OR_PARENTS, + AverageRatingQueryField.MANAGER_ID.getFieldName(), solrUtils.toFilterQueryString(this.getIdentifier()), + AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), escapedEntityReference, + AverageRatingQueryField.PARENTS.getFieldName(), escapedEntityReference); + int offset = 0; + SolrDocumentList rawRatings; + long result = 0; + do { + SolrQuery solrQuery = new SolrQuery() + .addFilterQuery(filterQuery) + .setStart(offset) + .setRows(BULK_OPERATIONS_BATCH_SIZE) + .setSort(AverageRatingQueryField.UPDATED_AT.getFieldName(), this.getOrder(true)); + + try { + QueryResponse queryResponse = this.getAverageRatingSolrClient().query(solrQuery); + rawRatings = queryResponse.getResults(); + + offset += BULK_OPERATIONS_BATCH_SIZE; + for (SolrDocument rawRating : rawRatings) { + SolrInputDocument solrInputDocument = new SolrInputDocument(); + this.solrUtils.setId(this.solrUtils.getId(rawRating), solrInputDocument); + + EntityReference ratingReference = this.solrUtils.get( + RatingsManager.RatingQueryField.ENTITY_REFERENCE.getFieldName(), rawRating, + EntityReference.class); + + if (oldReference.equals(ratingReference)) { + this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, + RatingsManager.RatingQueryField.ENTITY_REFERENCE.getFieldName(), newReference, + EntityReference.class, solrInputDocument); + } + + Collection<EntityReference> parentReferences = this.solrUtils + .getCollection(RatingsManager.RatingQueryField.PARENTS_REFERENCE.getFieldName(), rawRating, + EntityReference.class); + if (parentReferences.contains(oldReference)) { + this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, + RatingsManager.RatingQueryField.PARENTS_REFERENCE + .getFieldName(), oldReference, EntityReference.class, solrInputDocument); + this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, + RatingsManager.RatingQueryField.PARENTS_REFERENCE + .getFieldName(), newReference, EntityReference.class, solrInputDocument); + } + this.getAverageRatingSolrClient().add(solrInputDocument); + result++; + + } + if (!rawRatings.isEmpty()) { + this.getAverageRatingSolrClient().commit(); + } + } catch (SolrException | IOException | SolrServerException e) { + throw new RatingsException("Error while trying to update average rating reference", e); + } + } while (!rawRatings.isEmpty()); + return result; + } + @Override protected void saveAverageRating(AverageRating averageRating) throws RatingsException { diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/XObjectAverageRatingManager.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/XObjectAverageRatingManager.java index cb05c8e1e27..b9b8d13e2b0 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/XObjectAverageRatingManager.java +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/XObjectAverageRatingManager.java @@ -111,6 +111,14 @@ public long removeAverageRatings(EntityReference entityReference) throws Ratings return result; } + @Override + public long moveAverageRatings(EntityReference oldReference, EntityReference newReference) + throws RatingsException + { + // We don't need to do anything here: if a document reference has been moved then the xobject move with it. + return 0; + } + private BaseObject retrieveAverageRatingXObject(EntityReference entityReference) throws Exception { DocumentModelBridge documentInstance = this.documentAccessBridge.getDocumentInstance(entityReference); diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/resources/META-INF/components.txt index 43c088acd5f..6a0a786db27 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/resources/META-INF/components.txt +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/resources/META-INF/components.txt @@ -12,4 +12,5 @@ org.xwiki.ratings.internal.averagerating.AverageRatingProtectionListener org.xwiki.ratings.internal.migration.R120901000XWIKI17761DataMigration org.xwiki.ratings.internal.migration.SolrDocumentMigration120900000 org.xwiki.ratings.internal.RatingsConfigurationSource -org.xwiki.ratings.internal.RatingDeletedEntityListener \ No newline at end of file +org.xwiki.ratings.internal.RatingDeletedEntityListener +org.xwiki.ratings.internal.RatingMovedEntityListener \ No newline at end of file diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingDeletedEntityListenerTest.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingDeletedEntityListenerTest.java index cdf10b4d350..624d7b40722 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingDeletedEntityListenerTest.java +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingDeletedEntityListenerTest.java @@ -27,8 +27,10 @@ import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.EntityReference; import org.xwiki.model.reference.WikiReference; +import org.xwiki.observation.ObservationContext; import org.xwiki.ratings.RatingsManager; import org.xwiki.ratings.RatingsManagerFactory; +import org.xwiki.refactoring.event.DocumentRenamingEvent; import org.xwiki.test.junit5.mockito.ComponentTest; import org.xwiki.test.junit5.mockito.InjectMockComponents; import org.xwiki.test.junit5.mockito.MockComponent; @@ -37,6 +39,7 @@ import com.xpn.xwiki.internal.event.XObjectDeletedEvent; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -54,6 +57,9 @@ public class RatingDeletedEntityListenerTest @MockComponent private RatingsManagerFactory ratingsManagerFactory; + @MockComponent + private ObservationContext observationContext; + @Test void onDocumentDeletedEvent() throws Exception { @@ -70,6 +76,23 @@ void onDocumentDeletedEvent() throws Exception verify(manager2).removeRatings(reference); } + @Test + void onDocumentDeletedEventBecauseOfRenaming() throws Exception + { + RatingsManager manager1 = mock(RatingsManager.class); + RatingsManager manager2 = mock(RatingsManager.class); + when(ratingsManagerFactory.getInstantiatedManagers()).thenReturn(Arrays.asList(manager1, manager2)); + when(observationContext.isIn(new DocumentRenamingEvent())).thenReturn(true); + + XWikiDocument sourceDoc = mock(XWikiDocument.class); + DocumentReference reference = mock(DocumentReference.class); + when(sourceDoc.getDocumentReference()).thenReturn(reference); + + this.listener.onEvent(new DocumentDeletedEvent(reference), sourceDoc, null); + verify(manager1, never()).removeRatings(reference); + verify(manager2, never()).removeRatings(reference); + } + @Test void onXObjectDeletedEventEvent() throws Exception { diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingMovedEntityListenerTest.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingMovedEntityListenerTest.java new file mode 100644 index 00000000000..bdcb8a5a03a --- /dev/null +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingMovedEntityListenerTest.java @@ -0,0 +1,72 @@ +/* + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * + * This 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 2.1 of + * the License, or (at your option) any later version. + * + * This software 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 this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.xwiki.ratings.internal; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; +import org.xwiki.bridge.event.DocumentDeletedEvent; +import org.xwiki.model.reference.DocumentReference; +import org.xwiki.ratings.RatingsManager; +import org.xwiki.ratings.RatingsManagerFactory; +import org.xwiki.refactoring.event.DocumentRenamedEvent; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import com.xpn.xwiki.doc.XWikiDocument; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link RatingMovedEntityListener}. + * + * @version $Id$ + * @since 12.10 + */ +@ComponentTest +public class RatingMovedEntityListenerTest +{ + @InjectMockComponents + private RatingMovedEntityListener listener; + + @MockComponent + private RatingsManagerFactory ratingsManagerFactory; + + @Test + void onDocumentRenamedEvent() throws Exception + { + RatingsManager manager1 = mock(RatingsManager.class); + RatingsManager manager2 = mock(RatingsManager.class); + when(ratingsManagerFactory.getInstantiatedManagers()).thenReturn(Arrays.asList(manager1, manager2)); + + DocumentRenamedEvent documentRenamedEvent = mock(DocumentRenamedEvent.class); + DocumentReference oldReference = mock(DocumentReference.class); + DocumentReference newReference = mock(DocumentReference.class); + when(documentRenamedEvent.getSourceReference()).thenReturn(oldReference); + when(documentRenamedEvent.getTargetReference()).thenReturn(newReference); + + this.listener.onEvent(documentRenamedEvent, null, null); + verify(manager1).moveRatings(oldReference, newReference); + verify(manager2).moveRatings(oldReference, newReference); + } +} diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/SolrRatingsManagerTest.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/SolrRatingsManagerTest.java index f70d78a6ace..23669edbe6d 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/SolrRatingsManagerTest.java +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/SolrRatingsManagerTest.java @@ -20,6 +20,7 @@ package org.xwiki.ratings.internal; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -75,6 +76,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -182,7 +184,7 @@ void countRatings() throws Exception queryParameters.put(RatingQueryField.SCALE, 12); String managerId = "managerTest"; - this.manager.setIdentifer(managerId); + this.manager.setIdentifier(managerId); when(this.configuration.hasDedicatedCore()).thenReturn(true); when(this.solr.getClient(managerId)).thenReturn(this.solrClient); @@ -207,7 +209,7 @@ void getRatings() throws Exception queryParameters.put(RatingQueryField.SCALE, "6"); String managerId = "otherId"; - this.manager.setIdentifer(managerId); + this.manager.setIdentifier(managerId); when(this.configuration.hasDedicatedCore()).thenReturn(false); when(this.solr.getClient(RatingSolrCoreInitializer.DEFAULT_RATINGS_SOLR_CORE)).thenReturn(this.solrClient); @@ -311,7 +313,7 @@ void getRatings() throws Exception void getAverageRating() throws Exception { String managerId = "averageId2"; - this.manager.setIdentifer(managerId); + this.manager.setIdentifier(managerId); EntityReference reference = new EntityReference("xwiki:Something", EntityType.PAGE); AverageRating expectedAverageRating = new DefaultAverageRating("average1") .setAverageVote(2.341f) @@ -332,7 +334,7 @@ void removeRatingNotExisting() throws Exception { String ratingingId = "ratinging389"; String managerId = "removeRating1"; - this.manager.setIdentifer(managerId); + this.manager.setIdentifier(managerId); when(this.configuration.hasDedicatedCore()).thenReturn(false); when(this.solr.getClient(RatingSolrCoreInitializer.DEFAULT_RATINGS_SOLR_CORE)).thenReturn(this.solrClient); @@ -353,7 +355,7 @@ void removeRatingExisting() throws Exception { String ratingingId = "ratinging429"; String managerId = "removeRating2"; - this.manager.setIdentifer(managerId); + this.manager.setIdentifier(managerId); when(this.configuration.hasDedicatedCore()).thenReturn(false); when(this.solr.getClient(RatingSolrCoreInitializer.DEFAULT_RATINGS_SOLR_CORE)).thenReturn(this.solrClient); @@ -409,7 +411,7 @@ void removeRatingExisting() throws Exception void saveRatingOutScale() { when(this.configuration.getScaleUpperBound()).thenReturn(5); - this.manager.setIdentifer("saveRating1"); + this.manager.setIdentifier("saveRating1"); RatingsException exception = assertThrows(RatingsException.class, () -> { this.manager.saveRating(new EntityReference("test", EntityType.PAGE), mock(UserReference.class), -1); }); @@ -425,7 +427,7 @@ void saveRatingOutScale() void saveRatingZeroNotExisting() throws Exception { String managerId = "saveRating2"; - this.manager.setIdentifer(managerId); + this.manager.setIdentifier(managerId); int scale = 10; when(this.configuration.getScaleUpperBound()).thenReturn(scale); EntityReference reference = mock(EntityReference.class); @@ -505,7 +507,7 @@ void saveRatingZeroNotExisting() throws Exception void saveRatingExisting() throws Exception { String managerId = "saveRating3"; - this.manager.setIdentifer(managerId); + this.manager.setIdentifier(managerId); int scale = 8; int newVote = 2; int oldVote = 3; @@ -595,7 +597,7 @@ void saveRatingExisting() throws Exception void saveRatingExistingToZero() throws Exception { String managerId = "saveRating4"; - this.manager.setIdentifer(managerId); + this.manager.setIdentifier(managerId); int scale = 8; int newVote = 0; int oldVote = 3; @@ -751,7 +753,7 @@ void recomputeAverageRating() throws Exception when(inputReference.toString()).thenReturn("document:Input.Reference"); String managerId = "myManager"; - this.manager.setIdentifer(managerId); + this.manager.setIdentifier(managerId); String filterQuery = String.format("filter(%s:%s) AND filter(%s:%s)", RatingQueryField.ENTITY_REFERENCE.getFieldName(), "document\\:Input.Reference", @@ -824,7 +826,7 @@ void recomputeAverageRating() throws Exception void removeRatings() throws Exception { String managerId = "myRatingManager"; - this.manager.setIdentifer(managerId); + this.manager.setIdentifier(managerId); when(this.solr.getClient(RatingSolrCoreInitializer.DEFAULT_RATINGS_SOLR_CORE)).thenReturn(this.solrClient); EntityReference entityReference = mock(EntityReference.class); @@ -848,4 +850,162 @@ void removeRatings() throws Exception verify(this.solrClient).commit(); verify(this.averageRatingManager).removeAverageRatings(entityReference); } + + @Test + void moveRatings() throws Exception + { + String managerId = "moveRatingsManagerId"; + this.manager.setIdentifier(managerId); + when(this.solr.getClient(RatingSolrCoreInitializer.DEFAULT_RATINGS_SOLR_CORE)).thenReturn(this.solrClient); + + EntityReference oldReference = mock(EntityReference.class); + EntityReference newReference = mock(EntityReference.class); + when(oldReference.toString()).thenReturn("document:My.Old.Doc"); + + String filterQuery = String.format("filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))", + RatingQueryField.MANAGER_ID.getFieldName(), managerId, + RatingQueryField.ENTITY_REFERENCE.getFieldName(), "document\\:My.Old.Doc", + RatingQueryField.PARENTS_REFERENCE.getFieldName(), "document\\:My.Old.Doc"); + + SolrQuery expectedQuery1 = new SolrQuery() + .addFilterQuery(filterQuery) + .setRows(100) + .setStart(0) + .setSort(RatingQueryField.CREATED_DATE.getFieldName(), SolrQuery.ORDER.asc); + + SolrDocument rating1 = mock(SolrDocument.class); + SolrDocument rating2 = mock(SolrDocument.class); + SolrDocument rating3 = mock(SolrDocument.class); + SolrDocument rating4 = mock(SolrDocument.class); + + // rating1 have the appropriate reference, but not the appropriate parent + when(rating1.get(RatingQueryField.IDENTIFIER.getFieldName())).thenReturn("rating1"); + when(this.solrUtils.get(RatingQueryField.ENTITY_REFERENCE.getFieldName(), rating1, EntityReference.class)) + .thenReturn(oldReference); + when(this.solrUtils.getCollection(RatingQueryField.PARENTS_REFERENCE.getFieldName(), rating1, + EntityReference.class)) + .thenReturn(Collections.emptyList()); + + // rating2 have not the appropriate reference but the appropriate parent + when(rating2.get(RatingQueryField.IDENTIFIER.getFieldName())).thenReturn("rating2"); + when(this.solrUtils.get(RatingQueryField.ENTITY_REFERENCE.getFieldName(), rating2, EntityReference.class)) + .thenReturn(mock(EntityReference.class)); + when(this.solrUtils.getCollection(RatingQueryField.PARENTS_REFERENCE.getFieldName(), rating2, + EntityReference.class)) + .thenReturn(Collections.singletonList(oldReference)); + + // rating3 have the appropriate reference and also contain the appropriate parent + when(rating3.get(RatingQueryField.IDENTIFIER.getFieldName())).thenReturn("rating3"); + when(this.solrUtils.get(RatingQueryField.ENTITY_REFERENCE.getFieldName(), rating3, EntityReference.class)) + .thenReturn(oldReference); + when(this.solrUtils.getCollection(RatingQueryField.PARENTS_REFERENCE.getFieldName(), rating3, + EntityReference.class)) + .thenReturn(Arrays.asList(mock(EntityReference.class), oldReference, mock(EntityReference.class))); + + // rating4 only contain the appropriate parent + when(rating4.get(RatingQueryField.IDENTIFIER.getFieldName())).thenReturn("rating4"); + when(this.solrUtils.get(RatingQueryField.ENTITY_REFERENCE.getFieldName(), rating4, EntityReference.class)) + .thenReturn(mock(EntityReference.class)); + when(this.solrUtils.getCollection(RatingQueryField.PARENTS_REFERENCE.getFieldName(), rating4, + EntityReference.class)) + .thenReturn(Arrays.asList(mock(EntityReference.class), mock(EntityReference.class), oldReference)); + + when(this.documentList.iterator()) + .thenReturn(Arrays.asList(rating1, rating2, rating3, rating4).iterator()); + + SolrQuery expectedQuery2 = new SolrQuery() + .addFilterQuery(filterQuery) + .setRows(100) + .setStart(100) + .setSort(RatingQueryField.CREATED_DATE.getFieldName(), SolrQuery.ORDER.asc); + + QueryResponse response1 = mock(QueryResponse.class); + QueryResponse response2 = mock(QueryResponse.class); + + AtomicInteger queryCounter = new AtomicInteger(0); + when(solrClient.query(any())).then(invocationOnMock -> { + + SolrQuery givenQuery = invocationOnMock.getArgument(0); + QueryResponse result = null; + if (queryCounter.get() == 0) { + assertEquals(expectedQuery1.getQuery(), givenQuery.getQuery()); + assertArrayEquals(expectedQuery1.getFilterQueries(), givenQuery.getFilterQueries()); + assertEquals(expectedQuery1.getRows(), givenQuery.getRows()); + assertEquals(expectedQuery1.getStart(), givenQuery.getStart()); + assertEquals(expectedQuery1.getSorts(), givenQuery.getSorts()); + result = response1; + } else if (queryCounter.get() == 1) { + assertEquals(expectedQuery2.getQuery(), givenQuery.getQuery()); + assertArrayEquals(expectedQuery2.getFilterQueries(), givenQuery.getFilterQueries()); + assertEquals(expectedQuery2.getRows(), givenQuery.getRows()); + assertEquals(expectedQuery2.getStart(), givenQuery.getStart()); + assertEquals(expectedQuery2.getSorts(), givenQuery.getSorts()); + result = response2; + } else { + fail("Too many requests performed."); + } + queryCounter.getAndIncrement(); + return result; + }); + when(response1.getResults()).thenReturn(this.documentList); + when(response2.getResults()).thenReturn(new SolrDocumentList()); + + doAnswer(invocationOnMock -> { + String modifier = invocationOnMock.getArgument(0); + String fieldName = invocationOnMock.getArgument(1); + Object fieldValue = invocationOnMock.getArgument(2); + // we normally only invoke it with entity reference. + assertEquals(EntityReference.class, invocationOnMock.getArgument(3)); + SolrInputDocument solrInputDocument = invocationOnMock.getArgument(4); + solrInputDocument.setField(fieldName, Collections.singletonMap(modifier, fieldValue)); + return null; + }).when(this.solrUtils).setAtomic(any(), any(), any(), any(), any()); + + // expected committed documents + SolrInputDocument solrInputDocument1 = new SolrInputDocument(); + solrInputDocument1.setField("id", "rating1"); + solrInputDocument1.setField(RatingQueryField.ENTITY_REFERENCE.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, newReference)); + + SolrInputDocument solrInputDocument2 = new SolrInputDocument(); + solrInputDocument2.setField("id", "rating2"); + solrInputDocument2.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference)); + solrInputDocument2.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference)); + + SolrInputDocument solrInputDocument3 = new SolrInputDocument(); + solrInputDocument3.setField("id", "rating3"); + solrInputDocument3.setField(RatingQueryField.ENTITY_REFERENCE.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, newReference)); + solrInputDocument3.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference)); + solrInputDocument3.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference)); + + SolrInputDocument solrInputDocument4 = new SolrInputDocument(); + solrInputDocument4.setField("id", "rating4"); + solrInputDocument4.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference)); + solrInputDocument4.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference)); + + List<SolrInputDocument> expectedAddDocuments = new ArrayList<>(Arrays.asList( + solrInputDocument1, solrInputDocument2, solrInputDocument3, solrInputDocument4)); + + when(this.solrClient.add(any(SolrInputDocument.class))).then(invocationOnMock -> { + SolrInputDocument solrInputDocument = invocationOnMock.getArgument(0); + SolrInputDocument expectedSolrInputDocument = expectedAddDocuments.remove(0); + + // There's no proper equals method for SolrInputDocument, so we're comparing the toString + assertEquals(expectedSolrInputDocument.toString(), solrInputDocument.toString()); + return null; + }); + + when(this.configuration.isAverageStored()).thenReturn(true); + this.manager.moveRatings(oldReference, newReference); + verify(this.solrClient, times(4)).add(any(SolrInputDocument.class)); + verify(this.solrClient).commit(); + verify(this.averageRatingManager).moveAverageRatings(oldReference, newReference); + } } diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/AbstractAverageRatingManagerTest.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/AbstractAverageRatingManagerTest.java index 6d1180db4fb..e0db08415c4 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/AbstractAverageRatingManagerTest.java +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/AbstractAverageRatingManagerTest.java @@ -85,6 +85,13 @@ public long removeAverageRatings(EntityReference entityReference) throws Ratings return 0; } + @Override + public long moveAverageRatings(EntityReference oldReference, EntityReference newReference) + throws RatingsException + { + return 0; + } + @Override protected ObservationManager getObservationManager() { diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManagerTest.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManagerTest.java index 0e1682a82ba..0674c9e9db0 100644 --- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManagerTest.java +++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManagerTest.java @@ -20,9 +20,14 @@ package org.xwiki.ratings.internal.averagerating; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrQuery; @@ -38,6 +43,7 @@ import org.xwiki.model.reference.EntityReferenceSerializer; import org.xwiki.ratings.AverageRating; import org.xwiki.ratings.RatingsManager; +import org.xwiki.ratings.internal.RatingSolrCoreInitializer; import org.xwiki.search.solr.Solr; import org.xwiki.search.solr.SolrUtils; import org.xwiki.test.junit5.mockito.ComponentTest; @@ -46,9 +52,11 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -284,4 +292,162 @@ void saveAverageRating() throws Exception verify(averageSolrClient).add(any(SolrInputDocument.class)); verify(averageSolrClient).commit(); } + + @Test + void moveAverageRatings() throws Exception + { + String managerId = "moveRatingsManagerId"; + when(this.ratingsManager.getIdentifier()).thenReturn(managerId); + when(this.solr.getClient(AverageRatingSolrCoreInitializer.DEFAULT_AVERAGE_RATING_SOLR_CORE)) + .thenReturn(this.solrClient); + + EntityReference oldReference = mock(EntityReference.class); + EntityReference newReference = mock(EntityReference.class); + when(oldReference.toString()).thenReturn("document:My.Old.Doc"); + + String filterQuery = String.format("filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))", + AverageRatingQueryField.MANAGER_ID.getFieldName(), managerId, + AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), "document\\:My.Old.Doc", + AverageRatingQueryField.PARENTS.getFieldName(), "document\\:My.Old.Doc"); + + SolrQuery expectedQuery1 = new SolrQuery() + .addFilterQuery(filterQuery) + .setRows(100) + .setStart(0) + .setSort(AverageRatingQueryField.UPDATED_AT.getFieldName(), SolrQuery.ORDER.asc); + + SolrDocument rating1 = mock(SolrDocument.class); + SolrDocument rating2 = mock(SolrDocument.class); + SolrDocument rating3 = mock(SolrDocument.class); + SolrDocument rating4 = mock(SolrDocument.class); + + // rating1 have the appropriate reference, but not the appropriate parent + when(rating1.get("id")).thenReturn("rating1"); + when(this.solrUtils.get(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), rating1, + EntityReference.class)) + .thenReturn(oldReference); + when(this.solrUtils.getCollection(AverageRatingQueryField.PARENTS.getFieldName(), rating1, + EntityReference.class)) + .thenReturn(Collections.emptyList()); + + // rating2 have not the appropriate reference but the appropriate parent + when(rating2.get("id")).thenReturn("rating2"); + when(this.solrUtils.get(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), rating2, EntityReference.class)) + .thenReturn(mock(EntityReference.class)); + when(this.solrUtils.getCollection(AverageRatingQueryField.PARENTS.getFieldName(), rating2, + EntityReference.class)) + .thenReturn(Collections.singletonList(oldReference)); + + // rating3 have the appropriate reference and also contain the appropriate parent + when(rating3.get("id")).thenReturn("rating3"); + when(this.solrUtils.get(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), rating3, EntityReference.class)) + .thenReturn(oldReference); + when(this.solrUtils.getCollection(AverageRatingQueryField.PARENTS.getFieldName(), rating3, + EntityReference.class)) + .thenReturn(Arrays.asList(mock(EntityReference.class), oldReference, mock(EntityReference.class))); + + // rating4 only contain the appropriate parent + when(rating4.get("id")).thenReturn("rating4"); + when(this.solrUtils.get(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), rating4, EntityReference.class)) + .thenReturn(mock(EntityReference.class)); + when(this.solrUtils.getCollection(AverageRatingQueryField.PARENTS.getFieldName(), rating4, + EntityReference.class)) + .thenReturn(Arrays.asList(mock(EntityReference.class), mock(EntityReference.class), oldReference)); + + when(this.documentList.iterator()) + .thenReturn(Arrays.asList(rating1, rating2, rating3, rating4).iterator()); + + SolrQuery expectedQuery2 = new SolrQuery() + .addFilterQuery(filterQuery) + .setRows(100) + .setStart(100) + .setSort(AverageRatingQueryField.UPDATED_AT.getFieldName(), SolrQuery.ORDER.asc); + + QueryResponse response1 = mock(QueryResponse.class); + QueryResponse response2 = mock(QueryResponse.class); + + AtomicInteger queryCounter = new AtomicInteger(0); + when(solrClient.query(any())).then(invocationOnMock -> { + + SolrQuery givenQuery = invocationOnMock.getArgument(0); + QueryResponse result = null; + if (queryCounter.get() == 0) { + assertEquals(expectedQuery1.getQuery(), givenQuery.getQuery()); + assertArrayEquals(expectedQuery1.getFilterQueries(), givenQuery.getFilterQueries()); + assertEquals(expectedQuery1.getRows(), givenQuery.getRows()); + assertEquals(expectedQuery1.getStart(), givenQuery.getStart()); + assertEquals(expectedQuery1.getSorts(), givenQuery.getSorts()); + result = response1; + } else if (queryCounter.get() == 1) { + assertEquals(expectedQuery2.getQuery(), givenQuery.getQuery()); + assertArrayEquals(expectedQuery2.getFilterQueries(), givenQuery.getFilterQueries()); + assertEquals(expectedQuery2.getRows(), givenQuery.getRows()); + assertEquals(expectedQuery2.getStart(), givenQuery.getStart()); + assertEquals(expectedQuery2.getSorts(), givenQuery.getSorts()); + result = response2; + } else { + fail("Too many requests performed."); + } + queryCounter.getAndIncrement(); + return result; + }); + when(response1.getResults()).thenReturn(this.documentList); + when(response2.getResults()).thenReturn(new SolrDocumentList()); + + doAnswer(invocationOnMock -> { + String modifier = invocationOnMock.getArgument(0); + String fieldName = invocationOnMock.getArgument(1); + Object fieldValue = invocationOnMock.getArgument(2); + // we normally only invoke it with entity reference. + assertEquals(EntityReference.class, invocationOnMock.getArgument(3)); + SolrInputDocument solrInputDocument = invocationOnMock.getArgument(4); + solrInputDocument.setField(fieldName, Collections.singletonMap(modifier, fieldValue)); + return null; + }).when(this.solrUtils).setAtomic(any(), any(), any(), any(), any()); + + // expected committed documents + SolrInputDocument solrInputDocument1 = new SolrInputDocument(); + solrInputDocument1.setField("id", "rating1"); + solrInputDocument1.setField(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, newReference)); + + SolrInputDocument solrInputDocument2 = new SolrInputDocument(); + solrInputDocument2.setField("id", "rating2"); + solrInputDocument2.setField(AverageRatingQueryField.PARENTS.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference)); + solrInputDocument2.setField(AverageRatingQueryField.PARENTS.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference)); + + SolrInputDocument solrInputDocument3 = new SolrInputDocument(); + solrInputDocument3.setField("id", "rating3"); + solrInputDocument3.setField(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, newReference)); + solrInputDocument3.setField(AverageRatingQueryField.PARENTS.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference)); + solrInputDocument3.setField(AverageRatingQueryField.PARENTS.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference)); + + SolrInputDocument solrInputDocument4 = new SolrInputDocument(); + solrInputDocument4.setField("id", "rating4"); + solrInputDocument4.setField(AverageRatingQueryField.PARENTS.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference)); + solrInputDocument4.setField(AverageRatingQueryField.PARENTS.getFieldName(), + Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference)); + + List<SolrInputDocument> expectedAddDocuments = new ArrayList<>(Arrays.asList( + solrInputDocument1, solrInputDocument2, solrInputDocument3, solrInputDocument4)); + + when(this.solrClient.add(any(SolrInputDocument.class))).then(invocationOnMock -> { + SolrInputDocument solrInputDocument = invocationOnMock.getArgument(0); + SolrInputDocument expectedSolrInputDocument = expectedAddDocuments.remove(0); + + // There's no proper equals method for SolrInputDocument, so we're comparing the toString + assertEquals(expectedSolrInputDocument.toString(), solrInputDocument.toString()); + return null; + }); + + this.averageRatingManager.moveAverageRatings(oldReference, newReference); + verify(this.solrClient, times(4)).add(any(SolrInputDocument.class)); + verify(this.solrClient).commit(); + } } diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/SolrUtils.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/SolrUtils.java index 012e968915f..1a67a04265e 100644 --- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/SolrUtils.java +++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/SolrUtils.java @@ -162,6 +162,23 @@ public interface SolrUtils */ void setAtomic(String modifier, String fieldName, Object fieldValue, SolrInputDocument document); + /** + * Store in the document the value associated with the passed field name. + * + * @param modifier the atomic update modifier to apply (set, add, add-distinct, remove, removeregex, inc) + * @param fieldName the name of the field in the document + * @param fieldValue the value to store in the {@link SolrDocument} + * @param valueType the type to use as reference to serialize the value + * @param document the Solr document + * @since 12.10 + */ + @Unstable + default void setAtomic(String modifier, String fieldName, Object fieldValue, Type valueType, + SolrInputDocument document) + { + setAtomic(modifier, fieldName, fieldValue, document); + } + /** * Store in the document the values associated with the passed field name. * <p> @@ -173,6 +190,8 @@ public interface SolrUtils */ void set(String fieldName, Collection<?> fieldValue, SolrInputDocument document); + + /** * Store in the document the value associated with the passed field name. * <p> diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrUtils.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrUtils.java index 87c44d046a5..6223577bd30 100644 --- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrUtils.java +++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrUtils.java @@ -367,6 +367,13 @@ public void setAtomic(String modifier, String fieldName, Object fieldValue, Solr document.setField(fieldName, Collections.singletonMap(modifier, fieldValue)); } + @Override + public void setAtomic(String modifier, String fieldName, Object fieldValue, Type valueType, + SolrInputDocument document) + { + document.setField(fieldName, Collections.singletonMap(modifier, toString(fieldValue, valueType))); + } + @Override public void setString(String fieldName, Object fieldValue, SolrInputDocument document) { -- GitLab