From 3aa7e1533219859ce8ff253ce1bace361c5c42db Mon Sep 17 00:00:00 2001 From: Jan Vonka Date: Fri, 1 May 2009 12:57:04 +0000 Subject: [PATCH] MOB-822 - Activity feed generator - optimise to use embedded calls (as alternative to existing remote repo callbacks) git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@14163 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../activities/activities-feed-context.xml | 16 + .../repo/activities/ActivityServiceImpl.java | 33 +- .../repo/activities/SiteActivityTest.java | 545 ++++++++++++++++++ .../activities/feed/FeedTaskProcessor.java | 147 ++--- .../feed/local/LocalFeedGenerator.java | 15 +- .../feed/local/LocalFeedTaskProcessor.java | 224 +++++++ .../org/alfresco/generic.atomentry.ftl | 17 + .../activities/org/alfresco/generic.json.ftl | 2 + .../alfresco/site/user-joined.atomentry.ftl | 11 + .../org/alfresco/site/user-left.atomentry.ftl | 11 + .../site/user-role-changed.atomentry.ftl | 11 + 11 files changed, 954 insertions(+), 78 deletions(-) create mode 100644 source/java/org/alfresco/repo/activities/SiteActivityTest.java create mode 100644 source/test-resources/activities/org/alfresco/generic.atomentry.ftl create mode 100644 source/test-resources/activities/org/alfresco/generic.json.ftl create mode 100644 source/test-resources/activities/org/alfresco/site/user-joined.atomentry.ftl create mode 100644 source/test-resources/activities/org/alfresco/site/user-left.atomentry.ftl create mode 100644 source/test-resources/activities/org/alfresco/site/user-role-changed.atomentry.ftl diff --git a/config/alfresco/activities/activities-feed-context.xml b/config/alfresco/activities/activities-feed-context.xml index 02ce8e7fd3..d70208c742 100644 --- a/config/alfresco/activities/activities-feed-context.xml +++ b/config/alfresco/activities/activities-feed-context.xml @@ -9,6 +9,7 @@ + @@ -64,6 +65,21 @@ + + + false + + + + + + + + alfresco/extension/templates/activities + alfresco/templates/activities + + + diff --git a/source/java/org/alfresco/repo/activities/ActivityServiceImpl.java b/source/java/org/alfresco/repo/activities/ActivityServiceImpl.java index fe7d97832e..1eedc6f9fb 100644 --- a/source/java/org/alfresco/repo/activities/ActivityServiceImpl.java +++ b/source/java/org/alfresco/repo/activities/ActivityServiceImpl.java @@ -38,11 +38,14 @@ import org.alfresco.repo.domain.activities.ActivityPostEntity; import org.alfresco.repo.domain.activities.FeedControlDAO; import org.alfresco.repo.domain.activities.FeedControlEntity; import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; import org.alfresco.repo.tenant.TenantService; import org.alfresco.service.cmr.activities.ActivityService; import org.alfresco.service.cmr.activities.FeedControl; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.AuthorityService; +import org.alfresco.service.cmr.site.SiteInfo; +import org.alfresco.service.cmr.site.SiteService; import org.alfresco.service.namespace.QName; import org.alfresco.util.ParameterCheck; import org.apache.commons.logging.Log; @@ -69,6 +72,7 @@ public class ActivityServiceImpl implements ActivityService private FeedControlDAO feedControlDAO; private AuthorityService authorityService; private FeedGenerator feedGenerator; + private SiteService siteService; private TenantService tenantService; @@ -116,6 +120,11 @@ public class ActivityServiceImpl implements ActivityService this.tenantService = tenantService; } + public void setSiteService(SiteService siteService) + { + this.siteService = siteService; + } + /* (non-Javadoc) * @see org.alfresco.service.cmr.activities.ActivityService#postActivity(java.lang.String, java.lang.String, java.lang.String, java.lang.String) @@ -309,7 +318,16 @@ public class ActivityServiceImpl implements ActivityService { feedUserId = feedUserId.toLowerCase(); } - + + String currentUser = AuthenticationUtil.getFullyAuthenticatedUser(); + if (! ((currentUser == null) || + (currentUser.equals(AuthenticationUtil.getSystemUserName())) || + (authorityService.isAdminAuthority(currentUser)) || + (currentUser.equals(feedUserId)))) + { + throw new AccessDeniedException("Unable to get user feed entries for '" + feedUserId + "' - currently logged in as '" + currentUser +"'"); + } + try { List activityFeeds = null; @@ -358,9 +376,18 @@ public class ActivityServiceImpl implements ActivityService ParameterCheck.mandatoryString("format", format); List activityFeedEntries = new ArrayList(); - + try - { + { + if (siteService != null) + { + SiteInfo siteInfo = siteService.getSite(siteId); + if (siteInfo == null) + { + throw new AccessDeniedException("No such site: " + siteId); + } + } + siteId = tenantService.getName(siteId); List activityFeeds = feedDAO.selectSiteFeedEntries(siteId, format); diff --git a/source/java/org/alfresco/repo/activities/SiteActivityTest.java b/source/java/org/alfresco/repo/activities/SiteActivityTest.java new file mode 100644 index 0000000000..8c5fa275eb --- /dev/null +++ b/source/java/org/alfresco/repo/activities/SiteActivityTest.java @@ -0,0 +1,545 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program 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 General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ +package org.alfresco.repo.activities; + +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.security.authentication.AuthenticationUtil; +import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.repo.site.SiteModel; +import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.activities.FeedControl; +import org.alfresco.service.cmr.security.AuthenticationService; +import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteService; +import org.alfresco.service.cmr.site.SiteVisibility; +import org.alfresco.util.ApplicationContextHelper; +import org.alfresco.util.PropertyMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; + +/** + * Simple Activity Service unit test using site (membership) activities + * + * @author janv + */ +public class SiteActivityTest extends TestCase +{ + private static Log logger = LogFactory.getLog(SiteActivityTest.class); + + private static ApplicationContext applicationContext = ApplicationContextHelper.getApplicationContext(); + + private SiteService siteService; + private ActivityService activityService; + private AuthenticationService authenticationService; + private PersonService personService; + private PostLookup postLookup; + private FeedGenerator feedGenerator; + + // + // Test config & data + // + + // Location of activity type templates (for site activities) + private static final String TEST_TEMPLATES_LOCATION = "activities"; // assumes test-resources is on classpath + + // Test users + private static final String ADMIN_USER = "admin"; + private static final String ADMIN_PW = "admin"; + + private static String user1 = null; + private static String user2 = null; + private static String user3 = null; + private static String user4 = null; + + private static final String USER_PW = "password"; + + // Test sites + private static String site1 = null; + private static String site2 = null; + private static String site3 = null; + + // AppToolId for site membership activities + private static String appToolId = "siteService"; // refer to SiteService + + private static boolean membersAddedUpdated = false; + private static boolean membersRemoved = false; + private static boolean controlsCreated = false; + + public SiteActivityTest() + { + } + + @Override + protected void setUp() throws Exception + { + super.setUp(); + String testid = ""+System.currentTimeMillis(); + + // Get the required services + this.activityService = (ActivityService)applicationContext.getBean("activityService"); + this.siteService = (SiteService)applicationContext.getBean("SiteService"); + this.authenticationService = (AuthenticationService)applicationContext.getBean("AuthenticationService"); + this.personService = (PersonService)applicationContext.getBean("PersonService"); + + this.postLookup = (PostLookup)applicationContext.getBean("postLookup"); + this.feedGenerator = (FeedGenerator)applicationContext.getBean("feedGenerator"); + + LocalFeedTaskProcessor feedProcessor = (LocalFeedTaskProcessor)applicationContext.getBean("feedTaskProcessor"); + List templateSearchPaths = new ArrayList(1); + templateSearchPaths.add(TEST_TEMPLATES_LOCATION); + feedProcessor.setTemplateSearchPaths(templateSearchPaths); + feedProcessor.setUseRemoteCallbacks(false); + + site1 = "test_site1_" + testid; + site2 = "test_site2_" + testid; + site3 = "test_site3_" + testid; + + user1 = "test_user1_" + testid; + user2 = "test_user2_" + testid; + user3 = "test_user3_" + testid; + user4 = "test_user4_" + testid; + + + // create users + + login(ADMIN_USER, ADMIN_PW); + + createUser(user1, USER_PW); + createUser(user2, USER_PW); + createUser(user3, USER_PW); + createUser(user4, USER_PW); + + // create sites + + // create public site + createSite(site1, true); + + // create private sites + createSite(site2, false); + createSite(site3, false); + } + + @Override + protected void tearDown() throws Exception + { + login(ADMIN_USER, ADMIN_PW); + + deleteUser(user1); + deleteUser(user2); + deleteUser(user3); + deleteUser(user4); + + membersAddedUpdated = false; + membersRemoved = false; + controlsCreated = false; + + super.tearDown(); + } + + protected void createSite(String siteId, boolean isPublic) throws Exception + { + siteService.createSite("myPreset", siteId, "myTitle", "myDescription", (isPublic ? SiteVisibility.PUBLIC : SiteVisibility.PRIVATE)); + + if (logger.isDebugEnabled()) + { + logger.debug("createdSite: " + siteId); + } + } + + public void testGetSiteFeedsBefore() throws Exception + { + login(ADMIN_USER, ADMIN_PW); + + getSiteFeed(site1, 0); + getSiteFeed(site2, 0); // site 2 is private, but accessible to admins + getSiteFeed(site3, 0); // site 3 is private, but accessible to admins + + login(user4, USER_PW); + + getSiteFeed(site1, 0); // site 1 is public, hence site feed is accessible to any user of the system + + try + { + getSiteFeed(site2, 0); // site 2 is private, hence only accessible to members or admins + + fail("Site feed for private site should not be accessible to non-admin / non-member"); + } + catch (AccessDeniedException ade) + { + // ignore + } + + try + { + getSiteFeed(site3, 0); // site 3 is private, hence only accessible to members or admins + + fail("Site feed for private site should not be accessible to non-admin / non-member"); + } + catch (AccessDeniedException ade) + { + // ignore + } + } + + protected void getSiteFeed(String siteId, int expectedCount) throws Exception + { + assertEquals(expectedCount, activityService.getSiteFeedEntries(siteId, "json").size()); + } + + public void testGetUserFeedsBefore() throws Exception + { + // as admin + + login(ADMIN_USER, ADMIN_PW); + + getUserFeed(user1, true, 0); + getUserFeed(user2, true, 0); + getUserFeed(user3, true, 0); + getUserFeed(user4, true, 0); + + // as user1 + + login(user1, USER_PW); + + getUserFeed(user1, false, 0); + + // as user2 + + login(user2, USER_PW); + + try + { + getUserFeed(user1, true, 0); + + fail("User feed should only be accessible to user or an admin"); + } + catch (AccessDeniedException ade) + { + // ignore + } + + + // as user1 - with filter args ... + + login(user1, USER_PW); + + getUserFeed(null, site1, false, false, false, 0); + getUserFeed(null, site2, false, false, false, 0); + getUserFeed(null, site3, false, false, false, 0); + + getUserFeed(null, null, false, true, false, 0); + getUserFeed(null, null, false, false, true, 0); + getUserFeed(null, null, false, true, true, 0); + } + + protected void getUserFeed(String userId, boolean isAdmin, int expectedCount) throws Exception + { + getUserFeed(userId, null, isAdmin, false, false, expectedCount); + } + + protected void getUserFeed(String userId, String siteId, boolean isAdmin, boolean excludeThisUser, boolean excludeOtherUsers, int expectedCount) throws Exception + { + if (userId == null) + { + userId = AuthenticationUtil.getFullyAuthenticatedUser(); + } + assertEquals(expectedCount, activityService.getUserFeedEntries(userId, "json", siteId, excludeThisUser, excludeOtherUsers).size()); + } + + public void testUserFeedControls() throws Exception + { + if (! controlsCreated) + { + // user 1 opts out of all activities for site 1 + login(user1, USER_PW); + addFeedControl(site1, null); + + // user 2 opts out of site membership activities (across all sites) + login(user2, USER_PW); + addFeedControl(null, appToolId); + + // user 3 opts out of site membership activities for site 1 only + login(user3, USER_PW); + addFeedControl(site1, appToolId); + + // TODO add more here, once we have more appToolIds + + controlsCreated = true; + } + } + + public void testAddAndUpdateMemberships() throws Exception + { + if (! membersAddedUpdated) + { + login(ADMIN_USER, ADMIN_PW); + + addAndUpdateMemberships(site1, true); // public site, include all users + addAndUpdateMemberships(site2, true); // private site, include all users + addAndUpdateMemberships(site3, false); // private site, do not include user 4 + + generateFeed(); + + membersAddedUpdated = true; + } + } + + public void testGetSiteFeedsAfterAddAndUpdateMemberships() throws Exception + { + testAddAndUpdateMemberships(); + + login(ADMIN_USER, ADMIN_PW); + + getSiteFeed(site1, 8); // 8 = 4 users, each with 1 join, 1 role change + getSiteFeed(site2, 8); // 8 = 4 users, each with 1 join, 1 role change + getSiteFeed(site3, 6); // 6 = 3 users, each with 1 join, 1 role change (not user 4) + + login(user4, USER_PW); + + getSiteFeed(site1, 8); + getSiteFeed(site2, 8); // site 2 is private, user 4 is a member + + try + { + getSiteFeed(site3, 0); // site 3 is private, user 4 is not a member + + fail("Site feed for private site should not be accessible to non-admin / non-member"); + } + catch (AccessDeniedException ade) + { + // ignore + } + } + public void testRemoveMemberships() throws Exception + { + if (! membersRemoved) + { + testAddAndUpdateMemberships(); + + login(ADMIN_USER, ADMIN_PW); + + removeMemberships(site1, true); + removeMemberships(site2, true); + removeMemberships(site3, false); + + generateFeed(); + + membersRemoved = true; + } + } + + protected void addAndUpdateMemberships(String siteId, boolean includeUser4) throws Exception + { + // add member -> join site + addMembership(siteId, user1, SiteModel.SITE_CONSUMER); + addMembership(siteId, user2, SiteModel.SITE_MANAGER); + addMembership(siteId, user3, SiteModel.SITE_COLLABORATOR); + + if (includeUser4) { addMembership(siteId, user4, SiteModel.SITE_CONSUMER); } + + // update member -> change role + updateMembership(siteId, user1, SiteModel.SITE_MANAGER); + updateMembership(siteId, user2, SiteModel.SITE_COLLABORATOR); + updateMembership(siteId, user3, SiteModel.SITE_CONSUMER); + + if (includeUser4) { updateMembership(siteId, user4, SiteModel.SITE_COLLABORATOR); } + } + + protected void removeMemberships(String siteId, boolean includeUser4) throws Exception + { + // remove member -> leave site + removeMembership(siteId, user1); + removeMembership(siteId, user2); + removeMembership(siteId, user3); + + if (includeUser4) { removeMembership(siteId, user4); } + } + + private void addFeedControl(String siteId, String appToolId) throws Exception + { + // set feed control for current user + activityService.setFeedControl(new FeedControl(siteId, appToolId)); + } + + public void testGetSiteFeedsAfterRemoveMemberships() throws Exception + { + testAddAndUpdateMemberships(); + testRemoveMemberships(); + + login(ADMIN_USER, ADMIN_PW); + + getSiteFeed(site1, 12); // 12 = 4 users, each with 1 join, 1 role change, 1 leave + getSiteFeed(site2, 12); // 12 = 4 users, each with 1 join, 1 role change, 1 leave + getSiteFeed(site3, 9); // 9 = 3 users, each with 1 join, 1 role change, 1 leave (not user 4) + + login(user4, USER_PW); + + getSiteFeed(site1, 12); + + try + { + getSiteFeed(site2, 0); // site 2 is private, user 4 is no longer a member + + fail("Site feed for private site should not be accessible to non-admin / non-member"); + } + catch (AccessDeniedException ade) + { + // ignore + } + + try + { + getSiteFeed(site3, 0); // site 3 is private, user 4 was never a member + + fail("Site feed for private site should not be accessible to non-admin / non-member"); + } + catch (AccessDeniedException ade) + { + // ignore + } + } + + public void testGetUserFeedsAfter() throws Exception + { + testUserFeedControls(); + testAddAndUpdateMemberships(); + testRemoveMemberships(); + + // as admin + + login(ADMIN_USER, ADMIN_PW); + + // site 1, with 4 users, each with 1 join, 1 role change = 4x2 = 8 + // site 2, with 4 users, each with 1 join, 1 role change = 4x2 = 8 + // site 3, with 3 users, each with 1 join, 1 role change = 3x2 = 6 + + // user 1 belongs to 3 sites = (2x8)+(1x6) = 22 + // user 2 belongs to 3 sites = (2x8)+(1x6) = 22 + // user 3 belongs to 3 sites = (2x8)+(1x6) = 22 + // user 4 belongs to 2 sites = (2x8) = 16 + + getUserFeed(user1, true, 14); // 14 = (22 - 8) due to feed control - exclude site 1 + getUserFeed(user2, true, 0); // 0 = due to feed control - exclude site membership activities (across all sites) + getUserFeed(user3, true, 14); // 14 = (22 - 8) due to feed control - exclude site membership activities for site 1 + getUserFeed(user4, true, 16); // 16 = no feed control + + // as user1 + + login(user1, USER_PW); + + getUserFeed(user1, false, 14); + + // as user2 + + login(user2, USER_PW); + + try + { + getUserFeed(user1, true, 14); + + fail("User feed should only be accessible to user or an admin"); + } + catch (AccessDeniedException ade) + { + // ignore + } + + // as user1 - with filter args ... + + login(user1, USER_PW); + + getUserFeed(null, site1, false, false, false, 0); + getUserFeed(null, site2, false, false, false, 8); + getUserFeed(null, site3, false, false, false, 6); + + getUserFeed(null, null, false, false, false, 14); // no filter + getUserFeed(null, null, false, true, false, 14); // exclude any from user1 + getUserFeed(null, null, false, false, true, 0); // exclude all except user1 + getUserFeed(null, null, false, true, true, 0); // exclude all (NOOP) + + // TODO - add more (eg. other non-admin user activities) + } + + private void addMembership(String siteId, String userName, String role) throws Exception + { + updateMembership(siteId, userName, role); + } + + private void updateMembership(String siteId, String userName, String role) throws Exception + { + siteService.setMembership(siteId, userName, role); + } + + private void removeMembership(String siteId, String userName) throws Exception + { + siteService.removeMembership(siteId, userName); + } + + protected void createUser(String userName, String password) + { + if (authenticationService.authenticationExists(userName) == false) + { + authenticationService.createAuthentication(userName, password.toCharArray()); + + PropertyMap ppOne = new PropertyMap(4); + ppOne.put(ContentModel.PROP_USERNAME, userName); + ppOne.put(ContentModel.PROP_FIRSTNAME, "firstName"); + ppOne.put(ContentModel.PROP_LASTNAME, "lastName"); + ppOne.put(ContentModel.PROP_EMAIL, "email@email.com"); + ppOne.put(ContentModel.PROP_JOBTITLE, "jobTitle"); + + personService.createPerson(ppOne); + } + } + + protected void deleteUser(String userName) + { + if (authenticationService.authenticationExists(userName) == true) + { + personService.deletePerson(userName); + authenticationService.deleteAuthentication(userName); + } + } + + private void login(String username, String password) + { + AuthenticationUtil.setFullyAuthenticatedUser(username); + } + + private void generateFeed() throws Exception + { + postLookup.execute(); + feedGenerator.execute(); + } +} diff --git a/source/java/org/alfresco/repo/activities/feed/FeedTaskProcessor.java b/source/java/org/alfresco/repo/activities/feed/FeedTaskProcessor.java index 09de6af8ce..6d9e7bbf35 100644 --- a/source/java/org/alfresco/repo/activities/feed/FeedTaskProcessor.java +++ b/source/java/org/alfresco/repo/activities/feed/FeedTaskProcessor.java @@ -82,6 +82,8 @@ public abstract class FeedTaskProcessor public void process(int jobTaskNode, long minSeq, long maxSeq, RepoCtx ctx) throws Exception { + long startTime = System.currentTimeMillis(); + if (logger.isDebugEnabled()) { logger.debug(">>> Process: jobTaskNode '" + jobTaskNode + "' from seq '" + minSeq + "' to seq '" + maxSeq + "' on this node from grid job."); @@ -168,6 +170,14 @@ public abstract class FeedTaskProcessor } activityTemplates.put(baseActivityType, fmTemplates); + + if (logger.isTraceEnabled()) + { + for (String fmTemplate : fmTemplates) + { + logger.trace("For activityType '"+activityType+"' found activity type template: "+fmTemplate); + } + } } if (fmTemplates.size() == 0) @@ -232,9 +242,9 @@ public abstract class FeedTaskProcessor { startTransaction(); - if (logger.isDebugEnabled()) + if (logger.isTraceEnabled()) { - logger.debug(">>> Process: " + connectedUsers.size() + " candidate connections for activity post " + activityPost.getId()); + logger.trace(">>> Process: " + connectedUsers.size() + " candidate connections for activity post " + activityPost.getId()); } int excludedConnections = 0; @@ -335,7 +345,7 @@ public abstract class FeedTaskProcessor } finally { - logger.info(">>> Generated " + totalGenerated + " activity feed entries for " + (activityPosts == null ? 0 : activityPosts.size()) + " activity posts"); + logger.info(">>> Generated " + totalGenerated + " activity feed entries for " + (activityPosts == null ? 0 : activityPosts.size()) + " activity posts (in " + (System.currentTimeMillis() - startTime) + " msecs)"); } } @@ -413,7 +423,7 @@ public abstract class FeedTaskProcessor StringBuffer sbUrl = new StringBuffer(); sbUrl.append(ctx.getRepoEndPoint()). append(URL_SERVICE_SITES).append("/").append(siteId).append(URL_MEMBERSHIPS); - + String jsonArrayResult = callWebScript(sbUrl.toString(), ctx.getTicket()); if ((jsonArrayResult != null) && (jsonArrayResult.length() != 0)) { @@ -428,7 +438,7 @@ public abstract class FeedTaskProcessor { userName = userName.toLowerCase(); } - members.add(person.getString("userName")); + members.add(userName); } } } @@ -459,42 +469,47 @@ public abstract class FeedTaskProcessor for (int i = 0; i < ja.length(); i++) { String name = ja.getString(i); - if (! name.contains(" (Working Copy).")) - { - allTemplateNames.add(name); - } + allTemplateNames.add(name); } } + return getActivityTemplates(allTemplateNames); + } + + protected Map> getActivityTemplates(List allTemplateNames) + { Map> activityTemplates = new HashMap>(10); for (String template : allTemplateNames) { - // assume template path = /..ftl - // and base-activityType can contain "." - - String baseActivityType = template; - int idx1 = baseActivityType.lastIndexOf("/"); - if (idx1 != -1) + if (! template.contains(" (Working Copy).")) { - baseActivityType = baseActivityType.substring(idx1+1); - } - - int idx2 = baseActivityType.lastIndexOf("."); - if (idx2 != -1) - { - int idx3 = baseActivityType.substring(0, idx2).lastIndexOf("."); - if (idx3 != -1) + // assume template path = /..ftl + // and base-activityType can contain "." + + String baseActivityType = template; + int idx1 = baseActivityType.lastIndexOf("/"); + if (idx1 != -1) { - baseActivityType = baseActivityType.substring(0, idx3); - - List activityTypeTemplateList = activityTemplates.get(baseActivityType); - if (activityTypeTemplateList == null) + baseActivityType = baseActivityType.substring(idx1+1); + } + + int idx2 = baseActivityType.lastIndexOf("."); + if (idx2 != -1) + { + int idx3 = baseActivityType.substring(0, idx2).lastIndexOf("."); + if (idx3 != -1) { - activityTypeTemplateList = new ArrayList(1); - activityTemplates.put(baseActivityType, activityTypeTemplateList); + baseActivityType = baseActivityType.substring(0, idx3); + + List activityTypeTemplateList = activityTemplates.get(baseActivityType); + if (activityTypeTemplateList == null) + { + activityTypeTemplateList = new ArrayList(1); + activityTemplates.put(baseActivityType, activityTypeTemplateList); + } + activityTypeTemplateList.add(template); } - activityTypeTemplateList.add(template); } } } @@ -506,14 +521,14 @@ public abstract class FeedTaskProcessor { 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; + 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 @@ -574,7 +589,7 @@ public abstract class FeedTaskProcessor } } } - + return true; } @@ -626,36 +641,36 @@ public abstract class FeedTaskProcessor protected class TemplateWebScriptLoader extends URLTemplateLoader { - private String repoEndPoint; - private String ticketId; - - public TemplateWebScriptLoader(String repoEndPoint, String ticketId) - { - this.repoEndPoint = repoEndPoint; - this.ticketId = ticketId; - } - - public URL getURL(String templatePath) - { - try - { - StringBuffer sb = new StringBuffer(); - sb.append(this.repoEndPoint). - append(URL_SERVICE_TEMPLATE).append("/").append(templatePath). - append("?format=text"). - append("&alf_ticket=").append(ticketId); - - if (logger.isDebugEnabled()) + private String repoEndPoint; + private String ticketId; + + public TemplateWebScriptLoader(String repoEndPoint, String ticketId) + { + this.repoEndPoint = repoEndPoint; + this.ticketId = ticketId; + } + + public URL getURL(String templatePath) + { + try + { + StringBuffer sb = new StringBuffer(); + sb.append(this.repoEndPoint). + append(URL_SERVICE_TEMPLATE).append("/").append(templatePath). + append("?format=text"). + append("&alf_ticket=").append(ticketId); + + if (logger.isDebugEnabled()) { logger.debug(">>> getURL: " + sb.toString()); } - - return new URL(sb.toString()); - } - catch (Exception e) - { - throw new RuntimeException(e); - } - } + + return new URL(sb.toString()); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } } } diff --git a/source/java/org/alfresco/repo/activities/feed/local/LocalFeedGenerator.java b/source/java/org/alfresco/repo/activities/feed/local/LocalFeedGenerator.java index d8a7d50896..262a61fc9c 100644 --- a/source/java/org/alfresco/repo/activities/feed/local/LocalFeedGenerator.java +++ b/source/java/org/alfresco/repo/activities/feed/local/LocalFeedGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2008 Alfresco Software Limited. + * Copyright (C) 2005-2009 Alfresco Software Limited. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -25,7 +25,6 @@ package org.alfresco.repo.activities.feed.local; import java.util.Collection; -import java.util.Date; import org.alfresco.repo.activities.feed.AbstractFeedGenerator; import org.alfresco.repo.activities.feed.FeedGridJob; @@ -73,17 +72,17 @@ public class LocalFeedGenerator extends AbstractFeedGenerator { logger.debug(">>> Execute job cycle: " + gridName + " (maxSeq: " + maxSequence + ")"); } - - long startTime = new Date().getTime(); + + long startTime = System.currentTimeMillis(); // TODO ... or push this upto to job scheduler ... ? AuthenticationUtil.runAs(new RunAsWork() { public Object doWork() - { + { getWebScriptsCtx().setTicket(getAuthenticationService().getCurrentTicket()); return null; - } + } }, AuthenticationUtil.getSystemUserName()); // need web scripts to support System-level authentication ... see RepositoryContainer ! JobSettings js = new JobSettings(); @@ -102,11 +101,9 @@ public class LocalFeedGenerator extends AbstractFeedGenerator job.execute(); } - long endTime = new Date().getTime(); - if (logger.isDebugEnabled()) { - logger.debug(">>> Finish job cycle: " + gridName + " (time taken (secs) = " + ((endTime - startTime) / 1000) + ")"); + logger.debug(">>> Finish job cycle: " + gridName + " (in " + (System.currentTimeMillis() - startTime) + " msecs)"); } return true; } 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 dd6564c919..a32a84db7d 100644 --- a/source/java/org/alfresco/repo/activities/feed/local/LocalFeedTaskProcessor.java +++ b/source/java/org/alfresco/repo/activities/feed/local/LocalFeedTaskProcessor.java @@ -24,28 +24,57 @@ */ package org.alfresco.repo.activities.feed.local; +import java.io.IOException; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import org.alfresco.repo.activities.feed.FeedTaskProcessor; +import org.alfresco.repo.activities.feed.RepoCtx; import org.alfresco.repo.domain.activities.ActivityFeedDAO; import org.alfresco.repo.domain.activities.ActivityFeedEntity; import org.alfresco.repo.domain.activities.ActivityPostDAO; import org.alfresco.repo.domain.activities.ActivityPostEntity; import org.alfresco.repo.domain.activities.FeedControlDAO; import org.alfresco.repo.domain.activities.FeedControlEntity; +import org.alfresco.repo.security.authentication.AuthenticationUtil; +import org.alfresco.repo.template.ClassPathRepoTemplateLoader; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.NodeService; +import org.alfresco.service.cmr.site.SiteService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.core.io.support.ResourcePatternResolver; import com.ibatis.sqlmap.client.SqlMapClient; +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapper; + /** * The local (ie. not grid) feed task processor is responsible for processing the individual feed job */ public class LocalFeedTaskProcessor extends FeedTaskProcessor { + 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 String defaultEncoding; + private List templateSearchPaths; + private boolean useRemoteCallbacks; + // used to start/end/commit transaction // note: currently assumes that all dao services are configured with this mapper / data source private SqlMapClient sqlMapper; @@ -65,6 +94,36 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor 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 setDefaultEncoding(String defaultEncoding) + { + this.defaultEncoding = defaultEncoding; + } + + public void setTemplateSearchPaths(List templateSearchPaths) + { + this.templateSearchPaths = templateSearchPaths; + } + + public void setUseRemoteCallbacks(boolean useRemoteCallbacks) + { + this.useRemoteCallbacks = useRemoteCallbacks; + } + public void setSqlMapClient(SqlMapClient sqlMapper) { this.sqlMapper = sqlMapper; @@ -104,4 +163,169 @@ public class LocalFeedTaskProcessor extends FeedTaskProcessor { return feedControlDAO.selectFeedControls(userId); } + + @Override + protected Set getSiteMembers(final RepoCtx ctx, final String siteId) throws Exception + { + if (useRemoteCallbacks) + { + // 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 + return AuthenticationUtil.runAs(new AuthenticationUtil.RunAsWork>() + { + public Set doWork() throws Exception + { + Set members = new HashSet(); + 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()) + { + userName = userName.toLowerCase(); + } + members.add(userName); + } + } + } + + return members; + } + }, AuthenticationUtil.getSystemUserName()); + } + } + + @Override + protected Map> getActivityTypeTemplates(String repoEndPoint, String ticket, String subPath) throws Exception + { + if (useRemoteCallbacks) + { + // 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"; + } + } + + List allTemplateNames = getDocumentPaths(path, false, templatePattern); + + return getActivityTemplates(allTemplateNames); + } + } + + @Override + protected Configuration getFreemarkerConfiguration(RepoCtx ctx) + { + if (useRemoteCallbacks) + { + // 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) + { + if ((path == null) || (path.length() == 0)) + { + path = "/"; + } + + if (! path.startsWith("/")) + { + path = "/" + path; + } + + 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); + pattern.append("classpath*:").append(classPath) + .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 + { + ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + Resource[] resources = resolver.getResources(pattern); + List documentPaths = new ArrayList(resources.length); + for (Resource resource : resources) + { + String resourcePath = resource.getURL().toExternalForm(); + + int idx = resourcePath.lastIndexOf(classPath); + if (idx != -1) + { + String documentPath = resourcePath.substring(idx); + documentPath = documentPath.replace('\\', '/'); + if (logger.isTraceEnabled()) + { + logger.trace("Item resource path: " + resourcePath + " , item path: " + documentPath); + } + documentPaths.add(documentPath); + } + } + return documentPaths; + } } diff --git a/source/test-resources/activities/org/alfresco/generic.atomentry.ftl b/source/test-resources/activities/org/alfresco/generic.atomentry.ftl new file mode 100644 index 0000000000..dbae100e98 --- /dev/null +++ b/source/test-resources/activities/org/alfresco/generic.atomentry.ftl @@ -0,0 +1,17 @@ + + ${name!""} + + href="${repoEndPoint}/d/d/${nodeRef?replace("://","/")}/${name!""}" + <#else> + href="${repoEndPoint}" + + /> + + ${id} + ${xmldate(date)} + ${userId!""} ${activityType!""} ${displayPath!""} <#if siteNetwork?? && siteNetwork != "">(${siteNetwork} site) + + ${userId!""} + + \ No newline at end of file diff --git a/source/test-resources/activities/org/alfresco/generic.json.ftl b/source/test-resources/activities/org/alfresco/generic.json.ftl new file mode 100644 index 0000000000..46ecb90319 --- /dev/null +++ b/source/test-resources/activities/org/alfresco/generic.json.ftl @@ -0,0 +1,2 @@ +<#-- default JSON rendering is to pass-through - assumes activityData is posted in JSON format --> +${activityData!""} \ No newline at end of file diff --git a/source/test-resources/activities/org/alfresco/site/user-joined.atomentry.ftl b/source/test-resources/activities/org/alfresco/site/user-joined.atomentry.ftl new file mode 100644 index 0000000000..bdcd1f57ed --- /dev/null +++ b/source/test-resources/activities/org/alfresco/site/user-joined.atomentry.ftl @@ -0,0 +1,11 @@ + + ${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml}</#if> joined ${siteNetwork?xml} site + + + ${id} + ${xmldate(date)} + ${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml} joined ${siteNetwork?xml} site (with role ${role}) + + ${userId} + + \ No newline at end of file diff --git a/source/test-resources/activities/org/alfresco/site/user-left.atomentry.ftl b/source/test-resources/activities/org/alfresco/site/user-left.atomentry.ftl new file mode 100644 index 0000000000..a2fe4320c5 --- /dev/null +++ b/source/test-resources/activities/org/alfresco/site/user-left.atomentry.ftl @@ -0,0 +1,11 @@ + + ${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml}</#if> left ${siteNetwork?xml} site + + + ${id} + ${xmldate(date)} + ${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml} left ${siteNetwork?xml} site + + ${userId} + + \ No newline at end of file diff --git a/source/test-resources/activities/org/alfresco/site/user-role-changed.atomentry.ftl b/source/test-resources/activities/org/alfresco/site/user-role-changed.atomentry.ftl new file mode 100644 index 0000000000..6f52b41ae3 --- /dev/null +++ b/source/test-resources/activities/org/alfresco/site/user-role-changed.atomentry.ftl @@ -0,0 +1,11 @@ + + ${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml}</#if> role changed for ${siteNetwork?xml} site + + + ${id} + ${xmldate(date)} + ${memberFirstName?xml}<#if memberLastName??> ${memberLastName?xml} role changed to ${role} for ${siteNetwork?xml} site + + ${userId} + + \ No newline at end of file