Commits

Micha Kops committed d8414bd

initial release

Comments (0)

Files changed (23)

+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<parent>
+		<groupId>com.atlassian.confluence.plugin.base</groupId>
+		<artifactId>confluence-plugin-base</artifactId>
+		<version>25</version>
+	</parent>
+
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>com.hascode.confluence.plugin</groupId>
+	<artifactId>social-comments-plugin</artifactId>
+	<version>0.9.1</version>
+	<name>Confluence Social Comments Plugin</name>
+	<description>Allows you to notify Confluence users by typing @username: in a comment</description>
+	<packaging>atlassian-plugin</packaging>
+	<url>http://app.hascode.com/social-comments-plugin</url>
+
+	<organization>
+		<name>hasCode.com</name>
+		<url>http://www.hascode.com</url>
+	</organization>
+
+	<developers>
+		<developer>
+			<name>Micha Kops</name>
+		</developer>
+	</developers>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<atlassian.plugin.key>com.hascode.confluence.plugin.socialcomments</atlassian.plugin.key>
+		<atlassian.product.version>3.3</atlassian.product.version>
+		<atlassian.product.test-lib.version>1.4.1</atlassian.product.test-lib.version>
+		<atlassian.product.data.version>3.0</atlassian.product.data.version>
+	</properties>
+
+	<repositories>
+		<repository>
+			<id>atlassian-public</id>
+			<url>https://maven.atlassian.com/repository/public</url>
+			<snapshots>
+				<enabled>true</enabled>
+			</snapshots>
+			<releases>
+				<enabled>true</enabled>
+			</releases>
+		</repository>
+		<repository>
+			<id>atlassian-m1-repository</id>
+			<url>https://maven.atlassian.com/maven1</url>
+			<layout>legacy</layout>
+		</repository>
+		<repository>
+			<id>atlassian-unknown</id>
+			<name>atlassian-unknown</name>
+			<url>http://repository.atlassian.com/</url>
+		</repository>
+		<repository>
+			<id>maven2-repository.dev.java.net</id>
+			<name>Java.net Repository for Maven</name>
+			<url>http://download.java.net/maven/2/</url>
+			<layout>default</layout>
+		</repository>
+	</repositories>
+	<pluginRepositories>
+		<pluginRepository>
+			<id>atlassian-public</id>
+			<url>https://maven.atlassian.com/repository/public</url>
+			<snapshots>
+				<enabled>true</enabled>
+			</snapshots>
+			<releases>
+				<enabled>true</enabled>
+			</releases>
+		</pluginRepository>
+	</pluginRepositories>
+	<dependencies>
+		<dependency>
+			<groupId>com.atlassian.confluence</groupId>
+			<artifactId>confluence</artifactId>
+			<version>${atlassian.product.version}</version>
+			<scope>provided</scope>
+			<exclusions>
+				<exclusion>
+					<groupId>opensymphony</groupId>
+					<artifactId>pell-multipart</artifactId>
+				</exclusion>
+				<exclusion>
+					<groupId>seraph</groupId>
+					<artifactId>seraph</artifactId>
+				</exclusion>
+				<exclusion>
+					<groupId>tangosol-coherence</groupId>
+					<artifactId>coherence</artifactId>
+				</exclusion>
+				<exclusion>
+					<artifactId>tangosol</artifactId>
+					<groupId>tangosol-coherence</groupId>
+				</exclusion>
+			</exclusions>
+		</dependency>
+		<dependency>
+			<groupId>javax.validation</groupId>
+			<artifactId>validation-api</artifactId>
+			<version>1.0.0.GA</version>
+		</dependency>
+		<dependency>
+			<groupId>org.hibernate</groupId>
+			<artifactId>hibernate-validator</artifactId>
+			<version>4.0.2.GA</version>
+		</dependency>
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-api</artifactId>
+			<version>1.6.1</version>
+		</dependency>
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-log4j12</artifactId>
+			<version>1.6.1</version>
+		</dependency>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<version>4.8.2</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.mockito</groupId>
+			<artifactId>mockito-core</artifactId>
+			<version>1.8.5</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.powermock</groupId>
+			<artifactId>powermock-mockito-release-full</artifactId>
+			<version>1.4.8</version>
+			<type>pom</type>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.6</source>
+					<target>1.6</target>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+</project>

src/main/java/com/hascode/confluence/plugin/socialcomments/action/ActionOutcomeAware.java

+package com.hascode.confluence.plugin.socialcomments.action;
+
+public interface ActionOutcomeAware {
+	/**
+	 * default action outcome - no errors
+	 */
+	public static final String	OK				= "ok";
+
+	/**
+	 * outcome for an action with errors that should be displayed using a
+	 * different template
+	 */
+	public static final String	ERROR			= "error";
+
+	/**
+	 * outcome if the current user does not have sufficient permission to invoke
+	 * the action
+	 */
+	public static final String	NO_PERMISSION	= "no-permission";
+}

src/main/java/com/hascode/confluence/plugin/socialcomments/action/ConfigureAction.java

