diff --git a/config/alfresco/application-context-highlevel.xml b/config/alfresco/application-context-highlevel.xml index fc48eb0691..cfd3125057 100644 --- a/config/alfresco/application-context-highlevel.xml +++ b/config/alfresco/application-context-highlevel.xml @@ -16,6 +16,7 @@ + diff --git a/config/alfresco/bootstrap-context.xml b/config/alfresco/bootstrap-context.xml index e05465c075..08d9ae6b8c 100644 --- a/config/alfresco/bootstrap-context.xml +++ b/config/alfresco/bootstrap-context.xml @@ -142,6 +142,7 @@ + diff --git a/config/alfresco/dao/dao-context.xml b/config/alfresco/dao/dao-context.xml index 9ebc50cc63..f323277c29 100644 --- a/config/alfresco/dao/dao-context.xml +++ b/config/alfresco/dao/dao-context.xml @@ -41,6 +41,13 @@ + + + + + + + patchDAO.#bean.dialect# diff --git a/config/alfresco/dbscripts/create/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoCreate-SubscriptionTables.sql b/config/alfresco/dbscripts/create/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoCreate-SubscriptionTables.sql new file mode 100644 index 0000000000..650c7268ad --- /dev/null +++ b/config/alfresco/dbscripts/create/org.hibernate.dialect.MySQLInnoDBDialect/AlfrescoCreate-SubscriptionTables.sql @@ -0,0 +1,28 @@ +-- +-- Title: Subscription tables +-- Database: MySQL InnoDB +-- Since: V4.0 Schema 5011 +-- Author: Florian Mueller +-- +-- Please contact support@alfresco.com if you need assistance with the upgrade. +-- + +CREATE TABLE alf_subscriptions ( + user_node_id BIGINT NOT NULL, + node_id BIGINT NOT NULL, + PRIMARY KEY (user_node_id, node_id), + CONSTRAINT fk_alf_sub_user FOREIGN KEY (user_node_id) REFERENCES alf_node(id) ON DELETE CASCADE, + CONSTRAINT fk_alf_sub_node FOREIGN KEY (node_id) REFERENCES alf_node(id) ON DELETE CASCADE +) ENGINE=InnoDB; + +-- +-- Record script finish +-- +DELETE FROM alf_applied_patch WHERE id = 'patch.db-V4.0-SubscriptionTables'; +INSERT INTO alf_applied_patch + (id, description, fixes_from_schema, fixes_to_schema, applied_to_schema, target_schema, applied_on_date, applied_to_server, was_executed, succeeded, report) + VALUES + ( + 'patch.db-V4.0-SubscriptionTables', 'Manually executed script upgrade V4.0: Subscription Tables', + 0, 5010, -1, 5011, null, 'UNKNOWN', ${TRUE}, ${TRUE}, 'Script completed' + ); \ No newline at end of file diff --git a/config/alfresco/dbscripts/create/org.hibernate.dialect.PostgreSQLDialect/AlfrescoCreate-SubscriptionTables.sql b/config/alfresco/dbscripts/create/org.hibernate.dialect.PostgreSQLDialect/AlfrescoCreate-SubscriptionTables.sql new file mode 100644 index 0000000000..f5a676feba --- /dev/null +++ b/config/alfresco/dbscripts/create/org.hibernate.dialect.PostgreSQLDialect/AlfrescoCreate-SubscriptionTables.sql @@ -0,0 +1,28 @@ +-- +-- Title: Subscription tables +-- Database: PostgreSQL +-- Since: V4.0 Schema 5011 +-- Author: Florian Mueller +-- +-- Please contact support@alfresco.com if you need assistance with the upgrade. +-- + +CREATE TABLE alf_subscriptions ( + user_node_id INT8 NOT NULL, + node_id INT8 NOT NULL, + PRIMARY KEY (user_node_id, node_id), + CONSTRAINT fk_alf_sub_user FOREIGN KEY (user_node_id) REFERENCES alf_node(id) ON DELETE CASCADE, + CONSTRAINT fk_alf_sub_node FOREIGN KEY (node_id) REFERENCES alf_node(id) ON DELETE CASCADE +); + +-- +-- Record script finish +-- +DELETE FROM alf_applied_patch WHERE id = 'patch.db-V4.0-SubscriptionTables'; +INSERT INTO alf_applied_patch + (id, description, fixes_from_schema, fixes_to_schema, applied_to_schema, target_schema, applied_on_date, applied_to_server, was_executed, succeeded, report) + VALUES + ( + 'patch.db-V4.0-SubscriptionTables', 'Manually executed script upgrade V4.0: Subscription Tables', + 0, 5010, -1, 5011, null, 'UNKNOWN', ${TRUE}, ${TRUE}, 'Script completed' + ); diff --git a/config/alfresco/ibatis/alfresco-SqlMapConfig.xml b/config/alfresco/ibatis/alfresco-SqlMapConfig.xml index 7d16488347..4cda53081f 100644 --- a/config/alfresco/ibatis/alfresco-SqlMapConfig.xml +++ b/config/alfresco/ibatis/alfresco-SqlMapConfig.xml @@ -137,6 +137,11 @@ Inbound settings from iBatis + + + + + @@ -168,6 +173,7 @@ Inbound settings from iBatis + diff --git a/config/alfresco/ibatis/org.hibernate.dialect.Dialect/subscriptions-common-SqlMap.xml b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/subscriptions-common-SqlMap.xml new file mode 100644 index 0000000000..d20e1dff89 --- /dev/null +++ b/config/alfresco/ibatis/org.hibernate.dialect.Dialect/subscriptions-common-SqlMap.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into alf_subscriptions (user_node_id, node_id) + values (?, ?) + + + + delete from alf_subscriptions where user_node_id = #{userNodeId} and node_id = #{nodeId} + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/model/contentModel.xml b/config/alfresco/model/contentModel.xml index d8ce586f34..c182df62af 100644 --- a/config/alfresco/model/contentModel.xml +++ b/config/alfresco/model/contentModel.xml @@ -268,6 +268,10 @@ d:boolean + + d:boolean + + diff --git a/config/alfresco/patch/patch-services-context.xml b/config/alfresco/patch/patch-services-context.xml index 72db6397cb..07613be5cd 100644 --- a/config/alfresco/patch/patch-services-context.xml +++ b/config/alfresco/patch/patch-services-context.xml @@ -2871,4 +2871,15 @@ + + patch.db-V4.0-SubscriptionTables + patch.schemaUpgradeScript.description + 0 + 5010 + 5011 + + classpath:alfresco/dbscripts/create/${db.script.dialect}/AlfrescoCreate-SubscriptionTables.sql + + + diff --git a/config/alfresco/subscription-service-context.xml b/config/alfresco/subscription-service-context.xml new file mode 100644 index 0000000000..2bcb253296 --- /dev/null +++ b/config/alfresco/subscription-service-context.xml @@ -0,0 +1,29 @@ + + + + + + + + org.alfresco.service.cmr.subscriptions.SubscriptionService + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/alfresco/subsystems/ActivitiesFeed/default/activities-feed-context.xml b/config/alfresco/subsystems/ActivitiesFeed/default/activities-feed-context.xml index fab2a3ee16..c190fd9c2f 100644 --- a/config/alfresco/subsystems/ActivitiesFeed/default/activities-feed-context.xml +++ b/config/alfresco/subsystems/ActivitiesFeed/default/activities-feed-context.xml @@ -86,7 +86,8 @@ - + + alfresco/extension/templates/activities diff --git a/source/java/org/alfresco/repo/activities/ActivityPostServiceImpl.java b/source/java/org/alfresco/repo/activities/ActivityPostServiceImpl.java index 76ce293a3d..a4a7d2d161 100644 --- a/source/java/org/alfresco/repo/activities/ActivityPostServiceImpl.java +++ b/source/java/org/alfresco/repo/activities/ActivityPostServiceImpl.java @@ -235,7 +235,7 @@ public class ActivityPostServiceImpl implements ActivityPostService private String getCurrentUser() { - String userId = AuthenticationUtil.getFullyAuthenticatedUser(); + String userId = AuthenticationUtil.getRunAsUser(); if ((userId != null) && (! userId.equals(AuthenticationUtil.SYSTEM_USER_NAME)) && (! userNamesAreCaseSensitive)) { // user names are not case-sensitive diff --git a/source/java/org/alfresco/repo/activities/ActivityType.java b/source/java/org/alfresco/repo/activities/ActivityType.java index a76d60ccd7..1907fc73b2 100644 --- a/source/java/org/alfresco/repo/activities/ActivityType.java +++ b/source/java/org/alfresco/repo/activities/ActivityType.java @@ -19,12 +19,12 @@ package org.alfresco.repo.activities; public interface ActivityType -{ +{ // pre-defined alfresco activity types - + // generic fallback (if specific template is missing) public final String GENERIC_FALLBACK = "org.alfresco.generic"; - + // site membership public final String SITE_USER_JOINED = "org.alfresco.site.user-joined"; public final String SITE_USER_REMOVED = "org.alfresco.site.user-left"; @@ -32,4 +32,6 @@ public interface ActivityType public final String SITE_GROUP_ADDED = "org.alfresco.site.group-added"; public final String SITE_GROUP_REMOVED = "org.alfresco.site.group-removed"; public final String SITE_GROUP_ROLE_UPDATE = "org.alfresco.site.group-role-changed"; + public final String SUBSCRIPTIONS_SUBSCRIBE = "org.alfresco.subscriptions.subscribed"; + public final String SUBSCRIPTIONS_FOLLOW = "org.alfresco.subscriptions.followed"; } diff --git a/source/java/org/alfresco/repo/activities/feed/FeedTaskProcessor.java b/source/java/org/alfresco/repo/activities/feed/FeedTaskProcessor.java index c95d9838b9..501d5c0e35 100644 --- a/source/java/org/alfresco/repo/activities/feed/FeedTaskProcessor.java +++ b/source/java/org/alfresco/repo/activities/feed/FeedTaskProcessor.java @@ -63,6 +63,7 @@ import freemarker.template.TemplateException; public abstract class FeedTaskProcessor { private static final Log logger = LogFactory.getLog(FeedTaskProcessor.class); + public static final String FEED_FORMAT_JSON = "json"; public static final String FEED_FORMAT_ATOMENTRY = "atomentry"; @@ -117,6 +118,7 @@ public abstract class FeedTaskProcessor Map> activityTemplates = new HashMap>(10); Map> siteConnectedUsers = new TreeMap>(); + Map> followers = new TreeMap>(); Map> userFeedControls = new HashMap>(); Map templateCache = new TreeMap(); @@ -207,15 +209,6 @@ public abstract class FeedTaskProcessor String thisSite = (activityPost.getSiteNetwork() != null ? activityPost.getSiteNetwork() : ""); - if (thisSite.length() == 0) - { - // note: although we allow posts without site id - we currently require site context to generate feeds for site members (hence skip here with warning) - // (also Share currently only posts activities within site context) - logger.warn(">>> Skipping activity post " + activityPost.getId() + " since no site"); - updatePostStatus(activityPost.getId(), ActivityPostEntity.STATUS.PROCESSED); - continue; - } - model.put(ActivityFeedEntity.KEY_ACTIVITY_FEED_TYPE, activityPost.getActivityType()); model.put(ActivityFeedEntity.KEY_ACTIVITY_FEED_SITE, thisSite); model.put("userId", activityPost.getUserId()); @@ -224,49 +217,84 @@ public abstract class FeedTaskProcessor model.put("xmldate", new ISO8601DateFormatMethod()); model.put("repoEndPoint", ctx.getRepoEndPoint()); - // Get the members of this site - save hammering the repository by reusing cached site members - Set connectedUsers = siteConnectedUsers.get(thisSite); - if (connectedUsers == null) - { + // Recipients of this post + Set recipients = new HashSet(); + + // Add site members to recipient list + if (thisSite.length() > 0) + { + // Get the members of this site - save hammering the repository by reusing cached site members + Set connectedUsers = siteConnectedUsers.get(thisSite); + if (connectedUsers == null) + { + try + { + // Repository callback to get site members + connectedUsers = getSiteMembers(ctx, thisSite); + connectedUsers.add(""); // add empty posting userid - to represent site feed ! + } + catch(Exception e) + { + logger.error("Skipping activity post " + activityPost.getId() + " since failed to get site members: " + e); + updatePostStatus(activityPost.getId(), ActivityPostEntity.STATUS.ERROR); + continue; + } + + // Cache them for future use in this same invocation + siteConnectedUsers.put(thisSite, connectedUsers); + } + + recipients.addAll(connectedUsers); + } + + // Add followers to recipient list + Set followerUsers = followers.get(activityPost.getUserId()); + if(followerUsers == null) { try { - // Repository callback to get site members - connectedUsers = getSiteMembers(ctx, thisSite); - connectedUsers.add(""); // add empty posting userid - to represent site feed ! + followerUsers = getFollowers(activityPost.getUserId()); } catch(Exception e) { - logger.error("Skipping activity post " + activityPost.getId() + " since failed to get site members: " + e); + logger.error("Skipping activity post " + activityPost.getId() + " since failed to get followers: " + e); updatePostStatus(activityPost.getId(), ActivityPostEntity.STATUS.ERROR); continue; } - // Cache them for future use in this same invocation - siteConnectedUsers.put(thisSite, connectedUsers); + followers.put(activityPost.getUserId(), followerUsers); } + recipients.addAll(followerUsers); + if(recipients.size() == 0) { + if (logger.isDebugEnabled()) + { + logger.debug("No recipients for activity post " + activityPost.getId() + "."); + } + return; + } + try { startTransaction(); if (logger.isTraceEnabled()) { - logger.trace("Process: " + connectedUsers.size() + " candidate connections for activity post " + activityPost.getId()); + logger.trace("Process: " + recipients.size() + " candidate connections for activity post " + activityPost.getId()); } int excludedConnections = 0; - for (String connectedUser : connectedUsers) + for (String recipient : recipients) { List feedControls = null; - if (! connectedUser.equals("")) + if (! recipient.equals("")) { // Get user's feed controls - feedControls = userFeedControls.get(connectedUser); + feedControls = userFeedControls.get(recipient); if (feedControls == null) { - feedControls = getFeedControls(connectedUser); - userFeedControls.put(connectedUser, feedControls); + feedControls = getFeedControls(recipient); + userFeedControls.put(recipient, feedControls); } } @@ -278,7 +306,7 @@ public abstract class FeedTaskProcessor else { // read permission check - if (! canRead(ctx, connectedUser, model)) + if (! canRead(ctx, recipient, model)) { excludedConnections++; continue; @@ -306,7 +334,7 @@ public abstract class FeedTaskProcessor ActivityFeedEntity feed = new ActivityFeedEntity(); // Generate activity feed summary - feed.setFeedUserId(connectedUser); + feed.setFeedUserId(recipient); feed.setPostUserId(postingUserId); feed.setActivityType(activityType); @@ -356,7 +384,7 @@ public abstract class FeedTaskProcessor if (logger.isDebugEnabled()) { - logger.debug("Processed: " + (connectedUsers.size() - excludedConnections) + " connections for activity post " + activityPost.getId() + " (excluded " + excludedConnections + ")"); + logger.debug("Processed: " + (recipients.size() - excludedConnections) + " connections for activity post " + activityPost.getId() + " (excluded " + excludedConnections + ")"); } } finally @@ -381,24 +409,23 @@ public abstract class FeedTaskProcessor logger.info(sb.toString()); } } - + public abstract void startTransaction() throws SQLException; - + public abstract void commitTransaction() throws SQLException; - + public abstract void rollbackTransaction() throws SQLException; - + public abstract void endTransaction() throws SQLException; - + public abstract List selectPosts(ActivityPostEntity selector) throws SQLException; - + public abstract List selectUserFeedControls(String userId) throws SQLException; - + public abstract long insertFeedEntry(ActivityFeedEntity feed) throws SQLException; - + public abstract int updatePostStatus(long id, ActivityPostEntity.STATUS status) throws SQLException; - - + protected String callWebScript(String urlString, String ticket) throws MalformedURLException, URISyntaxException, IOException { URL url = new URL(urlString); @@ -450,7 +477,7 @@ public abstract class FeedTaskProcessor return result; } - + protected Set getSiteMembers(RepoCtx ctx, String siteId) throws Exception { Set members = new HashSet(); @@ -481,12 +508,14 @@ public abstract class FeedTaskProcessor return members; } + + protected abstract Set getFollowers(String userId) throws Exception; protected boolean canRead(RepoCtx ctx, final String connectedUser, Map model) throws Exception { throw new UnsupportedOperationException("FeedTaskProcessor: Remote callback for 'canRead' not implemented"); } - + protected Map> getActivityTypeTemplates(String repoEndPoint, String ticket, String subPath) throws Exception { StringBuffer sbUrl = new StringBuffer(); @@ -516,7 +545,7 @@ public abstract class FeedTaskProcessor return getActivityTemplates(allTemplateNames); } - + protected Map> getActivityTemplates(List allTemplateNames) { Map> activityTemplates = new HashMap>(10); @@ -557,21 +586,21 @@ public abstract class FeedTaskProcessor return activityTemplates; } - + protected Configuration getFreemarkerConfiguration(RepoCtx ctx) { Configuration cfg = new Configuration(); cfg.setObjectWrapper(new DefaultObjectWrapper()); - + // custom template loader cfg.setTemplateLoader(new TemplateWebScriptLoader(ctx.getRepoEndPoint(), ctx.getTicket())); - + // TODO review i18n cfg.setLocalizedLookup(false); - + return cfg; } - + protected String processFreemarker(Map templateCache, String fmTemplate, Configuration cfg, Map model) throws IOException, TemplateException, Exception { // Save on lots of modification date checking by caching templates locally @@ -587,12 +616,12 @@ public abstract class FeedTaskProcessor return textWriter.toString(); } - + protected List getFeedControls(String connectedUser) throws SQLException { return selectUserFeedControls(connectedUser); } - + protected boolean acceptActivity(ActivityPostEntity activityPost, List feedControls) { if (feedControls == null) @@ -632,7 +661,7 @@ public abstract class FeedTaskProcessor return true; } - + protected void addMissingFormats(String activityType, List fmTemplates, List templatesToAdd) { for (String templateToAdd : templatesToAdd) @@ -666,7 +695,7 @@ public abstract class FeedTaskProcessor } } } - + protected String getTemplateSubPath(String activityType) { return (! activityType.startsWith("/") ? "/" : "") + activityType.replace(".", "/"); diff --git a/source/java/org/alfresco/repo/activities/feed/local/LocalFeedTaskProcessor.java b/source/java/org/alfresco/repo/activities/feed/local/LocalFeedTaskProcessor.java index 2b49021b46..e2ef7cb843 100644 --- a/source/java/org/alfresco/repo/activities/feed/local/LocalFeedTaskProcessor.java +++ b/source/java/org/alfresco/repo/activities/feed/local/LocalFeedTaskProcessor.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.alfresco.query.PagingRequest; import org.alfresco.repo.activities.feed.FeedTaskProcessor; import org.alfresco.repo.activities.feed.RepoCtx; import org.alfresco.repo.activities.post.lookup.PostLookup; @@ -46,6 +47,8 @@ import org.alfresco.service.cmr.security.AccessStatus; import org.alfresco.service.cmr.security.AuthorityType; import org.alfresco.service.cmr.security.PermissionService; import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.subscriptions.PagingFollowingResults; +import org.alfresco.service.cmr.subscriptions.SubscriptionService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeansException; @@ -64,117 +67,123 @@ import freemarker.template.DefaultObjectWrapper; public class LocalFeedTaskProcessor extends FeedTaskProcessor implements ApplicationContextAware { private static final Log logger = LogFactory.getLog(LocalFeedTaskProcessor.class); - + private ActivityPostDAO postDAO; private ActivityFeedDAO feedDAO; private FeedControlDAO feedControlDAO; - + // can call locally (instead of remote repo callback) private SiteService siteService; private NodeService nodeService; private ContentService contentService; private PermissionService permissionService; - + private SubscriptionService subscriptionService; + private String defaultEncoding; private List templateSearchPaths; private boolean useRemoteCallbacks; private ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); - + public void setPostDAO(ActivityPostDAO postDAO) { this.postDAO = postDAO; } - + public void setFeedDAO(ActivityFeedDAO feedDAO) { this.feedDAO = feedDAO; } - + public void setFeedControlDAO(FeedControlDAO feedControlDAO) { this.feedControlDAO = feedControlDAO; } - + public void setSiteService(SiteService siteService) { this.siteService = siteService; } - + public void setNodeService(NodeService nodeService) { this.nodeService = nodeService; } - + public void setContentService(ContentService contentService) { this.contentService = contentService; } - + public void setPermissionService(PermissionService permissionService) { this.permissionService = permissionService; } - + + public void setSubscriptionService(SubscriptionService subscriptionService) + { + this.subscriptionService = subscriptionService; + } + public void setDefaultEncoding(String defaultEncoding) { this.defaultEncoding = defaultEncoding; } - + public void setTemplateSearchPaths(List templateSearchPaths) { this.templateSearchPaths = templateSearchPaths; } - + public void setUseRemoteCallbacks(boolean useRemoteCallbacks) { this.useRemoteCallbacks = useRemoteCallbacks; } - + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.resolver = applicationContext; } - + public void startTransaction() throws SQLException { // NOOP } - + public void commitTransaction() throws SQLException { // NOOP } - + public void rollbackTransaction() throws SQLException { // NOOP } - + public void endTransaction() throws SQLException { - // NOOP + // NOOP } - + public List selectPosts(ActivityPostEntity selector) throws SQLException { return postDAO.selectPosts(selector); } - + public long insertFeedEntry(ActivityFeedEntity feed) throws SQLException { return feedDAO.insertFeedEntry(feed); } - + public int updatePostStatus(long id, ActivityPostEntity.STATUS status) throws SQLException { return postDAO.updatePostStatus(id, status); } - + public List selectUserFeedControls(String userId) throws SQLException { - return feedControlDAO.selectFeedControls(userId); + return feedControlDAO.selectFeedControls(userId); } - + @Override protected Set getSiteMembers(final RepoCtx ctx, final String siteId) throws Exception { @@ -182,7 +191,7 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica { // as per 3.0, 3.1 return super.getSiteMembers(ctx, siteId); - } + } else { // optimise for non-remote implementation - override remote repo callback (to "List Site Memberships" web script) with embedded call @@ -194,12 +203,12 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica if ((siteId != null) && (siteId.length() != 0)) { Map mapResult = siteService.listMembers(siteId, null, null, 0, true); - + if ((mapResult != null) && (mapResult.size() != 0)) { for (String userName : mapResult.keySet()) { - if (! ctx.isUserNamesAreCaseSensitive()) + if (!ctx.isUserNamesAreCaseSensitive()) { userName = userName.toLowerCase(); } @@ -207,13 +216,13 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica } } } - + return members; } }, AuthenticationUtil.getSystemUserName()); } } - + protected boolean canRead(RepoCtx ctx, final String connectedUser, Map model) throws Exception { if (useRemoteCallbacks) @@ -228,17 +237,17 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica // if permission service not configured then fallback (ie. no read permission check) return true; } - - String nodeRefStr = (String)model.get(PostLookup.JSON_NODEREF); + + String nodeRefStr = (String) model.get(PostLookup.JSON_NODEREF); if (nodeRefStr == null) { - nodeRefStr = (String)model.get(PostLookup.JSON_NODEREF_PARENT); + nodeRefStr = (String) model.get(PostLookup.JSON_NODEREF_PARENT); } - + if (nodeRefStr != null) { final NodeRef nodeRef = new NodeRef(nodeRefStr); - + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() { public Boolean doWork() throws Exception @@ -247,16 +256,16 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica } }, AuthenticationUtil.getSystemUserName()); } - + return true; } } - + private boolean canReadImpl(final String connectedUser, final NodeRef nodeRef) throws Exception { // check for read permission long start = System.currentTimeMillis(); - + try { // note: deleted node does not exist (hence no permission, although default permission check would return true which is problematic) @@ -264,34 +273,34 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica if (nodeService.exists(nodeRef)) { checkNodeRef = nodeRef; - } + } else { // TODO: require ghosting - this is temp workaround (we should not rely on archive - may be permanently deleted, ie. not archived or already purged) NodeRef archiveNodeRef = new NodeRef(StoreRef.STORE_REF_ARCHIVE_SPACESSTORE, nodeRef.getId()); - if (! nodeService.exists(archiveNodeRef)) + if (!nodeService.exists(archiveNodeRef)) { return false; } checkNodeRef = archiveNodeRef; } - + if (connectedUser.equals("")) { // site feed (public site) Set perms = permissionService.getAllSetPermissions(checkNodeRef); for (AccessPermission perm : perms) { - if (perm.getAuthority().equals(PermissionService.ALL_AUTHORITIES) && - perm.getAuthorityType().equals(AuthorityType.EVERYONE) && - perm.getPermission().equals(PermissionService.READ_PERMISSIONS) && + if (perm.getAuthority().equals(PermissionService.ALL_AUTHORITIES) && + perm.getAuthorityType().equals(AuthorityType.EVERYONE) && + perm.getPermission().equals(PermissionService.READ_PERMISSIONS) && perm.getAccessStatus().equals(AccessStatus.ALLOWED)) { return true; } } return false; - } + } else { // user feed @@ -308,11 +317,11 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica { if (logger.isDebugEnabled()) { - logger.debug("canRead: " + nodeRef + " in "+(System.currentTimeMillis()-start)+" msecs"); + logger.debug("canRead: " + nodeRef + " in " + (System.currentTimeMillis() - start) + " msecs"); } } } - + @Override protected Map> getActivityTypeTemplates(String repoEndPoint, String ticket, String subPath) throws Exception { @@ -320,32 +329,32 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica { // as per 3.0, 3.1 return super.getActivityTypeTemplates(repoEndPoint, ticket, subPath); - } + } else { // optimisation - override remote repo callback (to "Activities Templates" web script) with local/embedded call - + String path = "/"; String templatePattern = "*.ftl"; - + if ((subPath != null) && (subPath.length() > 0)) { subPath = subPath + "*"; - + int idx = subPath.lastIndexOf("/"); if (idx != -1) { path = subPath.substring(0, idx); - templatePattern = subPath.substring(idx+1) + ".ftl"; + templatePattern = subPath.substring(idx + 1) + ".ftl"; } } - + List allTemplateNames = getDocumentPaths(path, false, templatePattern); - + return getActivityTemplates(allTemplateNames); } } - + @Override protected Configuration getFreemarkerConfiguration(RepoCtx ctx) { @@ -353,21 +362,21 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica { // as per 3.0, 3.1 return super.getFreemarkerConfiguration(ctx); - } + } else { Configuration cfg = new Configuration(); cfg.setObjectWrapper(new DefaultObjectWrapper()); - + cfg.setTemplateLoader(new ClassPathRepoTemplateLoader(nodeService, contentService, defaultEncoding)); - + // TODO review i18n cfg.setLocalizedLookup(false); - + return cfg; } } - + // Helper to get template document paths private List getDocumentPaths(String path, boolean includeSubPaths, String documentPattern) { @@ -375,24 +384,24 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica { path = "/"; } - - if (! path.startsWith("/")) + + if (!path.startsWith("/")) { path = "/" + path; } - - if (! path.endsWith("/")) + + if (!path.endsWith("/")) { path = path + "/"; } - + if ((documentPattern == null) || (documentPattern.length() == 0)) { documentPattern = "*"; } - + List documentPaths = new ArrayList(0); - + for (String classPath : templateSearchPaths) { final StringBuilder pattern = new StringBuilder(128); @@ -400,20 +409,20 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica .append(path) .append((includeSubPaths ? "**/" : "")) .append(documentPattern); - + try { documentPaths.addAll(getPaths(pattern.toString(), classPath)); - } + } catch (IOException e) { // Note: Ignore: no documents found } } - + return documentPaths; } - + // Helper to return a list of resource document paths based on a search pattern. private List getPaths(String pattern, String classPath) throws IOException { @@ -422,7 +431,7 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica for (Resource resource : resources) { String resourcePath = resource.getURL().toExternalForm(); - + int idx = resourcePath.lastIndexOf(classPath); if (idx != -1) { @@ -437,4 +446,21 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor implements Applica } return documentPaths; } + + protected Set getFollowers(String userId) throws Exception + { + Set result = new HashSet(); + + if (!subscriptionService.isSubscriptionListPrivate(userId)) + { + PagingFollowingResults fr = subscriptionService.getFollowers(userId, new PagingRequest(1000000, null)); + + if (fr.getPage() != null) + { + result.addAll(fr.getPage()); + } + } + + return result; + } } diff --git a/source/java/org/alfresco/repo/domain/subscriptions/AbstractSubscriptionsDAO.java b/source/java/org/alfresco/repo/domain/subscriptions/AbstractSubscriptionsDAO.java new file mode 100644 index 0000000000..4efcd49c1e --- /dev/null +++ b/source/java/org/alfresco/repo/domain/subscriptions/AbstractSubscriptionsDAO.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2005-2011 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.domain.subscriptions; + +import org.alfresco.query.PagingRequest; +import org.alfresco.repo.domain.node.NodeDAO; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.subscriptions.PagingFollowingResults; +import org.alfresco.service.cmr.subscriptions.PagingSubscriptionResults; +import org.alfresco.service.cmr.subscriptions.SubscriptionItemTypeEnum; + +public abstract class AbstractSubscriptionsDAO implements SubscriptionsDAO +{ + protected NodeDAO nodeDAO; + protected PersonService personService; + + public final void setNodeDAO(NodeDAO nodeDAO) + { + this.nodeDAO = nodeDAO; + } + + public final void setPersonService(PersonService personService) + { + this.personService = personService; + } + + @Override + public abstract PagingSubscriptionResults selectSubscriptions(String userId, SubscriptionItemTypeEnum type, + PagingRequest pagingRequest); + + @Override + public abstract int countSubscriptions(String userId, SubscriptionItemTypeEnum type); + + @Override + public abstract void insertSubscription(String userId, NodeRef node); + + @Override + public abstract void deleteSubscription(String userId, NodeRef node); + + @Override + public abstract boolean hasSubscribed(String userId, NodeRef node); + + @Override + public abstract PagingFollowingResults selectFollowers(String userId, PagingRequest pagingRequest); + + @Override + public abstract int countFollowers(String userId); + + protected NodeRef getUserNodeRef(String userId) + { + return personService.getPerson(userId, false); + } +} diff --git a/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionDAOTest.java b/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionDAOTest.java new file mode 100644 index 0000000000..9795007dce --- /dev/null +++ b/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionDAOTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2005-2011 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.domain.subscriptions; + +import junit.framework.TestCase; + +import org.alfresco.query.PagingRequest; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork; +import org.alfresco.repo.transaction.RetryingTransactionHelper; +import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback; +import org.alfresco.service.ServiceRegistry; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.subscriptions.PagingFollowingResults; +import org.alfresco.service.cmr.subscriptions.PagingSubscriptionResults; +import org.alfresco.service.cmr.subscriptions.SubscriptionItemTypeEnum; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.springframework.context.ApplicationContext; + +public class SubscriptionDAOTest extends TestCase +{ + private ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + private TransactionService transactionService; + private RetryingTransactionHelper txnHelper; + private PersonService personService; + + private SubscriptionsDAO subscriptionsDAO; + + protected NodeRef getUserNodeRef(final String userId) + { + final PersonService ps = personService; + + return AuthenticationUtil.runAs(new RunAsWork() + { + @Override + public NodeRef doWork() throws Exception + { + return ps.getPerson(userId); + } + }, AuthenticationUtil.getSystemUserName()); + } + + protected void insert(final String userId, final NodeRef node) throws Exception + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + subscriptionsDAO.insertSubscription(userId, node); + return null; + } + }; + txnHelper.doInTransaction(callback, false, false); + } + + protected void delete(final String userId, final NodeRef node) throws Exception + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public Object execute() throws Throwable + { + subscriptionsDAO.deleteSubscription(userId, node); + return null; + } + }; + txnHelper.doInTransaction(callback, false, false); + } + + protected int count(final String userId) throws Exception + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public Integer execute() throws Throwable + { + return subscriptionsDAO.countSubscriptions(userId, SubscriptionItemTypeEnum.USER); + } + }; + + return txnHelper.doInTransaction(callback, false, false); + } + + protected boolean hasSubscribed(final String userId, final NodeRef node) throws Exception + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public Boolean execute() throws Throwable + { + return subscriptionsDAO.hasSubscribed(userId, node); + } + }; + + return txnHelper.doInTransaction(callback, false, false); + } + + protected PagingSubscriptionResults select(final String userId) throws Exception + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public PagingSubscriptionResults execute() throws Throwable + { + return subscriptionsDAO.selectSubscriptions(userId, SubscriptionItemTypeEnum.USER, new PagingRequest( + 100000, null)); + } + }; + + return txnHelper.doInTransaction(callback, false, false); + } + + protected int countFollowers(final String userId) throws Exception + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public Integer execute() throws Throwable + { + return subscriptionsDAO.countFollowers(userId); + } + }; + + return txnHelper.doInTransaction(callback, false, false); + } + + protected PagingFollowingResults selectFollowing(final String userId) throws Exception + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public PagingFollowingResults execute() throws Throwable + { + return subscriptionsDAO.selectFollowing(userId, new PagingRequest(100000, null)); + } + }; + + return txnHelper.doInTransaction(callback, false, false); + } + + protected PagingFollowingResults selectFollowers(final String userId) throws Exception + { + RetryingTransactionCallback callback = new RetryingTransactionCallback() + { + public PagingFollowingResults execute() throws Throwable + { + return subscriptionsDAO.selectFollowers(userId, new PagingRequest(100000, null)); + } + }; + + return txnHelper.doInTransaction(callback, false, false); + } + + @Override + public void setUp() throws Exception + { + ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean(ServiceRegistry.SERVICE_REGISTRY); + transactionService = serviceRegistry.getTransactionService(); + txnHelper = transactionService.getRetryingTransactionHelper(); + + personService = serviceRegistry.getPersonService(); + + subscriptionsDAO = (SubscriptionsDAO) ctx.getBean("subscriptionsDAO"); + } + + public void testInsertAndDelete() throws Exception + { + String userId = "admin"; + String userId2 = "guest"; + NodeRef nodeRef = getUserNodeRef(userId2); + + // check subscription first + if (hasSubscribed(userId, nodeRef)) + { + delete(userId, nodeRef); + } + boolean hasSubscribed = hasSubscribed(userId, nodeRef); + assertFalse(hasSubscribed); + + // count subscriptions + int count = count(userId); + assertTrue(count >= 0); + + // insert + insert(userId, nodeRef); + insert(userId, nodeRef); + assertEquals(count + 1, count(userId)); + assertTrue(hasSubscribed(userId, nodeRef)); + + // select + PagingSubscriptionResults psr = select(userId); + assertNotNull(psr); + assertNotNull(psr.getPage()); + assertTrue(psr.getPage().contains(nodeRef)); + + PagingFollowingResults following = selectFollowing(userId); + assertNotNull(following); + assertNotNull(following.getPage()); + assertTrue(following.getPage().contains(userId2)); + + assertEquals(psr.getPage().size(), following.getPage().size()); + + // count followers + int followerCount = countFollowers(userId2); + assertTrue(followerCount >= 0); + + // select followers + PagingFollowingResults followers = selectFollowers(userId2); + assertNotNull(followers); + assertNotNull(followers.getPage()); + assertTrue(followers.getPage().contains(userId)); + + // delete + delete(userId, nodeRef); + assertEquals(count, count(userId)); + assertFalse(hasSubscribed(userId, nodeRef)); + } +} diff --git a/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionEntity.java b/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionEntity.java new file mode 100644 index 0000000000..056e01df79 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionEntity.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2005-2011 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.domain.subscriptions; + +public class SubscriptionEntity +{ + private Long userNodeId; + private Long nodeId; + + public Long getUserNodeId() + { + return userNodeId; + } + + public void setUserNodeId(Long userNodeId) + { + this.userNodeId = userNodeId; + } + + public Long getNodeId() + { + return nodeId; + } + + public void setNodeId(Long nodeId) + { + this.nodeId = nodeId; + } + + public boolean getFalse() + { + return false; + } +} diff --git a/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionNodeEntity.java b/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionNodeEntity.java new file mode 100644 index 0000000000..3994fcccc7 --- /dev/null +++ b/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionNodeEntity.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2005-2011 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.domain.subscriptions; + +import org.alfresco.service.cmr.repository.NodeRef; + +public class SubscriptionNodeEntity +{ + private String protocol; + private String identifier; + private String id; + + public String getProtocol() + { + return protocol; + } + + public void setProtocol(String protocol) + { + this.protocol = protocol; + } + + public String getIdentifier() + { + return identifier; + } + + public void setIdentifier(String identifier) + { + this.identifier = identifier; + } + + public String getId() + { + return id; + } + + public void setId(String id) + { + this.id = id; + } + + public NodeRef getNodeRef() + { + return new NodeRef(protocol, identifier, id); + } +} diff --git a/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionsDAO.java b/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionsDAO.java new file mode 100644 index 0000000000..8324a62a1b --- /dev/null +++ b/source/java/org/alfresco/repo/domain/subscriptions/SubscriptionsDAO.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2005-2011 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.domain.subscriptions; + +import org.alfresco.query.PagingRequest; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.subscriptions.PagingFollowingResults; +import org.alfresco.service.cmr.subscriptions.PagingSubscriptionResults; +import org.alfresco.service.cmr.subscriptions.SubscriptionItemTypeEnum; + +public interface SubscriptionsDAO +{ + PagingSubscriptionResults selectSubscriptions(String userId, SubscriptionItemTypeEnum type, + PagingRequest pagingRequest); + + int countSubscriptions(String userId, SubscriptionItemTypeEnum type); + + void insertSubscription(String userId, NodeRef node); + + void deleteSubscription(String userId, NodeRef node); + + boolean hasSubscribed(String userId, NodeRef node); + + PagingFollowingResults selectFollowing(String userId, PagingRequest pagingRequest); + + PagingFollowingResults selectFollowers(String userId, PagingRequest pagingRequest); + + int countFollowers(String userId); +} diff --git a/source/java/org/alfresco/repo/domain/subscriptions/ibatis/SubscriptionsDAOImpl.java b/source/java/org/alfresco/repo/domain/subscriptions/ibatis/SubscriptionsDAOImpl.java new file mode 100644 index 0000000000..47979176bd --- /dev/null +++ b/source/java/org/alfresco/repo/domain/subscriptions/ibatis/SubscriptionsDAOImpl.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2005-2011 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.domain.subscriptions.ibatis; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.repo.domain.qname.QNameDAO; +import org.alfresco.repo.domain.subscriptions.AbstractSubscriptionsDAO; +import org.alfresco.repo.domain.subscriptions.SubscriptionEntity; +import org.alfresco.repo.domain.subscriptions.SubscriptionNodeEntity; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.subscriptions.PagingFollowingResults; +import org.alfresco.service.cmr.subscriptions.PagingFollowingResultsImpl; +import org.alfresco.service.cmr.subscriptions.PagingSubscriptionResults; +import org.alfresco.service.cmr.subscriptions.PagingSubscriptionResultsImpl; +import org.alfresco.service.cmr.subscriptions.SubscriptionItemTypeEnum; +import org.alfresco.util.Pair; +import org.apache.ibatis.session.RowBounds; +import org.mybatis.spring.SqlSessionTemplate; + +public class SubscriptionsDAOImpl extends AbstractSubscriptionsDAO +{ + private SqlSessionTemplate template; + private QNameDAO qnameDAO; + + public final void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) + { + this.template = sqlSessionTemplate; + } + + public final void setQNameDAO(QNameDAO qnameDAO) + { + this.qnameDAO = qnameDAO; + } + + @Override + public PagingSubscriptionResults selectSubscriptions(String userId, SubscriptionItemTypeEnum type, + PagingRequest pagingRequest) + { + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + if (type == null) + { + throw new IllegalArgumentException("Type may not be null!"); + } + + NodeRef userNodeRef = getUserNodeRef(userId); + Pair userPair = nodeDAO.getNodePair(userNodeRef); + + Map map = new HashMap(); + map.put("userNodeId", userPair.getFirst()); + map.put("false", Boolean.FALSE); + + @SuppressWarnings("unchecked") + List nodeList = (List) template.selectList( + "alfresco.subscriptions.select_Subscriptions", map, new RowBounds(pagingRequest.getSkipCount(), + pagingRequest.getMaxItems() + 1)); + + boolean hasMore = nodeList.size() > pagingRequest.getMaxItems(); + + List result = new ArrayList(nodeList.size()); + for (SubscriptionNodeEntity sne : nodeList) + { + result.add(sne.getNodeRef()); + if (result.size() == pagingRequest.getMaxItems()) + { + break; + } + } + + Integer totalCount = null; + if (pagingRequest.getRequestTotalCountMax() > 0) + { + totalCount = countSubscriptions(userId, type); + } + + return new PagingSubscriptionResultsImpl(result, hasMore, totalCount); + } + + @Override + public int countSubscriptions(String userId, SubscriptionItemTypeEnum type) + { + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + if (type == null) + { + throw new IllegalArgumentException("Type may not be null!"); + } + + NodeRef userNodeRef = getUserNodeRef(userId); + Pair userPair = nodeDAO.getNodePair(userNodeRef); + + Map map = new HashMap(); + map.put("userNodeId", userPair.getFirst()); + map.put("false", Boolean.FALSE); + + Number count = (Number) template.selectOne("alfresco.subscriptions.select_countSubscriptions", map); + + return count.intValue(); + } + + @Override + public void insertSubscription(String userId, NodeRef node) + { + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + if (node == null) + { + throw new IllegalArgumentException("Node may not be null!"); + } + + NodeRef userNodeRef = getUserNodeRef(userId); + Pair userPair = nodeDAO.getNodePair(userNodeRef); + Pair nodePair = nodeDAO.getNodePair(node); + + SubscriptionEntity se = new SubscriptionEntity(); + se.setUserNodeId(userPair.getFirst()); + se.setNodeId(nodePair.getFirst()); + + if (((Number) template.selectOne("alfresco.subscriptions.select_hasSubscribed", se)).intValue() == 0) + { + template.insert("alfresco.subscriptions.insert_Subscription", se); + } + } + + @Override + public void deleteSubscription(String userId, NodeRef node) + { + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + if (node == null) + { + throw new IllegalArgumentException("Node may not be null!"); + } + + NodeRef userNodeRef = getUserNodeRef(userId); + Pair userPair = nodeDAO.getNodePair(userNodeRef); + Pair nodePair = nodeDAO.getNodePair(node); + + SubscriptionEntity se = new SubscriptionEntity(); + se.setUserNodeId(userPair.getFirst()); + se.setNodeId(nodePair.getFirst()); + + template.delete("alfresco.subscriptions.delete_Subscription", se); + } + + @Override + public boolean hasSubscribed(String userId, NodeRef node) + { + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + if (node == null) + { + throw new IllegalArgumentException("Node may not be null!"); + } + + NodeRef userNodeRef = getUserNodeRef(userId); + Pair userPair = nodeDAO.getNodePair(userNodeRef); + Pair nodePair = nodeDAO.getNodePair(node); + + SubscriptionEntity se = new SubscriptionEntity(); + se.setUserNodeId(userPair.getFirst()); + se.setNodeId(nodePair.getFirst()); + + return ((Number) template.selectOne("alfresco.subscriptions.select_hasSubscribed", se)).intValue() == 1; + } + + @Override + public PagingFollowingResults selectFollowing(String userId, PagingRequest pagingRequest) + { + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + NodeRef userNodeRef = getUserNodeRef(userId); + Pair userPair = nodeDAO.getNodePair(userNodeRef); + + Map map = new HashMap(); + map.put("userIdQname", qnameDAO.getQName(ContentModel.PROP_USERNAME).getFirst()); + map.put("userNodeId", userPair.getFirst()); + map.put("false", Boolean.FALSE); + + @SuppressWarnings("unchecked") + List userList = (List) template.selectList("alfresco.subscriptions.select_Following", map, + new RowBounds(pagingRequest.getSkipCount(), pagingRequest.getMaxItems() + 1)); + + boolean hasMore = userList.size() > pagingRequest.getMaxItems(); + if (hasMore && userList.size() > 0) + { + userList.remove(userList.size() - 1); + } + + Integer totalCount = null; + if (pagingRequest.getRequestTotalCountMax() > 0) + { + totalCount = countFollowers(userId); + } + + return new PagingFollowingResultsImpl(userList, hasMore, totalCount); + } + + @Override + public PagingFollowingResults selectFollowers(String userId, PagingRequest pagingRequest) + { + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + NodeRef userNodeRef = getUserNodeRef(userId); + Pair userPair = nodeDAO.getNodePair(userNodeRef); + + Map map = new HashMap(); + map.put("userIdQname", qnameDAO.getQName(ContentModel.PROP_USERNAME).getFirst()); + map.put("userNodeId", userPair.getFirst()); + map.put("false", Boolean.FALSE); + + @SuppressWarnings("unchecked") + List userList = (List) template.selectList("alfresco.subscriptions.select_Followers", map, + new RowBounds(pagingRequest.getSkipCount(), pagingRequest.getMaxItems() + 1)); + + boolean hasMore = userList.size() > pagingRequest.getMaxItems(); + if (hasMore && userList.size() > 0) + { + userList.remove(userList.size() - 1); + } + + Integer totalCount = null; + if (pagingRequest.getRequestTotalCountMax() > 0) + { + totalCount = countFollowers(userId); + } + + return new PagingFollowingResultsImpl(userList, hasMore, totalCount); + } + + @Override + public int countFollowers(String userId) + { + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + NodeRef userNodeRef = getUserNodeRef(userId); + Pair userPair = nodeDAO.getNodePair(userNodeRef); + + Map map = new HashMap(); + map.put("userNodeId", userPair.getFirst()); + map.put("false", Boolean.FALSE); + + Number count = (Number) template.selectOne("alfresco.subscriptions.select_countFollowers", map); + + return count.intValue(); + } +} diff --git a/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceActivitiesTest.java b/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceActivitiesTest.java new file mode 100644 index 0000000000..54c1dbf88a --- /dev/null +++ b/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceActivitiesTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2005-2011 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.subscriptions; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.activities.feed.FeedGenerator; +import org.alfresco.repo.activities.feed.local.LocalFeedTaskProcessor; +import org.alfresco.repo.activities.post.lookup.PostLookup; +import org.alfresco.repo.management.subsystems.ChildApplicationContextFactory; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.subscriptions.SubscriptionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.PropertyMap; +import org.quartz.Scheduler; +import org.springframework.context.ApplicationContext; + +public class SubscriptionServiceActivitiesTest extends TestCase +{ + // Location of activity type templates (for site activities) + // assumes test-resources is on classpath + protected static final String TEST_TEMPLATES_LOCATION = "activities"; + + protected ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + protected SubscriptionService subscriptionService; + protected PersonService personService; + protected PostLookup postLookup; + protected FeedGenerator feedGenerator; + + @Override + public void setUp() throws Exception + { + // Let's shut down the scheduler so that we aren't competing with the + // scheduled versions of the post lookup and + // feed generator jobs + Scheduler scheduler = (Scheduler) ctx.getBean("schedulerFactory"); + scheduler.shutdown(); + + // Get the required services + subscriptionService = (SubscriptionService) ctx.getBean("SubscriptionService"); + personService = (PersonService) ctx.getBean("PersonService"); + + ChildApplicationContextFactory activitiesFeed = (ChildApplicationContextFactory) ctx.getBean("ActivitiesFeed"); + ApplicationContext activitiesFeedCtx = activitiesFeed.getApplicationContext(); + postLookup = (PostLookup) activitiesFeedCtx.getBean("postLookup"); + feedGenerator = (FeedGenerator) activitiesFeedCtx.getBean("feedGenerator"); + + LocalFeedTaskProcessor feedProcessor = (LocalFeedTaskProcessor) activitiesFeedCtx.getBean("feedTaskProcessor"); + + List templateSearchPaths = new ArrayList(1); + templateSearchPaths.add(TEST_TEMPLATES_LOCATION); + feedProcessor.setTemplateSearchPaths(templateSearchPaths); + feedProcessor.setUseRemoteCallbacks(false); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + } + + protected void deletePerson(String userId) + { + personService.deletePerson(userId); + } + + protected NodeRef createPerson(String userId) + { + deletePerson(userId); + + PropertyMap properties = new PropertyMap(5); + properties.put(ContentModel.PROP_USERNAME, userId); + properties.put(ContentModel.PROP_FIRSTNAME, userId); + properties.put(ContentModel.PROP_LASTNAME, "Test"); + properties.put(ContentModel.PROP_EMAIL, userId + "@email.com"); + + return personService.createPerson(properties); + } + + protected void generateFeed() throws Exception + { + postLookup.execute(); + feedGenerator.execute(); + } + + public void testFollowingActivity() throws Exception + { + final String userId1 = "bob"; + final String userId2 = "tom"; + + createPerson(userId1); + createPerson(userId2); + + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + @Override + public Object doWork() throws Exception + { + subscriptionService.follow(userId1, userId2); + return null; + } + }, userId1); + + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + @Override + public Object doWork() throws Exception + { + subscriptionService.follow(userId2, userId1); + return null; + } + }, userId2); + + generateFeed(); + + deletePerson(userId1); + deletePerson(userId2); + } +} diff --git a/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImpl.java b/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImpl.java new file mode 100644 index 0000000000..c22e5cb8d5 --- /dev/null +++ b/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImpl.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2005-2011 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.subscriptions; + +import java.io.Serializable; +import java.util.Collections; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.repo.activities.ActivityType; +import org.alfresco.repo.domain.subscriptions.SubscriptionsDAO; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.subscriptions.PagingFollowingResults; +import org.alfresco.service.cmr.subscriptions.PagingFollowingResultsImpl; +import org.alfresco.service.cmr.subscriptions.PagingSubscriptionResults; +import org.alfresco.service.cmr.subscriptions.PagingSubscriptionResultsImpl; +import org.alfresco.service.cmr.subscriptions.PrivateSubscriptionListException; +import org.alfresco.service.cmr.subscriptions.SubscriptionItemTypeEnum; +import org.alfresco.service.cmr.subscriptions.SubscriptionService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; + +public class SubscriptionServiceImpl implements SubscriptionService +{ + /** Logger */ + private static Log logger = LogFactory.getLog(SubscriptionServiceImpl.class); + + /** Activity tool */ + private static final String ACTIVITY_TOOL = "subscriptionService"; + + /** Activity values */ + private static final String SUB_USER = "user"; + private static final String SUB_USER_TO_FOLLOW = "userToFollow"; + private static final String SUB_NODE = "node"; + + protected SubscriptionsDAO subscriptionsDAO; + protected NodeService nodeService; + protected PersonService personService; + protected ActivityService activityService; + + /** + * Sets the subscriptions DAO. + */ + public void setSubscriptionsDAO(SubscriptionsDAO subscriptionsDAO) + { + this.subscriptionsDAO = subscriptionsDAO; + } + + /** + * Sets the node service. + */ + public final void setNodeService(NodeService nodeService) + { + this.nodeService = nodeService; + } + + /** + * Sets the person service. + */ + public final void setPersonService(PersonService personService) + { + this.personService = personService; + } + + /** + * Sets the activity service. + */ + public final void setActivityService(ActivityService activictyService) + { + this.activityService = activictyService; + } + + @SuppressWarnings("unchecked") + @Override + public PagingSubscriptionResults getSubscriptions(String userId, SubscriptionItemTypeEnum type, + PagingRequest pagingRequest) + { + if (!subscriptionsEnabled()) + { + return new PagingSubscriptionResultsImpl(Collections.EMPTY_LIST, false, 0); + } + + checkRead(userId); + return subscriptionsDAO.selectSubscriptions(userId, type, pagingRequest); + } + + @Override + public int getSubscriptionCount(String userId, SubscriptionItemTypeEnum type) + { + if (!subscriptionsEnabled()) + { + return 0; + } + + return subscriptionsDAO.countSubscriptions(userId, type); + } + + @Override + public void subscribe(String userId, NodeRef node) + { + if (!subscriptionsEnabled()) + { + return; + } + + checkWrite(userId); + checkUserNode(node); + subscriptionsDAO.insertSubscription(userId, node); + + if (userId.equalsIgnoreCase(AuthenticationUtil.getRunAsUser())) + { + String activityDataJSON = null; + try + { + JSONObject activityData = new JSONObject(); + activityData.put(SUB_USER, userId); + activityData.put(SUB_NODE, node.toString()); + activityDataJSON = activityData.toString(); + } catch (JSONException je) + { + // log error, subsume exception + logger.error("Failed to get activity data: " + je); + } + + activityService.postActivity(ActivityType.SUBSCRIPTIONS_SUBSCRIBE, null, ACTIVITY_TOOL, activityDataJSON); + } + } + + @Override + public void unsubscribe(String userId, NodeRef node) + { + if (!subscriptionsEnabled()) + { + return; + } + + checkWrite(userId); + subscriptionsDAO.deleteSubscription(userId, node); + } + + @Override + public boolean hasSubscribed(String userId, NodeRef node) + { + if (!subscriptionsEnabled()) + { + return false; + } + + checkRead(userId); + return subscriptionsDAO.hasSubscribed(userId, node); + } + + @SuppressWarnings("unchecked") + @Override + public PagingFollowingResults getFollowing(String userId, PagingRequest pagingRequest) + { + if (!subscriptionsEnabled()) + { + return new PagingFollowingResultsImpl(Collections.EMPTY_LIST, false, 0); + } + + checkRead(userId); + return subscriptionsDAO.selectFollowing(userId, pagingRequest); + } + + @SuppressWarnings("unchecked") + @Override + public PagingFollowingResults getFollowers(String userId, PagingRequest pagingRequest) + { + if (!subscriptionsEnabled()) + { + return new PagingFollowingResultsImpl(Collections.EMPTY_LIST, false, 0); + } + + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + return subscriptionsDAO.selectFollowers(userId, pagingRequest); + } + + @Override + public int getFollowersCount(String userId) + { + if (!subscriptionsEnabled()) + { + return 0; + } + + return subscriptionsDAO.countFollowers(userId); + } + + @Override + public int getFollowingCount(String userId) + { + if (!subscriptionsEnabled()) + { + return 0; + } + + return getSubscriptionCount(userId, SubscriptionItemTypeEnum.USER); + } + + @Override + public void follow(String userId, String userToFollow) + { + if (!subscriptionsEnabled()) + { + return; + } + + checkWrite(userId); + subscriptionsDAO.insertSubscription(userId, getUserNodeRef(userToFollow)); + + if (userId.equalsIgnoreCase(AuthenticationUtil.getRunAsUser())) + { + String activityDataJSON = null; + try + { + JSONObject activityData = new JSONObject(); + activityData.put(SUB_USER, userId); + activityData.put(SUB_USER_TO_FOLLOW, userToFollow); + activityDataJSON = activityData.toString(); + } catch (JSONException je) + { + // log error, subsume exception + logger.error("Failed to get activity data: " + je); + } + + activityService.postActivity(ActivityType.SUBSCRIPTIONS_FOLLOW, null, ACTIVITY_TOOL, activityDataJSON); + } + } + + @Override + public void unfollow(String userId, String userToUnfollow) + { + if (!subscriptionsEnabled()) + { + return; + } + + checkWrite(userId); + subscriptionsDAO.deleteSubscription(userId, getUserNodeRef(userToUnfollow)); + } + + @Override + public boolean follows(String userId, String userToFollow) + { + if (!subscriptionsEnabled()) + { + return false; + } + + checkRead(userId); + return subscriptionsDAO.hasSubscribed(userId, getUserNodeRef(userToFollow)); + } + + @Override + public void setSubscriptionListPrivate(String userId, boolean isPrivate) + { + checkWrite(userId); + nodeService.setProperty(getUserNodeRef(userId), ContentModel.PROP_SUBSCRIPTIONS_PRIVATE, isPrivate); + } + + @Override + public boolean isSubscriptionListPrivate(String userId) + { + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + Serializable privateList = nodeService.getProperty(getUserNodeRef(userId), + ContentModel.PROP_SUBSCRIPTIONS_PRIVATE); + if (privateList == null) + { + return false; + } + + if (privateList instanceof Boolean && !((Boolean) privateList).booleanValue()) + { + return false; + } + + return true; + } + + protected boolean subscriptionsEnabled() + { + return true; + } + + /** + * Checks if the current user is allowed to get subscription data. + */ + protected void checkRead(String userId) + { + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + String currentUser = AuthenticationUtil.getRunAsUser(); + if (currentUser == null) + { + throw new IllegalArgumentException("No current user!"); + } + + if (currentUser.equalsIgnoreCase(userId) || currentUser.equalsIgnoreCase(AuthenticationUtil.getAdminUserName()) + || AuthenticationUtil.isRunAsUserTheSystemUser() || !isSubscriptionListPrivate(userId)) + { + return; + } + + throw new PrivateSubscriptionListException("subscription_service.err.private-list"); + } + + /** + * Checks if the current user is allowed to get change data. + */ + protected void checkWrite(String userId) + { + if (userId == null) + { + throw new IllegalArgumentException("User Id may not be null!"); + } + + String currentUser = AuthenticationUtil.getRunAsUser(); + if (currentUser == null) + { + throw new IllegalArgumentException("No current user!"); + } + + if (currentUser.equalsIgnoreCase(userId) || currentUser.equalsIgnoreCase(AuthenticationUtil.getAdminUserName()) + || AuthenticationUtil.isRunAsUserTheSystemUser()) + { + return; + } + + throw new AccessDeniedException("subscription_service.err.write-denied"); + } + + /** + * Gets the user node ref from the user id. + */ + protected NodeRef getUserNodeRef(String userId) + { + return personService.getPerson(userId, false); + } + + /** + * Checks if the node is a user node and throws an exception if it id not. + */ + protected void checkUserNode(NodeRef nodeRef) + { + // we only support user-to-user subscriptions in this release + if (!ContentModel.TYPE_USER.equals(nodeService.getType(nodeRef))) + { + throw new IllegalArgumentException("Only user nodes supported!"); + } + } +} diff --git a/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImplTest.java b/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImplTest.java new file mode 100644 index 0000000000..c1eb75e5d1 --- /dev/null +++ b/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImplTest.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2005-2011 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.subscriptions; + +import javax.transaction.Status; +import javax.transaction.UserTransaction; + +import junit.framework.TestCase; + +import org.alfresco.model.ContentModel; +import org.alfresco.query.PagingRequest; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.subscriptions.PagingFollowingResults; +import org.alfresco.service.cmr.subscriptions.PrivateSubscriptionListException; +import org.alfresco.service.cmr.subscriptions.SubscriptionService; +import org.alfresco.service.transaction.TransactionService; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.PropertyMap; +import org.springframework.context.ApplicationContext; + +public class SubscriptionServiceImplTest extends TestCase +{ + public static final String USER_BOB = "bob"; + public static final String USER_TOM = "tom"; + public static final String USER_LISA = "lisa"; + + private UserTransaction txn; + + protected ApplicationContext ctx = ApplicationContextHelper.getApplicationContext(); + protected TransactionService transactionService; + protected SubscriptionService subscriptionService; + protected PersonService personService; + + @Override + public void setUp() throws Exception + { + // Get the required services + transactionService = (TransactionService) ctx.getBean("TransactionService"); + subscriptionService = (SubscriptionService) ctx.getBean("SubscriptionService"); + personService = (PersonService) ctx.getBean("PersonService"); + + AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName()); + + txn = transactionService.getNonPropagatingUserTransaction(false); + txn.begin(); + + createPerson(USER_BOB); + createPerson(USER_TOM); + createPerson(USER_LISA); + } + + @Override + protected void tearDown() throws Exception + { + deletePerson(USER_BOB); + deletePerson(USER_TOM); + deletePerson(USER_LISA); + + if (txn != null) + { + if (txn.getStatus() == Status.STATUS_MARKED_ROLLBACK) + { + txn.rollback(); + } else + { + txn.commit(); + } + txn = null; + } + } + + protected void deletePerson(String userId) + { + personService.deletePerson(userId); + } + + protected NodeRef createPerson(String userId) + { + deletePerson(userId); + + PropertyMap properties = new PropertyMap(5); + properties.put(ContentModel.PROP_USERNAME, userId); + properties.put(ContentModel.PROP_FIRSTNAME, userId); + properties.put(ContentModel.PROP_LASTNAME, "Test"); + properties.put(ContentModel.PROP_EMAIL, userId + "@email.com"); + + return personService.createPerson(properties); + } + + public void testFollow() throws Exception + { + String userId1 = USER_BOB; + String userId2 = USER_TOM; + String userId3 = USER_LISA; + + // check follows first + if (subscriptionService.follows(userId1, userId2)) + { + subscriptionService.unfollow(userId1, userId2); + } + assertFalse(subscriptionService.follows(userId1, userId2)); + + // count the people user 1 is following + int count = subscriptionService.getFollowingCount(userId1); + assertTrue(count >= 0); + + // user 1 follows user 2 -- twice (the second follow request should be + // ignored) + subscriptionService.follow(userId1, userId2); + subscriptionService.follow(userId1, userId2); + assertEquals(count + 1, subscriptionService.getFollowingCount(userId1)); + assertTrue(subscriptionService.follows(userId1, userId2)); + + // user 1 follows user 3 + subscriptionService.follow(userId1, userId3); + assertEquals(count + 2, subscriptionService.getFollowingCount(userId1)); + assertTrue(subscriptionService.follows(userId1, userId3)); + + // get following list of user 1 + PagingFollowingResults following = subscriptionService.getFollowing(userId1, new PagingRequest(100000, null)); + assertNotNull(following); + assertNotNull(following.getPage()); + assertTrue(following.getPage().contains(userId2)); + assertTrue(following.getPage().contains(userId3)); + + // count followers of user 2 + int followerCount = subscriptionService.getFollowersCount(userId2); + assertTrue(followerCount > 0); + + // get followers of user 2 + PagingFollowingResults followers = subscriptionService.getFollowers(userId2, new PagingRequest(100000, null)); + assertNotNull(followers); + assertNotNull(followers.getPage()); + assertTrue(followers.getPage().contains(userId1)); + + // unfollow + subscriptionService.unfollow(userId1, userId2); + assertEquals(count + 1, subscriptionService.getFollowingCount(userId1)); + assertFalse(subscriptionService.follows(userId1, userId2)); + assertTrue(subscriptionService.follows(userId1, userId3)); + + subscriptionService.unfollow(userId1, userId3); + assertEquals(count, subscriptionService.getFollowingCount(userId1)); + assertFalse(subscriptionService.follows(userId1, userId3)); + } + + public void testDeletePerson() throws Exception + { + String userId1 = USER_BOB; + String userId2 = "subscription-temp-user"; + + createPerson(userId2); + + subscriptionService.follow(userId1, userId2); + assertTrue(subscriptionService.follows(userId1, userId2)); + + deletePerson(userId2); + + PagingFollowingResults following = subscriptionService.getFollowing(userId1, new PagingRequest(100000, null)); + assertNotNull(following); + assertNotNull(following.getPage()); + assertFalse(following.getPage().contains(userId2)); + } + + public void testPrivateList() throws Exception + { + final String userId1 = USER_BOB; + final String userId2 = USER_TOM; + + assertFalse(subscriptionService.isSubscriptionListPrivate(userId1)); + + subscriptionService.setSubscriptionListPrivate(userId1, false); + assertFalse(subscriptionService.isSubscriptionListPrivate(userId1)); + + subscriptionService.setSubscriptionListPrivate(userId1, true); + assertTrue(subscriptionService.isSubscriptionListPrivate(userId1)); + + subscriptionService.setSubscriptionListPrivate(userId1, false); + assertFalse(subscriptionService.isSubscriptionListPrivate(userId1)); + + subscriptionService.setSubscriptionListPrivate(userId1, true); + assertTrue(subscriptionService.isSubscriptionListPrivate(userId1)); + + subscriptionService.follow(userId1, userId2); + + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + @Override + public Object doWork() throws Exception + { + assertNotNull(subscriptionService.getFollowing(userId1, new PagingRequest(100000, null))); + return null; + } + }, userId1); + + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + @Override + public Object doWork() throws Exception + { + assertNotNull(subscriptionService.getFollowing(userId1, new PagingRequest(100000, null))); + return null; + } + }, AuthenticationUtil.getAdminUserName()); + + AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork() + { + @Override + public Object doWork() throws Exception + { + try + { + subscriptionService.getFollowing(userId1, new PagingRequest(100000, null)); + fail("Expected PrivateSubscriptionListException!"); + } catch (PrivateSubscriptionListException psle) + { + // expected + } + return null; + } + }, userId2); + } +} diff --git a/source/java/org/alfresco/service/cmr/subscriptions/PagingFollowingResults.java b/source/java/org/alfresco/service/cmr/subscriptions/PagingFollowingResults.java new file mode 100644 index 0000000000..f939491f20 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/subscriptions/PagingFollowingResults.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005-2011 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.subscriptions; + +import org.alfresco.query.PagingResults; + +/** + * Response object for follower or following paging requests. + * + * @author Florian Mueller + * @since 4.0 + */ +public interface PagingFollowingResults extends PagingResults +{ + +} diff --git a/source/java/org/alfresco/service/cmr/subscriptions/PagingFollowingResultsImpl.java b/source/java/org/alfresco/service/cmr/subscriptions/PagingFollowingResultsImpl.java new file mode 100644 index 0000000000..c722e517da --- /dev/null +++ b/source/java/org/alfresco/service/cmr/subscriptions/PagingFollowingResultsImpl.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2005-2011 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.subscriptions; + +import java.util.Collections; +import java.util.List; + +import org.alfresco.util.Pair; + +public class PagingFollowingResultsImpl implements PagingFollowingResults +{ + private List page; + private boolean hasMore; + private Pair totalCount; + + public PagingFollowingResultsImpl(List page, boolean hasMore, Integer total) + { + this.page = page; + this.hasMore = hasMore; + + if (total != null) + { + totalCount = new Pair(total, total); + } + } + + @Override + public List getPage() + { + return Collections.unmodifiableList(page); + } + + @Override + public boolean hasMoreItems() + { + return hasMore; + } + + @Override + public Pair getTotalResultCount() + { + return totalCount; + } + + @Override + public String getQueryExecutionId() + { + return null; + } + + @Override + public boolean permissionsApplied() + { + return false; + } +} diff --git a/source/java/org/alfresco/service/cmr/subscriptions/PagingSubscriptionResults.java b/source/java/org/alfresco/service/cmr/subscriptions/PagingSubscriptionResults.java new file mode 100644 index 0000000000..34c48db2e9 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/subscriptions/PagingSubscriptionResults.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2005-2011 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.subscriptions; + +import org.alfresco.query.PagingResults; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Response object for subscription paging requests. + * + * @author Florian Mueller + * @since 4.0 + */ +public interface PagingSubscriptionResults extends PagingResults +{ + +} diff --git a/source/java/org/alfresco/service/cmr/subscriptions/PagingSubscriptionResultsImpl.java b/source/java/org/alfresco/service/cmr/subscriptions/PagingSubscriptionResultsImpl.java new file mode 100644 index 0000000000..e7c6b077a4 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/subscriptions/PagingSubscriptionResultsImpl.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2005-2011 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.subscriptions; + +import java.util.Collections; +import java.util.List; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.util.Pair; + +public class PagingSubscriptionResultsImpl implements PagingSubscriptionResults +{ + private List page; + private boolean hasMore; + private Pair totalCount; + + public PagingSubscriptionResultsImpl(List page, boolean hasMore, Integer total) + { + this.page = page; + this.hasMore = hasMore; + + if (total != null) + { + totalCount = new Pair(total, total); + } + } + + @Override + public List getPage() + { + return Collections.unmodifiableList(page); + } + + @Override + public boolean hasMoreItems() + { + return hasMore; + } + + @Override + public Pair getTotalResultCount() + { + return totalCount; + } + + @Override + public String getQueryExecutionId() + { + return null; + } + + @Override + public boolean permissionsApplied() + { + return false; + } +} diff --git a/source/java/org/alfresco/service/cmr/subscriptions/PrivateSubscriptionListException.java b/source/java/org/alfresco/service/cmr/subscriptions/PrivateSubscriptionListException.java new file mode 100644 index 0000000000..3a5445feef --- /dev/null +++ b/source/java/org/alfresco/service/cmr/subscriptions/PrivateSubscriptionListException.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2005-2011 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.subscriptions; + +import org.alfresco.error.AlfrescoRuntimeException; + +/** + * This exception is thrown if a subscription list is private and the accessing + * user is not allowed to see it. + * + * @author Florian Mueller + * @since 4.0 + */ +public class PrivateSubscriptionListException extends AlfrescoRuntimeException +{ + private static final long serialVersionUID = 6971869799749343887L; + + public PrivateSubscriptionListException(String msg) + { + super(msg); + } + + public PrivateSubscriptionListException(String msg, Throwable cause) + { + super(msg, cause); + } +} diff --git a/source/java/org/alfresco/service/cmr/subscriptions/SubscriptionItemTypeEnum.java b/source/java/org/alfresco/service/cmr/subscriptions/SubscriptionItemTypeEnum.java new file mode 100644 index 0000000000..eb3d9f0091 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/subscriptions/SubscriptionItemTypeEnum.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2005-2011 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.subscriptions; + +/** + * Subscription types enum. + * + * @author Florian Mueller + * @since 4.0 + */ +public enum SubscriptionItemTypeEnum +{ + USER("user"); + + private String value; + + SubscriptionItemTypeEnum(String type) + { + value = type; + } + + public String getValue() + { + return value; + } + + public static SubscriptionItemTypeEnum fromValue(String v) + { + for (SubscriptionItemTypeEnum ste : SubscriptionItemTypeEnum.values()) + { + if (ste.value.equals(v)) + { + return ste; + } + } + throw new IllegalArgumentException(v); + } +} diff --git a/source/java/org/alfresco/service/cmr/subscriptions/SubscriptionService.java b/source/java/org/alfresco/service/cmr/subscriptions/SubscriptionService.java new file mode 100644 index 0000000000..890cffb505 --- /dev/null +++ b/source/java/org/alfresco/service/cmr/subscriptions/SubscriptionService.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2005-2011 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.subscriptions; + +import org.alfresco.query.PagingRequest; +import org.alfresco.service.Auditable; +import org.alfresco.service.NotAuditable; +import org.alfresco.service.PublicService; +import org.alfresco.service.cmr.repository.NodeRef; + +/** + * Subscription Service. + * + * @author Florian Mueller + * @since 4.0 + */ +@PublicService +public interface SubscriptionService +{ + // --- subscription --- + + /** + * Returns the nodes a user has subscribed to. + * + * @param userId + * the id of the user + * @param type + * the type of the nodes + * @param pagingRequest + * paging details + * + * @throws PrivateSubscriptionListException + * if the subscription list is private and the calling user is + * not allowed to see it + */ + @NotAuditable + PagingSubscriptionResults getSubscriptions(String userId, SubscriptionItemTypeEnum type, PagingRequest pagingRequest); + + /** + * Returns how many nodes the given user has subscribed to. + * + * @param userId + * the id of the user + * @param type + * the type of the nodes + */ + @NotAuditable + int getSubscriptionCount(String userId, SubscriptionItemTypeEnum type); + + /** + * Subscribes to a node. + * + * @param userId + * id of the user + * @param node + * the node + */ + @Auditable(parameters = { "userId", "node" }) + void subscribe(String userId, NodeRef node); + + /** + * Unsubscribes from a node. + * + * @param userId + * id of the user + * @param node + * the node + */ + @Auditable(parameters = { "userId", "node" }) + void unsubscribe(String userId, NodeRef node); + + /** + * Returns if the user has subscribed to the given node. + * + * @param userId + * id of the user + * @param node + * the node + */ + @NotAuditable + boolean hasSubscribed(String userId, NodeRef node); + + // --- follow --- + + /** + * Returns a list of users that the given user follows. + * + * @param userId + * id of the user + * @param pagingRequest + * paging details + * @throws PrivateSubscriptionListException + * if the subscription list is private and the calling user is + * not allowed to see it + */ + @NotAuditable + PagingFollowingResults getFollowing(String userId, PagingRequest pagingRequest); + + /** + * Returns a list of users that follow the given user. + * + * @param userId + * id of the user + * @param pagingRequest + * paging details + */ + @NotAuditable + PagingFollowingResults getFollowers(String userId, PagingRequest pagingRequest); + + /** + * Returns how many users the given user follows. + * + * @param userId + * the id of the user + * @param type + * the type of the nodes + */ + @NotAuditable + int getFollowingCount(String userId); + + /** + * Returns how many users follow the given user. + * + * @param userId + * the id of the user + * @param type + * the type of the nodes + */ + @NotAuditable + int getFollowersCount(String userId); + + /** + * Follows another + * + * @param userId + * the id of the user + * @param userToFollow + * the id of the user to follow + */ + @Auditable(parameters = { "userId", "userToFollow" }) + void follow(String userId, String userToFollow); + + @Auditable(parameters = { "userId", "userToUnfollow" }) + void unfollow(String userId, String userToUnfollow); + + /** + * Returns if the user follows to the given other user. + * + * @param userId + * id of the user + * @param userToFollow + * the id of the other user + */ + @NotAuditable + boolean follows(String userId, String userToFollow); + + // --- privacy settings --- + + /** + * Sets or unsets the subscription list of the given user to private. + * + * @param userId + * the id of the user + * @param isPrivate + * true - set list private, + * false - set list public + * + */ + @Auditable(parameters = { "userId", "isPrivate" }) + void setSubscriptionListPrivate(String userId, boolean isPrivate); + + /** + * Returns if the subscription list of the given user is set to private. + * + * @param userId + * the id of the user + */ + @NotAuditable + boolean isSubscriptionListPrivate(String userId); +}