diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml
index 53b7a6de92..60ad2dcf49 100644
--- a/config/alfresco/core-services-context.xml
+++ b/config/alfresco/core-services-context.xml
@@ -472,6 +472,7 @@
alfresco.messages.jbpm-engine-messages
alfresco.messages.activiti-engine-messages
alfresco.messages.wdr-messages
+ alfresco.messages.subscription-service
diff --git a/config/alfresco/import-export-context.xml b/config/alfresco/import-export-context.xml
index 5b40577104..befb9626de 100644
--- a/config/alfresco/import-export-context.xml
+++ b/config/alfresco/import-export-context.xml
@@ -525,6 +525,11 @@
alfresco/templates/activities-email-templates.acp
alfresco/messages/bootstrap-spaces
+
+ /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.email.childname}
+ alfresco/templates/following-email-templates.acp
+ alfresco/messages/bootstrap-spaces
+
/${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.rss.childname}
alfresco/templates/rss_templates.acp
diff --git a/config/alfresco/messages/bootstrap-spaces.properties b/config/alfresco/messages/bootstrap-spaces.properties
index b2a5d56714..a6af51e5a5 100644
--- a/config/alfresco/messages/bootstrap-spaces.properties
+++ b/config/alfresco/messages/bootstrap-spaces.properties
@@ -97,6 +97,11 @@ email.template.email_template_for_notifying_users_of_an_Invite=Email template fo
email.templates.email_template_for_notifying_new_users=Email template used to inform new users of their accounts
+spaces.templates.email.following.name=Following Email Templates
+spaces.templates.email.following.description=Following Email Templates
+
+email.templates.email_template_for_following_notifications=Email template used to generate following notification emails
+
version.default=Default version
version.french=French version
version.german=German version
diff --git a/config/alfresco/messages/patch-service.properties b/config/alfresco/messages/patch-service.properties
index f33888d2b7..f4b97d624d 100644
--- a/config/alfresco/messages/patch-service.properties
+++ b/config/alfresco/messages/patch-service.properties
@@ -409,5 +409,13 @@ patch.exampleJavaScript.description=Loads sample Javascript file into datadictio
patch.fixAclInheritance.description=Fixes any ACL inheritance issues.
patch.fixAclInheritance.result=Fixed {0} ACLs.
+patch.followingMailTemplates.description=Adds email templates for following notifications
+
+patch.activitiesTemplatesUpdate.description=Updates activities email templates.
+patch.activitiesTemplatesUpdate.err.template_folder_not_found=Activities email template folder could not be found.
+patch.activitiesTemplatesUpdate.err.source_not_found=New activities email template ACP could not be found.
+patch.activitiesTemplatesUpdate.err.update_failed=Could not update activities email template: {0}
+patch.activitiesTemplatesUpdate.result=Updated {0} activities email templates.
+
patch.avmToAdmRemoteStore.description=Migrates Share Surf config from AVM sitestore to DM Sites folder.
patch.avmToAdmRemoteStore.complete=Completed Share Surf config migration.
diff --git a/config/alfresco/messages/subscription-service.properties b/config/alfresco/messages/subscription-service.properties
new file mode 100644
index 0000000000..fa480dc1ed
--- /dev/null
+++ b/config/alfresco/messages/subscription-service.properties
@@ -0,0 +1,7 @@
+# Subscription service messages
+
+subscription.notification.email.subject={0} is now following you
+
+subscription_service.err.disabled=The subscription is disabled
+subscription_service.err.write-denied=No permissions to update
+subscription_service.err.private-list=This list is marked as private
\ No newline at end of file
diff --git a/config/alfresco/patch/patch-services-context.xml b/config/alfresco/patch/patch-services-context.xml
index 6ac042d986..8f35441964 100644
--- a/config/alfresco/patch/patch-services-context.xml
+++ b/config/alfresco/patch/patch-services-context.xml
@@ -2600,6 +2600,50 @@
+
+
+
+ patch.activitiesTemplatesUpdate
+
+
+ patch.activitiesTemplatesUpdate.description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ alfresco/templates/activities-email-templates.acp
+
+
+
+
+ patch.followingMailTemplates
+ patch.followingMailTemplates.description
+ 0
+ 5010
+ 5011
+
+
+
+
+
+ /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.email.childname}
+ alfresco/templates/following-email-templates.acp
+ alfresco/messages/bootstrap-spaces
+
+
+
patch.newUserEmailTemplates
diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties
index 9f8a7d3a42..02ce8c2d86 100644
--- a/config/alfresco/repository.properties
+++ b/config/alfresco/repository.properties
@@ -376,6 +376,7 @@ spaces.templates.content.childname=app:content_templates
spaces.templates.email.childname=app:email_templates
spaces.templates.email.invite1.childname=app:invite_email_templates
spaces.templates.email.notify.childname=app:notify_email_templates
+spaces.templates.email.following.childname=app:following
spaces.templates.rss.childname=app:rss_templates
spaces.savedsearches.childname=app:saved_searches
spaces.scripts.childname=app:scripts
diff --git a/config/alfresco/subscription-service-context.xml b/config/alfresco/subscription-service-context.xml
index c04e5888ca..71f9d3ed58 100644
--- a/config/alfresco/subscription-service-context.xml
+++ b/config/alfresco/subscription-service-context.xml
@@ -25,6 +25,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/config/alfresco/templates/activities-email-templates.acp b/config/alfresco/templates/activities-email-templates.acp
index de1f1a4bbb..54292f0086 100644
Binary files a/config/alfresco/templates/activities-email-templates.acp and b/config/alfresco/templates/activities-email-templates.acp differ
diff --git a/config/alfresco/templates/following-email-templates.acp b/config/alfresco/templates/following-email-templates.acp
new file mode 100644
index 0000000000..a7ab3083df
Binary files /dev/null and b/config/alfresco/templates/following-email-templates.acp differ
diff --git a/source/java/org/alfresco/repo/admin/patch/impl/ActivitiesTemplatesUpdatePatch.java b/source/java/org/alfresco/repo/admin/patch/impl/ActivitiesTemplatesUpdatePatch.java
new file mode 100644
index 0000000000..bc17faab71
--- /dev/null
+++ b/source/java/org/alfresco/repo/admin/patch/impl/ActivitiesTemplatesUpdatePatch.java
@@ -0,0 +1,210 @@
+/*
+ * 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.admin.patch.impl;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.admin.patch.AbstractPatch;
+import org.alfresco.repo.version.VersionModel;
+import org.alfresco.service.cmr.admin.PatchException;
+import org.alfresco.service.cmr.model.FileFolderService;
+import org.alfresco.service.cmr.repository.ContentWriter;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.cmr.version.VersionService;
+import org.alfresco.service.cmr.version.VersionType;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.TempFileProvider;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipFile;
+import org.springframework.extensions.surf.util.I18NUtil;
+import org.springframework.util.FileCopyUtils;
+
+/**
+ * Patch to update the activities email templates. Current templates become
+ * versions of the new templates.
+ *
+ * @author Florian Mueller
+ */
+public class ActivitiesTemplatesUpdatePatch extends AbstractPatch
+{
+ private static final String MSG_SUCCESS = "patch.activitiesTemplatesUpdate.result";
+
+ protected FileFolderService fileFolderService;
+ protected VersionService versionService;
+
+ protected String newTemplatesFile;
+ protected String newTemplatesName;
+
+ protected ZipFile zipFile;
+
+ public void setFileFolderService(FileFolderService fileFolderService)
+ {
+ this.fileFolderService = fileFolderService;
+ }
+
+ public void setVersionService(VersionService versionService)
+ {
+ this.versionService = versionService;
+ }
+
+ public void setNewTemplatesFile(String newTemplatesFile)
+ {
+ this.newTemplatesFile = newTemplatesFile;
+
+ int x = newTemplatesFile.lastIndexOf("/");
+ if (x < 0)
+ {
+ newTemplatesName = newTemplatesFile;
+ } else
+ {
+ newTemplatesName = newTemplatesFile.substring(x + 1);
+ }
+
+ x = newTemplatesName.lastIndexOf(".");
+ if (x > 0)
+ {
+ newTemplatesName = newTemplatesName.substring(0, x);
+ }
+ }
+
+ @Override
+ protected String applyInternal() throws Exception
+ {
+ // get templates folder
+ NodeRef templateFolder = getTemplateFolder();
+
+ // get ACP file
+ ZipFile zipFile = getZipFile();
+
+ // iterate over all templates in the ACP file and apply version
+ @SuppressWarnings("unchecked")
+ Enumeration zae = (Enumeration) zipFile.getEntries();
+ int count = 0;
+ while (zae.hasMoreElements())
+ {
+ ZipArchiveEntry entry = zae.nextElement();
+ if (!(entry.getName().startsWith(newTemplatesName + "/") && entry.getName().endsWith(".ftl")))
+ {
+ // ignore non-template files
+ continue;
+ }
+
+ // find matching node and add version
+ NodeRef nodeRef = findMatchingNode(templateFolder, entry);
+ if (nodeRef != null)
+ {
+ addNewVersion(nodeRef, zipFile, entry);
+ count++;
+ }
+ }
+
+ return I18NUtil.getMessage(MSG_SUCCESS, count);
+ }
+
+ protected NodeRef getTemplateFolder()
+ {
+ String xpath = "app:company_home/app:dictionary/app:email_templates/cm:activities";
+ try
+ {
+ NodeRef rootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
+ List nodeRefs = searchService.selectNodes(rootNodeRef, xpath, null, namespaceService, false);
+
+ if (nodeRefs.size() < 1)
+ {
+ throw new PatchException("patch.activitiesTemplatesUpdate.err.template_folder_not_found");
+ }
+
+ return nodeRefs.get(0);
+ } catch (Exception e)
+ {
+ throw new PatchException("patch.activitiesTemplatesUpdate.err.template_folder_not_found", e);
+ }
+ }
+
+ protected ZipFile getZipFile()
+ {
+ InputStream templateFileStream = ActivitiesTemplatesUpdatePatch.class.getClassLoader().getResourceAsStream(
+ newTemplatesFile);
+ if (templateFileStream == null)
+ {
+ throw new PatchException("patch.activitiesTemplatesUpdate.err.source_not_found");
+ }
+
+ try
+ {
+ File tempFile = TempFileProvider.createTempFile("templateFile", ".tmp");
+ FileOutputStream os = new FileOutputStream(tempFile);
+ FileCopyUtils.copy(templateFileStream, os);
+
+ return new ZipFile(tempFile, "Cp437");
+ } catch (IOException e)
+ {
+ throw new PatchException("patch.activitiesTemplatesUpdate.err.source_not_found", e);
+ }
+ }
+
+ protected NodeRef findMatchingNode(NodeRef parentNodeRef, ZipArchiveEntry entry)
+ {
+ String name = entry.getName();
+ int x = name.lastIndexOf("/");
+ if (x > 0)
+ {
+ name = name.substring(x + 1);
+ }
+
+ return nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, name);
+ }
+
+ protected void addNewVersion(NodeRef nodeRef, ZipFile zipFile, ZipArchiveEntry entry)
+ {
+ try
+ {
+ if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == false)
+ {
+ Map props = new HashMap();
+ props.put(ContentModel.PROP_INITIAL_VERSION, true);
+ props.put(ContentModel.PROP_AUTO_VERSION, false);
+ nodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, props);
+ }
+
+ Map versionProperties = new HashMap();
+ versionProperties.put(VersionModel.PROP_VERSION_TYPE, VersionType.MAJOR);
+
+ versionService.createVersion(nodeRef, versionProperties);
+
+ ContentWriter writer = fileFolderService.getWriter(nodeRef);
+ writer.setMimetype("text/plain");
+ writer.setEncoding("UTF-8");
+ writer.putContent(zipFile.getInputStream(entry));
+ } catch (Exception e)
+ {
+ throw new PatchException("patch.activitiesTemplatesUpdate.err.update_failed", e);
+ }
+ }
+}
diff --git a/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImpl.java b/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImpl.java
index 6d685fa414..fbd57f2fcc 100644
--- a/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImpl.java
+++ b/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImpl.java
@@ -19,16 +19,27 @@
package org.alfresco.repo.subscriptions;
import java.io.Serializable;
+import java.util.Collections;
+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.action.executer.MailActionExecuter;
import org.alfresco.repo.activities.ActivityType;
import org.alfresco.repo.domain.subscriptions.SubscriptionsDAO;
+import org.alfresco.repo.search.SearcherException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.permissions.AccessDeniedException;
+import org.alfresco.service.cmr.action.Action;
+import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.activities.ActivityService;
+import org.alfresco.service.cmr.model.FileFolderService;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.cmr.subscriptions.PagingFollowingResults;
@@ -37,10 +48,12 @@ import org.alfresco.service.cmr.subscriptions.PrivateSubscriptionListException;
import org.alfresco.service.cmr.subscriptions.SubscriptionItemTypeEnum;
import org.alfresco.service.cmr.subscriptions.SubscriptionService;
import org.alfresco.service.cmr.subscriptions.SubscriptionsDisabledException;
+import org.alfresco.service.namespace.NamespaceService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.json.JSONException;
import org.json.JSONObject;
+import org.springframework.extensions.surf.util.I18NUtil;
public class SubscriptionServiceImpl implements SubscriptionService
{
@@ -54,9 +67,12 @@ public class SubscriptionServiceImpl implements SubscriptionService
private static final String FOLLOWER_FIRSTNAME = "followerFirstName";
private static final String FOLLOWER_LASTNAME = "followerLastName";
private static final String FOLLOWER_USERNAME = "followerUserName";
+ private static final String FOLLOWER_JOBTITLE = "followerJobTitle";
private static final String USER_FIRSTNAME = "userFirstName";
private static final String USER_LASTNAME = "userLastName";
private static final String USER_USERNAME = "userUserName";
+ private static final String FOLLOWING_COUNT = "followingCount";
+ private static final String FOLLOWER_COUNT = "followerCount";
private static final String SUBSCRIBER_FIRSTNAME = "subscriberFirstName";
private static final String SUBSCRIBER_LASTNAME = "subscriberLastName";
@@ -68,6 +84,10 @@ public class SubscriptionServiceImpl implements SubscriptionService
protected PersonService personService;
protected ActivityService activityService;
protected AuthorityService authorityService;
+ protected ActionService actionService;
+ protected SearchService searchService;
+ protected NamespaceService namespaceService;
+ protected FileFolderService fileFolderService;
/**
* Sets the subscriptions DAO.
@@ -109,6 +129,38 @@ public class SubscriptionServiceImpl implements SubscriptionService
this.authorityService = authorityService;
}
+ /**
+ * Sets the action service.
+ */
+ public final void setActionService(ActionService actionService)
+ {
+ this.actionService = actionService;
+ }
+
+ /**
+ * Set the search service.
+ */
+ public final void setSearchService(SearchService searchService)
+ {
+ this.searchService = searchService;
+ }
+
+ /**
+ * Set the namespace service.
+ */
+ public final void setNamespaceService(NamespaceService namespaceService)
+ {
+ this.namespaceService = namespaceService;
+ }
+
+ /**
+ * Set the fileFolder service.
+ */
+ public final void setFileFolderService(FileFolderService fileFolderService)
+ {
+ this.fileFolderService = fileFolderService;
+ }
+
@Override
public PagingSubscriptionResults getSubscriptions(String userId, SubscriptionItemTypeEnum type,
PagingRequest pagingRequest)
@@ -215,9 +267,10 @@ public class SubscriptionServiceImpl implements SubscriptionService
if (userId.equalsIgnoreCase(AuthenticationUtil.getRunAsUser()))
{
- String activityDataJSON = null;
try
{
+ String activityDataJSON = null;
+
NodeRef followerNode = personService.getPerson(userId, false);
NodeRef userNode = personService.getPerson(userToFollow, false);
JSONObject activityData = new JSONObject();
@@ -229,13 +282,23 @@ public class SubscriptionServiceImpl implements SubscriptionService
activityData.put(USER_FIRSTNAME, nodeService.getProperty(userNode, ContentModel.PROP_FIRSTNAME));
activityData.put(USER_LASTNAME, nodeService.getProperty(userNode, ContentModel.PROP_LASTNAME));
activityDataJSON = activityData.toString();
+
+ activityService.postActivity(ActivityType.SUBSCRIPTIONS_FOLLOW, null, ACTIVITY_TOOL, activityDataJSON);
+
} catch (JSONException je)
{
// log error, subsume exception
logger.error("Failed to get activity data: " + je);
}
- activityService.postActivity(ActivityType.SUBSCRIPTIONS_FOLLOW, null, ACTIVITY_TOOL, activityDataJSON);
+ try
+ {
+ sendFollowingMail(userId, userToFollow);
+ } catch (Exception e)
+ {
+ // log error, subsume exception
+ logger.error("Failed to send following email: " + e);
+ }
}
}
@@ -379,4 +442,105 @@ public class SubscriptionServiceImpl implements SubscriptionService
throw new IllegalArgumentException("Only user nodes supported!");
}
}
+
+ /**
+ * Sends an email to the person that is followed.
+ */
+ protected void sendFollowingMail(String userId, String userToFollow)
+ {
+ NodeRef followerNode = personService.getPerson(userId, false);
+ NodeRef userNode = personService.getPerson(userToFollow, false);
+
+ Serializable emailFeedDisabled = nodeService.getProperty(userNode, ContentModel.PROP_EMAIL_FEED_DISABLED);
+ if (emailFeedDisabled instanceof Boolean && ((Boolean) emailFeedDisabled).booleanValue())
+ {
+ // this user doesn't want to be notified
+ return;
+ }
+
+ Serializable emailAddress = nodeService.getProperty(userNode, ContentModel.PROP_EMAIL);
+ if (emailAddress == null)
+ {
+ // we can't send an email without email address
+ return;
+ }
+
+ NodeRef templateNodeRef = getEmailTemplateRef();
+ if (templateNodeRef == null)
+ {
+ // we can't send an email without template
+ return;
+ }
+
+ // compile the mail subject
+ String followerFullName = (nodeService.getProperty(followerNode, ContentModel.PROP_FIRSTNAME) + " " + nodeService
+ .getProperty(followerNode, ContentModel.PROP_LASTNAME)).trim();
+ String subjectText = I18NUtil.getMessage("subscription.notification.email.subject", followerFullName);
+
+ Map model = new HashMap();
+
+ model.put(FOLLOWER_USERNAME, userId);
+ model.put(FOLLOWER_FIRSTNAME, nodeService.getProperty(followerNode, ContentModel.PROP_FIRSTNAME));
+ model.put(FOLLOWER_LASTNAME, nodeService.getProperty(followerNode, ContentModel.PROP_LASTNAME));
+ model.put(FOLLOWER_JOBTITLE, nodeService.getProperty(followerNode, ContentModel.PROP_JOBTITLE));
+ model.put(USER_USERNAME, userToFollow);
+ model.put(USER_FIRSTNAME, nodeService.getProperty(userNode, ContentModel.PROP_FIRSTNAME));
+ model.put(USER_LASTNAME, nodeService.getProperty(userNode, ContentModel.PROP_LASTNAME));
+ model.put(FOLLOWING_COUNT, -1);
+ try
+ {
+ model.put(FOLLOWING_COUNT, getFollowingCount(userId));
+ } catch (Exception e)
+ {
+ }
+ model.put(FOLLOWER_COUNT, -1);
+ try
+ {
+ model.put(FOLLOWER_COUNT, getFollowersCount(userId));
+ } catch (Exception e)
+ {
+ }
+
+ Action mail = actionService.createAction(MailActionExecuter.NAME);
+
+ mail.setParameterValue(MailActionExecuter.PARAM_TO, emailAddress);
+ mail.setParameterValue(MailActionExecuter.PARAM_SUBJECT, subjectText);
+ mail.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, templateNodeRef);
+ mail.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, (Serializable) model);
+
+ actionService.executeAction(mail, null);
+ }
+
+ /**
+ * Returns the NodeRef of the email template or null
if the
+ * template coudln't be found.
+ */
+ protected NodeRef getEmailTemplateRef()
+ {
+ // Find the following email template
+ String xpath = "app:company_home/app:dictionary/app:email_templates/app:following/cm:following-email.html.ftl";
+ try
+ {
+ NodeRef rootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
+ List nodeRefs = searchService.selectNodes(rootNodeRef, xpath, null, namespaceService, false);
+ if (nodeRefs.size() > 1)
+ {
+ logger.error("Found too many email templates using: " + xpath);
+ nodeRefs = Collections.singletonList(nodeRefs.get(0));
+ } else if (nodeRefs.size() == 0)
+ {
+ logger.error("Cannot find the email template using " + xpath);
+ return null;
+ }
+ // Now localise this
+ NodeRef base = nodeRefs.get(0);
+ NodeRef local = fileFolderService.getLocalizedSibling(base);
+ return local;
+ } catch (SearcherException e)
+ {
+ logger.error("Cannot find the email template!", e);
+ }
+
+ return null;
+ }
}