+package com.hascode.confluence.plugin.socialcomments.action;
+
+import java.util.Set;
+
+import javax.validation.ConstraintViolation;
+
+import org.apache.log4j.Logger;
+
+import com.atlassian.confluence.spaces.actions.AbstractSpaceAction;
+import com.hascode.confluence.plugin.socialcomments.config.SocialCommentsConfiguration;
+import com.hascode.confluence.plugin.socialcomments.persistence.SocialCommentsConfigurationService;
+import com.hascode.confluence.plugin.socialcomments.util.Util;
+
+public class ConfigureAction extends AbstractSpaceAction implements ActionOutcomeAware {
+	/**
+	 * the serial UID
+	 */
+	private static final long							serialVersionUID	= 1L;
+
+	/**
+	 * the logger
+	 */
+	private final Logger								logger				= Logger.getLogger(ConfigureAction.class);
+
+	/**
+	 * utility helper
+	 */
+	private Util										util;
+
+	/**
+	 * the config
+	 */
+	private SocialCommentsConfiguration					config;
+
+	/**
+	 * the configuration service
+	 */
+	private final SocialCommentsConfigurationService	configurationService;
+
+	public ConfigureAction(final SocialCommentsConfigurationService configurationService) {
+		this.configurationService = configurationService;
+		this.util = new Util();
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see com.opensymphony.xwork.ActionSupport#execute()
+	 */
+	@Override
+	public String execute() {
+		// permission check
+		if (!util.userIsAdmin()) {
+			logger.warn("user has not sufficient permission to invoke this action. user: " + util.getCurrentUser().getName());
+			return NO_PERMISSION;
+		}
+
+		config = configurationService.load();
+
+		if (util.getRequestParameter("action") != null) {
+			config.setFromEmail(util.getRequestParameter("from"));
+			config.setNotificationText(util.getRequestParameter("notification"));
+			config.setEmailSubject(util.getRequestParameter("subject"));
+			final Set<ConstraintViolation<SocialCommentsConfiguration>> errors = configurationService.validate(config);
+			if (errors.isEmpty()) {
+				configurationService.saveOrUpdate(config);
+			} else {
+				for (final ConstraintViolation<SocialCommentsConfiguration> error : errors) {
+					addActionError(error.getMessage());
+				}
+			}
+		}
+
+		return OK;
+	}
+	/**
+	 * @param util
+	 *            the util to set
+	 */
+	public void setUtil(final Util util) {
+		this.util = util;
+	}
+
+	/**
+	 * @return the config
+	 */
+	public SocialCommentsConfiguration getSocialCommentConfig() {
+		return config;
+	}
+
+}

src/main/java/com/hascode/confluence/plugin/socialcomments/config/SocialCommentsConfiguration.java

+package com.hascode.confluence.plugin.socialcomments.config;
+
+import java.io.Serializable;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+
+public class SocialCommentsConfiguration implements Serializable {
+	private static final long	serialVersionUID	= 1L;
+
+	@NotNull(message = "{com.hascode.socialcomments.validation.from.invalid}")
+	@Pattern(regexp = "^.+@.+\\..+ $", message = "{com.hascode.socialcomments.validation.from.invalid}")
+	private String				fromEmail;
+
+	@NotNull(message = "{com.hascode.socialcomments.validation.notification.invalid}")
+	@Size(min = 1, message = "{com.hascode.socialcomments.validation.notification.invalid}")
+	private String				notificationText;
+
+	@NotNull(message = "{com.hascode.socialcomments.validation.subject.invalid}")
+	@Size(min = 1, message = "{com.hascode.socialcomments.validation.subject.invalid}")
+	private String				emailSubject;
+
+	/**
+	 * @return the fromEmail
+	 */
+	public String getFromEmail() {
+		return fromEmail;
+	}
+
+	/**
+	 * @param fromEmail
+	 *            the fromEmail to set
+	 */
+	public void setFromEmail(final String fromEmail) {
+		this.fromEmail = fromEmail;
+	}
+
+	/**
+	 * @return the notificationText
+	 */
+	public String getNotificationText() {
+		return notificationText;
+	}
+
+	/**
+	 * @param notificationText
+	 *            the notificationText to set
+	 */
+	public void setNotificationText(final String notificationText) {
+		this.notificationText = notificationText;
+	}
+
+	/**
+	 * @return the emailSubject
+	 */
+	public String getEmailSubject() {
+		return emailSubject;
+	}
+
+	/**
+	 * @param emailSubject
+	 *            the emailSubject to set
+	 */
+	public void setEmailSubject(final String emailSubject) {
+		this.emailSubject = emailSubject;
+	}
+}

src/main/java/com/hascode/confluence/plugin/socialcomments/event/CommentEventListener.java

+package com.hascode.confluence.plugin.socialcomments.event;
+
+import com.atlassian.confluence.event.events.content.comment.CommentCreateEvent;
+import com.atlassian.confluence.event.events.content.comment.CommentUpdateEvent;
+
+public interface CommentEventListener {
+	/**
+	 * handles comment creation
+	 * 
+	 * @param commentCreateEvent
+	 *            the event
+	 */
+	public abstract void handleCommentCreateEvent(final CommentCreateEvent commentCreateEvent);
+
+	/**
+	 * handles comment updates
+	 * 
+	 * @param commentUpdateEvent
+	 *            the event
+	 */
+	public abstract void handleCommentUpdateEvent(final CommentUpdateEvent commentUpdateEvent);
+
+}

src/main/java/com/hascode/confluence/plugin/socialcomments/event/CommentEventListenerImpl.java

+package com.hascode.confluence.plugin.socialcomments.event;
+
+import java.util.Set;
+
+import org.apache.log4j.Logger;
+
+import com.atlassian.confluence.event.events.content.comment.CommentCreateEvent;
+import com.atlassian.confluence.event.events.content.comment.CommentUpdateEvent;
+import com.atlassian.confluence.pages.Comment;
+import com.atlassian.event.api.EventListener;
+import com.atlassian.event.api.EventPublisher;
+import com.atlassian.user.User;
+import com.hascode.confluence.plugin.socialcomments.service.RecipientExtractorService;
+import com.hascode.confluence.plugin.socialcomments.service.SocialNotificationService;
+public class CommentEventListenerImpl implements CommentEventListener {
+	/**
+	 * the logger
+	 */
+	private final Logger					logger	= Logger.getLogger(CommentEventListener.class);
+
+	/**
+	 * the recipient extractor
+	 */
+	private final RecipientExtractorService	recipientExtractorService;
+
+	/**
+	 * the social notification service
+	 */
+	private final SocialNotificationService	socialNotificationService;
+
+	public CommentEventListenerImpl(final EventPublisher eventPublisher, final RecipientExtractorService recipientExtractorService, final SocialNotificationService socialNotificationService) {
+		this.recipientExtractorService = recipientExtractorService;
+		this.socialNotificationService = socialNotificationService;
+		eventPublisher.register(this);
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see
+	 * com.hascode.confluence.plugin.socialcomments.event.CommentEventListener
+	 * #handleCommentCreateEvent
+	 * (com.atlassian.confluence.event.events.content.comment
+	 * .CommentCreateEvent)
+	 */
+	@Override
+	@EventListener
+	public void handleCommentCreateEvent(final CommentCreateEvent commentCreateEvent) {
+		logger.debug("comment create event handler called");
+		extractAndNotify(commentCreateEvent.getComment());
+	}
+
+	/**
+	 * extracts recipients and notifies them
+	 * 
+	 * @param comment
+	 *            the comment
+	 */
+	private void extractAndNotify(final Comment comment) {
+		try {
+			final Set<User> recipients = recipientExtractorService.extractUserFromComment(comment);
+			for (final User user : recipients) {
+				socialNotificationService.notifyUser(user, comment);
+			}
+		} catch (final Exception e) {
+			logger.error("handling social comment failed!", e);
+		}
+	}
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see
+	 * com.hascode.confluence.plugin.socialcomments.event.CommentEventListener
+	 * #handleCommentUpdateEvent
+	 * (com.atlassian.confluence.event.events.content.comment
+	 * .CommentUpdateEvent)
+	 */
+	@Override
+	@EventListener
+	public void handleCommentUpdateEvent(final CommentUpdateEvent commentUpdateEvent) {
+		logger.debug("comment update event handler called");
+		extractAndNotify(commentUpdateEvent.getComment());
+	}
+
+}

src/main/java/com/hascode/confluence/plugin/socialcomments/persistence/SocialCommentsConfigurationService.java

+package com.hascode.confluence.plugin.socialcomments.persistence;
+
+import java.util.Set;
+
+import javax.validation.ConstraintViolation;
+
+import com.hascode.confluence.plugin.socialcomments.config.SocialCommentsConfiguration;
+
+public interface SocialCommentsConfigurationService {
+	/**
+	 * validates a given configuration
+	 * 
+	 * @param config
+	 *            the configuration to validate
+	 * @return a set of generic constraint violations
+	 */
+	public abstract Set<ConstraintViolation<SocialCommentsConfiguration>> validate(final SocialCommentsConfiguration config);
+
+	/**
+	 * saves or updates the configuration
+	 * 
+	 * @param config
+	 *            the configuration
+	 */
+	public abstract void saveOrUpdate(final SocialCommentsConfiguration config);
+
+	/**
+	 * returns the configuration from the persistence api
+	 * 
+	 * @return the configuration
+	 */
+	public abstract SocialCommentsConfiguration load();
+}

src/main/java/com/hascode/confluence/plugin/socialcomments/persistence/SocialCommentsConfigurationServiceImpl.java

+package com.hascode.confluence.plugin.socialcomments.persistence;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import javax.validation.Configuration;
+import javax.validation.ConstraintViolation;
+import javax.validation.Validation;
+import javax.validation.ValidationProviderResolver;
+import javax.validation.Validator;
+import javax.validation.ValidatorFactory;
+import javax.validation.spi.ValidationProvider;
+
+import org.hibernate.validator.HibernateValidator;
+
+import com.hascode.confluence.plugin.socialcomments.config.SocialCommentsConfiguration;
+
+public class SocialCommentsConfigurationServiceImpl implements SocialCommentsConfigurationService {
+	/**
+	 * the validator instance
+	 */
+	private static Validator	validator;
+	static {
+		final Configuration<?> config = Validation.byDefaultProvider().providerResolver(new ValidationProviderResolver() {
+			@Override
+			public List<ValidationProvider<?>> getValidationProviders() {
+				List<ValidationProvider<?>> providers = new ArrayList<ValidationProvider<?>>();
+				providers.add(new HibernateValidator());
+				return providers;
+			}
+		}).configure();
+		final ValidatorFactory factory = config.buildValidatorFactory();
+		validator = factory.getValidator();
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see com.hascode.confluence.plugin.socialcomments.persistence.
+	 * SocialCommentsConfigurationService
+	 * #validate(com.hascode.confluence.plugin.
+	 * socialcomments.config.SocialCommentsConfiguration)
+	 */
+	@Override
+	public Set<ConstraintViolation<SocialCommentsConfiguration>> validate(final SocialCommentsConfiguration config) {
+		return validator.validate(config);
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see com.hascode.confluence.plugin.socialcomments.persistence.
+	 * SocialCommentsConfigurationService
+	 * #saveOrUpdate(com.hascode.confluence.plugin
+	 * .socialcomments.config.SocialCommentsConfiguration)
+	 */
+	@Override
+	public void saveOrUpdate(final SocialCommentsConfiguration config) {
+		// TODO Auto-generated method stub
+
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see com.hascode.confluence.plugin.socialcomments.persistence.
+	 * SocialCommentsConfigurationService#load()
+	 */
+	@Override
+	public SocialCommentsConfiguration load() {
+		return new SocialCommentsConfiguration();
+	}
+
+}

src/main/java/com/hascode/confluence/plugin/socialcomments/service/RecipientExtractorService.java

+package com.hascode.confluence.plugin.socialcomments.service;
+
+import java.util.Set;
+
+import com.atlassian.confluence.pages.Comment;
+import com.atlassian.user.User;
+
+public interface RecipientExtractorService {
+	/**
+	 * extracts possible recipients from a given comment
+	 * 
+	 * @param comment
+	 *            the comment
+	 * @return the set of recipients
+	 */
+	public abstract Set<User> extractUserFromComment(final Comment comment);
+}

src/main/java/com/hascode/confluence/plugin/socialcomments/service/RecipientExtractorServiceImpl.java

+package com.hascode.confluence.plugin.socialcomments.service;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.log4j.Logger;
+
+import com.atlassian.confluence.pages.Comment;
+import com.atlassian.confluence.user.UserAccessor;
+import com.atlassian.user.User;
+
+public class RecipientExtractorServiceImpl implements RecipientExtractorService {
+	/**
+	 * the pattern todo: make configurable
+	 */
+	private static final String	PATTERN	= "@(\\w+):";
+
+	/**
+	 * the logger
+	 */
+	private final Logger		logger	= Logger.getLogger(RecipientExtractorService.class);
+
+	/**
+	 * the user dao
+	 */
+	private final UserAccessor	userAccessor;
+
+	public RecipientExtractorServiceImpl(final UserAccessor userAccessor) {
+		this.userAccessor = userAccessor;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see com.hascode.confluence.plugin.socialcomments.service.
+	 * RecipientExtractorService
+	 * #extractUserFromComment(com.atlassian.confluence.pages.Comment)
+	 */
+	@Override
+	public Set<User> extractUserFromComment(final Comment comment) {
+		final Set<String> userNames = new HashSet<String>();
+
+		final String content = comment.getContent();
+		if (StringUtils.isEmpty(content)) {
+			logger.warn("comment content is empty .. we're done here. comment id: " + comment.getId());
+			return new HashSet<User>();
+		}
+
+		final Pattern pattern = Pattern.compile(PATTERN);
+		final Matcher m = pattern.matcher(content);
+
+		logger.debug("trying to extract recipients. regex used: " + PATTERN);
+		while (m.find()) {
+			if (m.groupCount() > 0) {
+				for (int i = 1; i <= m.groupCount(); i++) {
+					userNames.add(m.group(i));
+				}
+			}
+		}
+		logger.debug("usernames resolved: " + userNames.toString());
+
+		return getUserByNames(userNames);
+	}
+
+	/**
+	 * resolve user objects by a given string
+	 * 
+	 * @param userNames
+	 *            the set of names
+	 * @return a set of users
+	 */
+	private Set<User> getUserByNames(final Set<String> userNames) {
+		final Set<User> users = new HashSet<User>();
+		for (final String userName : userNames) {
+			final User user = userAccessor.getUser(userName);
+			if (user == null) {
+				logger.warn("unable to resolve user with given name: " + userName);
+				continue;
+			}
+
+			users.add(user);
+		}
+		logger.debug("users resolved: " + users.toString());
+		return users;
+	}
+}

src/main/java/com/hascode/confluence/plugin/socialcomments/service/SocialNotificationService.java

+package com.hascode.confluence.plugin.socialcomments.service;
+
+import com.atlassian.confluence.pages.Comment;
+import com.atlassian.user.User;
+
+public interface SocialNotificationService {
+	/**
+	 * notifies a given user that he is mentioned in a comment
+	 * 
+	 * @param user
+	 *            the user
+	 * @param comment
+	 *            the comment
+	 */
+	public abstract void notifyUser(final User user, final Comment comment);
+}

src/main/java/com/hascode/confluence/plugin/socialcomments/service/SocialNotificationServiceImpl.java

+package com.hascode.confluence.plugin.socialcomments.service;
+
+import org.apache.log4j.Logger;
+
+import com.atlassian.confluence.mail.template.ConfluenceMailQueueItem;
+import com.atlassian.confluence.pages.Comment;
+import com.atlassian.confluence.setup.settings.SettingsManager;
+import com.atlassian.mail.MailException;
+import com.atlassian.user.User;
+
+public class SocialNotificationServiceImpl implements SocialNotificationService {
+	/**
+	 * the logger instance
+	 */
+	private final Logger			logger	= Logger.getLogger(SocialNotificationService.class);
+
+	/**
+	 * the settings manager
+	 */
+	private final SettingsManager	settingsManager;
+
+	public SocialNotificationServiceImpl(final SettingsManager settingsManager) {
+		this.settingsManager = settingsManager;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see com.hascode.confluence.plugin.socialcomments.service.
+	 * SocialNotificationService#notifyUser(com.atlassian.user.User,
+	 * com.atlassian.confluence.pages.Comment)
+	 */
+	@Override
+	public void notifyUser(final User user, final Comment comment) {
+		final String subject = "Social Comment Notification";
+		final String body = generateMailBody(user, comment);
+		final String mimeType = "text/plain";
+		final String fromName = "test@test.com";
+
+		final ConfluenceMailQueueItem mailItem = new ConfluenceMailQueueItem(user.getEmail(), subject, body, mimeType);
+		mailItem.setFromName(fromName);
+		logger.debug("trying to send mail: " + mailItem.toString());
+
+		try {
+			mailItem.send();
+		} catch (MailException e) {
+			logger.error("mail transfer failed!", e);
+		}
+
+	}
+
+	/**
+	 * generates the mail body
+	 * 
+	 * @param user
+	 *            the user
+	 * @param comment
+	 *            the comment
+	 * @return the generated mail body
+	 */
+	private String generateMailBody(final User user, final Comment comment) {
+		final String url = settingsManager.getGlobalSettings().getBaseUrl() + comment.getUrlPath();
+		return String.format("Dear %s,\nsomeone wanted to notify you about the following wiki comment: \n\n%s", user.getFullName(), url);
+	}
+
+}

src/main/java/com/hascode/confluence/plugin/socialcomments/util/Util.java

+package com.hascode.confluence.plugin.socialcomments.util;
+
+import java.util.Map;
+
+import com.atlassian.confluence.renderer.radeox.macros.MacroUtils;
+import com.atlassian.confluence.user.AuthenticatedUserThreadLocal;
+import com.atlassian.confluence.util.velocity.VelocityUtils;
+import com.atlassian.user.User;
+import com.opensymphony.webwork.ServletActionContext;
+
+/**
+ * utility class
+ */
+public class Util {
+	/**
+	 * returns whether the current user is logged in or not
+	 * 
+	 * @return
+	 */
+	public boolean userLoggedin() {
+		return (AuthenticatedUserThreadLocal.getUser() != null);
+	}
+
+	/**
+	 * returns whether the current user has the administrator role
+	 * 
+	 * @return
+	 */
+	public boolean userIsAdmin() {
+		// User user = AuthenticatedUserThreadLocal.getUser();
+		// Group group =
+		// userAccessor.getGroup(PluginConfig.getAdminGroupName());
+		// if (user != null && group != null) {
+		// if (userAccessor.hasMembership(group, user)) {
+		// return true;
+		// }
+		// }
+
+		return true;
+	}
+
+	/**
+	 * returns parameters from the current request
+	 * 
+	 * @param key
+	 *            the param key
+	 * @return the param value
+	 */
+	public String getRequestParameter(final String key) {
+		return ServletActionContext.getRequest().getParameter(key);
+	}
+
+	/**
+	 * returns the current user
+	 * 
+	 * @return the user
+	 */
+	public User getCurrentUser() {
+		return AuthenticatedUserThreadLocal.getUser();
+	}
+
+	/**
+	 * returns a hashmap to use with the velocity context
+	 * 
+	 * @return
+	 */
+	public Map<String, Object> getDefaultVelocityContext() {
+		return MacroUtils.defaultVelocityContext();
+	}
+
+	/**
+	 * returns a rendered template enriched with context parameters
+	 * 
+	 * @param macroTemplate
+	 *            the template
+	 * @param context
+	 *            the context
+	 * @return the rendered template
+	 */
+	public String getVelocityRenderedTemplate(final String macroTemplate, final Map<String, Object> context) {
+		return VelocityUtils.getRenderedTemplate(macroTemplate, context);
+	}
+}

src/main/resources/ValidationMessages.properties

+com.hascode.socialcomments.validation.from.invalid            = Sender E-Mail Address: Please enter a valid e-mail address.
+com.hascode.socialcomments.validation.notification.invalid = Notification Text: Please enter a notification text.
+com.hascode.socialcomments.validation.subject.invalid = E-Mail Subject: Please enter an e-mail subject.

src/main/resources/atlassian-plugin.xml

+<atlassian-plugin key="${atlassian.plugin.key}" name="${project.name}"
+	pluginsVersion="2">
+	<resource type="i18n" name="i18n"
+		location="com.hascode.confluence.plugin.socialcomments.messages" />
+	<plugin-info>
+		<description>${project.description}</description>
+		<version>${project.version}</version>
+		<vendor name="${project.organization.name}" url="${project.organization.url}" />
+		<param name="configure.url">$req.getContextPath()/plugins/socialcomments/configure.action</param>
+	</plugin-info>
+
+	<!-- COMPONENTS -->
+	<component key="socialCommentEventListener"
+		i18n-name-key="socialcomments.component.component.commenteventlistener"
+		class="com.hascode.confluence.plugin.socialcomments.event.CommentEventListenerImpl">
+		<interface>com.hascode.confluence.plugin.socialcomments.event.CommentEventListener
+		</interface>
+	</component>
+	<component key="recipientExtractorService"
+		i18n-name-key="socialcomments.component.component.recipientextractor"
+		class="com.hascode.confluence.plugin.socialcomments.service.RecipientExtractorServiceImpl">
+		<interface>com.hascode.confluence.plugin.socialcomments.service.RecipientExtractorService
+		</interface>
+	</component>
+	<component key="socialNotificationService"
+		i18n-name-key="socialcomments.component.component.notificationservice"
+		class="com.hascode.confluence.plugin.socialcomments.service.SocialNotificationServiceImpl">
+		<interface>com.hascode.confluence.plugin.socialcomments.service.SocialNotificationService
+		</interface>
+	</component>
+	<component key="socialCommentsConfigurationService"
+		i18n-name-key="socialcomments.component.component.configservice"
+		class="com.hascode.confluence.plugin.socialcomments.persistence.SocialCommentsConfigurationServiceImpl">
+		<interface>com.hascode.confluence.plugin.socialcomments.persistence.SocialCommentsConfigurationService
+		</interface>
+	</component>
+	<!-- /COMPONENTS -->
+
+	<!-- XWORK ACTIONS -->
+	<xwork key="socialcomments.component.xwork.configmanager"
+		i18n-name-key="socialcomments.component.xwork.configmanager">
+		<package name="socialcommentsConfig" extends="default"
+			namespace="/plugins/socialcomments">
+			<default-interceptor-ref name="defaultStack" />
+			<action name="configure"
+				class="com.hascode.confluence.plugin.socialcomments.action.ConfigureAction">
+				<result name="ok" type="velocity">configuration.vm</result>
+				<result name="no-permission" type="velocity">no_permission.vm
+				</result>
+			</action>
+		</package>
+	</xwork>
+	<!-- /XWORK ACTIONS -->
+
+	<!-- WEB ITEMS -->
+	<web-item key="socialcomments.component.webitem.configurelink"
+		i18n-name-key="socialcomments.component.webitem.configurelink" section="system.admin/administration"
+		weight="200">
+		<label key="socialcomments.component.webitem.configurelink.label" />
+		<link>$req.getContextPath()/plugins/socialcomments/configure.action</link>
+	</web-item>
+	<!-- /WEB ITEMS -->
+
+</atlassian-plugin>

src/main/resources/com/hascode/confluence/plugin/socialcomments/messages.properties

+socialcomments.component.xwork.configmanager=Plugin Configuration Action
+socialcomments.msg.no-permission=No permission
+socialcomments.msg.no-permission.description=Sorry, you do not have sufficient permission to invoke this action!
+socialcomments.msg.configuration=Social Comments Plugin Configuration.
+socialcomments.component.component.commenteventlistener=Comment Event Listener.
+socialcomments.component.component.notificationservice=The Notification Service.
+socialcomments.component.component.recipientextractor=The Recipient Extractor Service
+socialcomments.msg.subject=E-Mail Subject
+socialcomments.msg.subject.example=e.g. Social Comment Notification Received
+socialcomments.msg.notification=Notification Text
+socialcomments.msg.notification.hint=The following placeholders are allowed: #name# #url#
+socialcomments.msg.notification.example=e.g. Dear #name#, someone wanted you to receive this notification for the following wiki comment: #url#
+socialcomments.msg.from.email=Sender's E-Mail Address
+socialcomments.msg.from.email.example=e.g. admin@host.com
+socialcomments.msg.submit=Save settings
+socialcomments.component.component.configservice=The Configuration Service.
+socialcomments.component.webitem.configurelink=Configuration Link.
+socialcomments.component.webitem.configurelink.label=Social Comments Configuration

src/main/resources/plugins/socialcomments/configuration.vm

+<html>
+        <head>
+                <meta name="decorator" content="atl.admin"/>
+                <title>$action.getText("socialcomments.msg.configuration")</title>
+        </head>
+        <body>
+			<style type="text/css">
+			.socialcomments-content {
+			    border: 1px solid rgb(221, 221, 221);
+			    -moz-border-radius: 15px 15px 15px 15px;
+			    padding: 20px;
+			}
+			</style>
+        
+                <h1>$action.getText("socialcomments.msg.configuration")</h1>
+                <div class="socialcomments-content">
+                	#parse("/template/includes/actionerrors.vm")
+                	<div class="socialcomments-config">
+                		<form action="$req.getContextPath()/plugins/socialcomments/configure.action" method="post">
+                			<input type="hidden" name="action" value="save"/>
+	                		<table>
+	                			<tbody>
+	                				<tr>
+	                					<th>$action.getText("socialcomments.msg.subject")</th>
+	                					<td>
+	                						<input type="text" name="subject" value="$!socialCommentConfig.getEmailSubject()"/><br/>
+	                						<small>$action.getText("socialcomments.msg.subject.example")</small>
+	                					</td>
+	                				</tr>
+	                				<tr>
+	                					<th>$action.getText("socialcomments.msg.notification")</th>
+	                					<td>
+	                						<textarea name="notification">$!socialCommentConfig.getNotificationText()</textarea><br/>
+	                						<small>$action.getText("socialcomments.msg.notification.hint")</small><br/>
+	                						<small>$action.getText("socialcomments.msg.notification.example")</small>
+	                					</td>
+	                				</tr>
+	                				<tr>
+	                					<th>$action.getText("socialcomments.msg.from.email")</th>
+	                					<td>
+	                						<input type="text" name="from" value="$!socialCommentConfig.getFromEmail()"/><br/>
+	                						<small>$action.getText("socialcomments.msg.from.email.example")</small>
+	                					</td>
+	                				</tr>
+	                				<tr>
+	                					<td colspan="2">
+	                						<input type="submit" value="$action.getText("socialcomments.msg.submit")"/>
+	                					</td>
+	                				</tr>
+	                			</tbody>
+	                		</table>
+                		</form>
+                	</div>
+                	<hr/>
+                	<div class="socialcomments-info">
+                		<small>
+                			<a href="http://www.hascode.com">by hasCode.com</a><br/>
+                			<a href="http://app.hascode.com/social-comments-plugin">Visit the plugin homepage</a>
+                		</small>
+                	</div>
+                </div>
+        </body>
+</html>

src/main/resources/plugins/socialcomments/no_permission.vm

+<html>
+        <head>
+                <meta name="decorator" content="atl.admin"/>
+                <title>$action.getText("toppages.msg.no-permission")</title>
+        </head>
+        <body>
+                <h1>$action.getText("toppages.msg.no-permission")</h1>
+                <div class="toppages-content">
+                	$action.getText("toppages.msg.no-permission.description")
+                	<br/>
+                	<div class="toppages-info">
+                		<small><a href="http://www.hascode.com">by hasCode.com</a></small>
+                	</div>
+                </div>
+        </body>
+</html>

src/test/java/com/hascode/confluence/plugin/socialcomments/action/ConfigureActionTest.java

+package com.hascode.confluence.plugin.socialcomments.action;
+
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import com.atlassian.user.User;
+import com.hascode.confluence.plugin.socialcomments.persistence.SocialCommentsConfigurationService;
+import com.hascode.confluence.plugin.socialcomments.util.Util;
+
+/**
+ * test for ConfigureAction
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ConfigureActionTest {
+	@Mock
+	private Util								util;
+
+	@Mock
+	private User								user;
+
+	@Mock
+	private SocialCommentsConfigurationService	configService;
+
+	@Before
+	public void setUp() {
+		MockitoAnnotations.initMocks(this);
+	}
+
+	@Test
+	public void testExecute() {
+		// stub
+		when(util.getCurrentUser()).thenReturn(user);
+		when(util.userIsAdmin()).thenReturn(false);
+		when(user.getName()).thenReturn("test-user");
+
+		// init
+		final ConfigureAction action = new ConfigureAction(configService);
+		action.setUtil(util);
+
+		// test
+		Assert.assertEquals(ActionOutcomeAware.NO_PERMISSION, action.execute());
+		when(util.userIsAdmin()).thenReturn(true);
+		Assert.assertEquals(ActionOutcomeAware.OK, action.execute());
+
+		// spy
+		verify(util, times(1)).getCurrentUser();
+		verify(util, times(2)).userIsAdmin();
+	}
+}

src/test/java/com/hascode/confluence/plugin/socialcomments/event/CommentEventListenerTest.java

+package com.hascode.confluence.plugin.socialcomments.event;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import com.atlassian.confluence.event.events.content.comment.CommentCreateEvent;
+import com.atlassian.confluence.event.events.content.comment.CommentUpdateEvent;
+import com.atlassian.confluence.pages.Comment;
+import com.atlassian.event.api.EventPublisher;
+import com.hascode.confluence.plugin.socialcomments.service.RecipientExtractorService;
+import com.hascode.confluence.plugin.socialcomments.service.SocialNotificationService;
+@RunWith(MockitoJUnitRunner.class)
+public class CommentEventListenerTest {
+	@Mock
+	private EventPublisher				eventPublisher;
+
+	@Mock
+	private RecipientExtractorService	recipientExtractorService;
+
+	@Mock
+	private CommentCreateEvent			commentCreateEvent;
+
+	@Mock
+	private CommentUpdateEvent			commentUpdateEvent;
+
+	@Mock
+	private SocialNotificationService	socialNotificationService;
+
+	@Before
+	public void setUp() {
+		MockitoAnnotations.initMocks(this);
+	}
+
+	@Test
+	public void testHandleCommentCreateEvent() {
+		// stub
+
+		// init
+		final CommentEventListener listener = new CommentEventListenerImpl(eventPublisher, recipientExtractorService, socialNotificationService);
+
+		// test
+		listener.handleCommentCreateEvent(commentCreateEvent);
+
+		// spy
+		verify(eventPublisher, times(1)).register(any(CommentEventListener.class));
+		verify(recipientExtractorService, times(1)).extractUserFromComment(any(Comment.class));
+	}
+
+	@Test
+	public void testHandleCommentUpdateEvent() {
+		// stub
+
+		// init
+		final CommentEventListener listener = new CommentEventListenerImpl(eventPublisher, recipientExtractorService, socialNotificationService);
+
+		// test
+		listener.handleCommentUpdateEvent(commentUpdateEvent);
+
+		// spy
+		verify(eventPublisher, times(1)).register(any(CommentEventListener.class));
+		verify(recipientExtractorService, times(1)).extractUserFromComment(any(Comment.class));
+	}
+}

src/test/java/com/hascode/confluence/plugin/socialcomments/service/RecipientExtractorServiceTest.java

+package com.hascode.confluence.plugin.socialcomments.service;
+
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Set;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import com.atlassian.confluence.pages.Comment;
+import com.atlassian.confluence.user.UserAccessor;
+import com.atlassian.user.User;
+@RunWith(MockitoJUnitRunner.class)
+public class RecipientExtractorServiceTest {
+	private static final String	COMMENT_TEXT	= "This is a really important information!\n@admin: you should notice this! @tester: - hey check out this comment .. @tim you won't receive this @nonexisting: you do not exist - sorry ...";
+
+	@Mock
+	private UserAccessor		userAccessor;
+
+	@Mock
+	private Comment				comment;
+
+	@Mock
+	private User				admin;
+
+	@Mock
+	private User				user;
+
+	@Before
+	public void setUp() {
+		MockitoAnnotations.initMocks(this);
+	}
+
+	@Test
+	public void testExtractUserFromComment() {
+		// stub
+		when(comment.getContent()).thenReturn(COMMENT_TEXT);
+		when(userAccessor.getUser("admin")).thenReturn(admin);
+		when(userAccessor.getUser("tester")).thenReturn(user);
+
+		// init
+		final RecipientExtractorService service = new RecipientExtractorServiceImpl(userAccessor);
+
+		// test
+		Set<User> recipients = service.extractUserFromComment(comment);
+		Assert.assertNotNull(recipients);
+		Assert.assertEquals(2, recipients.size());
+
+		// spy
+		verify(userAccessor, times(3)).getUser(anyString());
+	}
+}

src/test/java/com/hascode/confluence/plugin/socialcomments/service/SocialNotificationServiceTest.java

+package com.hascode.confluence.plugin.socialcomments.service;
+
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import com.atlassian.confluence.mail.template.ConfluenceMailQueueItem;
+import com.atlassian.confluence.pages.Comment;
+import com.atlassian.confluence.setup.settings.Settings;
+import com.atlassian.confluence.setup.settings.SettingsManager;
+import com.atlassian.user.User;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(ConfluenceMailQueueItem.class)
+public class SocialNotificationServiceTest {
+	@Mock
+	private SettingsManager	settingsManager;
+
+	@Mock
+	private User			user;
+
+	@Mock
+	private Comment			comment;
+
+	@Mock
+	private Settings		settings;
+
+	@Before
+	public void setUp() {
+		MockitoAnnotations.initMocks(this);
+	}
+
+	@Ignore("tbd")
+	@Test
+	public void testNotifyUser() {
+		// stub
+		when(comment.getUrlPath()).thenReturn("/somecommentulrstring");
+		when(user.getFullName()).thenReturn("I R admin");
+		when(user.getEmail()).thenReturn("tester@hascode.com");
+		when(settingsManager.getGlobalSettings()).thenReturn(settings);
+		when(settings.getBaseUrl()).thenReturn("http://localhost:8080/");
+		PowerMockito.mock(ConfluenceMailQueueItem.class);
+
+		// init
+		final SocialNotificationService service = new SocialNotificationServiceImpl(settingsManager);
+
+		// test
+		service.notifyUser(user, comment);
+
+		// spy
+	}
+}

src/test/resources/log4j.properties

+log4j.rootLogger=debug, stdout
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n