From bd50b3df70c7e315fa4152a00cba89aaf698eb8a Mon Sep 17 00:00:00 2001 From: Neil McErlean Date: Thu, 8 Jul 2010 13:59:58 +0000 Subject: [PATCH] RatingService Phase 1. The RatingService will allow users to apply ratings to content nodes in the repository. There will be a number of built-in Rating Schemes and support to add more by the usual extension mechanism. Out of the box, we envision a 'likes' scheme (user X likes this document) and a 'star' rating (user X gave this document 3 out of 5 stars). Content model for ratings. Spring config includes two out-of-the-box rating schemes Various basic infrastructure classes for Ratings, RatingSchemes and the service itself. Basic CRUD for ratings in a Java foundation layer. Associated JUnit tests. The next contribution will add support for per-node average and total ratings and associated tests. git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@21000 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../application-context-highlevel.xml | 1 + config/alfresco/model/contentModel.xml | 57 ++++ config/alfresco/rating-services-context.xml | 81 +++++ .../repo/rating/RatingSchemeImpl.java | 94 ++++++ .../repo/rating/RatingSchemeRegistry.java | 62 ++++ .../repo/rating/RatingServiceImpl.java | 285 ++++++++++++++++++ .../rating/RatingServiceIntegrationTest.java | 261 ++++++++++++++++ .../rendition/MockedTestServiceRegistry.java | 8 + .../service/ServiceDescriptorRegistry.java | 9 + .../org/alfresco/service/ServiceRegistry.java | 9 + .../alfresco/service/cmr/rating/Rating.java | 85 ++++++ .../service/cmr/rating/RatingScheme.java | 50 +++ .../service/cmr/rating/RatingService.java | 101 +++++++ .../cmr/rating/RatingServiceException.java | 53 ++++ 14 files changed, 1156 insertions(+) create mode 100644 config/alfresco/rating-services-context.xml create mode 100644 source/java/org/alfresco/repo/rating/RatingSchemeImpl.java create mode 100644 source/java/org/alfresco/repo/rating/RatingSchemeRegistry.java create mode 100644 source/java/org/alfresco/repo/rating/RatingServiceImpl.java create mode 100644 source/java/org/alfresco/repo/rating/RatingServiceIntegrationTest.java create mode 100644 source/java/org/alfresco/service/cmr/rating/Rating.java create mode 100644 source/java/org/alfresco/service/cmr/rating/RatingScheme.java create mode 100644 source/java/org/alfresco/service/cmr/rating/RatingService.java create mode 100644 source/java/org/alfresco/service/cmr/rating/RatingServiceException.java diff --git a/config/alfresco/application-context-highlevel.xml b/config/alfresco/application-context-highlevel.xml index 8a62ef61df..0d6e846270 100644 --- a/config/alfresco/application-context-highlevel.xml +++ b/config/alfresco/application-context-highlevel.xml @@ -9,6 +9,7 @@ + diff --git a/config/alfresco/model/contentModel.xml b/config/alfresco/model/contentModel.xml index 25b036161a..8ed0447374 100644 --- a/config/alfresco/model/contentModel.xml +++ b/config/alfresco/model/contentModel.xml @@ -422,6 +422,45 @@ + + Rating + + + Rating + d:int + true + true + + true + true + false + + + + Rating Scheme + d:text + true + true + + true + true + false + + + + Rated at + d:date + true + true + + true + true + false + + + + + @@ -941,6 +980,24 @@ + + + Rateable + + + + false + false + + + cm:rating + false + true + + false + + + Attachable diff --git a/config/alfresco/rating-services-context.xml b/config/alfresco/rating-services-context.xml new file mode 100644 index 0000000000..3d4af56032 --- /dev/null +++ b/config/alfresco/rating-services-context.xml @@ -0,0 +1,81 @@ + + + + + + + + + org.alfresco.service.cmr.rating.RatingService + + + + + + + + + + + + + + + + + + + + + + ${server.transaction.mode.default} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/java/org/alfresco/repo/rating/RatingSchemeImpl.java b/source/java/org/alfresco/repo/rating/RatingSchemeImpl.java new file mode 100644 index 0000000000..3cf8bb8a07 --- /dev/null +++ b/source/java/org/alfresco/repo/rating/RatingSchemeImpl.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2005-2010 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 . + */ + +package org.alfresco.repo.rating; + +import org.alfresco.service.cmr.rating.RatingScheme; +import org.springframework.beans.factory.BeanNameAware; + +/** + * TODO + * @author Neil McErlean + * @since 3.4 + */ +public class RatingSchemeImpl implements RatingScheme, BeanNameAware +{ + private final RatingSchemeRegistry ratingSchemeRegistry; + + private String name; + private int minRating, maxRating; + + public RatingSchemeImpl(RatingSchemeRegistry registry) + { + this.ratingSchemeRegistry = registry; + } + + public void init() + { + ratingSchemeRegistry.register(this.name, this); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.BeanNameAware#setBeanName(java.lang.String) + */ + public void setBeanName(String name) + { + this.name = name; + } + + public void setMinRating(int minRating) + { + this.minRating = minRating; + } + + public void setMaxRating(int maxRating) + { + this.maxRating = maxRating; + } + + //TODO afterPropertiesSet assert Max > Min + + public int getMaxRating() + { + return this.maxRating; + } + + public int getMinRating() + { + return this.minRating; + } + + public String getName() + { + return this.name; + } + + @Override + public String toString() + { + StringBuilder msg = new StringBuilder(); + msg.append(this.getClass().getSimpleName()) + .append(" ").append(this.name) + .append(" [").append(this.minRating) + .append("..").append(this.maxRating) + .append("]"); + return msg.toString(); + } +} diff --git a/source/java/org/alfresco/repo/rating/RatingSchemeRegistry.java b/source/java/org/alfresco/repo/rating/RatingSchemeRegistry.java new file mode 100644 index 0000000000..3437f8b9f8 --- /dev/null +++ b/source/java/org/alfresco/repo/rating/RatingSchemeRegistry.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2005-2010 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 . + */ + +package org.alfresco.repo.rating; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.alfresco.service.cmr.rating.RatingScheme; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * This class maintains a registry of all known {@link RatingScheme rating schemes} in the system. + * @author Neil McErlean + * @since 3.4 + */ +public class RatingSchemeRegistry +{ + private static final Log log = LogFactory.getLog(RatingSchemeRegistry.class); + + Map ratingSchemes = new HashMap(); + + public void register(String name, RatingScheme ratingScheme) + { + ratingSchemes.put(name, ratingScheme); + if (log.isDebugEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Registering ") + .append(ratingScheme); + + log.debug(msg.toString()); + } + } + + /** + * This method returns an unmodifiable map of the registered rating schemes. + * @return + */ + public Map getRatingSchemes() + { + return Collections.unmodifiableMap(ratingSchemes); + } +} diff --git a/source/java/org/alfresco/repo/rating/RatingServiceImpl.java b/source/java/org/alfresco/repo/rating/RatingServiceImpl.java new file mode 100644 index 0000000000..62f86629fb --- /dev/null +++ b/source/java/org/alfresco/repo/rating/RatingServiceImpl.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2005-2010 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 . + */ + +package org.alfresco.repo.rating; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.rating.Rating; +import org.alfresco.service.cmr.rating.RatingScheme; +import org.alfresco.service.cmr.rating.RatingService; +import org.alfresco.service.cmr.rating.RatingServiceException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/* + * @author Neil McErlean + * @since 3.4 + */ +public class RatingServiceImpl implements RatingService +{ + //TODO Add links to ActivityService. Straight calls? Behaviours? + + private static final Log log = LogFactory.getLog(RatingServiceImpl.class); + private RatingSchemeRegistry schemeRegistry; + + // Injected services + private NodeService nodeService; + + public void setRatingSchemeRegistry(RatingSchemeRegistry schemeRegistry) + { + this.schemeRegistry = schemeRegistry; + } + + public void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + public Map getRatingSchemes() + { + // This is already an unmodifiable Map. + return schemeRegistry.getRatingSchemes(); + } + + public RatingScheme getRatingScheme(String ratingSchemeName) + { + return schemeRegistry.getRatingSchemes().get(ratingSchemeName); + } + + /* + * (non-Javadoc) + * @see org.alfresco.service.cmr.rating.RatingService#applyRating(org.alfresco.service.cmr.repository.NodeRef, int, java.lang.String) + */ + public void applyRating(NodeRef targetNode, int rating, + String ratingSchemeName) throws RatingServiceException + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + this.applyRating(targetNode, rating, ratingSchemeName, currentUser); + } + + @SuppressWarnings("unchecked") + private void applyRating(NodeRef targetNode, int rating, + String ratingSchemeName, final String userName) throws RatingServiceException + { + // Sanity check the rating scheme being used and the rating being applied. + final RatingScheme ratingScheme = this.getRatingScheme(ratingSchemeName); + if (ratingScheme == null) + { + throw new RatingServiceException("Unrecognised rating scheme: " + ratingSchemeName); + } + if (rating < ratingScheme.getMinRating() || rating > ratingScheme.getMaxRating()) + { + throw new RatingServiceException("Rating " + rating + " violates range for " + ratingScheme); + } + + // Add the cm:rateable aspect if it's not there already. + if (nodeService.hasAspect(targetNode, ContentModel.ASPECT_RATEABLE) == false) + { + nodeService.addAspect(targetNode, ContentModel.ASPECT_RATEABLE, null); + } + + // We're looking for child cm:rating nodes whose qname matches the current user. + // i.e. we're looking for previously applied ratings by this user. + final QName assocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, userName); + List myRatingChildren = nodeService.getChildAssocs(targetNode, ContentModel.ASSOC_RATINGS, assocQName); + if (myRatingChildren.isEmpty()) + { + // There are no previous ratings from this user, so we create a new cm:rating child node. + + // These are multivalued properties. + Map ratingProps = new HashMap(); + ratingProps.put(ContentModel.PROP_RATING_SCORE, new Integer[]{rating}); + ratingProps.put(ContentModel.PROP_RATING_SCORE, toSerializableList(new Integer[]{rating})); + ratingProps.put(ContentModel.PROP_RATED_AT, toSerializableList(new Date[]{new Date()})); + ratingProps.put(ContentModel.PROP_RATING_SCHEME, toSerializableList(new String[]{ratingSchemeName})); + + nodeService.createNode(targetNode, ContentModel.ASSOC_RATINGS, assocQName, ContentModel.TYPE_RATING, ratingProps); + } + else + { + // There are previous ratings by this user. Things are a little more complex. + if (myRatingChildren.size() > 1) + { + //TODO This should not happen. Log + } + NodeRef myPreviousRatingsNode = myRatingChildren.get(0).getChildRef(); + Map existingProps = nodeService.getProperties(myPreviousRatingsNode); + List existingRatingSchemes = (List)existingProps.get(ContentModel.PROP_RATING_SCHEME); + List existingRatingScores = (List)existingProps.get(ContentModel.PROP_RATING_SCORE); + List existingRatingDates = (List)existingProps.get(ContentModel.PROP_RATED_AT); + + //TODO These should all be the same length lists. Log if not. + + // If the schemes list already contains an entry matching the rating we're setting + // we need to delete it and then delete the score and date at the corresponding indexes. + int indexOfExistingRating = existingRatingSchemes.indexOf(ratingSchemeName); + if (indexOfExistingRating != -1) + { + existingRatingSchemes.remove(indexOfExistingRating); + existingRatingScores.remove(indexOfExistingRating); + existingRatingDates.remove(indexOfExistingRating); + } + + existingRatingSchemes.add(ratingSchemeName); + existingRatingScores.add(rating); + existingRatingDates.add(new Date()); + + nodeService.setProperties(myPreviousRatingsNode, existingProps); + } + } + + private Serializable toSerializableList(Object[] array) + { + return (Serializable)Arrays.asList(array); + } + + /* + * (non-Javadoc) + * @see org.alfresco.service.cmr.rating.RatingService#getRating(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.rating.RatingScheme) + */ + public Rating getRatingByCurrentUser(NodeRef targetNode, RatingScheme ratingScheme) + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + return this.getRating(targetNode, ratingScheme, currentUser); + } + + @SuppressWarnings("unchecked") + private Rating getRating(NodeRef targetNode, RatingScheme ratingScheme, String user) + { + List ratingChildren = getRatingNodeChildren(targetNode, user); + + // If there are none, return null + if (ratingChildren.isEmpty()) + { + return null; + } + + // Take the last node pertaining to the current user. + ChildAssociationRef lastChild = ratingChildren.get(ratingChildren.size() - 1); + Map properties = nodeService.getProperties(lastChild.getChildRef()); + + // Find the index of the rating scheme we're interested in. + int index = findIndexOfRatingScheme(properties, ratingScheme); + if (index == -1) + { + // There is no rating in this scheme by the specified user. + return null; + } + else + { + // There is a rating and the associated data are at the index'th place in each multivalued property. + List ratingScores = (List)properties.get(ContentModel.PROP_RATING_SCORE); + List ratingDates = (List)properties.get(ContentModel.PROP_RATED_AT); + + Rating result = new Rating(ratingScheme, + ratingScores.get(index), + user, + ratingDates.get(index)); + return result; + } + + //TODO Don't forget that it is possible on read to have out-of-range ratings. + } + + @SuppressWarnings("unchecked") + private int findIndexOfRatingScheme(Map properties, RatingScheme scheme) + { + List ratingSchemes = (List)properties.get(ContentModel.PROP_RATING_SCHEME); + return ratingSchemes.indexOf(scheme.getName()); + } + + /* + * (non-Javadoc) + * @see org.alfresco.service.cmr.rating.RatingService#removeRatingByCurrentUser(org.alfresco.service.cmr.repository.NodeRef, org.alfresco.service.cmr.rating.RatingScheme) + */ + public Rating removeRatingByCurrentUser(NodeRef targetNode, + RatingScheme ratingScheme) + { + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + return removeRating(targetNode, ratingScheme, currentUser); + } + + @SuppressWarnings("unchecked") + private Rating removeRating(NodeRef targetNode, RatingScheme ratingScheme, String user) + { + List ratingChildren = getRatingNodeChildren(targetNode, user); + if (ratingChildren.isEmpty()) + { + // There are no ratings by any user. + return null; + } + // Take the last node pertaining to the specified user. + ChildAssociationRef lastChild = ratingChildren.get(ratingChildren.size() - 1); + Map properties = nodeService.getProperties(lastChild.getChildRef()); + + // Find the index of the rating scheme we're interested in. + int index = this.findIndexOfRatingScheme(properties, ratingScheme); + if (index == -1) + { + // There is no rating in this scheme by the specified user. + return null; + } + else + { + // There is a rating and the associated data are at the index'th place in each property. + List ratingScores = (List)properties.get(ContentModel.PROP_RATING_SCORE); + List ratingDates = (List)properties.get(ContentModel.PROP_RATED_AT); + List ratingSchemes = (List)properties.get(ContentModel.PROP_RATING_SCHEME); + + Integer oldScore = ratingScores.remove(index); + Date oldDate = ratingDates.remove(index); + String oldScheme = ratingSchemes.remove(index); + + return new Rating(this.getRatingScheme(oldScheme), + oldScore, user, oldDate); + } + } + + /** + * This method gets all the cm:rating child nodes of the specified targetNode that + * have been applied by the specified user. + * + * @param targetNode the target node under which the cm:rating nodes reside. + * @param user the user name of the user whose ratings are sought. + * @return + */ + private List getRatingNodeChildren(NodeRef targetNode, + String user) + { + QName userAssocQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, user); + + // Get all the rating nodes which are from the specified user. + List ratingChildren = + nodeService.getChildAssocs(targetNode, ContentModel.ASSOC_RATINGS, userAssocQName); + return ratingChildren; + } +} diff --git a/source/java/org/alfresco/repo/rating/RatingServiceIntegrationTest.java b/source/java/org/alfresco/repo/rating/RatingServiceIntegrationTest.java new file mode 100644 index 0000000000..10a1abf99f --- /dev/null +++ b/source/java/org/alfresco/repo/rating/RatingServiceIntegrationTest.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2005-2010 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 . + */ + +package org.alfresco.repo.rating; + +import java.io.Serializable; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.model.Repository; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.rating.Rating; +import org.alfresco.service.cmr.rating.RatingScheme; +import org.alfresco.service.cmr.rating.RatingService; +import org.alfresco.service.cmr.rating.RatingServiceException; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.BaseAlfrescoSpringTest; + +/** + * @author Neil McErlean + * @since 3.4 + */ +public class RatingServiceIntegrationTest extends BaseAlfrescoSpringTest +{ + private RatingService ratingService; + private Repository repositoryHelper; + private NodeRef companyHome; + + // These NodeRefs are used by the test methods. + private NodeRef testFolder; + private NodeRef testSubFolder; + private NodeRef testDocInFolder; + private NodeRef testDocInSubFolder; + + // The out of the box scheme names. + private static final String LIKES_SCHEME_NAME = "likesRatingScheme"; + private static final String FIVE_STAR_SCHEME_NAME = "fiveStarRatingScheme"; + + @Override + protected void onSetUpInTransaction() throws Exception + { + super.onSetUpInTransaction(); + this.ratingService = (RatingService) this.applicationContext.getBean("ratingService"); + this.repositoryHelper = (Repository) this.applicationContext.getBean("repositoryHelper"); + + // Set the current security context as admin + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getAdminUserName()); + + companyHome = this.repositoryHelper.getCompanyHome(); + + //TODO These could be created @BeforeClass + testFolder = createNode(companyHome, "testFolder", ContentModel.TYPE_FOLDER); + testSubFolder = createNode(testFolder, "testSubFolder", ContentModel.TYPE_FOLDER); + + testDocInFolder = createNode(testFolder, "testDocInFolder", ContentModel.TYPE_CONTENT); + testDocInSubFolder = createNode(testSubFolder, "testDocInSubFolder", ContentModel.TYPE_CONTENT); + } + + private NodeRef createNode(NodeRef parentNode, String name, QName type) + { + Map props = new HashMap(); + String fullName = name + System.currentTimeMillis(); + props.put(ContentModel.PROP_NAME, fullName); + QName docContentQName = QName.createQName(NamespaceService.APP_MODEL_1_0_URI, fullName); + NodeRef node = nodeService.createNode(parentNode, + ContentModel.ASSOC_CONTAINS, + docContentQName, + type, + props).getChildRef(); + return node; + } + + /** + * This method tests that the expected 'out of the box' rating schemes are available + * and correctly initialised. + */ + public void testOutOfTheBoxRatingSchemes() throws Exception + { + Map schemes = this.ratingService.getRatingSchemes(); + + assertNotNull("rating scheme collection was null.", schemes); + assertTrue("rating scheme collection was empty.", schemes.isEmpty() == false); + + RatingScheme likesRS = schemes.get(LIKES_SCHEME_NAME); + assertNotNull("'likes' rating scheme was missing.", likesRS); + assertEquals("'likes' rating scheme had wrong name.", LIKES_SCHEME_NAME, likesRS.getName()); + assertEquals("'likes' rating scheme had wrong min.", 1, likesRS.getMinRating()); + assertEquals("'likes' rating scheme had wrong max.", 1, likesRS.getMaxRating()); + + RatingScheme fiveStarRS = schemes.get(FIVE_STAR_SCHEME_NAME); + assertNotNull("'5*' rating scheme was missing.", fiveStarRS); + assertEquals("'5*' rating scheme had wrong name.", FIVE_STAR_SCHEME_NAME, fiveStarRS.getName()); + assertEquals("'5*' rating scheme had wrong min.", 0, fiveStarRS.getMinRating()); + assertEquals("'5*' rating scheme had wrong max.", 5, fiveStarRS.getMaxRating()); + } + + /** + * This test method ensures that an attempt to apply an out-of-range rating value + * throws the expected exception. + */ + public void testApplyIllegalRatings() throws Exception + { + // See rating-services-context.xml for definitions of these rating schemes. + int[] illegalRatings = new int[]{0, 2}; + for (int illegalRating : illegalRatings) + { + applyIllegalRating(testDocInFolder, illegalRating, LIKES_SCHEME_NAME); + } + } + + private void applyIllegalRating(NodeRef nodeRef, int illegalRating, String schemeName) + { + try + { + ratingService.applyRating(nodeRef, illegalRating, schemeName); + } catch (RatingServiceException expectedException) + { + return; + } + fail("Illegal rating " + illegalRating + " should have caused exception."); + } + + public void testApplyUpdateDeleteRatings_SingleUserMultipleSchemes() throws Exception + { + //Before we start, let's ensure the read behaviour on a pristine node is correct. + final RatingScheme likesRatingScheme = ratingService.getRatingScheme(LIKES_SCHEME_NAME); + Rating nullRating = ratingService.getRatingByCurrentUser(testDocInFolder, likesRatingScheme); + assertNull("Expected a null rating,", nullRating); + assertNull("Expected a null remove result.", ratingService.removeRatingByCurrentUser(testDocInFolder, likesRatingScheme)); + + final int likesScore = 1; + final int fiveStarScore = 5; + + // Both of these ratings will be applied by the same user: the 'current' user. + ratingService.applyRating(testDocInFolder, likesScore, LIKES_SCHEME_NAME); + ratingService.applyRating(testDocInFolder, fiveStarScore, FIVE_STAR_SCHEME_NAME); + + // Some basic node structure tests. + assertTrue(ContentModel.ASPECT_RATEABLE + " aspect missing.", + nodeService.hasAspect(testDocInFolder, ContentModel.ASPECT_RATEABLE)); + + List allChildren = nodeService.getChildAssocs(testDocInFolder, + ContentModel.ASSOC_RATINGS, RegexQNamePattern.MATCH_ALL); + + // It's one cm:rating node per user + assertEquals("Wrong number of ratings nodes.", 1, allChildren.size()); + // child-assoc of type cm:ratings + assertEquals("Wrong type qname on ratings assoc", ContentModel.ASSOC_RATINGS, allChildren.get(0).getTypeQName()); + // child-assoc of name cm: + QName expectedAssocName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, AuthenticationUtil.getFullyAuthenticatedUser()); + assertEquals("Wrong qname on ratings assoc", expectedAssocName, allChildren.get(0).getQName()); + // node structure seems ok. + + + // Now to check the persisted ratings data are ok. + Rating likeRating = ratingService.getRatingByCurrentUser(testDocInFolder, likesRatingScheme); + + final RatingScheme fiveStarRatingScheme = ratingService.getRatingScheme(FIVE_STAR_SCHEME_NAME); + Rating fiveStarRating = ratingService.getRatingByCurrentUser(testDocInFolder, fiveStarRatingScheme); + + assertNotNull("'like' rating was null.", likeRating); + assertEquals("Wrong score for rating", likesScore, likeRating.getScore()); + assertEquals("Wrong user for rating", AuthenticationUtil.getFullyAuthenticatedUser(), likeRating.getAppliedBy()); + final Date likeRatingAppliedAt = likeRating.getAppliedAt(); + assertDateIsCloseToNow(likeRatingAppliedAt); + + assertNotNull("'5*' rating was null.", fiveStarRating); + assertEquals("Wrong score for rating", fiveStarScore, fiveStarRating.getScore()); + assertEquals("Wrong user for rating", AuthenticationUtil.getFullyAuthenticatedUser(), fiveStarRating.getAppliedBy()); + final Date fiveStarRatingAppliedAt = fiveStarRating.getAppliedAt(); + assertDateIsCloseToNow(fiveStarRatingAppliedAt); + + // Now we'll update a rating + final int updatedFiveStarScore = 3; + ratingService.applyRating(testDocInFolder, updatedFiveStarScore, FIVE_STAR_SCHEME_NAME); + + // Some basic node structure tests. + allChildren = nodeService.getChildAssocs(testDocInFolder, + ContentModel.ASSOC_RATINGS, RegexQNamePattern.MATCH_ALL); + + // Still one cm:rating node + assertEquals("Wrong number of ratings nodes.", 1, allChildren.size()); + // Same assoc names + assertEquals("Wrong type qname on ratings assoc", ContentModel.ASSOC_RATINGS, allChildren.get(0).getTypeQName()); + assertEquals("Wrong qname on ratings assoc", expectedAssocName, allChildren.get(0).getQName()); + // node structure seems ok. + + + // Now to check the updated ratings data are ok. + Rating updatedFiveStarRating = ratingService.getRatingByCurrentUser(testDocInFolder, fiveStarRatingScheme); + + // 'like' rating data should be unchanged. + assertNotNull("'like' rating was null.", likeRating); + assertEquals("Wrong score for rating", likesScore, likeRating.getScore()); + assertEquals("Wrong user for rating", AuthenticationUtil.getFullyAuthenticatedUser(), likeRating.getAppliedBy()); + assertEquals("Wrong date for rating", likeRatingAppliedAt, likeRating.getAppliedAt()); + + // But these 'five star' data should be changed - new score, new date + assertNotNull("'5*' rating was null.", updatedFiveStarRating); + assertEquals("Wrong score for rating", updatedFiveStarScore, updatedFiveStarRating.getScore()); + assertEquals("Wrong user for rating", AuthenticationUtil.getFullyAuthenticatedUser(), updatedFiveStarRating.getAppliedBy()); + assertTrue("five star rating date was unchanged.", fiveStarRatingAppliedAt.equals(updatedFiveStarRating.getAppliedAt()) == false); + assertDateIsCloseToNow(updatedFiveStarRating.getAppliedAt()); + + // Now we'll delete the 'likes' rating. + Rating deletedLikesRating = ratingService.removeRatingByCurrentUser(testDocInFolder, likesRatingScheme); + // 'like' rating data should be unchanged. + assertNotNull("'like' rating was null.", deletedLikesRating); + assertEquals("Wrong score for rating", likesScore, deletedLikesRating.getScore()); + assertEquals("Wrong user for rating", AuthenticationUtil.getFullyAuthenticatedUser(), deletedLikesRating.getAppliedBy()); + assertEquals("Wrong date for rating", likeRatingAppliedAt, deletedLikesRating.getAppliedAt()); + + // And delete the 'five star' rating. + Rating deletedStarRating = ratingService.removeRatingByCurrentUser(testDocInFolder, fiveStarRatingScheme); + // 'five star' rating data should be unchanged. + assertNotNull("'5*' rating was null.", deletedStarRating); + assertEquals("Wrong score for rating", updatedFiveStarScore, deletedStarRating.getScore()); + assertEquals("Wrong user for rating", AuthenticationUtil.getFullyAuthenticatedUser(), deletedStarRating.getAppliedBy()); + assertEquals("Wrong date for rating", updatedFiveStarRating.getAppliedAt(), deletedStarRating.getAppliedAt()); + } + + /** + * This test method asserts that the specified date is effectively equal to now. + * We can't assert that the two dates are exactly equal but we do assert that + * they are equal to within a specified tolerance. + * @param d the date to check + */ + private void assertDateIsCloseToNow(Date d) + { + assertNotNull("Date was unexpected null", d); + Date now = new Date(); + assertTrue(now.after(d)); + final long millisTolerance = 5000l; // 5 seconds + assertTrue("Date was not within " + millisTolerance + "ms of 'now'.", now.getTime() - d.getTime() < millisTolerance); + } + + //TODO Multiple users applying ratings to a doc. +} diff --git a/source/java/org/alfresco/repo/rendition/MockedTestServiceRegistry.java b/source/java/org/alfresco/repo/rendition/MockedTestServiceRegistry.java index 24095f1a2d..bfdf469b71 100644 --- a/source/java/org/alfresco/repo/rendition/MockedTestServiceRegistry.java +++ b/source/java/org/alfresco/repo/rendition/MockedTestServiceRegistry.java @@ -46,6 +46,7 @@ import org.alfresco.service.cmr.ml.ContentFilterLanguagesService; import org.alfresco.service.cmr.ml.EditionService; import org.alfresco.service.cmr.ml.MultilingualContentService; import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.rating.RatingService; import org.alfresco.service.cmr.rendition.RenditionService; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.CopyService; @@ -317,6 +318,13 @@ public class MockedTestServiceRegistry implements ServiceRegistry } + public RatingService getRatingService() + { + // TODO Auto-generated method stub + return null; + } + + public FileFolderService getFileFolderService() { // TODO Auto-generated method stub diff --git a/source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java b/source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java index b4a4ecc91c..04163d43e1 100644 --- a/source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java +++ b/source/java/org/alfresco/repo/service/ServiceDescriptorRegistry.java @@ -44,6 +44,7 @@ import org.alfresco.service.cmr.ml.ContentFilterLanguagesService; import org.alfresco.service.cmr.ml.EditionService; import org.alfresco.service.cmr.ml.MultilingualContentService; import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.rating.RatingService; import org.alfresco.service.cmr.rendition.RenditionService; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.CopyService; @@ -521,6 +522,14 @@ public class ServiceDescriptorRegistry return (RenditionService)getService(RENDITION_SERVICE); } + /* + * (non-Javadoc) + * @see org.alfresco.service.ServiceRegistry#getRatingService() + */ + public RatingService getRatingService() + { + return (RatingService)getService(RATING_SERVICE); + } /* (non-Javadoc) * @see org.alfresco.service.ServiceRegistry#getInvitationService() diff --git a/source/java/org/alfresco/service/ServiceRegistry.java b/source/java/org/alfresco/service/ServiceRegistry.java index e2b9f149f1..fa0f1b6d87 100644 --- a/source/java/org/alfresco/service/ServiceRegistry.java +++ b/source/java/org/alfresco/service/ServiceRegistry.java @@ -43,6 +43,7 @@ import org.alfresco.service.cmr.ml.ContentFilterLanguagesService; import org.alfresco.service.cmr.ml.EditionService; import org.alfresco.service.cmr.ml.MultilingualContentService; import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.rating.RatingService; import org.alfresco.service.cmr.rendition.RenditionService; import org.alfresco.service.cmr.repository.ContentService; import org.alfresco.service.cmr.repository.CopyService; @@ -132,6 +133,7 @@ public interface ServiceRegistry static final QName INVITATION_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "InvitationService"); static final QName PREFERENCE_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "PreferenceService"); static final QName RENDITION_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "RenditionService"); + static final QName RATING_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "RatingService"); // WCM / AVM static final QName AVM_SERVICE = QName.createQName(NamespaceService.ALFRESCO_URI, "AVMService"); @@ -496,6 +498,13 @@ public interface ServiceRegistry */ @NotAuditable RenditionService getRenditionService(); + + /** + * Get the rating service (or null if one is not provided) + * @return + */ + @NotAuditable + RatingService getRatingService(); /** * Get the invitation service (or null if one is not provided) diff --git a/source/java/org/alfresco/service/cmr/rating/Rating.java b/source/java/org/alfresco/service/cmr/rating/Rating.java new file mode 100644 index 0000000000..a5db35125c --- /dev/null +++ b/source/java/org/alfresco/service/cmr/rating/Rating.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2005-2010 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 . + */ + +package org.alfresco.service.cmr.rating; + +import java.util.Date; + +/** + * This struct class holds the essential data of a rating. + * + * @author Neil McErlean + * @since 3.4 + */ +public class Rating +{ + private final int ratingScore; + private final String ratingAppliedBy; + private final Date ratingAppliedAt; + private final RatingScheme ratingScheme; + + public Rating(RatingScheme scheme, int score, String appliedBy, Date appliedAt) + { + this.ratingScheme = scheme; + this.ratingScore = score; + this.ratingAppliedBy = appliedBy; + this.ratingAppliedAt = appliedAt; + } + + /** + * Gets the score applied as part of this rating. In normal circumstances a score + * should always lie within the bounds defined by the {@link RatingScheme}. + * + * @return the score. + */ + public int getScore() + { + return ratingScore; + } + + /** + * Gets the user name of the user who applied this rating. + * + * @return the user who applied the rating. + */ + public String getAppliedBy() + { + return ratingAppliedBy; + } + + /** + * Gets the time/date at which the rating was applied. + * + * @return the date/time at which the rating was applied. + */ + public Date getAppliedAt() + { + return ratingAppliedAt; + } + + /** + * Gets the {@link RatingScheme} under which the rating was applied. + * + * @return the rating scheme used for this rating. + */ + public RatingScheme getScheme() + { + return ratingScheme; + } +} diff --git a/source/java/org/alfresco/service/cmr/rating/RatingScheme.java b/source/java/org/alfresco/service/cmr/rating/RatingScheme.java new file mode 100644 index 0000000000..f6cf4838e2 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/rating/RatingScheme.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005-2010 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 . + */ + +package org.alfresco.service.cmr.rating; + +/** + * TODO + * + * @author Neil McErlean + * @since 3.4 + */ +public interface RatingScheme +{ + /** + * This method returns the name which uniquely identifies the rating scheme. + * + * @return the name. + */ + public String getName(); + + /** + * This method returns the minimum rating defined for this scheme. + * + * @return the minimum rating. + */ + public int getMinRating(); + + /** + * This method returns the maximum rating defined for this scheme. + * + * @return the maximum rating. + */ + public int getMaxRating(); +} diff --git a/source/java/org/alfresco/service/cmr/rating/RatingService.java b/source/java/org/alfresco/service/cmr/rating/RatingService.java new file mode 100644 index 0000000000..d1303b0dbf --- /dev/null +++ b/source/java/org/alfresco/service/cmr/rating/RatingService.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2005-2010 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 . + */ + +package org.alfresco.service.cmr.rating; + +import java.util.Map; + +import org.alfresco.service.NotAuditable; +import org.alfresco.service.PublicService; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * The Rating service. TODO + * + * @author Neil McErlean + * @since 3.4 + */ +@PublicService +public interface RatingService +{ + /** + * Returns the available {@link RatingScheme rating schemes} keyed by name. + * + * @return The {@link RatingScheme rating schemes}. + */ + @NotAuditable + Map getRatingSchemes(); + + /** + * Returns the named {@link RatingScheme rating scheme} if there is one. + * + * @param ratingSchemeName name of the rating scheme. + * @return The {@link RatingScheme rating schemes} if one of that name is registered, + * else null. + */ + @NotAuditable + RatingScheme getRatingScheme(String ratingSchemeName); + + /** + * This method applies the given rating to the specified target node. If a rating + * from the current user in the specified scheme already exists, it will be replaced. + * + * @param targetNode the node to which the rating is to be applied. + * @param rating the rating which is to be applied. + * @param ratingSchemeName the name of the rating scheme to use. + * + * @throws RatingServiceException if the rating is not within the range defined by the named scheme + * or if the named scheme is not registered. + * @see RatingService#getRatingSchemes() + * @see RatingScheme + */ + @NotAuditable + void applyRating(NodeRef targetNode, int rating, String ratingSchemeName) throws RatingServiceException; + + /** + * This method gets the {@link Rating} applied by the current user to the specified node in the specified + * {@link RatingScheme} - if there is one. + * + * @param targetNode the node on which the rating is sought. + * @param ratingScheme the rating scheme to use. + * + * @return the Rating object if there is one, else null. + * @see RatingService#getRatingSchemes() + * @see RatingScheme + */ + @NotAuditable + + // TODO Get average/total ratings on node + + Rating getRatingByCurrentUser(NodeRef targetNode, RatingScheme ratingScheme); + + /** + * This method removes any {@link Rating} applied by the current user to the specified node in the specified + * {@link RatingScheme}. + * + * @param targetNode the node from which the rating is to be removed. + * @param ratingScheme the rating scheme to use. + * + * @return the deleted Rating object if there was one, else null. + * @see RatingService#getRatingSchemes() + * @see RatingScheme + */ + @NotAuditable + Rating removeRatingByCurrentUser(NodeRef targetNode, RatingScheme ratingScheme); +} diff --git a/source/java/org/alfresco/service/cmr/rating/RatingServiceException.java b/source/java/org/alfresco/service/cmr/rating/RatingServiceException.java new file mode 100644 index 0000000000..6d1dc7dbba --- /dev/null +++ b/source/java/org/alfresco/service/cmr/rating/RatingServiceException.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2005-2010 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 . + */ +package org.alfresco.service.cmr.rating; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * Rating Service Exception Class. + * + * @author Neil McErlean + * @since 3.4 + */ +public class RatingServiceException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 6035456870472850041L; + + /** + * Constructs a Rating Service Exception with the specified message. + * + * @param message the message string + */ + public RatingServiceException(String message) + { + super(message); + } + + /** + * Constructs a Rating Service Exception with the specified message and source exception. + * + * @param message the message string + * @param source the source exception + */ + public RatingServiceException(String message, Throwable source) + { + super(message, source); + } +}