diff --git a/.gitignore b/.gitignore index 928f9c5195..858a538de4 100644 --- a/.gitignore +++ b/.gitignore @@ -17,10 +17,9 @@ *.ipr *.iws *.war -*.jar #log files -*.log +*.log* # Netbeans project files nbproject @@ -29,12 +28,8 @@ nbproject target logs -/jcommune-view/jcommune-web-controller/test-output -/jcommune-model/test-output -/jcommune-service/test-output -/jcommune-view/jcommune-web-view/test-output -/jcommune-view/jcommune-web-controller/bin -/jcommune-view/jcommune-web-view/bin +test-output +bin /jcommune-view/jcommune-web-view/src/main/webapp/resources/wro -/jcommune-service/bin -/jcommune-model/bin +dependency-reduced-pom.xml +pom.xml.versionsBackup diff --git a/README.md b/README.md index 08d1a39faa..db32c7c92b 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,12 @@ JCommune is a part of [JTalks](http://jtalks.org) ecosystem of projects which re * [Installation Guide](docs/installation/general-installation-guide.md) is to be read if you want just to try the app or you want to install it locally * You can find live production instance: [JavaTalks Forum](http://javatalks.ru) +* If you want to install plugins, read [plugins page](jcommune-plugins/README.md) For developers/QA/PMs/Leads: * [How to join us](docs/how-to-join-us.md) * [Sonar](http://sonar.jtalks.org/dashboard/index/1) -* [Deployment Pipeline](http://ci.jtalks.org/view/JCommune.%20Pipeline) +* [Deployment Pipeline](http://ci.jtalks.org/view/JC.%20Pipeline/) * [Stack of Technologies](http://wiki.jtalks.org/display/jtalks/Stack+of+technologies) * [Commit Conventions](docs/commit-conventions.md) * [Code Conventions](http://wiki.jtalks.org/display/jtalks/Code+Conventions) @@ -16,7 +17,6 @@ For developers/QA/PMs/Leads: ####State of Project - Questions'n'Answers plugins is under development -- Plugin API to be worked on soon - First the project was connected to Poulpe a lot - everything was administrated via admin panel. Now we think that Poulpe should manage only Users and Groups and thus the project is in transition from "configuration in Poulpe" state to "configuration in JCommune itself" state. diff --git a/docs/installation/general-installation-guide.md b/docs/installation/general-installation-guide.md index e867c0ff48..0c7fc7e897 100644 --- a/docs/installation/general-installation-guide.md +++ b/docs/installation/general-installation-guide.md @@ -23,3 +23,22 @@ So if you want to run the app locally (from binaries or sources), you need to in - Clone the project: `git@github.com:jtalks-org/jcommune.git`. Now you can work with that project from your IDE. - If you want to deploy it from command line.. Step into the folder and build it: `mvn clean package` - Repeat everything from _Installing from Binaries_ but instead of downloading war-file, just grab it from `jcommune/jcommune-view/jcommunew-web-view/target` + +####Advanced configuration of MySQL Server + - Make sure that you are using MySQL Server ver. 5.7.9 or higher. Edit the my.cnf file (my.ini on Windows operating systems) in your MySQL server. The configuration file may be located on one of the following paths (in priority order): + WINDOWS: + - %PROGRAMDATA%\MySQL\MySQL Server X.X\my.ini (my.cnf) + - %WINDIR%\my.ini (my.cnf) + - C:\my.ini (my.cnf) + - INSTALLDIR\my.ini (my.cnf) + Unix, Linux и OS X: + - /etc/my.cnf + - /etc/mysql/my.cnf + - SYSCONFDIR/my.cnf + - Locate the [mysqld] section in the file, and add or modify the following parameters: + [mysqld] + character-set-client-handshake = FALSE + character-set-server = utf8mb4 + collation-server = utf8mb4_unicode_ci + - Without making this changes symbols from UTF8MB4 character set will be displayed like '??'. + - Restart your MySQL server for the changes to take effect. \ No newline at end of file diff --git a/docs/installation/jcommune.xml b/docs/installation/jcommune.xml index aff2e2a1dc..9f6f9c7715 100644 --- a/docs/installation/jcommune.xml +++ b/docs/installation/jcommune.xml @@ -13,16 +13,21 @@ - + - - - + + + + + + + + diff --git a/docs/installation/linux/basic-environment.md b/docs/installation/linux/basic-environment.md index ef6d65ff39..2969616851 100644 --- a/docs/installation/linux/basic-environment.md +++ b/docs/installation/linux/basic-environment.md @@ -6,9 +6,10 @@ Java: - Add it to your PATH: `PATH=$JAVA_HOME/bin:$PATH` MySQL: - - Install it: `sudo apt-get install mysql-server-5.5` + - Install it: `sudo apt-get install mysql-server-5.7` - Log into MySQL terminal, it may look like `mysql -uroot -proot` - - Create database: `create database jtalks character set utf8` + - Create database: `create database jtalks character set utf8mb4` + - NB: some functionality (like posting extended unicode symbols e.g. smiles) is not going to work with default MySQL configuration, read details in the [Advanced configuration of MySQL Server](https://github.com/jtalks-org/jcommune/blob/master/docs/installation/general-installation-guide.md#advanced-configuration-of-mysql-server) Tomcat: - Download it: `wget http://apache.softded.ru/tomcat/tomcat-7/v7.0.35/bin/apache-tomcat-7.0.35.zip` diff --git a/docs/installation/windows/basic-environment.md b/docs/installation/windows/basic-environment.md index 8337d8b8cb..3b2243a6e6 100644 --- a/docs/installation/windows/basic-environment.md +++ b/docs/installation/windows/basic-environment.md @@ -7,7 +7,9 @@ Java: MySQL: - [Install latest version](http://dev.mysql.com/downloads/mysql) - Log into MySQL terminal, it may look like `mysql -uroot -proot` - - Create database: `create database jtalks character set utf8` + - Create database: `create database jtalks character set utf8mb4` + - NB: some functionality (like posting extended unicode symbols e.g. smiles) is not going to work with default MySQL configuration, read details in the [Advanced configuration of MySQL Server](https://github.com/jtalks-org/jcommune/blob/master/docs/installation/general-installation-guide.md#advanced-configuration-of-mysql-server) + Tomcat: - Download it: `wget http://apache.softded.ru/tomcat/tomcat-7/v7.0.35/bin/apache-tomcat-7.0.35.zip` diff --git a/jcommune-model/pom.xml b/jcommune-model/pom.xml index 80d3d54faa..4b57564712 100644 --- a/jcommune-model/pom.xml +++ b/jcommune-model/pom.xml @@ -4,7 +4,7 @@ jcommune org.jtalks.jcommune - 2.14-SNAPSHOT + 3.13-SNAPSHOT jcommune-model ${project.artifactId} @@ -22,10 +22,6 @@ org.jtalks.common jtalks-common-service - - org.jtalks.common - jtalks-common-security - org.springframework spring-web @@ -129,10 +125,6 @@ net.sf.ehcache ehcache-core - - net.sf.ehcache - ehcache-jgroupsreplication - @@ -144,10 +136,18 @@ com.googlecode.flyway flyway-core + + org.apache.commons + commons-lang3 + com.googlecode.lambdaj lambdaj + + io.qala.datagen + qala-datagen + diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/GroupDao.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/GroupDao.java index a3bd1bb1c4..50c0e24b5e 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/GroupDao.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/GroupDao.java @@ -15,13 +15,15 @@ package org.jtalks.jcommune.model.dao; import org.jtalks.common.model.entity.Group; +import org.jtalks.jcommune.model.dto.GroupAdministrationDto; +import org.jtalks.jcommune.model.dto.PageRequest; +import org.jtalks.jcommune.model.dto.UserDto; import java.util.List; -import java.util.Set; /** * Data access object for manipulating groups - * + * * @author Mikhail Stryzhonok */ public interface GroupDao extends org.jtalks.common.model.dao.GroupDao { @@ -32,4 +34,38 @@ public interface GroupDao extends org.jtalks.common.model.dao.GroupDao { * @return the list of found groups */ List getGroupsByIds(List ids); + + /** + * Get the list of all groups. + * + * @return list of groups + */ + List getAll(); + + /** + * Get the list of all groups which names contains the specified name. + * + * @param name group name + * @return list of groups + * @throws IllegalArgumentException if name is null + */ + List getByNameContains(String name); + + /** + * Get the list of all groups which name matches ignoring case the specified name. + * + * @param name group name + * @return list of groups + * @throws IllegalArgumentException if name is null + */ + List getByName(String name); + + /** + * @return list of GroupAdministrationDto + */ + List getGroupNamesWithCountOfUsers(); + + List getGroupUsersPage(long id, PageRequest pageRequest); + + int getGroupUserCount(long id); } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/SpamRuleDao.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/SpamRuleDao.java new file mode 100644 index 0000000000..72242b0a9d --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/SpamRuleDao.java @@ -0,0 +1,30 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.model.dao; + + +import org.jtalks.common.model.dao.Crud; +import org.jtalks.jcommune.model.entity.SpamRule; + +import java.util.List; + +/** + * @author Oleg Tkachenko + */ +public interface SpamRuleDao extends Crud { + List getAllRules(); + List getEnabledRules(); +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/TopicDraftDao.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/TopicDraftDao.java new file mode 100644 index 0000000000..f841a3f8e2 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/TopicDraftDao.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.dao; + +import org.jtalks.common.model.dao.Crud; +import org.jtalks.jcommune.model.dao.hibernate.TopicDraftHibernateDao; +import org.jtalks.jcommune.model.entity.TopicDraft; +import org.jtalks.jcommune.model.entity.JCUser; + +/** + * DAO for {@link TopicDraft} objects + * + * @author Dmitry S. Dolzhenko + * @see TopicDraftHibernateDao + */ +public interface TopicDraftDao extends Crud { + /** + * Get a draft topic of specified user + * + * @param user the user + * @return the draft topic or null + */ + TopicDraft getForUser(JCUser user); + + /** + * Delete the draft topic of specified user + * @param user the user + */ + void deleteByUser(JCUser user); +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/UserDao.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/UserDao.java index f407c4a3eb..ac919ff46e 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/UserDao.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/UserDao.java @@ -14,7 +14,9 @@ */ package org.jtalks.jcommune.model.dao; +import org.hibernate.ObjectNotFoundException; import org.jtalks.common.model.entity.User; +import org.jtalks.jcommune.model.dto.UserDto; import org.jtalks.jcommune.model.entity.JCUser; import java.util.Collection; @@ -97,4 +99,23 @@ public interface UserDao extends org.jtalks.common.model.dao.UserDao { * @return the list of found user names */ List getUsernames(String pattern, int count); + + /** + * Gets list of users by part of username or email + * + * @param pattern part of username (case insensitive) + * @param count max count of users in result + * @return list of found users + */ + List findByUsernameOrEmail(String pattern, int count); + + /** + * May return a proxy (without hitting DB). If no row found the object is returned but it + * throws {@link ObjectNotFoundException} when properties are first accessed. + * @param id stored user identifier. + * @return proxy of JCUser object. + */ + JCUser loadById(Long id); + + List findByUsernameOrEmailNotInGroup(String pattern, long groupId, int count); } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/GroupHibernateDao.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/GroupHibernateDao.java index 794023516e..c35fd4da19 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/GroupHibernateDao.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/GroupHibernateDao.java @@ -20,10 +20,12 @@ import org.hibernate.SessionFactory; import org.jtalks.common.model.dao.hibernate.GenericDao; import org.jtalks.common.model.entity.Group; - import org.jtalks.common.model.entity.User; import org.jtalks.jcommune.model.dao.GroupDao; import org.jtalks.jcommune.model.dao.utils.SqlLikeEscaper; +import org.jtalks.jcommune.model.dto.GroupAdministrationDto; +import org.jtalks.jcommune.model.dto.PageRequest; +import org.jtalks.jcommune.model.dto.UserDto; import ru.javatalks.utils.general.Assert; import java.util.List; @@ -36,6 +38,9 @@ * @author Leonid Kazancev */ public class GroupHibernateDao extends GenericDao implements GroupDao { + private static final String FIND_GROUP_BY_NAME = "findGroupByName", FIND_ALL_GROUPS = "findAllGroups"; + private static final String FIND_EXACTLY_BY_NAME = "findGroupExactlyByName"; + /** * @param sessionFactory The SessionFactory. */ @@ -112,4 +117,56 @@ public List getGroupsByIds(List ids) { return (List)session().getNamedQuery("getGroupsByIds") .setParameterList("ids", ids).list(); } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @Override + public List getByNameContains(String name) { + Validate.notNull(name, "User Group name can't be null"); + if (org.apache.commons.lang3.StringUtils.isBlank(name)) { + return this.getAll(); + } + Query query = session().getNamedQuery(FIND_GROUP_BY_NAME); + query.setString("name", SqlLikeEscaper.escapeControlCharacters(name)); + return query.list(); + } + + /** + * {@inheritDoc} + */ + @Override + public List getByName(String name) { + Validate.notNull(name, "User Group name can't be null"); + Query query = session().getNamedQuery(FIND_EXACTLY_BY_NAME); + // we should use lower case to search ignoring case + query.setString("name", name); + return query.list(); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @Override + public List getGroupNamesWithCountOfUsers() { + return (List) session().getNamedQuery("selectGroupsWithUserCount").list(); + } + + @Override + public List getGroupUsersPage(long id, PageRequest pageRequest) { + Query query = session().getNamedQuery("getUserDTOListFromGroupById"); + query.setParameter("id", id); + query.setFirstResult(pageRequest.getOffset()).setMaxResults(pageRequest.getPageSize()); + return query.list(); + } + + @Override + public int getGroupUserCount(long id) { + Query query = session().getNamedQuery("getCountUsersInGroup"); + query.setParameter("id", id); + Number count = (Number) query.uniqueResult(); + return count.intValue(); + } } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/SpamRuleHibernateDao.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/SpamRuleHibernateDao.java new file mode 100644 index 0000000000..97d73d4d0d --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/SpamRuleHibernateDao.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.model.dao.hibernate; + +import org.hibernate.SessionFactory; +import org.jtalks.common.model.dao.hibernate.GenericDao; +import org.jtalks.jcommune.model.dao.SpamRuleDao; +import org.jtalks.jcommune.model.entity.SpamRule; + +import java.util.List; + +/** + * @author Oleg Tkachenko + */ +@SuppressWarnings("unchecked") +public class SpamRuleHibernateDao extends GenericDao implements SpamRuleDao { + public SpamRuleHibernateDao(SessionFactory sessionFactory) { + super(sessionFactory, SpamRule.class); + } + + @Override + public List getAllRules() { + return session().createCriteria(SpamRule.class).setCacheable(true).list(); + } + + @Override + public List getEnabledRules() { + return session().getNamedQuery("getEnabledSpamRules").list(); + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/TopicDraftHibernateDao.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/TopicDraftHibernateDao.java new file mode 100644 index 0000000000..6e21d6841b --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/TopicDraftHibernateDao.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.dao.hibernate; + +import org.hibernate.Query; +import org.hibernate.SessionFactory; +import org.jtalks.common.model.dao.hibernate.GenericDao; +import org.jtalks.jcommune.model.dao.TopicDraftDao; +import org.jtalks.jcommune.model.entity.TopicDraft; +import org.jtalks.jcommune.model.entity.JCUser; + +/** + * Hibernate DAO implementation for {@link TopicDraft} + * + * @author Dmitry S. Dolzhenko + */ +public class TopicDraftHibernateDao extends GenericDao implements TopicDraftDao { + + public TopicDraftHibernateDao(SessionFactory sessionFactory) { + super(sessionFactory, TopicDraft.class); + } + + /** + * {@inheritDoc} + */ + @Override + public TopicDraft getForUser(JCUser user) { + Query query = session().getNamedQuery("getForUser") + .setEntity("user", user); + return (TopicDraft) query.uniqueResult(); + } + + /** + * {@inheritDoc} + */ + @Override + public void deleteByUser(JCUser user) { + session().getNamedQuery("deleteByUser") + .setEntity("user", user) + .executeUpdate(); + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/UserHibernateDao.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/UserHibernateDao.java index 4f15109f48..0fabaa9e2b 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/UserHibernateDao.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dao/hibernate/UserHibernateDao.java @@ -20,6 +20,7 @@ import org.jtalks.common.model.entity.User; import org.jtalks.jcommune.model.dao.UserDao; import org.jtalks.jcommune.model.dao.utils.SqlLikeEscaper; +import org.jtalks.jcommune.model.dto.UserDto; import org.jtalks.jcommune.model.entity.JCUser; import java.util.Collection; @@ -129,4 +130,41 @@ public List getUsernames(String pattern, int count) { .setMaxResults(count) .list(); } + + @SuppressWarnings("unchecked") + @Override + public List findByUsernameOrEmail(String pattern, int count) { + pattern = SqlLikeEscaper.escapeControlCharacters(pattern).toLowerCase(); + return session().getNamedQuery("searchByEmailOrUsername") + .setParameter("pattern", "%" + pattern + "%") + .setParameter("exactMatch", pattern) + .setParameter("primaryPattern", pattern + "%") + .setParameter("secondaryPattern", "%" + pattern + "%") + .setParameter("thirdaryPattern", "%" + pattern) + .setMaxResults(count) + .list(); + } + + /** + * {@inheritDoc} + */ + @Override + public JCUser loadById(Long id) { + return (JCUser) session().load(JCUser.class, id); + } + + @SuppressWarnings("unchecked") + @Override + public List findByUsernameOrEmailNotInGroup(String pattern, long groupId, int count) { + pattern = SqlLikeEscaper.escapeControlCharacters(pattern).toLowerCase(); + return session().getNamedQuery("searchByEmailOrUsernameNotInGroupId") + .setParameter("groupId", groupId) + .setParameter("pattern", "%" + pattern + "%") + .setParameter("exactMatch", pattern) + .setParameter("primaryPattern", pattern + "%") + .setParameter("secondaryPattern", "%" + pattern + "%") + .setParameter("thirdaryPattern", "%" + pattern) + .setMaxResults(count) + .list(); + } } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/AnonymousGroup.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/AnonymousGroup.java new file mode 100644 index 0000000000..f374309b63 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/AnonymousGroup.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.dto; + +import org.jtalks.common.model.entity.Group; + +/** + * Represents Anonymous Group for permissions granting by UI. This class contains only {@code ANONYMOUS_GROUP} field. + * Application doesn't store {@code ANONYMOUS_GROUP} in data base. + */ +public final class AnonymousGroup extends Group { + public static final Group ANONYMOUS_GROUP = new AnonymousGroup("Anonymous Users"); + + /** + * @param name group name + */ + private AnonymousGroup(String name) { + super(name); + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/GroupAdministrationDto.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/GroupAdministrationDto.java new file mode 100644 index 0000000000..2283f32a4d --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/GroupAdministrationDto.java @@ -0,0 +1,111 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.dto; + +import org.hibernate.validator.constraints.Length; +import org.jtalks.common.model.entity.Group; +import org.jtalks.jcommune.model.validation.annotations.Unique; + +/** + * DTO class for user group containing name and number of users in the group + * + * @author Oleg Tkachenko + */ + +public class GroupAdministrationDto { + public static final int GROUP_NAME_MIN_LENGTH = 1; + + /** + * Contains org.jtalks.common.model.entity.Group.id in case of group modification. + * Contains null if new group should be created. + */ + private Long id; + + @Length(min = GROUP_NAME_MIN_LENGTH, max = Group.GROUP_NAME_MAX_LENGTH, message = "{group.name.illegal_length}") + private String name; + + @Length(max = Group.GROUP_DESCRIPTION_MAX_LENGTH, message = "{group.description.illegal_length}") + private String description; + + private long numberOfUsers; + private boolean editable; + + public GroupAdministrationDto(){} + + public GroupAdministrationDto(String name, int count) { + setName(name); + setNumberOfUsers(count); + } + + public GroupAdministrationDto(Long id, String name, String description, int count) { + setId(id); + setName(name); + setDescription(description); + setNumberOfUsers(count); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name.trim(); + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public long getNumberOfUsers() { + return numberOfUsers; + } + + public void setNumberOfUsers(long numberOfUsers) { + this.numberOfUsers = numberOfUsers; + } + + public boolean isEditable() { + //TODO + // Implement logic that evaluates is group editable. Something like this: + // Arrays.asList("Administrators", "Registered Users", "Banned Users").contains(name) + return editable; + } + + public void setEditable(boolean editable) { + this.editable = editable; + } + + /** + * Copy group name and description to the provided Group. + * @return modified group + */ + public Group fillEntity(Group group) { + group.setName(name); + group.setDescription(description); + return group; + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/PageRequest.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/PageRequest.java index 8f60923e6b..62f1675c3e 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/PageRequest.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/PageRequest.java @@ -42,16 +42,15 @@ public class PageRequest implements Pageable { * @param pageSize size of page */ public PageRequest(String requestedPageNumber, int pageSize) { - this.pageNumber = preparePageNumber(requestedPageNumber); - this.pageSize = preparePageSize(pageSize); + this(preparePageNumber(requestedPageNumber), preparePageSize(pageSize)); } - private PageRequest(int pageNumber, int pageSize) { + public PageRequest(int pageNumber, int pageSize) { this.pageNumber = pageNumber; this.pageSize = pageSize; } - private int preparePageSize(int pageSize) { + private static int preparePageSize(int pageSize) { return (pageSize <= 0) ? DEFAULT_PAGE_SIZE : pageSize; } @@ -64,7 +63,7 @@ private int preparePageSize(int pageSize) { * {@link Integer#MAX_VALUE} if specified string is greater than {@link PageRequest#MAX_PAGE}
* {@link PageRequest#FIRST_PAGE_NUMBER} if specified string is not a number */ - private int preparePageNumber(String pageNumber) { + private static int preparePageNumber(String pageNumber) { int result; pageNumber = pageNumber.replaceFirst("^0+(?!$)", "");//removing trailing zeroes if (pageNumber.matches("\\d{1,9}")) { diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/SecurityGroupList.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/SecurityGroupList.java new file mode 100644 index 0000000000..15e19860bf --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/SecurityGroupList.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.dto; + +import org.jtalks.common.model.entity.Group; + +import java.util.ArrayList; +import java.util.List; + +/** + * While in all the places we work only with persistent groups, when it comes to security, we also have additional + * 'groups' that do not exist as persisted unit - groups like Anonymous Users. It is impossible to add or edit members + * of that group thus it doesn't make sense to create one, but we need to be able to set permissions for anonymous + * users, thus we need to present them.

This class is a list that can represent both persistent and 'special' + * groups. + * + * @author stanislav bashkirtsev + */ +public class SecurityGroupList { + private final List allGroups; + + /** Creates empty group list */ + public SecurityGroupList() { + this(new ArrayList()); + } + + /** Creates and fill group list another {@link List} + * @param allGroups list of groups + */ + public SecurityGroupList(List allGroups) { + this.allGroups = new ArrayList(allGroups); + } + + /** Adds predefined {@link AnonymousGroup} to this list + * @return {@code this} + */ + public SecurityGroupList withAnonymousGroup() { + if (!containsAnonymousGroup()) { + allGroups.add(AnonymousGroup.ANONYMOUS_GROUP); + } + return this; + } + + /** Removes predefined {@link AnonymousGroup} from this list + * @return {@link AnonymousGroup} - if group was removed, {@code null} - otherwise + */ + public Group removeAnonymousGroup() { + if (allGroups.remove(AnonymousGroup.ANONYMOUS_GROUP)) { + return AnonymousGroup.ANONYMOUS_GROUP; + } + return null; + } + + /** Checks predefined {@link AnonymousGroup} in this list + * @return {@link AnonymousGroup} - if group included in the list, {@code null} - otherwise + */ + public Group getAnonymousGroup() { + if (containsAnonymousGroup()) { + return AnonymousGroup.ANONYMOUS_GROUP; + } + return null; + } + + /** @return current group list as {@link List} */ + public List getAllGroups() { + return allGroups; + } + + /** Checks predefined {@link AnonymousGroup} in this list + * @return {@code true} - if list contains {@link AnonymousGroup}, {@code false} - otherwise + */ + public boolean containsAnonymousGroup() { + return allGroups.contains(AnonymousGroup.ANONYMOUS_GROUP); + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/SpamRuleDto.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/SpamRuleDto.java new file mode 100644 index 0000000000..c145c52f67 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/SpamRuleDto.java @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.model.dto; + +import org.hibernate.validator.constraints.Length; +import org.jtalks.jcommune.model.entity.SpamRule; + +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Oleg Tkachenko + */ +public class SpamRuleDto { + private long id; + @NotNull + @Length(min = 1, max = 255, message = "{spam.regex.illegal_length}") + private String regex; + @Length(max = 255, message = "{spam.description.illegal_length}") + private String description; + private boolean enabled; + + protected SpamRuleDto() { + } + + public SpamRuleDto(long id, String regex, String description, boolean enabled) { + this.id = id; + this.regex = regex; + this.description = description; + this.enabled = enabled; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getRegex() { + return regex; + } + + public SpamRuleDto setRegex(String regex) { + this.regex = regex; + return this; + } + + public String getDescription() { + return description; + } + + public SpamRuleDto setDescription(String description) { + this.description = description; + return this; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public static SpamRuleDto fromEntity(SpamRule entity) { + if (entity == null) return null; + return new SpamRuleDto(entity.getId(), entity.getRegex(), entity.getDescription(), entity.isEnabled()); + } + + public SpamRule toEntity() { + return new SpamRule(this.getRegex(), this.getDescription(), this.isEnabled()); + } + + public static List fromEntities(List original) { + List dtoList = new ArrayList<>(original.size()); + for (SpamRule spamRule : original) { + dtoList.add(SpamRuleDto.fromEntity(spamRule)); + } + return dtoList; + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/UserDto.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/UserDto.java index 5c6deae8b6..5ccad82b9d 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/UserDto.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/dto/UserDto.java @@ -24,8 +24,7 @@ import org.jtalks.jcommune.model.validation.annotations.Unique; import javax.validation.constraints.Size; -import java.util.HashMap; -import java.util.Map; +import java.util.*; /** * Contains user information. In some cases serves for transmitting to external services. @@ -34,6 +33,8 @@ */ public class UserDto { + private long id; + @Size(min = User.USERNAME_MIN_LENGTH, max = User.USERNAME_MAX_LENGTH, message = "{user.username.length_constraint_violation}") @Unique(entity = JCUser.class, field = "username", message = "{user.username.already_exists}", ignoreCase = true) @@ -53,6 +54,29 @@ public class UserDto { private Map captchas = new HashMap<>(); + public UserDto() { + } + + public UserDto(User user) { + this.username = user.getUsername(); + this.email = user.getEmail(); + this.id = user.getId(); + } + + public UserDto(String username, String email, Long id) { + this.username = username; + this.email = email; + this.id = id; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + /** * @return username */ @@ -132,4 +156,20 @@ public Map getCaptchas() { public void setCaptchas(Map captchas) { this.captchas = captchas; } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserDto userDto = (UserDto) o; + + return id == userDto.id; + } + + @Override + public int hashCode() { + return (int) (id ^ (id >>> 32)); + } } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Branch.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Branch.java index 3fb8e3f936..4748558f81 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Branch.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Branch.java @@ -14,14 +14,14 @@ */ package org.jtalks.jcommune.model.entity; -import ch.lambdaj.Lambda; -import org.apache.commons.lang.Validate; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import ch.lambdaj.Lambda; +import org.apache.commons.lang.Validate; + /** * Forum branch that contains topics related to branch theme. * @@ -38,8 +38,8 @@ public class Branch extends org.jtalks.common.model.entity.Branch public static final int BRANCH_DESCRIPTION_MAX_LENGTH = 255; public static final String URL_SUFFIX = "/branches/"; - private List topics = new ArrayList(); - private Set subscribers = new HashSet(); + private List topics = new ArrayList<>(); + private Set subscribers = new HashSet<>(); private Integer topicsCount; private Integer postsCount; @@ -172,6 +172,7 @@ public int getPostCount() { /** * {@inheritDoc} */ + @Override public Set getSubscribers() { return subscribers; } @@ -190,10 +191,21 @@ public void setSubscribers(Set subscribers) { * The target URL has the next format http://{forum root}/branches/{id} */ @Override - public String prepareUrlSuffix() { + public String getUrlSuffix() { return URL_SUFFIX + getId(); } + /** + * {@inheritDoc} + */ + @Override + public String getUnsubscribeLinkForSubscribersOf(Class clazz) { + if (Branch.class.isAssignableFrom(clazz)) { + return String.format("/branches/%s/unsubscribe", getId()); + } + return null; + } + /** * Set count of topics in this branch. * @@ -242,8 +254,7 @@ public Post getLastPost() { /** * Set last post in this branch. - * - * @return last post in this branch + * */ public void setLastPost(Post lastPost) { this.lastPost = lastPost; diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/ComponentInformation.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/ComponentInformation.java index e8d02f544a..eced3a7012 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/ComponentInformation.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/ComponentInformation.java @@ -16,6 +16,7 @@ package org.jtalks.jcommune.model.entity; import org.jtalks.common.model.entity.Component; +import org.jtalks.jcommune.model.validation.annotations.IntegerRange; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; @@ -27,6 +28,8 @@ */ public class ComponentInformation { private static final int PARAM_MIN_SIZE = 1; + private static final int SESSION_MAX_TIMEOUT = 1440; + private static final int AVATAR_MAX_SIZE = 1048576; private Long id; @@ -54,6 +57,16 @@ public class ComponentInformation { private String icon; + @NotNull(message = "{validation.not_null}") + @IntegerRange(min = 0, max = SESSION_MAX_TIMEOUT) + private String sessionTimeout; + + @NotNull(message = "{validation.not_null}") + @IntegerRange(min = 0, max = AVATAR_MAX_SIZE) + private String avatarMaxSize; + + @NotNull(message = "{validation.not_null}") + private boolean emailNotification; /** * Gets the string with encoded logo picture @@ -185,6 +198,28 @@ public String getCopyright() { public void setCopyright(String copyright) { this.copyright = copyright; } - - + + public String getSessionTimeout() { + return sessionTimeout; + } + + public void setSessionTimeout(String sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + + public String getAvatarMaxSize() { + return avatarMaxSize; + } + + public void setAvatarMaxSize(String avatarMaxSize) { + this.avatarMaxSize = avatarMaxSize; + } + + public boolean isEmailNotification() { + return emailNotification; + } + + public void setEmailNotification(boolean emailNotification) { + this.emailNotification = emailNotification; + } } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/JCUser.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/JCUser.java index 6239edeb55..aaeb426d8b 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/JCUser.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/JCUser.java @@ -19,7 +19,12 @@ import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.entity.User; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -51,7 +56,18 @@ public class JCUser extends User { public static final int[] PAGE_SIZES_AVAILABLE = new int[]{15, 25, 50}; private static final long serialVersionUID = 19981017L; - private Set contacts = new HashSet<>(); + + /** + * The {@link org.jtalks.jcommune.model.entity.JCUser} uses serialization for saving own state between + * Tomcat' session restarts. But there is not urgent needs to save state of + * the {@link org.jtalks.jcommune.model.entity.UserContact}, and moreover serialization of this one + * will pull serialization of even more classes which is undesirable. + * + * While we won't serialize full {@link org.jtalks.jcommune.model.entity.UserContact} entity + * we mark {@link org.jtalks.jcommune.model.entity.JCUser#contacts} as transient to avoid the problems + * during serialization of the {@link org.jtalks.common.model.entity.User} and his successors. + */ + private transient Set contacts = new HashSet<>(); private DateTime avatarLastModificationTime = new DateTime(System.currentTimeMillis()); @@ -130,6 +146,10 @@ public void setPostCount(int postCount) { * @return user language */ public Language getLanguage() { + // add verification if language is not SPANISH. if it is SPANISH, language changed to default ENGLISH + if (language.equals(Language.SPANISH)) { + language = Language.ENGLISH; + } return language; } @@ -288,7 +308,7 @@ public void setSendPmNotification(boolean sendPmNotification) { } /** - * @return last modification time of avatar + * @return last modification time of avatar or {@code null} if it's the default avatar that has never changed */ public DateTime getAvatarLastModificationTime() { return avatarLastModificationTime; @@ -334,7 +354,36 @@ public JCUser addGroup(Group group) { } /** - * Creates copy of user needed in plugins. + * Delete a user from the group and remove group from the user. + * + * @param group a group for delete + * @return this + */ + public JCUser deleteGroup(Group group) { + getGroups().remove(group); + group.getUsers().remove(this); + return this; + } + + /** + * Get only IDs for user groups + * + * @return group IDs + */ + public List getGroupsIDs() { + List groupIDs = new ArrayList(); + List groups = getGroups(); + for (Group group : groups) { + groupIDs.add(group.getId()); + } + return groupIDs; + } + + /** + * Creates copy of user needed in plugins. Since TransactionalUserService.getCurrentUser() returns instance + * of JCUser stored in Security Context it is not possible to load contacts lazily, also User contacts are + * not used in this scope, so we just don't need it. But if you need user contacts you can setup fetch.MODE + * on contacts collection in JCUser.hbm.xml * @param user user to be copied */ public static JCUser copyUser(JCUser user) { @@ -365,12 +414,14 @@ public static JCUser copyUser(JCUser user) { copy.setAutosubscribe(user.isAutosubscribe()); copy.setMentioningNotificationsEnabled(user.isMentioningNotificationsEnabled()); copy.setSendPmNotification(user.isSendPmNotification()); - for (UserContact contact : user.getContacts()) { - copy.getContacts().add(copyUserContact(contact, copy)); - } copy.setAvatarLastModificationTime(user.getAvatarLastModificationTime()); copy.setAllForumMarkedAsReadTime(user.getAllForumMarkedAsReadTime()); copy.setUuid(user.getUuid()); + /* + for (UserContact contact : user.getContacts()) { + copy.getContacts().add(copyUserContact(contact, copy)); + } + */ return copy; } @@ -412,4 +463,51 @@ static Group copyUserGroup(Group group, JCUser user) { copy.getUsers().add(user); return copy; } + + /** + * Customized deserialization of the fields {@link org.jtalks.common.model.entity.Entity#id}, + * {@link org.jtalks.common.model.entity.Entity#uuid} + * + * Note: The {@link org.jtalks.common.model.entity.User#groups} is marked as transient and will not be serialized + * (for more details pls. see at JIRA). + * + * @serialData {@link org.jtalks.common.model.entity.Entity#id}, + * {@link org.jtalks.common.model.entity.Entity#uuid}, + * and the hole entities {@link org.jtalks.common.model.entity.User}, + * {@link org.jtalks.jcommune.model.entity.JCUser} + * expect for the transient fields {@link org.jtalks.common.model.entity.User#groups} and + * {@link org.jtalks.jcommune.model.entity.JCUser#contacts} + * @param s + * @throws IOException + * @throws ClassNotFoundException + */ + private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException { + s.defaultReadObject(); + long id = s.readLong(); + String uuid = (String)s.readObject(); + setId(id); + setUuid(uuid); + } + + /** + * Customized serialization of the fields {@link org.jtalks.common.model.entity.Entity#id}, + * {@link org.jtalks.common.model.entity.Entity#uuid} + * + * Note: The {@link org.jtalks.common.model.entity.User#groups} is marked as transient and will not be serialized + * (for more details pls. see at JIRA). + * + * @serialData {@link org.jtalks.common.model.entity.Entity#id}, + * {@link org.jtalks.common.model.entity.Entity#uuid}, + * and the hole entities {@link org.jtalks.common.model.entity.User}, + * {@link org.jtalks.jcommune.model.entity.JCUser} + * expect for the transient fields {@link org.jtalks.common.model.entity.User#groups} and + * {@link org.jtalks.jcommune.model.entity.JCUser#contacts} + * @param s + * @throws IOException + */ + private void writeObject(ObjectOutputStream s) throws IOException { + s.defaultWriteObject(); + s.writeLong(getId()); + s.writeObject(getUuid()); + } } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/JCommuneProperty.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/JCommuneProperty.java index 920e44eb56..3967253279 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/JCommuneProperty.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/JCommuneProperty.java @@ -103,8 +103,9 @@ public enum JCommuneProperty { /** title prefix - should be displayed at the beginning of the title of the every page */ ALL_PAGES_TITLE_PREFIX, - COPYRIGHT - ; + COPYRIGHT, + /** List of email domains restricted to register with*/ + EMAIL_DOMAINS_BLACK_LIST; private static final Logger LOGGER = LoggerFactory.getLogger(JCommuneProperty.class); diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Language.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Language.java index 72d73e305e..5e64052add 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Language.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Language.java @@ -23,7 +23,7 @@ public enum Language { //simple alphabetical order, no holywars please ENGLISH("label.english", "en"), - RUSSIAN("label.russian", "ru"), + RUSSIAN("label.russian", "ru"), SPANISH("label.spanish", "es"), UKRAINIAN("label.ukrainian", "uk"); diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/ObjectsFactory.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/ObjectsFactory.java index 3b81c60295..6665d0acd1 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/ObjectsFactory.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/ObjectsFactory.java @@ -14,9 +14,15 @@ */ package org.jtalks.jcommune.model.entity; -import org.apache.commons.lang.math.RandomUtils; -import org.jtalks.common.model.entity.*; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.RandomUtils; +import org.joda.time.DateTime; +import org.jtalks.common.model.entity.Group; +import org.jtalks.common.model.entity.Section; +import org.jtalks.common.model.entity.User; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @@ -40,7 +46,7 @@ public static JCUser getDefaultUser() { } public static JCUser getRandomUser() { - return getUser("username" + RandomUtils.nextInt(10000), RandomUtils.nextInt(10000) + "username@mail.com"); + return getUser("username" + RandomUtils.nextInt(0, 10000), RandomUtils.nextInt(0, 10000) + "username@mail.com"); } public static JCUser getUser(String username, String email) { @@ -50,6 +56,36 @@ public static JCUser getUser(String username, String email) { return newUser; } + public static JCUser getUserWithAllFieldsFilled() throws NoSuchMethodException, InvocationTargetException, + IllegalAccessException { + JCUser user = getDefaultUser(); + DateTime dateTime = new DateTime(); + user.setId(1); + user.setLanguage(Language.RUSSIAN); + user.setPageSize(1); + user.setLocation("location"); + user.setSignature("signature"); + user.setRegistrationDate(dateTime); + user.setEnabled(true); + user.setAutosubscribe(true); + user.setMentioningNotificationsEnabled(true); + user.setSendPmNotification(true); + user.getContacts().add(ObjectsFactory.getDefaultUserContact()); + user.setAvatarLastModificationTime(dateTime); + user.setAllForumMarkedAsReadTime(dateTime); + user.setAvatar(new byte[]{1}); + user.setVersion(1L); + user.setBanReason("Ban Reason"); + user.setRole("Role"); + Method setLastLogin = User.class.getDeclaredMethod("setLastLogin", DateTime.class); + Method setEncodedUsername = User.class.getDeclaredMethod("setEncodedUsername", String.class); + setLastLogin.setAccessible(true); + setEncodedUsername.setAccessible(true); + setLastLogin.invoke(user, new DateTime()); + setEncodedUsername.invoke(user, "Encoded Username"); + return user; + } + public static Branch getDefaultBranch() { return new Branch("branch name", "branch description"); } @@ -122,6 +158,19 @@ public static Topic getDefaultTopic() { return new Topic(getDefaultUser(), "title", "Discussion"); } + public static TopicDraft getDefaultTopicDraft() { + TopicDraft draft = new TopicDraft(getDefaultUser(), + RandomStringUtils.random(5), + RandomStringUtils.random(15)); + + draft.setPollTitle(RandomStringUtils.random(5)); + draft.setPollItemsValue(RandomStringUtils.random(5) + "\n" + RandomStringUtils.random(5)); + draft.setTopicType(TopicTypeName.DISCUSSION.getName()); + draft.setBranchId(1L); + + return draft; + } + public static Topic getTopic(JCUser author, int numberOfPosts) { Topic topic = new Topic(author, "some topic"); for (int i = 0; i < numberOfPosts; i++) { @@ -141,7 +190,6 @@ public static UserContactType getDefaultUserContactType() { type.setIcon("/some/icon"); type.setMask("12345"); type.setDisplayPattern("protocol://" + UserContactType.CONTACT_MASK_PLACEHOLDER); - type.setValidationPattern("\\d+"); return type; } @@ -208,11 +256,17 @@ public static List getBanners() { * @return group with random name and description */ public static Group getRandomGroup() { - return new Group("group" + RandomUtils.nextInt(10000), "description" + RandomUtils.nextInt(10000)); + return new Group("group" + RandomUtils.nextInt(0, 10000), "description" + RandomUtils.nextInt(0, 10000)); } public static PostVote getDefaultPostVote() { return new PostVote(getDefaultUser()); } + public static Post getPostWithComments() { + Post post = new Post(getDefaultUser(), "test"); + post.addComment(new PostComment()); + post.addComment(new PostComment()); + return post; + } } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PersistedObjectsFactory.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PersistedObjectsFactory.java index fa570729b0..475de77d60 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PersistedObjectsFactory.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PersistedObjectsFactory.java @@ -14,6 +14,7 @@ */ package org.jtalks.jcommune.model.entity; +import org.apache.commons.lang3.RandomStringUtils; import org.hibernate.Session; import org.joda.time.DateTime; import org.jtalks.common.model.entity.*; @@ -58,6 +59,17 @@ public static Topic getDefaultTopic() { return newTopic; } + public static TopicDraft getDefaultTopicDraft() { + JCUser user = persist(ObjectsFactory.getDefaultUser()); + Branch branch = persist(ObjectsFactory.getDefaultBranch()); + TopicDraft newTopicDraft = new TopicDraft(user, + RandomStringUtils.random(5), RandomStringUtils.random(15)); + newTopicDraft.setTopicType(TopicTypeName.DISCUSSION.getName()); + newTopicDraft.setBranchId(branch.getId()); + persist(newTopicDraft); + return newTopicDraft; + } + public static void createAndSaveViewTopicsBranchesEntity(Long branchId, String sid, Boolean granting) { ViewTopicsBranches viewTopicsBranches = new ViewTopicsBranches(); viewTopicsBranches.setBranchId(branchId); @@ -252,13 +264,24 @@ public static JCUser getDefaultUserWithGroups() { */ public static PostComment getDefaultPostComment() { PostComment comment2 = new PostComment(); - comment2.setAuthor(getUser("user2", "mail2@mail.ru")); + comment2.setAuthor(persist(ObjectsFactory.getRandomUser())); comment2.setBody("Comment2 body"); comment2.setCreationDate(new DateTime(2)); comment2.setPost(getDefaultPost()); return comment2; } + public static PostComment getModifiedPostComment() { + PostComment comment3 = new PostComment(); + comment3.setAuthor(persist(ObjectsFactory.getRandomUser())); + comment3.setBody("Comment3 body"); + comment3.setCreationDate(new DateTime(3)); + comment3.setPost(getDefaultPost()); + comment3.setUserChanged(persist(ObjectsFactory.getRandomUser())); + comment3.setModificationDate(new DateTime(4)); + return comment3; + } + public static PostVote getDefaultPostVote() { return new PostVote(getDefaultUser()); } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Post.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Post.java index ea614ef7a2..74173ceeb7 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Post.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Post.java @@ -37,8 +37,8 @@ * @author Anuar Nurmakanov */ -public class Post extends Entity { - +public class Post extends Entity implements SubscriptionAwareEntity { + public static final String URL_SUFFIX = "/posts/"; private DateTime creationDate; private DateTime modificationDate; private JCUser userCreated; @@ -128,6 +128,19 @@ public DateTime updateModificationDate() { this.modificationDate = new DateTime(); return this.modificationDate; } + + /** + * Updates creation date of post. Sets it to NOW + * + * We need this operation for drafts. We can't update modification date of draft because + * if we set it after posting draft it will be marked with "last modified" label + * + * @return new creation date of post + */ + public DateTime updateCreationDate() { + this.creationDate = new DateTime(); + return this.creationDate; + } /** * @return date and time where post what last time modified or created otherwise @@ -188,13 +201,6 @@ public void setTopic(Topic topic) { this.topic = topic; } - /** - * @return subscribers of topic of this post - */ - public Set getTopicSubscribers() { - return getTopic().getSubscribers(); - } - /** * Gets list of comments of the post * @@ -351,4 +357,46 @@ public int calculateRatingChanges(PostVote vote) { return 0; } + /** + * Creates and returns new list of comments of the current post which is not marked as removed. We cant use + * hibernate "WHERE" clause due caching issue. Additionally we may need to retrieve comments which is marked as + * removed in future. To manipulate with list of comments use {@link #getComments()} and + * {@link #setComments(java.util.List)} methods. + * + * @return newly created list of comments which not marked as deleted + */ + public List getNotRemovedComments() { + List notRemovedComments = new ArrayList<>(); + for (PostComment comment : getComments()) { + if (comment.getDeletionDate() == null) { + notRemovedComments.add(comment); + } + } + return notRemovedComments; + } + + /** + * {@inheritDoc} + */ + @Override + public Set getSubscribers() { + return getTopic().getSubscribers(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getUrlSuffix() { + return URL_SUFFIX + getId(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getUnsubscribeLinkForSubscribersOf(Class clazz) { + return getTopic().getUnsubscribeLinkForSubscribersOf(clazz); + } + } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PostComment.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PostComment.java index 91b13b2361..f583082119 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PostComment.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PostComment.java @@ -42,6 +42,9 @@ public class PostComment extends Entity { private String body; private Post post; private Map attributes = new HashMap<>(); + private DateTime deletionDate; + private DateTime modificationDate; + private JCUser userChanged; /** * @return the author @@ -152,4 +155,42 @@ public void putAttribute(String attributeName, String attributeValue) { public void putAllAttributes(Map attributes) { this.attributes.putAll(attributes); } + + /** + * Get a deletion date of the comment. We use {@link #deletionDate} to mark comment as deleted. So, if + * {@link #deletionDate} is not null, it means that comment was deleted. + * We need this functional because deletion records from database leads to errors with concurrency. + * @see JC-1757 + * + * @return true if the comment has been deleted + */ + public DateTime getDeletionDate() { + return deletionDate; + } + + /** + * Set a deletion date of the comment + * + * @param deletionDate + * @see #getDeletionDate() + */ + public void setDeletionDate(DateTime deletionDate) { + this.deletionDate = deletionDate; + } + + public DateTime getModificationDate() { + return modificationDate; + } + + public void setModificationDate(DateTime modificationDate) { + this.modificationDate = modificationDate; + } + + public JCUser getUserChanged() { + return userChanged; + } + + public void setUserChanged(JCUser userChanged) { + this.userChanged = userChanged; + } } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PostDraft.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PostDraft.java new file mode 100644 index 0000000000..3a35645029 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PostDraft.java @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.entity; + +import org.joda.time.DateTime; +import org.jtalks.common.model.entity.Entity; + +/** + * Represents draft of a post + * + * @author Mikhail Stryzhonok + */ +public class PostDraft extends Entity { + + private String content; + private Topic topic; + private JCUser author; + private DateTime lastSaved; + + public PostDraft() { + } + + public PostDraft(String content, JCUser author) { + this.content = content; + this.author = author; + lastSaved = new DateTime(); + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Topic getTopic() { + return topic; + } + + public void setTopic(Topic topic) { + this.topic = topic; + } + + public JCUser getAuthor() { + return author; + } + + public void setAuthor(JCUser author) { + this.author = author; + } + + public DateTime getLastSaved() { + return lastSaved; + } + + protected void setLastSaved(DateTime lastSaved) { + this.lastSaved = lastSaved; + } + + /** + * Sets current datetime to last saved property + */ + public void updateLastSavedTime() { + lastSaved = new DateTime(); + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PostState.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PostState.java new file mode 100644 index 0000000000..08a147062f --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/PostState.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.entity; + +/** + * @author Mikhail Stryzhonok + */ +public enum PostState { + + /** + * Post can be displayed in list of posts of topic + */ + DISPLAYED, + + /** + * Post is a draft. It can be displayed only for author, can be edited, + * stored as #DISPLAYED and removed + */ + DRAFT +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/SpamRule.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/SpamRule.java new file mode 100644 index 0000000000..9e9bd29ff8 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/SpamRule.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.model.entity; + +import org.jtalks.common.model.entity.Entity; + +/** + * @author Oleg Tkachenko + */ +public class SpamRule extends Entity { + private String regex; + private String description; + private boolean enabled; + + protected SpamRule() { + } + + public SpamRule(String regex, String description, boolean enabled) { + this.regex = regex; + this.description = description; + this.enabled = enabled; + } + + public String getRegex() { + return regex; + } + + public SpamRule setRegex(String regex) { + this.regex = regex; + return this; + } + + public String getDescription() { + return description; + } + + public SpamRule setDescription(String description) { + this.description = description; + return this; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/SubscriptionAwareEntity.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/SubscriptionAwareEntity.java index 48d28d4bf4..c7b22dd0c4 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/SubscriptionAwareEntity.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/SubscriptionAwareEntity.java @@ -29,13 +29,6 @@ public interface SubscriptionAwareEntity { */ Set getSubscribers(); - /** - * Sets subscribers list for this branch. For Hibernate use only. - * - * @param subscribers users to send notifications on update to - */ - void setSubscribers(Set subscribers); - /** * If user wants to see subscription updates she will get a notification with a link to particular forum location. * This method prepares a URL suffix to this location. Example: http://javatalks.ru/{url_suffix}/12. Url suffix is @@ -43,6 +36,18 @@ public interface SubscriptionAwareEntity { * * @return URL suffix */ - String prepareUrlSuffix(); + String getUrlSuffix(); + /** + * Gets link for unsubscription form updates of entity for subscribes of entity of specified class. + * Sometimes if entity updates we need to send notifications to subscribers of parent entity (if topic + * moved we need to notify target branch subscribers) so in this case we should show different urls + * for subscribers of branch and subscribers of topic + * + * @param clazz class of entity user subscribed to + * @param subclass of {@link org.jtalks.jcommune.model.entity.SubscriptionAwareEntity} + * + * @return link for unsubscription + */ + String getUnsubscribeLinkForSubscribersOf(Class clazz); } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Topic.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Topic.java index 65b8903db2..7b18cec05f 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Topic.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/Topic.java @@ -14,14 +14,6 @@ */ package org.jtalks.jcommune.model.entity; -import org.apache.solr.analysis.*; -import org.hibernate.search.annotations.*; -import org.joda.time.DateTime; -import org.jtalks.common.model.entity.Entity; -import org.jtalks.jcommune.model.validation.annotations.NotBlankSized; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import javax.validation.Valid; import java.util.ArrayList; import java.util.HashMap; @@ -30,6 +22,29 @@ import java.util.Map; import java.util.Set; +import com.google.common.base.Optional; +import org.apache.solr.analysis.LowerCaseFilterFactory; +import org.apache.solr.analysis.SnowballPorterFilterFactory; +import org.apache.solr.analysis.StandardFilterFactory; +import org.apache.solr.analysis.StandardTokenizerFactory; +import org.apache.solr.analysis.StopFilterFactory; +import org.hibernate.search.annotations.Analyzer; +import org.hibernate.search.annotations.AnalyzerDef; +import org.hibernate.search.annotations.AnalyzerDefs; +import org.hibernate.search.annotations.DocumentId; +import org.hibernate.search.annotations.Field; +import org.hibernate.search.annotations.Fields; +import org.hibernate.search.annotations.Indexed; +import org.hibernate.search.annotations.IndexedEmbedded; +import org.hibernate.search.annotations.Parameter; +import org.hibernate.search.annotations.TokenFilterDef; +import org.hibernate.search.annotations.TokenizerDef; +import org.joda.time.DateTime; +import org.jtalks.common.model.entity.Entity; +import org.jtalks.jcommune.model.validation.annotations.NotBlankSized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Represents the topic of the forum. * Contains the list of related {@link Post}. @@ -120,7 +135,7 @@ public class Topic extends Entity implements SubscriptionAwareEntity { private DateTime creationDate; private DateTime modificationDate; private JCUser topicStarter; - @NotBlankSized(min = MIN_NAME_SIZE, max = MAX_NAME_SIZE, message = "{length.constraint}") + @NotBlankSized(min = MIN_NAME_SIZE, max = MAX_NAME_SIZE, message = "{javax.validation.constraints.Size.message}") private String title; private boolean sticked; private boolean announcement; @@ -132,6 +147,7 @@ public class Topic extends Entity implements SubscriptionAwareEntity { private String type; private Map attributes = new HashMap<>(); private List posts = new ArrayList<>(); + private List drafts = new ArrayList<>(); private Set subscribers = new HashSet<>(); // transient, makes sense for current user only if set explicitly @@ -189,8 +205,8 @@ public Topic(JCUser topicStarter, String title, String topicType) { * @param post post to add */ public void addPost(Post post) { + setModificationDate(post.getCreationDate()); post.setTopic(this); - updateModificationDate(); this.posts.add(post); } @@ -201,6 +217,11 @@ public void addPost(Post post) { */ public void removePost(Post postToRemove) { posts.remove(postToRemove); + Topic topic = postToRemove.getTopic(); + if (postToRemove.getCreationDate().withMillisOfSecond(0) + .equals(topic.getModificationDate().withMillisOfSecond(0))) { + topic.recalculateModificationDate(); + } } /** @@ -338,7 +359,7 @@ public Post getLastPost() { * Used to find closest post which is good to be displayed after deletion of * post we pass as a parameter. * - * @param post + * @param post post to search next * @return Neighbor post */ public Post getNeighborPost(Post post) { @@ -365,24 +386,13 @@ protected void setModificationDate(DateTime modificationDate) { this.modificationDate = modificationDate; } - /** - * Set modification date to now. - * Used after addition of the post. It is necessary to save the sort order of topics in the future. - * - * @return new modification date - */ - public DateTime updateModificationDate() { - this.modificationDate = new DateTime(); - return this.modificationDate; - } - /** * Calculates modification date of topic taking it as last post in topic creation date. * Used after deletion of the post. It is necessary to save the sort order of topics in the future. */ public void recalculateModificationDate() { DateTime newTopicModificationDate = getFirstPost().getCreationDate(); - for (Post post : posts) { + for (Post post : getPosts()) { if (post.getCreationDate().isAfter(newTopicModificationDate.toInstant())) { newTopicModificationDate = post.getCreationDate(); } @@ -395,7 +405,7 @@ public void recalculateModificationDate() { */ public DateTime getLastModificationPostDate() { DateTime newTopicModificationDate = getFirstPost().getLastTouchedDate(); - for (Post post : posts) { + for (Post post : getPosts()) { if (post.getLastTouchedDate().isAfter(newTopicModificationDate.toInstant())) { newTopicModificationDate = post.getLastTouchedDate(); } @@ -519,9 +529,9 @@ private Post getFirstNewerPost(DateTime time) { /** * This method will return true if there are unread posts in that topic * for the current user. This state is NOT persisted and must be - * explicitly set by calling Topic.setLastReadPostIndex(). + * explicitly set by calling Topic.setLastReadPostDate(). *

- * If setter has not been called this method will always return no updates + * If setter has not been called this method will always return true * * @return if current topic has posts still unread by the current user */ @@ -559,7 +569,6 @@ public Set getSubscribers() { /** * {@inheritDoc} */ - @Override public void setSubscribers(Set subscribers) { this.subscribers = subscribers; } @@ -571,10 +580,24 @@ public void setSubscribers(Set subscribers) { * The target URL has the next format http://{forum root}/posts/{id} */ @Override - public String prepareUrlSuffix() { + public String getUrlSuffix() { return URL_SUFFIX + getLastPost().getId(); } + /** + * {@inheritDoc} + */ + @Override + public String getUnsubscribeLinkForSubscribersOf(Class clazz) { + + if (Branch.class.isAssignableFrom(clazz)) { + return getBranch().getUnsubscribeLinkForSubscribersOf(clazz); + } else { + //In case of post unsubscribe from topic too + return String.format("/topics/%s/unsubscribe", getId()); + } + } + /** * @return True if topic is closed */ @@ -655,4 +678,113 @@ public boolean isCodeReview() { public boolean isPlugable() { return type != null && !(this.isCodeReview() || type.equals(TopicTypeName.DISCUSSION.getName())); } + + /** + * Checks if this topic contains only posts added by topic Owner. + * + * @return true if condition is followed, otherwise false + */ + public boolean isContainsOwnerPostsOnly() { + for (Post post : getPosts()) { + if (!post.getUserCreated().equals(topicStarter)) { + return false; + } + } + return true; + } + + /** + * Gets list of drafts for current topic + * + * @return list of drafts + */ + public List getDrafts() { + return drafts; + } + + /** + * Sets list of drafts to current topic + * + * @param drafts list of drafts to set + */ + public void setDrafts(List drafts) { + this.drafts = drafts; + } + + /** + * Adds draft to current topic + * + * @param draft draft to add + */ + public void addDraft(PostDraft draft) { + draft.setTopic(this); + getDrafts().add(draft); + } + + /** + * Get draft of specified user in current topic + * + * @param user user to search draft + * + * @return draft of specified user or null if draft not found + */ + public PostDraft getDraftForUser(JCUser user) { + for (PostDraft draft : getDrafts()) { + if (draft.getAuthor().equals(user)) { + return draft; + } + } + return null; + } + + /** + * removes specified draft from topic + * + * @param draft draft to be removed + */ + public void removeDraft(PostDraft draft) { + getDrafts().remove(draft); + } + + /** + * Removes draft of specified user if exist + * + * @param user user to search draft + */ + public void removeDraftOfUser(JCUser user) { + PostDraft draft = getDraftForUser(user); + if (draft != null) { + removeDraft(draft); + } + } + + public int getUserPostCount(JCUser user) { + int count = 0; + for (Post post : getPosts()) { + if (post.getUserCreated().equals(user)) { + count ++; + } + } + return count; + } + + /** + * Makes URL to mark topic page as read. + * For anonymous user returns empty optional. + * + * @param user current user + * @param page page to mark as read + * @return Optional url string + */ + public Optional getMarkAsReadUrl(JCUser user, String page) { + if (user.isAnonymous()) { + return Optional.absent(); + } + return Optional.of("{topicId}/page/{pageNum}/markread?userId={userId}&lastModified={lastModified}" + .replace("{topicId}", String.valueOf(getId())) + .replace("{pageNum}", page) + .replace("{userId}", String.valueOf(user.getId())) + .replace("{lastModified}", String.valueOf(getLastModificationPostDate().getMillis())) + ); + } } diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/TopicDraft.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/TopicDraft.java new file mode 100644 index 0000000000..9d4ee24447 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/TopicDraft.java @@ -0,0 +1,171 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.entity; + +import org.joda.time.DateTime; +import org.jtalks.common.model.entity.Entity; +import org.jtalks.jcommune.model.validation.annotations.AtLeastOneFieldIsNotNull; +import org.jtalks.jcommune.model.validation.annotations.TopicDraftNumberOfPollItems; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.Objects; + +/** + * Represents a draft topic. + * + * @author Dmitry S. Dolzhenko + */ +@AtLeastOneFieldIsNotNull(fields = { + "title", "content", "pollTitle", "pollItemsValue" +}, message = "{topicDraft.fields.not_null}") +public class TopicDraft extends Entity { + + @Size(max = Topic.MAX_NAME_SIZE, + message = "{javax.validation.constraints.Size.message}") + private String title; + + @Size(max = Post.MAX_LENGTH, + message = "{javax.validation.constraints.Size.message}") + private String content; + + @Size(max = Poll.MAX_TITLE_LENGTH, + message = "{javax.validation.constraints.Size.message}") + private String pollTitle; + + @TopicDraftNumberOfPollItems(max = Poll.MAX_ITEMS_NUMBER) + private String pollItemsValue; + + private JCUser topicStarter; + private DateTime lastSaved; + + /** + * These fields are transient, since we do not need to save them in DB + * and and we use them only for permissions check during topic creation. + * Later on the user may close the page and start creating it in another + * branch - the permissions may be different and we're ok with that. + * Draft is not bound to the branch and can be started in one branch + * and finished in another one. + * + * Here we check that branchId is greater than 0, to be sure in that + * user passed it to check permissions + */ + @Min(value = 1) + private transient long branchId; + @NotNull + private transient String topicType; + + public TopicDraft() { + } + + public TopicDraft(JCUser topicStarter, String title, String content) { + this.topicStarter = topicStarter; + this.title = title; + this.content = content; + this.lastSaved = new DateTime(); + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public JCUser getTopicStarter() { + return topicStarter; + } + + public void setTopicStarter(JCUser topicStarter) { + this.topicStarter = topicStarter; + } + + public DateTime getLastSaved() { + return lastSaved; + } + + public void setLastSaved(DateTime lastSaved) { + this.lastSaved = lastSaved; + } + + public String getPollTitle() { + return pollTitle; + } + + public void setPollTitle(String pollTitle) { + this.pollTitle = pollTitle; + } + + public String getPollItemsValue() { + return pollItemsValue; + } + + public void setPollItemsValue(String pollItemsValue) { + this.pollItemsValue = pollItemsValue; + } + + public long getBranchId() { + return branchId; + } + + public void setBranchId(long branchId) { + this.branchId = branchId; + } + + public String getTopicType() { + return topicType; + } + + public void setTopicType(String topicType) { + this.topicType = topicType; + } + + /** + * Sets current datetime to 'lastSaved' property + */ + public void updateLastSavedTime() { + lastSaved = new DateTime(); + } + + /** + * Determines if this draft is draft for code review + * + * @return true if code review, otherwise false + */ + public boolean isCodeReview() { + return Objects.equals(topicType, TopicTypeName.CODE_REVIEW.getName()); + } + + /** + * Determines if this draft is draft for provided by plugin topic. + * NOTE: currently jcommune provides two topic types: "Code review" and "Discussion" all other + * topic types are provided by plugins + * + * @return true if topic is provided by plugin otherwise false + */ + public boolean isPlugable() { + return topicType != null && !(this.isCodeReview() || topicType.equals(TopicTypeName.DISCUSSION.getName())); + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/UserContactType.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/UserContactType.java index 7ac65b5174..e8e0a3e454 100644 --- a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/UserContactType.java +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/UserContactType.java @@ -29,7 +29,6 @@ public class UserContactType extends Entity { private String icon; private String mask; private String displayPattern; - private String validationPattern; /** * Only for hibernate usage. @@ -97,20 +96,6 @@ public void setDisplayPattern(String displayPattern) { this.displayPattern = displayPattern; } - /** - * @return the validation regexp of contact type - */ - public String getValidationPattern() { - return validationPattern; - } - - /** - * @param validationPattern validation regexp of contact type - */ - public void setValidationPattern(String validationPattern) { - this.validationPattern = validationPattern; - } - /** * Get value ready to display based on displayPattern and * given contact value diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/UserInfo.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/UserInfo.java new file mode 100644 index 0000000000..2ea7585b21 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/entity/UserInfo.java @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.model.entity; + +import org.jtalks.common.model.entity.User; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; + +/** + * Provides core user information which is later encapsulated. This class is immutable which is very important + * as it's going to be accessed from many threads. All the fields here are loaded only once into HTTP Session + * and don't ever change within that session. + * + * into {@link Authentication} objects. + * + * @author Oleg Tkachenko + */ +public class UserInfo implements UserDetails { + private final long id; + private final String uuid; + private final String username; + private final String password; + private final boolean enabled; + + public UserInfo(User user) { + this.id = user.getId(); + this.uuid = user.getUuid(); + this.username = user.getUsername(); + this.password = user.getPassword(); + this.enabled = user.isEnabled(); + } + + public long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getPassword() { + return password; + } + + /** + * Returns user's activation status in forum. After registration user has status "disabled" and receives an e-mail + * with activation link. When user confirm his e-mail address, status changed to "enabled". Only users with + * activation status "enabled" can login. + * + * @return return true if user is activated in forum and can login, otherwise false. + */ + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UserInfo)) return false; + UserInfo userInfo = (UserInfo) o; + return getUuid().equals(userInfo.getUuid()); + } + + @Override + public int hashCode() { + return getUuid().hashCode(); + } + + /* + * Next methods from UserDetails interface, indicating that user can or can't authenticate. + * We don't need this functionality. + */ + + /** + * Used for role based access in spring security. + * Jcommune uses Domain Object Security (ACLs) so we don't have any roles. + * + * @return {@link Collections.EmptyList} + */ + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/logic/UserBanner.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/logic/UserBanner.java new file mode 100644 index 0000000000..e432be876e --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/logic/UserBanner.java @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.logic; + +import org.jtalks.common.model.entity.Group; +import org.jtalks.jcommune.model.dao.GroupDao; +import org.jtalks.jcommune.model.dao.UserDao; + +import java.util.List; + +/** + * Class for working with users banning + * + * @author stanislav bashkirtsev + * @author maxim reshetov + */ +public class UserBanner { + public static final String BANNED_USERS_GROUP_NAME = "Banned Users"; + private final GroupDao groupDao; + private final UserDao userDao; + + /** Constructor for initialization variables */ + public UserBanner(GroupDao groupDao, UserDao userDao) { + this.groupDao = groupDao; + this.userDao = userDao; + } + + /** + * Revokes ban from users, deleting them from banned users group. + * + * @param usersToRevoke {@link UserList} with users to revoke ban. + */ + public void revokeBan(UserList usersToRevoke) { + Group bannedUserGroup = getBannedUsersGroups().get(0); + bannedUserGroup.getUsers().removeAll(usersToRevoke.getUsers()); + groupDao.saveOrUpdate(bannedUserGroup); + } + + + /** + * Create group to ban + * + * @return {@link Group} of ban + */ + private Group createBannedUserGroup() { + Group bannedUsersGroup = new Group(BANNED_USERS_GROUP_NAME, "Banned Users"); + groupDao.saveOrUpdate(bannedUsersGroup); + return bannedUsersGroup; + } + + /** + * Search and return list of banned groups. If groups wasn't found in database, then creates new one.Note, that + * creating of this group is a temporal solution until we implement Permission Schemas. + * + * @return List of banned groups + */ + public List getBannedUsersGroups() { + List bannedUserGroups = groupDao.getByName(BANNED_USERS_GROUP_NAME); + if (bannedUserGroups.isEmpty()) { + bannedUserGroups.add(createBannedUserGroup()); + } + + return bannedUserGroups; + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/logic/UserList.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/logic/UserList.java new file mode 100644 index 0000000000..aebed57a58 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/logic/UserList.java @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.logic; + +import org.jtalks.common.model.entity.User; +import org.jtalks.jcommune.model.entity.JCUser; + +import java.util.*; + +/** + * A wrapper to work with list of users with convenient methods. + * + * @author alexander afanasiev + */ +public class UserList { + private final List users = new LinkedList(); + + /** Constructor for initialization variables with array*/ + public UserList(JCUser... users) { + this.users.addAll(Arrays.asList(users)); + } + + /** Constructor for initialization variables with {@link List}*/ + public UserList(List users) { + this.users.addAll(users); + } + + /** + * Creates and fills the list of {@link JCUser}s from the list of Users. Note, that this constructor actually + * accepts a list of {@link JCUser}s and then casts them, the list of {@link User}s will cause an exception. + * + * @param users the list of {@link JCUser}s to be casted + * @throws ClassCastException if the specified users are not of type {@link JCUser} + * @return list of {@link JCUser}s + */ + public static UserList ofCommonUsers(List users) { + List JCUsers = new ArrayList(); + for (User user : users) { + JCUsers.add((JCUser) user); + } + return new UserList(JCUsers); + } + + /** + * Returns true if this list contains no elements. + * + * @return true if this list contains no elements + */ + public boolean isEmpty() { + return users.isEmpty(); + } + + /** + * Gets an unmodifiable list of underlying users. + * + * @return an unmodifiable list of underlying users + */ + public List getUsers() { + return Collections.unmodifiableList(users); + } +} \ No newline at end of file diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/logic/package-info.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/logic/package-info.java new file mode 100644 index 0000000000..e730e9df8e --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/logic/package-info.java @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * Logical package for Poulpe + */ +package org.jtalks.jcommune.model.logic; diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/annotations/AtLeastOneFieldIsNotNull.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/annotations/AtLeastOneFieldIsNotNull.java new file mode 100644 index 0000000000..748d170d31 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/annotations/AtLeastOneFieldIsNotNull.java @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.validation.annotations; + +import org.jtalks.jcommune.model.validation.validators.AtLeastOneFieldIsNotNullValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; + +/** + * Constraint for checking that at least one of specified fields is not null + * + * @author Dmitry S. Dolzhenko + */ +@Target({TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint(validatedBy = AtLeastOneFieldIsNotNullValidator.class) +public @interface AtLeastOneFieldIsNotNull { + /** + * Resource bundle code for error message. + */ + String message() default "{org.jtalks.jcommune.model.validation.annotations.AtLeastOneFieldIsNotNull.message}"; + + /** + * Groups settings for this validation constraint + */ + Class[] groups() default {}; + + /** + * Payload element that specifies the payload with which the the + * constraint declaration is associated. + */ + Class[] payload() default {}; + + /** + * Names of fields used for validation. + */ + String[] fields(); +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/annotations/IntegerRange.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/annotations/IntegerRange.java new file mode 100644 index 0000000000..f43aff3c63 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/annotations/IntegerRange.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.validation.annotations; + + +import org.hibernate.validator.constraints.NotBlank; +import org.jtalks.jcommune.model.validation.validators.IntegerRangeValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import javax.validation.ReportAsSingleViolation; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Validate that the annotated string is not null, integer and between the range. + * + * @see org.jtalks.jcommune.model.validation.validators.IntegerRangeValidator + * + * @author skythet + */ +@Constraint(validatedBy = {IntegerRangeValidator.class}) +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) +@Retention(RUNTIME) +@ReportAsSingleViolation +@NotBlank +public @interface IntegerRange { + + String message() default "{javax.validation.constraints.IntegerRange.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + /** + * @return min value of the number + */ + int min() default 0; + + /** + * @return max value of the number + */ + int max() default Integer.MAX_VALUE; + /** + * Defines several @IntegerRange annotations on the same element + * @see IntegerRange + * + */ + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) + @Retention(RUNTIME) + @Documented + @interface List { + IntegerRange[] value(); + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/annotations/TopicDraftNumberOfPollItems.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/annotations/TopicDraftNumberOfPollItems.java new file mode 100644 index 0000000000..575bcd2397 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/annotations/TopicDraftNumberOfPollItems.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.validation.annotations; + +import org.jtalks.jcommune.model.validation.validators.TopicDraftNumberOfPollItemsValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; + +/** + * Constraint for checking number of poll items in draft topic. + * + * @author Dmitry S. Dolzhenko + */ +@Target({FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint(validatedBy = TopicDraftNumberOfPollItemsValidator.class) +public @interface TopicDraftNumberOfPollItems { + /** + * Resource bundle code for error message + */ + String message() default "{poll.items.size}"; + + /** + * Groups settings for this validation constraint + */ + Class[] groups() default {}; + + /** + * Payload element that specifies the payload with which the the + * constraint declaration is associated. + */ + Class[] payload() default {}; + + /** + * Min value for poll items number. + */ + int min() default 0; + + /** + * Max value for poll items number. + */ + int max() default Integer.MAX_VALUE; +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/validators/AtLeastOneFieldIsNotNullValidator.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/validators/AtLeastOneFieldIsNotNullValidator.java new file mode 100644 index 0000000000..5864f5161e --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/validators/AtLeastOneFieldIsNotNullValidator.java @@ -0,0 +1,81 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.validation.validators; + +import org.apache.commons.beanutils.PropertyUtils; +import org.jtalks.jcommune.model.validation.annotations.AtLeastOneFieldIsNotNull; + +import javax.validation.ConstraintDeclarationException; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; + +/** + * Validator for {@link AtLeastOneFieldIsNotNull} + * + * @author Dmitry S. Dolzhenko + */ +public class AtLeastOneFieldIsNotNullValidator + implements ConstraintValidator { + + private String[] fields; + + /** + * {@inheritDoc} + */ + @Override + public void initialize(AtLeastOneFieldIsNotNull constraintAnnotation) { + this.validateParameters(constraintAnnotation); + this.fields = constraintAnnotation.fields(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isValid(Object entity, ConstraintValidatorContext constraintValidatorContext) { + try { + for (String field : fields) { + try { + if (PropertyUtils.getProperty(entity, field) != null) { + return true; + } + } catch (NoSuchMethodException e) { + /* + * Method PropertyUtils.getProperty can get value of the field + * only if it has public accessor. Therefore, if the field doesn't have + * public accessor, we try to get its value manually. + */ + Field fieldObject = entity.getClass().getDeclaredField(field); + fieldObject.setAccessible(true); + if (fieldObject.get(entity) != null) { + return true; + } + } + } + } catch (IllegalAccessException | InvocationTargetException | NoSuchFieldException e) { + throw new ConstraintDeclarationException(e); + } + + return false; + } + + private void validateParameters(AtLeastOneFieldIsNotNull constraintAnnotation) { + if (constraintAnnotation.fields().length == 0) { + throw new IllegalArgumentException("The parameter \"fields\" must not be empty."); + } + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/validators/IntegerRangeValidator.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/validators/IntegerRangeValidator.java new file mode 100644 index 0000000000..0d9932c7a1 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/validators/IntegerRangeValidator.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.validation.validators; + +import org.jtalks.jcommune.model.validation.annotations.IntegerRange; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * Validator for {@link IntegerRange} annotation + * @author skythet + */ +public class IntegerRangeValidator implements ConstraintValidator { + + private int min; + private int max; + + /** + * {@inheritDoc} + */ + @Override + public void initialize(IntegerRange constraintAnnotation) { + min = constraintAnnotation.min(); + max = constraintAnnotation.max(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return false; + } + try { + int intValue = Integer.parseInt(value); + return intValue >= min && intValue <= max; + } catch (NumberFormatException exception) { + return false; + } + } +} diff --git a/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/validators/TopicDraftNumberOfPollItemsValidator.java b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/validators/TopicDraftNumberOfPollItemsValidator.java new file mode 100644 index 0000000000..60d76f01f3 --- /dev/null +++ b/jcommune-model/src/main/java/org/jtalks/jcommune/model/validation/validators/TopicDraftNumberOfPollItemsValidator.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.validation.validators; + +import org.jtalks.jcommune.model.validation.annotations.TopicDraftNumberOfPollItems; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * Validator for {@link TopicDraftNumberOfPollItems} + * + * @author Dmitry S. Dolzhenko + */ +public class TopicDraftNumberOfPollItemsValidator + implements ConstraintValidator { + + private int min; + private int max; + + @Override + public void initialize(TopicDraftNumberOfPollItems constraintAnnotation) { + this.min = constraintAnnotation.min(); + this.max = constraintAnnotation.max(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) { + if (value == null) { + return true; + } + + String[] items = value.split("\n"); + return items.length >= min && items.length <= max; + } +} diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/Restore_Drafts.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/Restore_Drafts.sql new file mode 100644 index 0000000000..1603c9bd7c --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/Restore_Drafts.sql @@ -0,0 +1,2 @@ +insert into POST (UUID, TOPIC_ID, USER_CREATED, POST_CONTENT, POST_DATE, STATE) + select (select UUID() from dual), TOPIC_ID, USER_ID, CONTENT, LAST_SAVED, 'DRAFT' from POST_DRAFT; \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V65__Add_admin_user.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V65__Add_admin_user.sql new file mode 100644 index 0000000000..ceb8a97a07 --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V65__Add_admin_user.sql @@ -0,0 +1,110 @@ +-- vars that have different types of collation can not be compared by '=', +-- that's why we need to set connection collation the same as database collation +-- in spite of server configuration. +SET @default_collation_connection = @@collation_connection; +SET @@collation_connection = @@collation_database; + +set @adminUserName := 'admin'; +set @adminPassword := 'admin'; +set @adminGroupName := 'Administrators'; +set @adminGroupDescription := 'Administrators group.'; +set @adminRoleName := 'ADMIN_ROLE'; +set @adminEMail := 'admin@jtalks.org'; +set @moderatorsGroupName := 'Moderators'; +set @moderatorsGroupDescription := 'General group for all moderators'; +set @forumComponentName := 'JTalks Sample Forum'; +set @forumComponentType := 'FORUM'; +set @forumComponentAclClass :='COMPONENT'; +set @forumComponentId := 2; +set @availableUsersText := 'Available users: admin/admin'; +set @isPrincipal := true; +set @notPrincipal := false; +set @adminMask := 16; + +insert into COMPONENTS (CMP_ID, COMPONENT_TYPE, UUID, `NAME`, DESCRIPTION) + select @forumComponentId, @forumComponentType, UUID(), @forumComponentName, @availableUsersText + from dual + where not exists (select 1 from COMPONENTS where COMPONENT_TYPE = @forumComponentType); + +-- 'FROM COMPONENTS' are not used, but query mast contain 'FROM dual' clause +-- @see http://dev.mysql.com/doc/refman/5.0/en/select.html/a>. +insert into GROUPS (UUID, `NAME`, DESCRIPTION) + select UUID(), @moderatorsGroupName, @moderatorsGroupDescription + from dual + where not exists (select GROUP_ID from GROUPS where `NAME` = @moderatorsGroupName); + +insert into GROUPS (UUID, `NAME`, DESCRIPTION) + select UUID(), @adminGroupName, @adminGroupDescription + from dual + where not exists (select gr.GROUP_ID from GROUPS gr where gr.NAME = @adminGroupName); + +insert into USERS (UUID, FIRST_NAME, LAST_NAME, USERNAME, ENCODED_USERNAME, EMAIL, PASSWORD, ROLE, SALT, ENABLED) + select UUID(), @adminUserName, @adminUserName, @adminUserName, @adminUserName, @adminEMail, MD5(@adminPassword), @adminRoleName, '', true + from dual + where not exists (select 1 from USERS where USERNAME = @adminUserName); + +alter table JC_USER_DETAILS add unique (USER_ID); +insert into JC_USER_DETAILS (USER_ID, REGISTRATION_DATE, POST_COUNT) + select ID, NOW(), 0 + from USERS + where USERNAME = @adminUserName and not exists (select 1 from JC_USER_DETAILS where USER_ID = USERS.ID); + +-- Adding created Admin to Administrators group(created at this migration or common migration) ). +set @admin_group_id := (select GROUP_ID from GROUPS where `NAME` = @adminGroupName); +insert into GROUP_USER_REF (GROUP_ID, USER_ID) + select @admin_group_id, ID + from USERS + where USERNAME = @adminUserName and not exists (select * + from GROUP_USER_REF + where GROUP_ID = @admin_group_id and USER_ID = USERS.ID); + +-- Adding record with added component class. +set @component_acl_class := 1; +set @group_acl_class := 2; +set @branch_acl_class := 3; + +insert ignore into acl_class (ID, CLASS) + values (@branch_acl_class, 'BRANCH'), (@group_acl_class, 'GROUP'), (@component_acl_class, 'COMPONENT'); + +set @acl_sid_admin_group := (select GROUP_CONCAT('usergroup:', CONVERT(GROUP_ID, char(19))) + from GROUPS g + where g.NAME = @adminGroupName); +set @acl_sid_admin_user := (select GROUP_CONCAT('user:', CONVERT(ID, char(19))) + from USERS u + where u.USERNAME = @adminUserName); +set @forum_object_id_identity := (select component.CMP_ID + from COMPONENTS component + where component.COMPONENT_TYPE = @forumComponentType); + +-- Adding record to acl_sid table, this record wires sid and user id. +insert into acl_sid (principal, sid) + select @isPrincipal, @acl_sid_admin_user + from dual + where not exists (select acl_sid.sid from acl_sid where sid = @acl_sid_admin_user); + +set @acl_sid_admin_user_id := (select sid.id from acl_sid sid where sid.sid = @acl_sid_admin_user); + +-- Adding record to acl_sid table, this record wires sid and group id. +insert ignore into acl_sid (principal, sid) + values(@notPrincipal, @acl_sid_admin_group); + +set @acl_sid_admin_group_id := (select sid.id from acl_sid sid where sid.sid = @acl_sid_admin_group); +set @forum_component_acl_class_id := (select class.id from acl_class class where class.class = @forumComponentAclClass); + +insert ignore into acl_object_identity (object_id_class, object_id_identity, owner_sid, entries_inheriting) + select @forum_component_acl_class_id, @forum_object_id_identity, @acl_sid_admin_user_id, 1 + from dual; + +set @forum_acl_object_identity_id := (select aoi.id from acl_object_identity aoi + where aoi.object_id_class = @forum_component_acl_class_id + and aoi.object_id_identity = @forum_object_id_identity); + +set @ace_order_max := (select MAX(ae.ace_order) from acl_entry ae); +set @ace_order := (case when @ace_order_max is null then 0 else @ace_order_max + 1 end); + +insert ignore into acl_entry (acl_object_identity, sid, ace_order, mask, granting, audit_success, audit_failure) + select @forum_acl_object_identity_id, @acl_sid_admin_group_id, @ace_order, @adminMask, 1, 0 , 0 + from dual; + +-- return collation_connection to a previous state. +SET @@collation_connection = @default_collation_connection; diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V66__Add_column_DELETION_DATE_to_Post_Comment.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V66__Add_column_DELETION_DATE_to_Post_Comment.sql new file mode 100644 index 0000000000..4a27fe39b5 --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V66__Add_column_DELETION_DATE_to_Post_Comment.sql @@ -0,0 +1,2 @@ +alter table POST_COMMENT add(DELETION_DATE DATETIME default null); +create index DELETION_DATE_INDEX ON POST_COMMENT (DELETION_DATE) \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V67__Add_State_Column_To_Post.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V67__Add_State_Column_To_Post.sql new file mode 100644 index 0000000000..e53502558a --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V67__Add_State_Column_To_Post.sql @@ -0,0 +1 @@ +alter table POST add STATE varchar(255) not null default 'DISPLAYED'; \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V68__Add_Post_Draft_Table.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V68__Add_Post_Draft_Table.sql new file mode 100644 index 0000000000..6d60d6da8a --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V68__Add_Post_Draft_Table.sql @@ -0,0 +1,17 @@ +create table POST_DRAFT ( + ID bigint(20) not null auto_increment, + UUID varchar(255) not null, + TOPIC_ID bigint(20) not null, + USER_ID bigint(20) not null, + CONTENT longtext not null, + LAST_SAVED datetime not null, + primary key(ID), + unique key(TOPIC_ID, USER_ID), + constraint FK_TOPIC_POST_DRAFT foreign key (TOPIC_ID) references TOPIC (TOPIC_ID) on delete cascade, + constraint FK_USER_POST_DRAFT foreign key (USER_ID) references USERS (ID) +)engine=InnoDb default charset='utf8' collate='utf8_bin'; + +insert ignore into POST_DRAFT (UUID, TOPIC_ID, USER_ID, CONTENT, LAST_SAVED) + select (select UUID() from dual), TOPIC_ID, USER_CREATED, POST_CONTENT, POST_DATE from POST where STATE='DRAFT'; + +delete from POST where STATE='DRAFT'; \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V69__Remove_Column_State_From_Post_Table.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V69__Remove_Column_State_From_Post_Table.sql new file mode 100644 index 0000000000..0924982a1d --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V69__Remove_Column_State_From_Post_Table.sql @@ -0,0 +1,3 @@ +# Since we store drafts of posts in separate table +# we don't need column STATE in POST table anymore +alter table POST drop column STATE; \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V6__Acl_Sid_encoding_fix.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V6__Acl_Sid_encoding_fix.sql deleted file mode 100644 index 88d6cefb07..0000000000 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V6__Acl_Sid_encoding_fix.sql +++ /dev/null @@ -1,2 +0,0 @@ -alter table `acl_sid` charset "utf8"; -alter table `acl_sid` modify `sid` varchar(100) charset "utf8" not null; \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V6__Acl_encoding_fix.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V6__Acl_encoding_fix.sql new file mode 100644 index 0000000000..f0403055e9 --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V6__Acl_encoding_fix.sql @@ -0,0 +1,5 @@ +-- COLLATE DEFAULT means that we want to set TABLE collation the same as database. +ALTER TABLE `acl_sid` CHAR SET 'UTF8' COLLATE DEFAULT; +ALTER TABLE `acl_sid` MODIFY `sid` VARCHAR(100) not null; +ALTER TABLE `acl_class` CHAR SET 'UTF8' COLLATE DEFAULT; +ALTER TABLE `acl_class` MODIFY `class` VARCHAR(255) diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V70__Add_Topic_Draft_Table.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V70__Add_Topic_Draft_Table.sql new file mode 100644 index 0000000000..d18fa19cd9 --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V70__Add_Topic_Draft_Table.sql @@ -0,0 +1,14 @@ +create table TOPIC_DRAFT ( + ID bigint(20) not null auto_increment, + UUID varchar(255) not null, + TOPIC_STARTER_ID bigint(20) not null, + TITLE varchar(255) default null, + CONTENT longtext default null, + POLL_TITLE varchar(255) default null, + POLL_ITEMS_VALUE longtext default null, + LAST_SAVED datetime not null, + primary key(ID), + unique key(UUID), + unique key(TOPIC_STARTER_ID), + constraint FK_USER_TOPIC_DRAFT foreign key (TOPIC_STARTER_ID) references USERS (ID) +) engine=InnoDb default charset='utf8' collate='utf8_bin'; \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V71__Add_Unique_Key_To_Last_Read_Posts_Table.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V71__Add_Unique_Key_To_Last_Read_Posts_Table.sql new file mode 100644 index 0000000000..a6ad696a94 --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V71__Add_Unique_Key_To_Last_Read_Posts_Table.sql @@ -0,0 +1,10 @@ +-- Remove duplicates from LAST_READ_POSTS table +DELETE p FROM LAST_READ_POSTS AS p + CROSS JOIN (SELECT ID + FROM LAST_READ_POSTS + GROUP BY ID + HAVING COUNT(ID) > 1 + ) AS p2 + USING (ID); + +ALTER TABLE LAST_READ_POSTS ADD UNIQUE INDEX `UNIQUE_TOPIC_ID_USER_ID` (TOPIC_ID, USER_ID); \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V72__Remove_column_VALIDATION_PATTERN_from_tabl_contact_type.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V72__Remove_column_VALIDATION_PATTERN_from_tabl_contact_type.sql new file mode 100644 index 0000000000..76b5162c9f --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V72__Remove_column_VALIDATION_PATTERN_from_tabl_contact_type.sql @@ -0,0 +1 @@ +ALTER TABLE `CONTACT_TYPE` DROP `VALIDATION_PATTERN`; \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V73__Convert_Database_To_UTF8MB4_Encoding.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V73__Convert_Database_To_UTF8MB4_Encoding.sql new file mode 100644 index 0000000000..1b8784ab3c --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V73__Convert_Database_To_UTF8MB4_Encoding.sql @@ -0,0 +1,39 @@ +DELIMITER $$ +DROP PROCEDURE IF EXISTS changeCollation$$ +-- Stored procedure to convert all tables character set and collations +CREATE PROCEDURE changeCollation(IN character_set VARCHAR(255), IN collation_type VARCHAR(255)) + BEGIN + DECLARE is_finished INTEGER DEFAULT 0; + DECLARE tableName varchar(255) DEFAULT ""; + DECLARE t_cursor CURSOR FOR SELECT DISTINCT TABLE_NAME + FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_TYPE = "BASE TABLE"; + DECLARE CONTINUE HANDLER + FOR NOT FOUND SET is_finished = 1; + OPEN t_cursor; + + get_table: LOOP + FETCH t_cursor INTO tableName; + IF is_finished = 1 THEN + LEAVE get_table; + END IF; +-- Flyway cannot convert schema_version table by self because it uses 'SELECT FOR UPDATE' query inside the lib. + IF (tableName != '' AND tableName != 'jcommune_schema_version') THEN + + SET @s = CONCAT('ALTER TABLE ', tableName, + ' CONVERT TO CHARACTER SET ', character_set, ' COLLATE ', collation_type); + PREPARE stmt FROM @s; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + SET tableName = ''; + + END IF; + END LOOP get_table; + CLOSE t_cursor; + END $$ +DELIMITER ; + +ALTER DATABASE CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'; +SET FOREIGN_KEY_CHECKS = 0; +CALL changeCollation('utf8mb4','utf8mb4_unicode_ci'); +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V74__Add_Email_Black_List_Property.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V74__Add_Email_Black_List_Property.sql new file mode 100644 index 0000000000..8cbe90001f --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V74__Add_Email_Black_List_Property.sql @@ -0,0 +1,2 @@ +INSERT IGNORE INTO PROPERTIES(UUID, NAME, CMP_ID) +VALUES ((SELECT UUID() FROM dual), 'jcommune.email_domains_black_list', 2) \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V75__Add_Spam_Protection_Table.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V75__Add_Spam_Protection_Table.sql new file mode 100644 index 0000000000..eb2f9b2e0f --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V75__Add_Spam_Protection_Table.sql @@ -0,0 +1,7 @@ +create table `SPAM_RULES` ( + `ID` bigint(20) not null auto_increment, + `UUID` varchar(255) not null, + `REGEX` varchar(255) not null, + `DESCRIPTION` varchar(255) null, + `ENABLED` bit not null default 0, + primary key (`ID`)); diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V76__Add_column_Modification_Date and_User_Changed_to_Post_Comment_Table.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V76__Add_column_Modification_Date and_User_Changed_to_Post_Comment_Table.sql new file mode 100644 index 0000000000..12a7dd591f --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/migrations/V76__Add_column_Modification_Date and_User_Changed_to_Post_Comment_Table.sql @@ -0,0 +1,6 @@ +ALTER TABLE POST_COMMENT ADD ( + MODIFICATION_DATE datetime DEFAULT NULL, + USER_CHANGED BIGINT DEFAULT NULL, + KEY FK_USER_CHANGED (USER_CHANGED), + CONSTRAINT FK_USER_CHANGED FOREIGN KEY (USER_CHANGED) REFERENCES USERS (ID) +); diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Branch.hbm.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Branch.hbm.xml index 16f6ffe05b..7395328c71 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Branch.hbm.xml +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Branch.hbm.xml @@ -72,8 +72,8 @@ + not in (select v.branchId from org.jtalks.jcommune.model.entity.ViewTopicsBranches v where v.granting=0 and cast(v.sid as integer) in elements(bsg.id)) + AND branch.id in (select v.branchId from org.jtalks.jcommune.model.entity.ViewTopicsBranches v where v.granting=1 and cast(v.sid as integer) in elements(bsg.id))]]> diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Group.hbm.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Group.hbm.xml index 0a2b8406f9..3987672f91 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Group.hbm.xml +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Group.hbm.xml @@ -56,7 +56,7 @@ - + @@ -66,4 +66,16 @@ + + + + + + + + + + + + diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/JCUser.hbm.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/JCUser.hbm.xml index 1e51281fef..fcaa509ba3 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/JCUser.hbm.xml +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/JCUser.hbm.xml @@ -91,4 +91,41 @@ where lower(username) like (:pattern) escape '|' and enabled = 1 order by lower(username)]]> + + + + + + + + diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/LastReadPost.hbm.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/LastReadPost.hbm.xml index 8644b698ac..18fb8f8c67 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/LastReadPost.hbm.xml +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/LastReadPost.hbm.xml @@ -25,12 +25,17 @@ - - + + + + - + + + diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Post.hbm.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Post.hbm.xml index 5fb737329d..bd21e1ae5d 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Post.hbm.xml +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Post.hbm.xml @@ -36,7 +36,6 @@ fetch="join" foreign-key="FK_USER" lazy="false" not-null="true" cascade="save-update"/> - diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/PostComment.hbm.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/PostComment.hbm.xml index 109677b18e..b6ea07e6d0 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/PostComment.hbm.xml +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/PostComment.hbm.xml @@ -29,13 +29,19 @@ type="org.joda.time.contrib.hibernate.PersistentDateTime" not-null="true"/> - + + + + + diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/PostDraft.hbm.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/PostDraft.hbm.xml new file mode 100644 index 0000000000..b9c57a9320 --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/PostDraft.hbm.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/SpamRule.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/SpamRule.xml new file mode 100644 index 0000000000..81e075468f --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/SpamRule.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Topic.hbm.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Topic.hbm.xml index 33ffe1217b..7810678107 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Topic.hbm.xml +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/Topic.hbm.xml @@ -44,6 +44,13 @@ This might be tuned further in the future if the page size itself changes.--> + + + + + + + @@ -70,8 +77,8 @@ This might be tuned further in the future if the page size itself changes.--> + not in (select v.branchId from org.jtalks.jcommune.model.entity.ViewTopicsBranches v where v.granting=0 and cast(v.sid as integer) in elements(tsg.id)) + AND topic.branch.id in (select v.branchId from org.jtalks.jcommune.model.entity.ViewTopicsBranches v where v.granting=1 and cast(v.sid as integer) in elements(tsg.id))]]> diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/TopicDraft.hbm.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/TopicDraft.hbm.xml new file mode 100644 index 0000000000..5f52dd54e6 --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/TopicDraft.hbm.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/UserContactType.hbm.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/UserContactType.hbm.xml index 02d6e16b9c..b8597933e1 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/UserContactType.hbm.xml +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/UserContactType.hbm.xml @@ -29,7 +29,6 @@ - diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/applicationContext-dao.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/applicationContext-dao.xml index 4e6d728eee..09a48156f7 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/applicationContext-dao.xml +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/applicationContext-dao.xml @@ -47,6 +47,9 @@ + + @@ -103,6 +106,11 @@ value="org.jtalks.jcommune.model.entity.PostComment"/> + + + + @@ -120,6 +128,9 @@ + + @@ -183,6 +194,7 @@ /org/jtalks/jcommune/model/entity/Group.hbm.xml /org/jtalks/jcommune/model/entity/Post.hbm.xml /org/jtalks/jcommune/model/entity/Topic.hbm.xml + /org/jtalks/jcommune/model/entity/TopicDraft.hbm.xml /org/jtalks/jcommune/model/entity/Branch.hbm.xml /org/jtalks/jcommune/model/entity/PrivateMessage.hbm.xml /org/jtalks/jcommune/model/entity/Section.hbm.xml @@ -194,16 +206,16 @@ /org/jtalks/jcommune/model/entity/Property.hbm.xml /org/jtalks/jcommune/model/entity/SimplePage.hbm.xml /org/jtalks/jcommune/model/entity/Component.hbm.xml - /org/jtalks/jcommune/model/entity/ViewTopicsBranches.hbm.xml - - /org/jtalks/jcommune/model/entity/PostComment.hbm.xml - + /org/jtalks/jcommune/model/entity/ViewTopicsBranches.hbm.xml + /org/jtalks/jcommune/model/entity/PostComment.hbm.xml + /org/jtalks/jcommune/model/entity/SpamRule.xml /org/jtalks/jcommune/model/entity/Banner.hbm.xml /org/jtalks/jcommune/model/entity/ExternalLink.hbm.xml /org/jtalks/jcommune/model/entity/PluginConfiguration.hbm.xml /org/jtalks/jcommune/model/entity/PluginProperty.hbm.xml /org/jtalks/jcommune/model/entity/BranchReadedMarker.hbm.xml /org/jtalks/jcommune/model/entity/PostVote.hbm.xml + /org/jtalks/jcommune/model/entity/PostDraft.hbm.xml @@ -226,7 +238,7 @@ ${hibernate.use_query_cache} - ${EH_CACHE_CONFIG} + ${EH_CACHE_CONFIG:/org/jtalks/jcommune/model/entity/ehcache.xml} ${hibernate.search.default.directory_provider} @@ -252,41 +264,24 @@ - + - - - + + - - - - - - - - - - - - + depends-on="flyway_common"> - - + + + diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/applicationContext-properties.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/applicationContext-properties.xml index 7f19b91af4..d22bcb4435 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/applicationContext-properties.xml +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/applicationContext-properties.xml @@ -35,6 +35,14 @@ + + + + + + + diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/ehcache.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/ehcache.xml index 14e897a71f..87381a77b3 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/ehcache.xml +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/ehcache.xml @@ -20,13 +20,6 @@ - - - @@ -54,40 +47,17 @@ overflowToDisk="false"> - - - - - - \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/localCache.xml b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/localCache.xml new file mode 100644 index 0000000000..294769978d --- /dev/null +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/entity/localCache.xml @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/sample-forum.sql b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/sample-forum.sql index 906cdeacb2..287336ab44 100644 --- a/jcommune-model/src/main/resources/org/jtalks/jcommune/model/sample-forum.sql +++ b/jcommune-model/src/main/resources/org/jtalks/jcommune/model/sample-forum.sql @@ -1,5 +1,13 @@ -SET @forum_component_id := 2; -insert ignore into COMPONENTS (CMP_ID, COMPONENT_TYPE, UUID, `NAME`, DESCRIPTION) VALUES (2, 'FORUM', (SELECT UUID() FROM dual), 'JTalks Sample Forum', 'Available users: admin/admin registered/registered moderator/moderator banned/banned'); +# We have collation_connection = utf8mb4_general_ci by default and if user has SUPER privileges then +# init-connect command in mysql config file has no effect (only for SUPER users) and we need to set up +# collation_connection to utf8mb4_unicode_ci to prevent Illegal Mix of Collations for '=' operations, +# because on the one hand we have utf8mb4_general_ci and utf8mb4_unicode_ci on the other. +SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'; + +SET @forum_component_id := 2; +-- Update description of FORUM component for new users baing created +update COMPONENTS set DESCRIPTION = 'Available users: admin/admin registered/registered moderator/moderator banned/banned and others' + where CMP_ID = @forum_component_id; insert ignore into SECTIONS (SECTION_ID, UUID, `NAME`, DESCRIPTION, POSITION, COMPONENT_ID) VALUES (1,(SELECT UUID() FROM dual),'Sport', 'All about sport', 1, @forum_component_id), @@ -11,21 +19,62 @@ insert ignore into SECTIONS (SECTION_ID, UUID, `NAME`, DESCRIPTION, POSITION, CO (7,(SELECT UUID() FROM dual),'TV', 'Has this zombobox something interesting?', 7, @forum_component_id), (8,(SELECT UUID() FROM dual),'Hi-tech', 'Technologies', 8, @forum_component_id), (9,(SELECT UUID() FROM dual),'People', 'All about mankind', 9, @forum_component_id), - (10,(SELECT UUID() FROM dual),'Leisure', 'Have free time?', 9, @forum_component_id); + (10,(select UUID() from dual),'Leisure', 'Have free time?', 10, @forum_component_id), + (11,(select UUID() from dual),'For Automated testing only', 'This Section is used for Automated testing only', 11, @forum_component_id), + (12,(select UUID() from dual),'Invisible section', 'Invisible section', 12, @forum_component_id); -- GROUPS BEGIN -insert ignore into GROUPS (UUID, `NAME`, DESCRIPTION) VALUES ((SELECT UUID() FROM dual), 'Moderators', 'General group for all moderators'); -SET @admin_group_id := (select GROUP_ID from GROUPS where `NAME`='Administrators'); -SET @registered_group_id := (select GROUP_ID from GROUPS where `NAME`='Registered Users'); -SET @banned_group_id := (select GROUP_ID from GROUPS where `NAME`='Banned Users'); -SET @moderator_group_id := (select GROUP_ID from GROUPS where `NAME`='Moderators'); - -SET @admin_group_sid := concat('usergroup:',@admin_group_id); -SET @registered_group_sid := concat('usergroup:',@registered_group_id); -SET @banned_group_sid := concat('usergroup:',@banned_group_id); -SET @moderator_group_sid := concat('usergroup:',@moderator_group_id); +insert ignore into GROUPS (UUID, `NAME`, DESCRIPTION) + select UUID(), 'Moderators', 'General group for all moderators' + from dual + where not exists (select GROUP_ID from GROUPS where `NAME` = 'Moderators'); +insert ignore into GROUPS (UUID, `NAME`) values + ((select UUID() from dual), 'createPosts'), + ((select UUID() from dual), 'createStickedPosts'), + ((select UUID() from dual), 'createAnnouncements'), + ((select UUID() from dual), 'editOwnPosts'), + ((select UUID() from dual), 'editOtherPosts'), + ((select UUID() from dual), 'deleteOwnPosts'), + ((select UUID() from dual), 'deleteOthersPosts'), + ((select UUID() from dual), 'moveTopics'), + ((select UUID() from dual), 'closeTopics'), + ((select UUID() from dual), 'createCodeReview'), + ((select UUID() from dual), 'leaveCommentsInCodeReview'); + +set @admin_group_id = (select GROUP_ID from GROUPS where `NAME`='Administrators'); +set @registered_group_id = (select GROUP_ID from GROUPS where `NAME`='Registered Users'); +set @banned_group_id = (select GROUP_ID from GROUPS where `NAME`='Banned Users'); +set @moderator_group_id = (select GROUP_ID from GROUPS where `NAME`='Moderators'); +set @createPosts_group_id = (select GROUP_ID from GROUPS where `NAME`='createPosts'); +set @createStickedPosts_group_id = (select GROUP_ID from GROUPS where `NAME`='createStickedPosts'); +set @createAnnouncements_group_id = (select GROUP_ID from GROUPS where `NAME`='createAnnouncements'); +set @editOwnPosts_group_id = (select GROUP_ID from GROUPS where `NAME`='editOwnPosts'); +set @editOtherPosts_group_id = (select GROUP_ID from GROUPS where `NAME`='editOtherPosts'); +set @deleteOwnPosts_group_id = (select GROUP_ID from GROUPS where `NAME`='deleteOwnPosts'); +set @deleteOthersPosts_group_id = (select GROUP_ID from GROUPS where `NAME`='deleteOthersPosts'); +set @moveTopics_group_id = (select GROUP_ID from GROUPS where `NAME`='moveTopics'); +set @closetopics_group_id = (select GROUP_ID from GROUPS where `NAME`='closeTopics'); +set @createCodeReview_group_id = (select GROUP_ID from GROUPS where `NAME`='createCodeReview'); +set @leaveCommentsInCodeReview_group_id = (select GROUP_ID from GROUPS where `NAME`='leaveCommentsInCodeReview'); + +set @admin_group_sid := concat('usergroup:',@admin_group_id); +set @registered_group_sid := concat('usergroup:',@registered_group_id); +set @banned_group_sid := concat('usergroup:',@banned_group_id); +set @moderator_group_sid := concat('usergroup:',@moderator_group_id); +set @createPosts_group_sid = concat('usergroup:',@createPosts_group_id); +set @createStickedPosts_group_sid = concat('usergroup:',@createStickedPosts_group_id); +set @createAnnouncements_group_sid = concat('usergroup:',@createAnnouncements_group_id); +set @editOwnPosts_group_sid = concat('usergroup:',@editOwnPosts_group_id); +set @editOtherPosts_group_sid = concat('usergroup:',@editOtherPosts_group_id); +set @deleteOwnPosts_group_sid = concat('usergroup:',@deleteOwnPosts_group_id); +set @deleteOthersPosts_group_sid = concat('usergroup:',@deleteOthersPosts_group_id); +set @moveTopics_group_sid = concat('usergroup:',@moveTopics_group_id); +set @closetopics_group_sid = concat('usergroup:',@closetopics_group_id); +set @createCodeReview_group_sid = concat('usergroup:',@createCodeReview_group_id); +set @leaveCommentsInCodeReview_group_sid = concat('usergroup:',@leaveCommentsInCodeReview_group_id); -- GROUPS END +-- BRANCHES BEGIN insert ignore into BRANCHES (BRANCH_ID, UUID, `NAME`, DESCRIPTION, POSITION, SECTION_ID, MODERATORS_GROUP_ID) VALUES (1, UUID(), 'Curling', 'Brooms and stones', 0, 1, 1), (2, UUID(), 'Cricet', 'Balls and bats', 1, 1 ,1), @@ -84,27 +133,61 @@ insert ignore into BRANCHES (BRANCH_ID, UUID, `NAME`, DESCRIPTION, POSITION, SEC (46, UUID(), 'Theatre', 'Performances', 0, 10, 1), (47, UUID(), 'Cinema', 'New blockbusters', 1, 10 ,1), (48, UUID(), 'Exhibitions', 'Art', 2, 10 ,1), - (49, UUID(), 'Competitions', 'Some sport', 3, 10 ,1); + (49, UUID(), 'Competitions', 'Some sport', 3, 10 ,1), + (50, UUID(), 'Notification tests', 'All permissions for registered users', 0, 11 ,1), + (51, UUID(), 'Invisible Branch', 'used to check permission for admin', 0, 12, null); + -- BRANCHES END -- ****USERS CREATION BEGIN**** --- Creates a default users with admin/admin, registered/registered, moderator/moderator, banned/banned credentials to be able to log in without manual registration +-- Creates a default users with registered/registered, moderator/moderator, banned/banned credentials to be able to log in without manual registration +-- admin/admin has been already created by JC script. insert ignore into USERS (UUID, USERNAME, ENCODED_USERNAME, EMAIL, PASSWORD, ROLE, SALT, ENABLED) VALUES - ((SELECT UUID() FROM dual), 'admin', 'admin', 'admin@jtalks.org', MD5('admin'), 'USER_ROLE', '',true), ((SELECT UUID() FROM dual), 'registered', 'registered', 'registered@jtalks.org', MD5('registered'), 'USER_ROLE', '',true), ((SELECT UUID() FROM dual), 'moderator', 'moderator', 'moderator@jtalks.org', MD5('moderator'), 'USER_ROLE', '', true), - ((SELECT UUID() FROM dual), 'banned', 'banned', 'banned@jtalks.org', MD5('banned'), 'USER_ROLE', '', true); + ((SELECT UUID() FROM dual), 'banned', 'banned', 'banned@jtalks.org', MD5('banned'), 'USER_ROLE', '', true), + ((select UUID() from dual), 'post_creator', 'post_creator', 'post_creator@jtalks.org', MD5('qwerty'), 'USER_ROLE', '',true), + ((select UUID() from dual), 'sticked_post_creator', 'sticked_post_creator', 'sticked_post_creator@jtalks.org', MD5('qwerty'), 'USER_ROLE', '',true), + ((select UUID() from dual), 'announcement_creator', 'announcement_creator', 'announcement_creator@jtalks.org', MD5('qwerty'), 'USER_ROLE', '',true), + ((select UUID() from dual), 'ownpost_editor', 'ownpost_editor', 'ownpost_editor@jtalks.org', MD5('qwerty'), 'USER_ROLE', '',true), + ((select UUID() from dual), 'otherpost_editor', 'otherpost_editor', 'otherpost_editor@jtalks.org', MD5('qwerty'), 'USER_ROLE', '',true), + ((select UUID() from dual), 'ownpost_remover', 'ownpost_remover', 'ownpost_remover@jtalks.org', MD5('qwerty'), 'USER_ROLE', '',true), + ((select UUID() from dual), 'otherpost_remover', 'otherpost_remover', 'otherpost_remover@jtalks.org', MD5('qwerty'), 'USER_ROLE', '',true), + ((select UUID() from dual), 'topic_mover', 'topic_mover', 'topic_mover@jtalks.org', MD5('qwerty'), 'USER_ROLE', '',true), + ((select UUID() from dual), 'topic_closer', 'topic_closer', 'topic_closer@jtalks.org', MD5('qwerty'), 'USER_ROLE', '',true), + ((select UUID() from dual), 'codeReview_creator', 'codeReview_creator', 'codeReview_creator@jtalks.org', MD5('qwerty'), 'USER_ROLE', '',true), + ((select UUID() from dual), 'codeReview_commentator', 'codeReview_commentator', 'codeReview_commentator@jtalks.org', MD5('qwerty'), 'USER_ROLE', '',true); insert ignore into JC_USER_DETAILS (USER_ID, REGISTRATION_DATE, POST_COUNT) values - ((select ID from USERS where USERNAME = 'admin'), NOW(), 0), ((select ID from USERS where USERNAME = 'registered'), NOW(), 0), ((select ID from USERS where USERNAME = 'moderator'), NOW(), 0), - ((select ID from USERS where USERNAME = 'banned'), NOW(), 0) ; + ((select ID from USERS where USERNAME = 'banned'), NOW(), 0), + ((select ID from USERS where USERNAME = 'post_creator'), NOW(), 0), + ((select ID from USERS where USERNAME = 'sticked_post_creator'), NOW(), 0), + ((select ID from USERS where USERNAME = 'announcement_creator'), NOW(), 0), + ((select ID from USERS where USERNAME = 'ownpost_editor'), NOW(), 0), + ((select ID from USERS where USERNAME = 'otherpost_editor'), NOW(), 0), + ((select ID from USERS where USERNAME = 'ownpost_remover'), NOW(), 0), + ((select ID from USERS where USERNAME = 'otherpost_remover'), NOW(), 0), + ((select ID from USERS where USERNAME = 'topic_mover'), NOW(), 0), + ((select ID from USERS where USERNAME = 'topic_closer'), NOW(), 0), + ((select ID from USERS where USERNAME = 'codeReview_creator'), NOW(), 0), + ((select ID from USERS where USERNAME = 'codeReview_commentator'), NOW(), 0); -- ****USERS CREATION END**** -- Add users to appropriate groups insert ignore into GROUP_USER_REF select @registered_group_id, ID from USERS; insert ignore into GROUP_USER_REF select @moderator_group_id, ID from USERS where USERNAME in ('moderator', 'admin'); -insert ignore into GROUP_USER_REF select @admin_group_id, ID from USERS where USERNAME = 'admin'; insert ignore into GROUP_USER_REF select @banned_group_id, ID from USERS where USERNAME = 'banned'; +insert ignore into GROUP_USER_REF select @createPosts_group_id, ID from USERS where USERNAME = 'post_creator'; +insert ignore into GROUP_USER_REF select @createStickedPosts_group_id, ID from USERS where USERNAME = 'sticked_post_creator'; +insert ignore into GROUP_USER_REF select @createAnnouncements_group_id, ID from USERS where USERNAME = 'announcement_creator'; +insert ignore into GROUP_USER_REF select @editOwnPosts_group_id, ID from USERS where USERNAME = 'ownpost_editor'; +insert ignore into GROUP_USER_REF select @editOtherPosts_group_id, ID from USERS where USERNAME = 'otherpost_editor'; +insert ignore into GROUP_USER_REF select @deleteOwnPosts_group_id, ID from USERS where USERNAME = 'ownpost_remover'; +insert ignore into GROUP_USER_REF select @deleteOthersPosts_group_id, ID from USERS where USERNAME = 'otherpost_remover'; +insert ignore into GROUP_USER_REF select @moveTopics_group_id, ID from USERS where USERNAME = 'topic_mover'; +insert ignore into GROUP_USER_REF select @closetopics_group_id, ID from USERS where USERNAME = 'topic_closer'; +insert ignore into GROUP_USER_REF select @createCodeReview_group_id, ID from USERS where USERNAME = 'codeReview_creator'; +insert ignore into GROUP_USER_REF select @leaveCommentsInCodeReview_group_id, ID from USERS where USERNAME = 'codeReview_commentator'; set @component_acl_class=1; set @group_acl_class=2; @@ -112,109 +195,230 @@ set @branch_acl_class=3; insert ignore into acl_class values (@branch_acl_class,'BRANCH'), (@group_acl_class,'GROUP'), (@component_acl_class,'COMPONENT'); insert into acl_sid(principal, sid) values (0, @moderator_group_sid); +insert into acl_sid(principal, sid) values (0, @createPosts_group_sid); +insert into acl_sid(principal, sid) values (0, @createStickedPosts_group_sid); +insert into acl_sid(principal, sid) values (0, @createAnnouncements_group_sid); +insert into acl_sid(principal, sid) values (0, @editOwnPosts_group_sid); +insert into acl_sid(principal, sid) values (0, @editOtherPosts_group_sid); +insert into acl_sid(principal, sid) values (0, @deleteOwnPosts_group_sid); +insert into acl_sid(principal, sid) values (0, @deleteOthersPosts_group_sid); +insert into acl_sid(principal, sid) values (0, @moveTopics_group_sid); +insert into acl_sid(principal, sid) values (0, @closetopics_group_sid); +insert into acl_sid(principal, sid) values (0, @createCodeReview_group_sid); +insert into acl_sid(principal, sid) values (0, @leaveCommentsInCodeReview_group_sid); -SET @admin_group_sid_id := (select id from acl_sid where sid=@admin_group_sid); -SET @registered_group_sid_id := (select id from acl_sid where sid=@registered_group_sid); -SET @banned_group_sid_id := (select id from acl_sid where sid=@banned_group_sid); -SET @moderator_group_sid_id := (select id from acl_sid where sid=@moderator_group_sid); -SET @anonymous_sid_id := (select id from acl_sid where sid='user:anonymousUser'); +set @admin_group_sid_id := (select id from acl_sid where sid=@admin_group_sid); +set @registered_group_sid_id := (select id from acl_sid where sid=@registered_group_sid); +set @banned_group_sid_id := (select id from acl_sid where sid=@banned_group_sid); +set @moderator_group_sid_id := (select id from acl_sid where sid=@moderator_group_sid); +set @anonymous_sid_id := (select id from acl_sid where sid='user:anonymousUser'); +set @createPosts_group_sid_id = (select id from acl_sid where sid=@createPosts_group_sid); +set @createStickedPosts_group_sid_id = (select id from acl_sid where sid=@createStickedPosts_group_sid); +set @createAnnouncements_group_sid_id = (select id from acl_sid where sid=@createAnnouncements_group_sid); +set @editOwnPosts_group_sid_id = (select id from acl_sid where sid=@editOwnPosts_group_sid); +set @editOtherPosts_group_sid_id = (select id from acl_sid where sid=@editOtherPosts_group_sid); +set @deleteOwnPosts_group_sid_id = (select id from acl_sid where sid=@deleteOwnPosts_group_sid); +set @deleteOthersPosts_group_sid_id = (select id from acl_sid where sid=@deleteOthersPosts_group_sid); +set @moveTopics_group_sid_id = (select id from acl_sid where sid=@moveTopics_group_sid); +set @closetopics_group_sid_id = (select id from acl_sid where sid=@closetopics_group_sid); +set @createCodeReview_group_sid_id = (select id from acl_sid where sid=@createCodeReview_group_sid); +set @leaveCommentsInCodeReview_group_sid_id = (select id from acl_sid where sid=@leaveCommentsInCodeReview_group_sid); -- PERMISSIONS BEGIN -SET @SEND_PRIVATE_MESSAGES_MASK := 14; -SET @CREATE_FORUM_FAQ_MASK := 20; -SET @EDIT_OWN_PROFILE_MASK := 15; -SET @EDIT_OTHERS_PROFILE_MASK := 23; - -SET @VIEW_TOPICS_MASK := 6; -SET @MOVE_TOPICS_MASK := 8; -SET @CLOSE_TOPICS_MASK := 11; -SET @CREATE_POSTS_MASK := 12; -SET @DELETE_OWN_POSTS_MASK := 7; -SET @DELETE_OTHERS_POSTS_MASK := 13; -SET @EDIT_OWN_POSTS_MASK := 133; -SET @EDIT_OTHERS_POSTS_MASK := 17; -SET @CREATE_ANNOUNCEMENTS_MASK := 18; -SET @CREATE_STICKED_TOPICS_MASK := 19; -SET @CREATE_CODE_REVIEW_MASK := 21; -SET @LEAVE_COMMENTS_IN_CODE_REVIEW_MASK := 22; - -SET @ADMIN_MASK := 16; +set @SEND_PRIVATE_MESSAGES_MASK := 14; +set @CREATE_FORUM_FAQ_MASK := 20; +set @EDIT_OWN_PROFILE_MASK := 15; +set @EDIT_OTHERS_PROFILE_MASK := 23; + +set @VIEW_TOPICS_MASK := 6; +set @MOVE_TOPICS_MASK := 8; +set @CLOSE_TOPICS_MASK := 11; +set @CREATE_POSTS_MASK := 12; +set @DELETE_OWN_POSTS_MASK := 7; +set @DELETE_OTHERS_POSTS_MASK := 13; +set @EDIT_OWN_POSTS_MASK := 133; +set @EDIT_OTHERS_POSTS_MASK := 17; +set @CREATE_ANNOUNCEMENTS_MASK := 18; +set @CREATE_STICKED_TOPICS_MASK := 19; +set @CREATE_CODE_REVIEW_MASK := 21; +set @LEAVE_COMMENTS_IN_CODE_REVIEW_MASK := 22; + +set @ADMIN_MASK := 16; -- PERMISSIONS END -insert ignore into acl_object_identity - SELECT BranchTable.BRANCH_ID, @branch_acl_class, BranchTable.BRANCH_ID, NULL, 1, 1 +-- Records for branches will start from this ID+1 in acl_object_identity table +set @branches_acl_object_identity_id_start = (SELECT MAX(ID) FROM acl_object_identity); + +insert into acl_object_identity + SELECT @branches_acl_object_identity_id_start + BranchTable.BRANCH_ID, @branch_acl_class, BranchTable.BRANCH_ID, NULL, 1, 1 FROM (SELECT BRANCH_ID FROM BRANCHES) BranchTable; +-- owner получается anonymous user - is it right? set @branches_count = (SELECT COUNT(*) FROM BRANCHES); -set @registered_group_object_identity=@branches_count + 1; -set @admin_group_object_identity=@branches_count + 2; -set @banned_group_object_identity=@branches_count + 3; +set @registered_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 1; +set @admin_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 2; +set @banned_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 3; +set @createPosts_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 5; +set @createStickedPosts_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 6; +set @createAnnouncements_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 7; +set @editOwnPosts_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 8; +set @editOtherPosts_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 9; +set @deleteOwnPosts_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 10; +set @deleteOthersPosts_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 11; +set @moveTopics_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 12; +set @closetopics_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 13; +set @createCodeReview_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 14; +set @leaveCommentsInCodeReview_group_object_identity=@branches_acl_object_identity_id_start + @branches_count + 15; insert ignore into acl_object_identity values (@registered_group_object_identity, @group_acl_class, @registered_group_id, NULL, 1, 1), (@admin_group_object_identity, @group_acl_class, @admin_group_id, NULL, 1, 1), - (@banned_group_object_identity, @group_acl_class, @banned_group_id, NULL, 1, 1), - (53, @component_acl_class, @forum_component_id, NULL, 1, 1); + (@banned_group_object_identity, @group_acl_class, @banned_group_id, NULL, 1, 1); -- VIEW_TOPICS for anonymous users insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1000, @anonymous_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1000, @anonymous_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; -- permissions for registered users on all branches insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1001, @registered_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1001, @registered_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1002, @registered_group_sid_id, @CREATE_POSTS_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1002, @registered_group_sid_id, @CREATE_POSTS_MASK, 1, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1003, @registered_group_sid_id, @EDIT_OWN_POSTS_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1003, @registered_group_sid_id, @EDIT_OWN_POSTS_MASK, 1, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1004, @registered_group_sid_id, @DELETE_OWN_POSTS_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1004, @registered_group_sid_id, @DELETE_OWN_POSTS_MASK, 1, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1006, @registered_group_sid_id, @LEAVE_COMMENTS_IN_CODE_REVIEW_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1006, @registered_group_sid_id, @LEAVE_COMMENTS_IN_CODE_REVIEW_MASK, 1, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1007, @registered_group_sid_id, @CREATE_CODE_REVIEW_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1007, @registered_group_sid_id, @CREATE_CODE_REVIEW_MASK, 1, 0, 0 from BRANCHES; -- permissions for moderator users on all branches insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1008, @moderator_group_sid_id, @MOVE_TOPICS_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1008, @moderator_group_sid_id, @MOVE_TOPICS_MASK, 1, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1009, @moderator_group_sid_id, @CLOSE_TOPICS_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1009, @moderator_group_sid_id, @CLOSE_TOPICS_MASK, 1, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1010, @moderator_group_sid_id, @DELETE_OTHERS_POSTS_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1010, @moderator_group_sid_id, @DELETE_OTHERS_POSTS_MASK, 1, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1011, @moderator_group_sid_id, @EDIT_OTHERS_POSTS_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1011, @moderator_group_sid_id, @EDIT_OTHERS_POSTS_MASK, 1, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1012, @moderator_group_sid_id, @CREATE_STICKED_TOPICS_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1012, @moderator_group_sid_id, @CREATE_STICKED_TOPICS_MASK, 1, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1013, @moderator_group_sid_id, @CREATE_ANNOUNCEMENTS_MASK, 1, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1013, @moderator_group_sid_id, @CREATE_ANNOUNCEMENTS_MASK, 1, 0, 0 from BRANCHES; -- setting permissions for banned users insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1015, @banned_group_sid_id, @CREATE_POSTS_MASK, 0, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1015, @banned_group_sid_id, @CREATE_POSTS_MASK, 0, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1016, @banned_group_sid_id, @EDIT_OWN_POSTS_MASK, 0, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1016, @banned_group_sid_id, @EDIT_OWN_POSTS_MASK, 0, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1017, @banned_group_sid_id, @DELETE_OWN_POSTS_MASK, 0, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1017, @banned_group_sid_id, @DELETE_OWN_POSTS_MASK, 0, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1018, @banned_group_sid_id, @LEAVE_COMMENTS_IN_CODE_REVIEW_MASK, 0, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1018, @banned_group_sid_id, @LEAVE_COMMENTS_IN_CODE_REVIEW_MASK, 0, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1019, @banned_group_sid_id, @CREATE_CODE_REVIEW_MASK, 0, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1019, @banned_group_sid_id, @CREATE_CODE_REVIEW_MASK, 0, 0, 0 from BRANCHES; insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - select BRANCH_ID, 1020, @banned_group_sid_id, @VIEW_TOPICS_MASK, 0, 0, 0 from BRANCHES; + select @branches_acl_object_identity_id_start + BRANCH_ID, 1020, @banned_group_sid_id, @VIEW_TOPICS_MASK, 0, 0, 0 from BRANCHES; + +-- setting permissions for createPosts users on all branches +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1022, @createPosts_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1023, @createPosts_group_sid_id, @CREATE_POSTS_MASK, 1, 0, 0 from BRANCHES; + + -- setting permissions for createStickedPosts users on all branches +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1024, @createStickedPosts_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1025, @createStickedPosts_group_sid_id, @CREATE_POSTS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1026, @createStickedPosts_group_sid_id, @CREATE_STICKED_TOPICS_MASK, 1, 0, 0 from BRANCHES; + + -- setting permissions for createAnnouncements users on all branches +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1027, @createAnnouncements_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1028, @createAnnouncements_group_sid_id, @CREATE_POSTS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1029, @createAnnouncements_group_sid_id, @CREATE_ANNOUNCEMENTS_MASK, 1, 0, 0 from BRANCHES; + +-- setting permissions for editOwnPosts users on all branches +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1030, @editOwnPosts_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1031, @editOwnPosts_group_sid_id, @CREATE_POSTS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1032, @editOwnPosts_group_sid_id, @EDIT_OWN_POSTS_MASK, 1, 0, 0 from BRANCHES; + +-- setting permissions for editOtherPosts users on all branches +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1033, @editOtherPosts_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1034, @editOtherPosts_group_sid_id, @EDIT_OTHERS_POSTS_MASK, 1, 0, 0 from BRANCHES; + +-- setting permissions for deleteOwnPosts users on all branches +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1035, @deleteOwnPosts_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1036, @deleteOwnPosts_group_sid_id, @CREATE_POSTS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1037, @deleteOwnPosts_group_sid_id, @DELETE_OWN_POSTS_MASK, 1, 0, 0 from BRANCHES; + +-- setting permissions for deleteOtherPosts users on all branches +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1038, @deleteOthersPosts_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1039, @deleteOthersPosts_group_sid_id, @CREATE_POSTS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1040, @deleteOthersPosts_group_sid_id, @DELETE_OTHERS_POSTS_MASK, 1, 0, 0 from BRANCHES; + +-- setting permissions for moveTopics users on all branches +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1041, @moveTopics_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1042, @moveTopics_group_sid_id, @MOVE_TOPICS_MASK, 1, 0, 0 from BRANCHES; + +-- setting permissions for closetopics users on all branches +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1043, @closetopics_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1044, @closetopics_group_sid_id, @CLOSE_TOPICS_MASK, 1, 0, 0 from BRANCHES; + +-- setting permissions for createCodeReview users on all branches +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1045, @createCodeReview_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1046, @createCodeReview_group_sid_id, @CREATE_CODE_REVIEW_MASK, 1, 0, 0 from BRANCHES; + +-- setting permissions for leaveCommentsInCodeReview users on all branches +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1047, @leaveCommentsInCodeReview_group_sid_id, @VIEW_TOPICS_MASK, 1, 0, 0 from BRANCHES; +insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + select @branches_acl_object_identity_id_start + BRANCH_ID, 1048, @leaveCommentsInCodeReview_group_sid_id, @LEAVE_COMMENTS_IN_CODE_REVIEW_MASK, 1, 0, 0 from BRANCHES; + +-- deleting all permissions for all user groups from BRANCH_ID = 51 +delete from acl_entry where acl_object_identity = @branches_acl_object_identity_id_start + 51; -- personal permissions -insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - values (@registered_group_object_identity, 1000, @registered_group_sid_id, @SEND_PRIVATE_MESSAGES_MASK, 1, 0, 0); -insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - values (@registered_group_object_identity, 1001, @registered_group_sid_id, @EDIT_OWN_PROFILE_MASK, 1, 0, 0); +-- variables for prevention key duplicate from groups which were created via poulpe and via script +set @registered_group_personal_identity = (SELECT id FROM acl_object_identity WHERE object_id_class = @group_acl_class AND object_id_identity = @registered_group_id); +set @admin_group_personal_identity = (SELECT id FROM acl_object_identity WHERE object_id_class = @group_acl_class AND object_id_identity = @admin_group_id); +set @banned_group_personal_identity = (SELECT id FROM acl_object_identity WHERE object_id_class = @group_acl_class AND object_id_identity = @banned_group_id); + +-- registered +insert ignore into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + values (@registered_group_personal_identity, 1000, @registered_group_sid_id, @SEND_PRIVATE_MESSAGES_MASK, 1, 0, 0); +insert ignore into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + values (@registered_group_personal_identity, 1001, @registered_group_sid_id, @EDIT_OWN_PROFILE_MASK, 1, 0, 0); -- admin -insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - values (@admin_group_object_identity, 1002, @admin_group_sid_id, @EDIT_OTHERS_PROFILE_MASK, 1, 0, 0); +insert ignore into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + values (@admin_group_personal_identity, 1002, @admin_group_sid_id, @EDIT_OTHERS_PROFILE_MASK, 1, 0, 0); -- banned -insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - values (@banned_group_object_identity, 1003, @banned_group_sid_id, @SEND_PRIVATE_MESSAGES_MASK, 0, 0, 0); -insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - values (@banned_group_object_identity, 1004, @banned_group_sid_id, @EDIT_OWN_PROFILE_MASK, 0, 0, 0); +insert ignore into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + values (@banned_group_personal_identity, 1003, @banned_group_sid_id, @SEND_PRIVATE_MESSAGES_MASK, 0, 0, 0); +insert ignore into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) + values (@banned_group_personal_identity, 1004, @banned_group_sid_id, @EDIT_OWN_PROFILE_MASK, 0, 0, 0); -- admin permissions for the component insert into acl_entry (acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) - values (@admin_group_object_identity, 1000, @admin_group_sid_id, @ADMIN_MASK, 1, 0, 0); \ No newline at end of file + values (@admin_group_personal_identity, 1000, @admin_group_sid_id, @ADMIN_MASK, 1, 0, 0); \ No newline at end of file diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/GroupHibernateDaoTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/GroupHibernateDaoTest.java index 3926f57260..f4e977bd07 100644 --- a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/GroupHibernateDaoTest.java +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/GroupHibernateDaoTest.java @@ -19,10 +19,11 @@ import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.entity.Section; import org.jtalks.jcommune.model.dao.GroupDao; -import org.jtalks.jcommune.model.entity.PersistedObjectsFactory; +import org.jtalks.jcommune.model.dto.GroupAdministrationDto; import org.jtalks.jcommune.model.entity.Branch; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.ObjectsFactory; +import org.jtalks.jcommune.model.entity.PersistedObjectsFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests; @@ -31,10 +32,18 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import java.util.List; - +import javax.annotation.Nonnull; +import javax.annotation.Resource; +import javax.sql.DataSource; +import java.sql.SQLException; +import java.text.Collator; +import java.text.ParseException; +import java.text.RuleBasedCollator; +import java.util.*; + +import static io.qala.datagen.RandomShortApi.alphanumeric; +import static io.qala.datagen.RandomValue.between; import static org.testng.Assert.*; -import static org.testng.Assert.assertEquals; import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals; /** @@ -45,7 +54,22 @@ @Transactional public class GroupHibernateDaoTest extends AbstractTransactionalTestNGSpringContextTests { static final String NO_FILTER = ""; - + private static final String SORTING_TEST_RULES = + "< A< a< B< b< C< c< D< d< E< e< F< f< G< g< " + + "H< h< I< i< J< j< K< k< L< l< M< m< " + + "N< n< O< o< P< p< Q< q< R< r< S< s< " + + "T< t< U< u< V< v< W< w< X< x< Y< y< Z< z< " + + "А< а< Б< б< В< в< Г< г< Д< д< Е< е< " + + "Ё< ё< Ж< ж< З< з< И< и< Й< й< К< к< " + + "Л< л< М< м< Н< н< О< о< П< п< Р< р< " + + "С< с< Т< т< У< у< Ф< ф< Х< х< Ц< ц< " + + "Ч< ч< Ш< ш< Щ< щ< Ь< ь< Ы< ы< Ъ< ъ< " + + "Э< э< Ю< ю< Я< я"; + private static final String DICTIONARY_RU = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯ" + + "абвгдеёжзийклмнопрстуфхцчшщьыъэюя" + + "0123456789"; + private static final int MIN_GROUP_NAME_LENGTH = 1; + private static final int MAX_GROUP_NAME_LENGTH = Group.GROUP_NAME_MAX_LENGTH; @Autowired private GroupDao groupDao; @@ -54,6 +78,9 @@ public class GroupHibernateDaoTest extends AbstractTransactionalTestNGSpringCont private Session session; + @Resource(lookup = "org/jtalks/jcommune/model/datasource.properties") + private DataSource dataSource; + @BeforeMethod public void setUp() throws Exception { session = sessionFactory.getCurrentSession(); @@ -101,11 +128,21 @@ public void testGetAll() { saveAndEvict(group1); List actual = groupDao.getAll(); + sortById(actual); assertEquals(actual.size(), 2); assertReflectionEquals(actual.get(0), group0); assertReflectionEquals(actual.get(1), group1); } + private void sortById(List groups) { + Collections.sort(groups, new Comparator() { + @Override + public int compare(@Nonnull Group group, @Nonnull Group group1) { + return Long.compare(group.getId(), group1.getId()); + } + }); + } + @Test public void testGetByNameContains() { Group group = ObjectsFactory.getRandomGroup(); @@ -227,6 +264,51 @@ private void saveAndEvict(Iterable users) { } } + /** + * Works properly only with MySql database + * @throws ParseException + */ + @Test + public void listOfGroupsMustBeSortedAlphabetically() throws ParseException{ + if (isMySql()){ + List expected = new LinkedList<>(); + for (int i = 0; i < 10; i++) { + Group en = new Group(alphanumeric(MIN_GROUP_NAME_LENGTH, MAX_GROUP_NAME_LENGTH)); + Group ru = new Group(between(MIN_GROUP_NAME_LENGTH, MAX_GROUP_NAME_LENGTH).string(DICTIONARY_RU)); + saveAndEvict(en); + saveAndEvict(ru); + expected.add(new GroupAdministrationDto(en.getName(), en.getUsers().size())); + expected.add(new GroupAdministrationDto(ru.getName(), ru.getUsers().size())); + } + sortByName(expected); + List actual = groupDao.getGroupNamesWithCountOfUsers(); + assertReflectionEquals(expected, actual); + } + } -} + private void sortByName(List dtoList) throws ParseException { + RuleBasedCollator enUS = (RuleBasedCollator) Collator.getInstance(new Locale("en", "US")); + final RuleBasedCollator finalCollator = new RuleBasedCollator(enUS.getRules() + SORTING_TEST_RULES); + Collections.sort(dtoList, new Comparator() { + @Override + public int compare(GroupAdministrationDto o1, GroupAdministrationDto o2) { + return finalCollator.compare(o1.getName(), o2.getName()); + } + }); + } + /** + * listOfGroupsMustBeSortedAlphabetically test can run only with MySql database, + * so we need to check whether jdbc driver is MySql + * @return + */ + private boolean isMySql(){ + String driverName = ""; + try { + driverName = dataSource.getConnection().getMetaData().getDriverName(); + } catch (SQLException e) { + logger.warn(e.getMessage()); + } + return driverName.equalsIgnoreCase("MySQL Connector Java"); + } +} \ No newline at end of file diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/LastReadPostHibernateDaoTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/LastReadPostHibernateDaoTest.java index f0dd232356..26810b0fc8 100644 --- a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/LastReadPostHibernateDaoTest.java +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/LastReadPostHibernateDaoTest.java @@ -24,6 +24,7 @@ import org.jtalks.jcommune.model.entity.LastReadPost; import org.jtalks.jcommune.model.entity.Topic; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.testng .AbstractTransactionalTestNGSpringContextTests; @@ -84,6 +85,16 @@ public void dateOfTheLastReadPostShouldBeUpdated() { "Update doesn't work, because field value didn't change."); } + @Test(expectedExceptions = DataIntegrityViolationException.class) + public void saveOrUpdateShouldFailWhenSavingDuplicate() { + LastReadPost post = PersistedObjectsFactory.getDefaultLastReadPost(); + session.save(post); + + // Create entity with the same user_id and post_id + LastReadPost otherPost = new LastReadPost(post.getUser(), post.getTopic(), new DateTime()); + lastReadPostDao.saveOrUpdate(otherPost); + } + @Test public void testMarkAsReadTopicsToUser() { List topics = PersistedObjectsFactory.createAndSaveTopicListWithPosts(10); diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/PostCommentHibernateDaoTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/PostCommentHibernateDaoTest.java index 172b37927a..6feac48700 100644 --- a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/PostCommentHibernateDaoTest.java +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/PostCommentHibernateDaoTest.java @@ -45,12 +45,12 @@ public void setUp() throws Exception { session = sessionFactory.getCurrentSession(); PersistedObjectsFactory.setSession(session); } - + /*===== Common methods =====*/ @Test public void testGet() { - PostComment comment = PersistedObjectsFactory.getDefaultPostComment(); + PostComment comment = PersistedObjectsFactory.getModifiedPostComment(); session.save(comment); flushAndClearSession(); @@ -61,6 +61,8 @@ public void testGet() { assertEquals(result.getBody(), comment.getBody()); assertEquals(result.getCreationDate(), comment.getCreationDate()); assertEquals(result.getAuthor(), comment.getAuthor()); + assertEquals(result.getUserChanged(), comment.getUserChanged()); + assertEquals(result.getModificationDate(), comment.getModificationDate()); } diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/PostHibernateDaoTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/PostHibernateDaoTest.java index 5e16dc27a6..d97920515e 100644 --- a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/PostHibernateDaoTest.java +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/PostHibernateDaoTest.java @@ -390,6 +390,20 @@ public void testOrphanCommentRemoving() { } + @Test + public void commentMarkedAsDeletedShouldExistInDatabase() { + Post post = PersistedObjectsFactory.getDefaultPost(); + PostComment comment = PersistedObjectsFactory.getDefaultPostComment(); + comment.setDeletionDate(new DateTime()); + post.addComment(comment); + session.save(post); + flushAndClearSession(); + + PostComment commentFromDb = (PostComment)session.get(PostComment.class, comment.getId()); + + assertNotNull(commentFromDb); + } + @Test public void testChangeRating() { Post post = PersistedObjectsFactory.getDefaultPost(); diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/SpamRuleHibernateDaoTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/SpamRuleHibernateDaoTest.java new file mode 100644 index 0000000000..9d5e0c3666 --- /dev/null +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/SpamRuleHibernateDaoTest.java @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.model.dao.hibernate; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.jtalks.jcommune.model.dao.SpamRuleDao; +import org.jtalks.jcommune.model.entity.SpamRule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests; +import org.springframework.test.context.transaction.TransactionConfiguration; +import org.springframework.transaction.annotation.Transactional; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; + +import static io.qala.datagen.RandomValue.length; +import static io.qala.datagen.StringModifier.Impls.suffix; +import static org.jtalks.jcommune.model.utils.SpamRuleUtils.listOfRandomSpamRules; +import static org.jtalks.jcommune.model.utils.SpamRuleUtils.randomSpamRule; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals; + +/** + * @author Oleg Tkachenko + */ +@ContextConfiguration(locations = {"classpath:/org/jtalks/jcommune/model/entity/applicationContext-dao.xml"}) +@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true) +@Transactional +public class SpamRuleHibernateDaoTest extends AbstractTransactionalTestNGSpringContextTests { + private @Autowired SpamRuleDao dao; + private @Autowired SessionFactory sessionFactory; + + @Test + public void shouldReturnAllSavedRules() { + List expectedList = persistObjects(listOfRandomSpamRules(10)); + List actualList = dao.getAllRules(); + assertEquals(actualList.size(), expectedList.size()); + for (SpamRule expected : expectedList) { + assertTrue(actualList.contains(expected)); + } + } + + @Test + public void shouldReturnAllEnabledRules(){ + List randomSpamRules = persistObjects(listOfRandomSpamRules(10)); + List expectedList = getEnabledRulesFrom(randomSpamRules); + List actualList = dao.getEnabledRules(); + assertEquals(actualList.size(), expectedList.size()); + for (SpamRule expected : expectedList) { + assertTrue(actualList.contains(expected)); + } + } + + @Test + public void shouldNotBeAbleToInsertSqlInjection() { + String sqlInjection = length(100).with(suffix("'\"")).alphanumeric(); + SpamRule expected = randomSpamRule().setRegex(sqlInjection).setDescription(sqlInjection); + dao.saveOrUpdate(expected); + flushAndClearCurrentSession(); + SpamRule actual = dao.get(expected.getId()); + assertReflectionEquals(expected, actual); + } + + private List getEnabledRulesFrom(List rules) { + List enabledRules = new ArrayList<>(rules.size()); + for (SpamRule spamRule : rules) { + if (spamRule.isEnabled()) enabledRules.add(spamRule); + } + return enabledRules; + } + + private List persistObjects(List objects){ + Session session = sessionFactory.getCurrentSession(); + for (SpamRule object : objects) { + session.save(object); + } + flushAndClearCurrentSession(); + return objects; + } + + private void flushAndClearCurrentSession(){ + Session session = sessionFactory.getCurrentSession(); + session.flush(); + session.clear(); + } +} diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/TopicDraftHibernateDaoTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/TopicDraftHibernateDaoTest.java new file mode 100644 index 0000000000..629b099781 --- /dev/null +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/TopicDraftHibernateDaoTest.java @@ -0,0 +1,153 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.dao.hibernate; + +import org.hibernate.SessionFactory; +import org.hibernate.classic.Session; +import org.jtalks.jcommune.model.dao.TopicDraftDao; +import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.ObjectsFactory; +import org.jtalks.jcommune.model.entity.PersistedObjectsFactory; +import org.jtalks.jcommune.model.entity.TopicDraft; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests; +import org.springframework.test.context.transaction.TransactionConfiguration; +import org.springframework.transaction.annotation.Transactional; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import javax.validation.ConstraintViolationException; + +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals; + +/** + * @author Dmitry S. Dolzhenko + */ +@ContextConfiguration(locations = {"classpath:/org/jtalks/jcommune/model/entity/applicationContext-dao.xml"}) +@TransactionConfiguration(transactionManager = "transactionManager", defaultRollback = true) +@Transactional +public class TopicDraftHibernateDaoTest extends AbstractTransactionalTestNGSpringContextTests { + @Autowired + private SessionFactory sessionFactory; + @Autowired + private TopicDraftDao dao; + + private Session session; + + @BeforeMethod + public void setUp() throws Exception { + session = sessionFactory.getCurrentSession(); + PersistedObjectsFactory.setSession(session); + } + + /*===== Common methods =====*/ + + @Test + public void getShouldReturnDraftByValidId() { + TopicDraft topicDraft = PersistedObjectsFactory.getDefaultTopicDraft(); + + session.flush(); + session.clear(); + + TopicDraft result = dao.get(topicDraft.getId()); + + assertNotNull(result); + assertReflectionEquals(topicDraft, result); + } + + @Test + public void getShouldReturnNullWhenSearchingByInvalidId() { + assertNull(dao.get(-567890L)); + } + + @Test + public void saveOrUpdateShouldUpdateExistingDraft() { + TopicDraft topicDraft = PersistedObjectsFactory.getDefaultTopicDraft(); + + session.flush(); + session.clear(); + + dao.saveOrUpdate(topicDraft); + + session.flush(); + session.clear(); + + TopicDraft result = dao.get(topicDraft.getId()); + + assertReflectionEquals(topicDraft, result); + } + + /*===== TopicDraftDao specific methods =====*/ + + @Test + public void getForUserShouldReturnDraftIfUserHasOne() { + JCUser user = PersistedObjectsFactory.getDefaultUser(); + TopicDraft topicDraft = ObjectsFactory.getDefaultTopicDraft(); + + topicDraft.setTopicStarter(user); + session.save(topicDraft); + + session.flush(); + session.clear(); + + TopicDraft result = dao.getForUser(user); + + assertNotNull(result); + assertReflectionEquals(user, result.getTopicStarter()); + } + + @Test + public void getForUserShouldReturnNullIfUserHasNoDraft() { + JCUser user = PersistedObjectsFactory.getDefaultUser(); + + session.flush(); + session.clear(); + + assertNull(dao.getForUser(user)); + } + + @Test(expectedExceptions = ConstraintViolationException.class) + public void saveOrUpdateShouldThrowConstraintViolationExceptionIfAllContentFieldsIsNull() { + JCUser user = PersistedObjectsFactory.getDefaultUser(); + + TopicDraft topicDraft = new TopicDraft(); + topicDraft.setTopicStarter(user); + + topicDraft.setTitle(null); + topicDraft.setContent(null); + topicDraft.setPollTitle(null); + topicDraft.setPollItemsValue(null); + + dao.saveOrUpdate(topicDraft); + } + + @Test + public void deleteByUserShouldDeleteTopicDraft() { + TopicDraft topicDraft = PersistedObjectsFactory.getDefaultTopicDraft(); + + session.flush(); + session.clear(); + + dao.deleteByUser(topicDraft.getTopicStarter()); + + session.flush(); + session.clear(); + + assertNull(dao.getForUser(topicDraft.getTopicStarter())); + } +} diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/TopicHibernateDaoTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/TopicHibernateDaoTest.java index 30fba2f4aa..f6a56b9a28 100644 --- a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/TopicHibernateDaoTest.java +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/TopicHibernateDaoTest.java @@ -261,7 +261,7 @@ public void testGetUpdateTopicsUpdatedForNoneExistingBranchAndUserGroup() { @Test public void testGetUnansweredTopicsForRegisteredUsers() { - JCUser user = createAndSaveTopicsWithUnansweredTopics(); + JCUser user = createAndSaveBranchesWithUnansweredTopics(); PageRequest pageRequest = new PageRequest("1", 2); Page result = dao.getUnansweredTopics(pageRequest, user); @@ -271,7 +271,7 @@ public void testGetUnansweredTopicsForRegisteredUsers() { @Test public void testGetUnansweredTopicsForAnonymousUsers() { - createAndSaveTopicsWithUnansweredTopics(); + createAndSaveBranchesWithUnansweredTopics(); PageRequest pageRequest = new PageRequest("1", 2); Page result = dao.getUnansweredTopics(pageRequest, new AnonymousUser()); @@ -281,7 +281,7 @@ public void testGetUnansweredTopicsForAnonymousUsers() { @Test public void testGetUnansweredTopicsWithPaging() { - JCUser user = createAndSaveTopicsWithUnansweredTopics(); + JCUser user = createAndSaveBranchesWithUnansweredTopics(); PageRequest pageRequest = new PageRequest("2", 1); Page result = dao.getUnansweredTopics(pageRequest, user); assertEquals(result.getContent().size(), 1); @@ -290,7 +290,7 @@ public void testGetUnansweredTopicsWithPaging() { @Test public void testGetUnansweredTopicsWithPagingPageTooLow() { - JCUser user = createAndSaveTopicsWithUnansweredTopics(); + JCUser user = createAndSaveBranchesWithUnansweredTopics(); PageRequest pageRequest = new PageRequest("0", 1); Page result = dao.getUnansweredTopics(pageRequest, user); assertEquals(result.getContent().size(), 1); @@ -300,7 +300,7 @@ public void testGetUnansweredTopicsWithPagingPageTooLow() { @Test public void testGetUnansweredTopicsWithPagingPageTooBig() { - JCUser user = createAndSaveTopicsWithUnansweredTopics(); + JCUser user = createAndSaveBranchesWithUnansweredTopics(); PageRequest pageRequest = new PageRequest("1000", 1); Page result = dao.getUnansweredTopics(pageRequest, user); assertEquals(result.getContent().size(), 1); @@ -308,7 +308,7 @@ public void testGetUnansweredTopicsWithPagingPageTooBig() { assertEquals(result.getNumber(), 2); } - private JCUser createAndSaveTopicsWithUnansweredTopics() { + private JCUser createAndSaveBranchesWithUnansweredTopics() { JCUser author = PersistedObjectsFactory.getDefaultUserWithGroups(); Branch branch = ObjectsFactory.getDefaultBranch(); @@ -602,6 +602,54 @@ public void topicAttributeShouldBeUpdatedByCascade() { assertEquals("newValue", result.getAttributes().get("name")); } + @Test + public void postDraftShouldBeSavedByCascade() { + Topic topic = PersistedObjectsFactory.getDefaultTopic(); + PostDraft draft = new PostDraft("content", PersistedObjectsFactory.getDefaultUser()); + + topic.addDraft(draft); + dao.saveOrUpdate(topic); + flushAndClearSession(); + + PostDraft result = (PostDraft)session.get(PostDraft.class, draft.getId()); + + assertReflectionEquals(draft, result); + } + + @Test + public void postDraftShouldBeUpdatedByCascade() { + Topic topic = PersistedObjectsFactory.getDefaultTopic(); + PostDraft draft = new PostDraft("content", PersistedObjectsFactory.getDefaultUser()); + topic.addDraft(draft); + dao.saveOrUpdate(topic); + flushAndClearSession(); + String newContent = "newContent"; + + topic.getDrafts().get(0).setContent(newContent); + dao.saveOrUpdate(topic); + flushAndClearSession(); + + PostDraft result = (PostDraft)session.get(PostDraft.class, draft.getId()); + + assertEquals(result.getContent(), newContent); + } + + @Test + public void postDraftShouldBeDeletedByCascade() { + Topic topic = PersistedObjectsFactory.getDefaultTopic(); + PostDraft draft = new PostDraft("content", PersistedObjectsFactory.getDefaultUser()); + topic.addDraft(draft); + dao.saveOrUpdate(topic); + flushAndClearSession(); + + topic.getDrafts().clear(); + dao.saveOrUpdate(topic); + + PostDraft result = (PostDraft)session.get(PostDraft.class, draft.getId()); + + assertNull(result); + } + private void flushAndClearSession() { session.flush(); session.clear(); diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/UserContactsHibernateDaoTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/UserContactsHibernateDaoTest.java index 4f1f8c300c..204d67fd6f 100644 --- a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/UserContactsHibernateDaoTest.java +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/UserContactsHibernateDaoTest.java @@ -71,7 +71,6 @@ public void testGet() { assertEquals(type.getTypeName(), result.getTypeName()); assertEquals(type.getMask(), result.getMask()); assertEquals(type.getDisplayPattern(), result.getDisplayPattern()); - assertEquals(type.getValidationPattern(), result.getValidationPattern()); } @Test @@ -99,7 +98,6 @@ public void testUpdate() { assertEquals(type.getTypeName(), result.getTypeName()); assertEquals(type.getMask(), result.getMask()); assertEquals(type.getDisplayPattern(), result.getDisplayPattern()); - assertEquals(type.getValidationPattern(), result.getValidationPattern()); } diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/UserHibernateDaoTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/UserHibernateDaoTest.java index a27da23bd0..acccd3d7f6 100644 --- a/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/UserHibernateDaoTest.java +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/dao/hibernate/UserHibernateDaoTest.java @@ -19,6 +19,7 @@ import org.jtalks.common.model.dao.GroupDao; import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.entity.User; +import org.jtalks.jcommune.model.dto.UserDto; import org.jtalks.jcommune.model.entity.PersistedObjectsFactory; import org.jtalks.jcommune.model.dao.UserDao; import org.jtalks.jcommune.model.entity.JCUser; @@ -29,15 +30,18 @@ import org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests; import org.springframework.test.context.transaction.TransactionConfiguration; import org.springframework.transaction.annotation.Transactional; +import org.testng.Assert; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; +import static io.qala.datagen.RandomShortApi.bool; +import static io.qala.datagen.RandomShortApi.integer; +import static io.qala.datagen.RandomShortApi.sample; import static java.util.Arrays.asList; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; import static org.testng.Assert.*; import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals; @@ -50,7 +54,7 @@ @Transactional public class UserHibernateDaoTest extends AbstractTransactionalTestNGSpringContextTests { @Autowired - private UserDao dao; + private UserDao userDao; @Autowired private GroupDao groupDao; @Autowired @@ -69,7 +73,7 @@ public void setUp() throws Exception { public void testSave() { JCUser user = ObjectsFactory.getDefaultUser(); - dao.saveOrUpdate(user); + userDao.saveOrUpdate(user); assertNotSame(user.getId(), 0, "Id not created"); @@ -84,8 +88,8 @@ public void testSaveUserWithUniqueViolation() { JCUser user = ObjectsFactory.getDefaultUser(); JCUser user2 = ObjectsFactory.getDefaultUser(); - dao.saveOrUpdate(user); - dao.saveOrUpdate(user2); + userDao.saveOrUpdate(user); + userDao.saveOrUpdate(user2); } @Test @@ -93,7 +97,7 @@ public void testGet() { JCUser user = ObjectsFactory.getDefaultUser(); session.save(user); - JCUser result = dao.get(user.getId()); + JCUser result = userDao.get(user.getId()); assertNotNull(result); assertEquals(result.getId(), user.getId()); @@ -101,7 +105,7 @@ public void testGet() { @Test public void testGetInvalidId() { - JCUser result = dao.get(-567890L); + JCUser result = userDao.get(-567890L); assertNull(result); } @@ -112,7 +116,7 @@ public void testUpdate() { JCUser user = ObjectsFactory.getDefaultUser(); session.save(user); user.setFirstName(newName); - dao.saveOrUpdate(user); + userDao.saveOrUpdate(user); session.flush(); session.evict(user); JCUser result = (JCUser) session.get(JCUser.class, user.getId());//! @@ -124,7 +128,7 @@ public void testUpdateNotNullViolation() { JCUser user = ObjectsFactory.getDefaultUser(); session.save(user); user.setEmail(null); - dao.saveOrUpdate(user); + userDao.saveOrUpdate(user); session.flush(); } @@ -133,7 +137,7 @@ public void testDelete() { JCUser user = ObjectsFactory.getDefaultUser(); session.save(user); - boolean result = dao.delete(user.getId()); + boolean result = userDao.delete(user.getId()); int userCount = getCount(); assertTrue(result, "Entity is not deleted"); @@ -142,7 +146,7 @@ public void testDelete() { @Test public void testDeleteInvalidId() { - boolean result = dao.delete(-100500L); + boolean result = userDao.delete(-100500L); assertFalse(result, "Entity deleted"); } @@ -154,7 +158,7 @@ public void testGetByUsernameSameCase() { JCUser user = ObjectsFactory.getDefaultUser(); session.save(user); - JCUser result = dao.getByUsername(user.getUsername()); + JCUser result = userDao.getByUsername(user.getUsername()); assertNotNull(result); assertReflectionEquals(user, result); @@ -165,7 +169,7 @@ public void testGetByUsernameDifferentCases() { JCUser user = ObjectsFactory.getDefaultUser(); session.save(user); - JCUser result = dao.getByUsername(user.getUsername().toUpperCase()); + JCUser result = userDao.getByUsername(user.getUsername().toUpperCase()); assertNotNull(result); assertReflectionEquals(user, result); @@ -177,7 +181,7 @@ public void testGetByUsernameMultipleUsersWithSameNameWhenIgnoringCase() { session.save(user); session.save(ObjectsFactory.getUser("Username", "Username@mail.com")); - JCUser result = dao.getByUsername("usernamE"); + JCUser result = userDao.getByUsername("usernamE"); assertNotNull(result); assertReflectionEquals(user, result); @@ -188,7 +192,7 @@ public void testGetByUsernameNotExist() { JCUser user = ObjectsFactory.getDefaultUser(); session.save(user); - JCUser result = dao.getByUsername("Name"); + JCUser result = userDao.getByUsername("Name"); assertNull(result); } @@ -198,7 +202,7 @@ public void testGetByUsernameNotFoundWhenMultipleUsersWithSameNameWhenIgnoringCa session.save(ObjectsFactory.getUser("usernamE", "username@mail.com")); session.save(ObjectsFactory.getUser("Username", "Username@mail.com")); - JCUser result = dao.getByUsername("username"); + JCUser result = userDao.getByUsername("username"); assertNull(result); } @@ -207,7 +211,7 @@ public void testGetByUsernameNotFoundWhenMultipleUsersWithSameNameWhenIgnoringCa public void getCommonUserByUsernameShouldFindOne() { User expected = givenCommonUserWithUsernameStoredInDb("username"); - User actual = dao.getCommonUserByUsername("username"); + User actual = userDao.getCommonUserByUsername("username"); assertNotNull(actual); assertReflectionEquals(actual, expected); } @@ -216,13 +220,13 @@ public void getCommonUserByUsernameShouldFindOne() { public void getCommonUserByUsernameShouldNotFind() { givenCommonUserWithUsernameStoredInDb("username"); - User actual = dao.getCommonUserByUsername("wrong username there is no such user"); + User actual = userDao.getCommonUserByUsername("wrong username there is no such user"); assertNull(actual); } @Test public void getCommonUserByUsernameShouldNotFindInEmptyDb() { - User actual = dao.getCommonUserByUsername("username"); + User actual = userDao.getCommonUserByUsername("username"); assertNull(actual); } @@ -232,7 +236,7 @@ public void testGetByUuid() { String uuid = user.getUuid(); session.save(user); - JCUser result = dao.getByUuid(uuid); + JCUser result = userDao.getByUuid(uuid); assertReflectionEquals(user, result); } @@ -242,7 +246,7 @@ public void testGetByUuidNotExist() { JCUser user = ObjectsFactory.getDefaultUser(); session.save(user); - JCUser result = dao.getByUuid("uuid"); + JCUser result = userDao.getByUuid("uuid"); assertNull(result); } @@ -251,7 +255,7 @@ public void testGetByUuidNotExist() { public void testFetchByEMail() { JCUser user = ObjectsFactory.getDefaultUser(); session.save(user); - assertNotNull(dao.getByEmail(user.getEmail())); + assertNotNull(userDao.getByEmail(user.getEmail())); } @Test @@ -262,7 +266,7 @@ public void testFetchNonActivatedAccounts() { session.save(activated); session.save(nonActivated); - Collection users = dao.getNonActivatedUsers(); + Collection users = userDao.getNonActivatedUsers(); assertTrue(users.contains(nonActivated)); assertEquals(users.size(), 1); @@ -296,7 +300,7 @@ public void getByUsernamesShouldReturnExistsInRepoUsers() { JCUser firstExistsUser = givenJCUserWithUsernameStoredInDb(firstExistsUsername); Set existsUsernames = new HashSet<>(asList(firstExistsUsername)); - List foundByUsernames = dao.getByUsernames(existsUsernames); + List foundByUsernames = userDao.getByUsernames(existsUsernames); assertTrue(foundByUsernames.size() == existsUsernames.size(), "It should return all users by their names."); assertTrue(foundByUsernames.contains(firstExistsUser), firstExistsUser.getUsername() + "should be found by his name."); @@ -307,7 +311,7 @@ public void addingValidUserToValidGroupShouldSucceed() { JCUser user = givenJCUserWithUsernameStoredInDb("test-user"); Group group = PersistedObjectsFactory.group("test-group"); user.addGroup(group); - dao.saveOrUpdate(user); + userDao.saveOrUpdate(user); flushAndClearSession(session); Group selected = groupDao.get(group.getId()); @@ -319,7 +323,7 @@ public void addingValidUserToValidGroupShouldSucceed() { public void getByUsernamesShouldReturnEmptyListWhenFoundUsersDoNotExist() { Set existsUsernames = new HashSet<>(asList("Shogun", "jk1", "masyan")); - List foundByUsernames = dao.getByUsernames(existsUsernames); + List foundByUsernames = userDao.getByUsernames(existsUsernames); assertTrue(foundByUsernames.isEmpty(), "It should return empty list, cause found users not exist."); } @@ -331,7 +335,7 @@ public void getUsernamesResultCount() { createUser("User1", true); createUser("uSer2", true); createUser("user3", true); - assertEquals(dao.getUsernames(usernamePattern, resultCount).size(), 2); + assertEquals(userDao.getUsernames(usernamePattern, resultCount).size(), 2); } @Test @@ -341,7 +345,7 @@ public void getUsernamesEnabledUsers() { createUser("User1", true); createUser("uSer2", true); createUser("user3", false); - assertEquals(dao.getUsernames(usernamePattern, resultCount).size(), 2); + assertEquals(userDao.getUsernames(usernamePattern, resultCount).size(), 2); } @Test @@ -352,7 +356,7 @@ public void getUsernamesWithSpecialCharacters() { createUserWithMail("user2", "user2@mail.com", true); createUserWithMail("@/|\"&' <>#${}()", "user3@mail.com", true); - assertEquals(dao.getUsernames(usernamePattern, resultCount).size(), 1); + assertEquals(userDao.getUsernames(usernamePattern, resultCount).size(), 1); } @Test @@ -363,7 +367,435 @@ public void specialCharactersShouldBeEscapedCorrectly() { createUserWithMail("user2", "user2@mail.com", true); createUserWithMail("Some_us%2r", "user3@mail.com", true); - assertEquals(dao.getUsernames(usernamePattern, resultCount).size(), 1); + assertEquals(userDao.getUsernames(usernamePattern, resultCount).size(), 1); + } + + @Test + public void findByUsernameOrEmailShouldSearchByUsername() { + JCUser user1 = createUserWithMail("Arthur", "email1@mail.com", true); + JCUser user2 = createUserWithMail("Barbara", "email2@mail.com", true); + createUserWithMail("Epolit", "email3@mail.com", true); + + List result = userDao.findByUsernameOrEmail("ar", 20); + + assertEquals(result.size(), 2); + assertTrue(result.contains(user1)); + assertTrue(result.contains(user2)); + } + + @Test + public void findByUsernameOrEmailShouldSearchByEmail() { + JCUser user1 = createUserWithMail("Arthur", "emAIL1@mail.com", true); + JCUser user2 = createUserWithMail("Barbara", "email2@mail.com", true); + createUserWithMail("Epolit", "post@google.com", true); + + List result = userDao.findByUsernameOrEmail("email", 20); + + assertEquals(result.size(), 2); + assertTrue(result.contains(user1)); + assertTrue(result.contains(user2)); + } + + @Test + public void findByUsernameOrEmailShouldSearchDisabledUsers() { + JCUser user1 = createUserWithMail("Arthur", "emAIL1@mail.com", true); + JCUser user2 = createUserWithMail("Barbara", "email2@mail.com", false); + + List result = userDao.findByUsernameOrEmail("email", 20); + + assertEquals(result.size(), 2); + assertTrue(result.contains(user1)); + assertTrue(result.contains(user2)); + } + + @Test + public void findByUsernameShouldNotReturnMoreUsersThanSpecified() { + createUserWithMail("user1", "emai1@mail.com", true); + createUserWithMail("user2", "email2@mail.com", true); + createUserWithMail("user3", "email3@mail.com", true); + + List result = userDao.findByUsernameOrEmail("user", 2); + + assertEquals(result.size(), 2); + } + + @Test + public void findByUsernameOrEmailShouldCorrectlyEscapeSpecialCharacters() { + String usernamePattern = "_us%"; + createUserWithMail("Some_user1", "user1@mail.com", true); + createUserWithMail("user2", "user2@mail.com", true); + JCUser user = createUserWithMail("Some_us%2r", "user3@mail.com", true); + + List result = userDao.findByUsernameOrEmail(usernamePattern, 20); + + assertEquals(result.size(), 1); + assertTrue(result.contains(user)); + } + + @Test + public void testFindByUsernameOrEmailWihSpecialCharacters() { + String usernamePattern = "@/|\"&' <>#${}()"; + createUserWithMail("Some_user1", "user1@mail.com", true); + createUserWithMail("user2", "user2@mail.com", true); + JCUser user = createUserWithMail("@/|\"&' <>#${}()", "user3@mail.com", true); + + List result = userDao.findByUsernameOrEmail(usernamePattern, 20); + + assertEquals(result.size(), 1); + assertTrue(result.contains(user)); + } + + + @Test + public void findByUsernameOrEmailTestPrimaryOrderUsername() { + String keyWord = "keyword@email.com"; + JCUser user4 = createUserWithMail("1" + keyWord , "user4@email.com", true); + JCUser user3 = createUserWithMail("1" + keyWord + "1", "user3@email.com", true); + JCUser user2 = createUserWithMail(keyWord + "1", "user2@email.com", true); + JCUser user1 = createUserWithMail(keyWord, "user1@email.com", true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user2); + assertEquals(result.get(2), user3); + assertEquals(result.get(3), user4); + } + + @Test + public void findByUsernameOrEmailTestPrimaryOrderEmail() { + String keyWord = "keyword@email.com"; + JCUser user4 = createUserWithMail("user4", "a" + keyWord, true); + JCUser user3 = createUserWithMail("user3", "a" + keyWord + "a", true); + JCUser user2 = createUserWithMail("user2", keyWord + "a", true); + JCUser user1 = createUserWithMail("user1", keyWord, true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user2); + assertEquals(result.get(2), user3); + assertEquals(result.get(3), user4); + } + + @Test + public void findByUsernameOrEmailTestPrimaryOrderMixed() { + String keyWord = "keyword@email.com"; + JCUser user4 = createUserWithMail("a" + keyWord, "user4@email.com", true); + JCUser user3 = createUserWithMail("user3", "a" + keyWord + "a", true); + JCUser user2 = createUserWithMail(keyWord + "a", "user2@email.com", true); + JCUser user1 = createUserWithMail("user1", keyWord, true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user2); + assertEquals(result.get(2), user3); + assertEquals(result.get(3), user4); + } + + @Test + public void testSecondaryOrderExactMatch() { + String keyWord = "keyword@email.com"; + JCUser user2 = createUserWithMail("user2", keyWord, true); + JCUser user1 = createUserWithMail(keyWord, "user1@email.com", true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user2); + } + + @Test + public void testSecondaryOrderStartFromKeyWord() { + String keyWord = "keyword"; + JCUser user2 = createUserWithMail("user2", keyWord + "@email.com", true); + JCUser user1 = createUserWithMail(keyWord + "1", "user1@email.com", true); + JCUser user3 = createUserWithMail(keyWord + "11", keyWord + "1@email.com", true); + + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user3); + assertEquals(result.get(1), user1); + assertEquals(result.get(2), user2); + } + + @Test + public void testThirdaryOrderUsernameStartsFromKeyWord() { + String keyWord = "keyword"; + JCUser user2 = createUserWithMail(keyWord + "z", "user2@email.com", true); + JCUser user1 = createUserWithMail(keyWord + "a", "user1@email.com", true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user2); + + } + + @Test + public void testThirdaryOrderEmailStartsFromKeyWord() { + String keyWord = "keyword"; + JCUser user3 = createUserWithMail("bbbb", keyWord + "3@email.com", true); + JCUser user2 = createUserWithMail("zzzz", keyWord + "1@email.com", true); + JCUser user1 = createUserWithMail("aaaa", keyWord + "2@email.com", true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user3); + assertEquals(result.get(2), user2); + + } + + @Test + public void testSecondaryOrderKeywordInTheMiddle() { + String keyWord = "keyword"; + JCUser user3 = createUserWithMail("1" + keyWord + "1", "user1@email.com", true); + JCUser user2 = createUserWithMail("user2", "a" + keyWord + "@email.com", true); + JCUser user1 = createUserWithMail("1" + keyWord + "11", "a" + keyWord + "1@email.com", true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user3); + assertEquals(result.get(2), user2); + + } + + @Test + public void testThirdaryOrderUsernameWithKeywordItTheMiddle() { + String keyWord = "keyword"; + JCUser user2 = createUserWithMail("z" + keyWord + "aa", "user2@email.com", true); + JCUser user1 = createUserWithMail("a" + keyWord + "aa", "user1@email.com", true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user2); + } + + @Test + public void testThirdaryOrderEmailWithKeyWordInTheMiddle() { + String keyWord = "keyword"; + JCUser user2 = createUserWithMail("zuser", "11" + keyWord + "@email.com", true); + JCUser user1 = createUserWithMail("auser", "1" + keyWord + "@email.com", true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user2); + } + + @Test + public void testSecondaryOrderKeywordAtTheEnd() { + String keyWord = "keyword"; + JCUser user3 = createUserWithMail("user3", "user3@email." + keyWord, true); + JCUser user2 = createUserWithMail("user2" + keyWord, "user2@email.com", true); + JCUser user1 = createUserWithMail("1" + keyWord, "user1@email." + keyWord, true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user2); + assertEquals(result.get(2), user3); + + } + + @Test + public void testThirdaryOrderKeyWordAtTheEndOfUsername() { + String keyWord = "keyword"; + JCUser user2 = createUserWithMail("z" + keyWord, "user2@email.com", true); + JCUser user1 = createUserWithMail("a" + keyWord, "user1@email.com", true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user2); + + } + + @Test + public void testThirdaryOrderEmailWithKeywordInTheEnd() { + String keyWord = "keyword"; + JCUser user2 = createUserWithMail("zuser", "user2@email." + keyWord, true); + JCUser user1 = createUserWithMail("auser", "user1@email." + keyWord, true); + + List result = userDao.findByUsernameOrEmail(keyWord, 20); + + assertEquals(result.get(0), user1); + assertEquals(result.get(1), user2); + } + + @Test + public void findByUsernameNotInGroupId() { + String keyWord = randomAlphanumeric(7);; + Group group1 = createGroup("testGroup1"); + Group group2 = createGroup("testGroup2"); + + JCUser jcUser1group1 = createUserWithGroup(keyWord + "1gr1", "mail1gr1@mail.ru", group1); + addRandomGroupsToUser(jcUser1group1); + createUserWithGroup("petrgr1", "petr1gr1@mail.ru", group1); + JCUser jcUser1group2 = createUserWithGroup(keyWord + "1gr2", "mail2gr2@mail.ru", group2); + JCUser jcUser2group2 = createUserWithGroup("petrgr2", "petr1gr2@mail.ru", group2); + addRandomGroupsToUser(jcUser2group2); + + List result; + result = userDao.findByUsernameOrEmailNotInGroup(keyWord, group1.getId(), 20); + Assert.assertEquals(result.size(), 1); + Assert.assertEquals(jcUser1group2.getUsername(), result.get(0).getUsername()); + + result = userDao.findByUsernameOrEmailNotInGroup(keyWord, group2.getId(), 20); + Assert.assertEquals(result.size(), 1); + Assert.assertEquals(jcUser1group1.getUsername(), result.get(0).getUsername()); + } + + @Test + public void findByEmailNotInGroupId() { + String keyWord = randomAlphanumeric(7);; + Group group1 = createGroup("testGroup1"); + Group group2 = createGroup("testGroup2"); + + JCUser jcUser1group1 = createUserWithGroup("user1gr1", keyWord + "mail1gr1@mail.ru", group1); + addRandomGroupsToUser(jcUser1group1); + createUserWithGroup("petrgr1", "petr1gr1@mail.ru", group1); + JCUser jcUser1group2 = createUserWithGroup("user1gr2", keyWord + "mail2gr2@mail.ru", group2); + JCUser jcUser1group3 = createUserWithGroup("petrgr2", "petr1gr2@mail.ru", group2); + addRandomGroupsToUser(jcUser1group3); + + List result; + result = userDao.findByUsernameOrEmailNotInGroup(keyWord, group1.getId(), 20); + Assert.assertEquals(result.size(), 1); + Assert.assertEquals(jcUser1group2.getUsername(), result.get(0).getUsername()); + + result = userDao.findByUsernameOrEmailNotInGroup(keyWord, group2.getId(), 20); + Assert.assertEquals(result.size(), 1); + Assert.assertEquals(jcUser1group1.getUsername(), result.get(0).getUsername()); + } + + @Test(invocationCount = 5) + public void findsUser_ifHeIsNotInAnyGroup() { + final String keyword = randomAlphanumeric(7); + Group group1 = createGroup("testGroup1"); + JCUser jcUser = createUserWithMail( + sample("username", keyword.toUpperCase()), + sample("m@m.ru", keyword.toUpperCase() + "@mm.ru"), true); + List result = userDao.findByUsernameOrEmailNotInGroup(keyword, group1.getId(), 20); + Assert.assertTrue(result.size() <= 1); + if (result.size() == 1) { + Assert.assertEquals(jcUser.getUsername(), result.get(0).getUsername()); + } else { + Assert.assertFalse(jcUser.getUsername().contains(keyword)); + Assert.assertFalse(jcUser.getEmail().contains(keyword)); + } + } + + @Test(invocationCount = 5) + public void doesNotFindUser_ifHeIsInGroup() { + final String keyword = randomAlphanumeric(7); + final String word = randomAlphanumeric(7); + JCUser user = createUserWithGroup( + sample(keyword, word), + sample(keyword + "@mail.ru", word + "@gmail.com"), createGroup("testGroup0")); + List grpList = addRandomGroupsToUser(user); + for (Group group : grpList) { + List result = userDao.findByUsernameOrEmailNotInGroup(keyword, group.getId(), 20); + Assert.assertEquals(result.size(), 0); + } + } + + @Test(invocationCount = 5) + public void testFindUserNotInGroupIdLimit() { + final int limit = 10 + integer(20); + String keyWord = randomAlphanumeric(7); + Group group1 = createGroup("testGroup1"); + for (int i=0; i result = userDao.findByUsernameOrEmailNotInGroup(keyWord, emptyGroup.getId(), limit); + Assert.assertEquals(result.size(), limit); + Assert.assertEquals(result.get(limit - 1).getUsername(), keyWord + String.format("%02d", limit - 1)); + } + + @Test(invocationCount = 5) + public void testFindUserNotInGroupByEmptyPattern() { + final int count = 15 + integer(50); + String keyWord = randomAlphanumeric(7); + Group group1 = createGroup("testGroup1"); + long group2id = group1.getId() + 1; + for (int i=0; i result = userDao.findByUsernameOrEmailNotInGroup("", group2id, count); + Assert.assertEquals(result.size(), count); + Assert.assertEquals(result.get(count - 1).getUsername(), keyWord + String.format("%02d", count - 1)); + } + + @Test(dataProvider = "usersForTestResultOrderDataProvider") + public void testFindUserNotInGroupResultOrder(List shuffledList, List userPatternList) { + String pattern = "keyword"; + Group group1 = createGroup("testGroup1"); + for (String str : shuffledList) { + createUserWithGroup(str, str + "@mail.ru", group1); + } + List result = userDao.findByUsernameOrEmailNotInGroup(pattern, group1.getId() + 1, 20); + Assert.assertEquals(result.size(), userPatternList.size()); + for (int i=0; i userPatternList = new ArrayList<>(); + userPatternList.add(pattern); + userPatternList.add(pattern + "wqrrw"); + userPatternList.add("a_rtet" + pattern + "gghghgh"); + userPatternList.add("b_rtet" + pattern + "gghghgh"); + userPatternList.add("a_rtert" + pattern); + userPatternList.add("b_rtert" + pattern); + + List shuffledList = new ArrayList<>(userPatternList); + shuffledList.add("someNotMatchingPattern1"); + shuffledList.add("someNotMatchingPattern2"); + shuffledList.add("someNotMatchingPattern3"); + Collections.shuffle(shuffledList, new Random()); + + return new Object[][] { + {shuffledList, userPatternList} + }; + } + + private JCUser createUserWithGroup(String userName, String email, Group group) { + JCUser jcUser = createUserWithMail(userName, email, true); + jcUser.addGroup(group); + userDao.saveOrUpdate(jcUser); + return jcUser; + } + + private List addRandomGroupsToUser(JCUser user) { + List result = new ArrayList<>(); + Random rnd = new Random(); + int count = 3 + rnd.nextInt(10); + for (int i=0; i entities = listOfRandomSpamRules(5); + List dtoList = SpamRuleDto.fromEntities(entities); + for (int i = 0; i < entities.size(); i++) { + SpamRule expected = entities.get(i); + SpamRule actual = dtoList.get(i).toEntity(); + actual.setUuid(expected.getUuid()); + assertReflectionEquals(expected, actual); + } + } + + @Test + public void validationOfValidSpamRulePass(){ + SpamRuleDto spamRuleDto = randomSpamRuleDto(); + Set> constraintViolations = validator.validate(spamRuleDto); + assertEquals(constraintViolations.size(), 0); + } + + @Test + public void emptyRegexValidationFails(){ + SpamRuleDto spamRuleDto = randomSpamRuleDto().setRegex(nullOrEmpty()); + Set> constraintViolations = validator.validate(spamRuleDto); + assertEquals(constraintViolations.size(), 1); + } + + @Test + public void regexValidationFailsIfLengthMoreThan255(){ + SpamRuleDto spamRuleDto = randomSpamRuleDto().setRegex(alphanumeric(256)); + Set> constraintViolations = validator.validate(spamRuleDto); + assertEquals(constraintViolations.size(), 1); + } + + @Test + public void descriptionValidationFailsIfLengthMoreThan255(){ + SpamRuleDto spamRuleDto = randomSpamRuleDto().setDescription(alphanumeric(256)); + Set> constraintViolations = validator.validate(spamRuleDto); + assertEquals(constraintViolations.size(), 1); + } +} diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/BranchTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/BranchTest.java index cf33a95ea6..f498d92393 100644 --- a/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/BranchTest.java +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/BranchTest.java @@ -14,15 +14,13 @@ */ package org.jtalks.jcommune.model.entity; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - import java.util.ArrayList; import java.util.List; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertTrue; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.testng.Assert.*; /** @@ -38,7 +36,7 @@ public class BranchTest { @BeforeMethod public void setUp() { branch = new Branch("test branch", "test branch"); - List topics = new ArrayList(); + List topics = new ArrayList<>(); first = new Topic(null, null); second = new Topic(null, null); third = new Topic(null, null); @@ -125,4 +123,34 @@ public void testIsLastPostWhenCheckedPostIsNotLastPost() { assertFalse(isLastPost); } + + @Test + public void getUnsubscribeLinkForSubscribersOfBranchShouldReturnBranchUnsubscribeLink() { + branch.setId(1); + + assertEquals(branch.getUnsubscribeLinkForSubscribersOf(Branch.class), "/branches/1/unsubscribe"); + } + + + /** + * Case for hibernate proxies + */ + @Test + public void getUnsubscribeLinkForSubscribersOfBranchSubclassShouldReturnBranchUnsubscribeLink() { + class BranchSubClass extends Branch { + } + branch.setId(1); + + assertEquals(branch.getUnsubscribeLinkForSubscribersOf(BranchSubClass.class), "/branches/1/unsubscribe"); + } + + @Test + public void getUnsubscribeLinkForSubscribersOfTopicShouldReturnNull() { + assertNull(branch.getUnsubscribeLinkForSubscribersOf(Topic.class)); + } + + @Test + public void getUnsubscribeLinkForSubscribersOfPostShouldReturnNull() { + assertNull(branch.getUnsubscribeLinkForSubscribersOf(Post.class)); + } } diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/JCUserTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/JCUserTest.java index 99ead1ceb3..5280a2e626 100644 --- a/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/JCUserTest.java +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/JCUserTest.java @@ -20,8 +20,10 @@ import org.jtalks.common.model.entity.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.GrantedAuthorityImpl; +import org.springframework.util.SerializationUtils; import org.testng.annotations.Test; +import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.List; @@ -205,6 +207,39 @@ private void assertUserGroupEqualsIgnoreUsers(Group actual, Group expected) { assertEquals(actual.getDescription(), expected.getDescription()); } + /** + * Accert that getLanguage return English in case of Spanish language of user + * */ + @Test + public void spanishLanguageHandling () { + JCUser user = new JCUser("username", "email@mail.com", "pass"); + user.setLanguage(Language.SPANISH); + + Language language = user.getLanguage(); + assertEquals(language, Language.ENGLISH); + } + + @Test + public void entityFieldsShouldBeSerialized() throws NoSuchMethodException, InvocationTargetException, + IllegalAccessException { + JCUser user = ObjectsFactory.getUserWithAllFieldsFilled(); + byte[] serialize = SerializationUtils.serialize(user); + JCUser serializedUser = (JCUser)SerializationUtils.deserialize(serialize); + assertReflectionEquals(user, serializedUser); + } + + @Test + public void groupsFieldIsNotSerializable(){ + List groups = Group.createGroupsWithNames("Group"); + JCUser userInGroup = new JCUser("user","email","password"); + userInGroup.setGroups(groups); + + byte[] serialize = SerializationUtils.serialize(userInGroup); + JCUser serializedUser = (JCUser) SerializationUtils.deserialize(serialize); + + assertNull(serializedUser.getGroups(), + "After deserialiation, the transient field `List groups` must be null"); + } } diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/PostTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/PostTest.java index f01c92f1bf..e2a57af112 100644 --- a/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/PostTest.java +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/PostTest.java @@ -14,17 +14,16 @@ */ package org.jtalks.jcommune.model.entity; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertTrue; - import java.util.HashSet; +import java.util.List; import java.util.Set; import org.joda.time.DateTime; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import static org.testng.Assert.*; + /** * @author Evgeniy Naumenko */ @@ -80,7 +79,7 @@ public void getTopicSubscribersShouldReturnSubscribersOfParentTopic() { expectedSubscribers.add(new JCUser()); post.getTopic().setSubscribers(expectedSubscribers); - Set actualSubscribers = post.getTopicSubscribers(); + Set actualSubscribers = post.getSubscribers(); assertEquals(actualSubscribers, expectedSubscribers, "Post should have the same subscribers as parent topic."); @@ -297,4 +296,107 @@ public void testCalculateRatingChangesWhenUserChangesVoteFromDownToUp() { assertEquals(post.calculateRatingChanges(vote2), 2); } + + @Test + public void testGetNotRemovedComments() { + Post post = ObjectsFactory.getPostWithComments(); + int size = post.getComments().size(); + post.getComments().get(0).setDeletionDate(new DateTime()); + + List notRemovedComments = post.getNotRemovedComments(); + + assertEquals(size - 1, notRemovedComments.size()); + for (PostComment comment : notRemovedComments) { + assertNull(comment.getDeletionDate()); + } + } + + @Test + public void getCommentsShouldReturnAllCommentsIncludingMarkedAsDeleted() { + Post post = ObjectsFactory.getPostWithComments(); + int size = post.getComments().size(); + post.getComments().get(0).setDeletionDate(new DateTime()); + + List result = post.getComments(); + + assertEquals(size, result.size()); + } + + + @Test + public void getUnsubscribeLinkForSubscribersOfPostShouldReturnTopicUnsubscribeLink() + { + Topic topic = new Topic(); + topic.setId(1); + Post post = new Post(); + post.setTopic(topic); + + assertEquals(post.getUnsubscribeLinkForSubscribersOf(Post.class), "/topics/1/unsubscribe"); + } + + + @Test + public void getUnsubscribeLinkForSubscribersOfPostSubclassShouldReturnTopicUnsubscribeLink() + { + class PostSubClass extends Post { + } + Topic topic = new Topic(); + topic.setId(1); + Post post = new Post(); + post.setTopic(topic); + + assertEquals(post.getUnsubscribeLinkForSubscribersOf(PostSubClass.class), "/topics/1/unsubscribe"); + } + + @Test + public void getUnsubscribeLinkForSubscribersOfTopicShouldReturnTopicUnsubscribeLink() + { + Topic topic = new Topic(); + topic.setId(1); + Post post = new Post(); + post.setTopic(topic); + + assertEquals(post.getUnsubscribeLinkForSubscribersOf(Topic.class), "/topics/1/unsubscribe"); + } + + @Test + public void getUnsubscribeLinkForSubscribersOfTopicSubclassShouldReturnTopicUnsubscribeLink() + { + class TopicSubClass extends Topic { + + } + Topic topic = new Topic(); + topic.setId(1); + Post post = new Post(); + post.setTopic(topic); + + assertEquals(post.getUnsubscribeLinkForSubscribersOf(TopicSubClass.class), "/topics/1/unsubscribe"); + } + + @Test + public void getUnsubscribeLinkForSubscribersOfBranchShouldReturnBranchUnsubscribeLink() { + Topic topic = new Topic(); + Post post = new Post(); + post.setTopic(topic); + Branch branch = new Branch(); + branch.setId(1); + topic.setBranch(branch); + + assertEquals(post.getUnsubscribeLinkForSubscribersOf(Branch.class), "/branches/1/unsubscribe"); + } + + @Test + public void getUnsubscribeLinkForSubscribersOfBranchSubClassShouldReturnBranchUnsubscribeLink() { + class BranchSubClass extends Branch { + + } + Topic topic = new Topic(); + Post post = new Post(); + post.setTopic(topic); + Branch branch = new Branch(); + branch.setId(1); + topic.setBranch(branch); + + assertEquals(post.getUnsubscribeLinkForSubscribersOf(BranchSubClass.class), "/branches/1/unsubscribe"); + } } diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/TopicDraftTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/TopicDraftTest.java new file mode 100644 index 0000000000..0d3ca7ea7e --- /dev/null +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/TopicDraftTest.java @@ -0,0 +1,167 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.entity; + +import org.apache.commons.lang3.RandomStringUtils; +import org.joda.time.DateTimeUtils; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import javax.validation.*; +import java.util.Set; + +import static org.testng.Assert.assertEquals; + +/** + * @author Dmitry S. Dolzhenko + */ +public class TopicDraftTest { + + private static Validator validator; + + @BeforeMethod + public void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + public void validationOfFilledDraftShouldPass() { + TopicDraft draft = ObjectsFactory.getDefaultTopicDraft(); + + Set> constraintViolations = validator.validate(draft); + + assertEquals(constraintViolations.size(), 0); + } + + @Test + public void validationOfEmptyDraftShouldFail() { + TopicDraft draft = ObjectsFactory.getDefaultTopicDraft(); + + draft.setTitle(null); + draft.setContent(null); + draft.setPollTitle(null); + draft.setPollItemsValue(null); + + Set> constraintViolations = validator.validate(draft); + + assertEquals(constraintViolations.size(), 1); + } + + @Test + public void validationOfDraftWithoutTitleShouldPass() { + TopicDraft draft = ObjectsFactory.getDefaultTopicDraft(); + + draft.setTitle(null); + + Set> constraintViolations = validator.validate(draft); + + assertEquals(constraintViolations.size(), 0); + } + + @Test + public void validationOfDraftWithTooLongTitleShouldFail() { + TopicDraft draft = ObjectsFactory.getDefaultTopicDraft(); + + draft.setTitle(RandomStringUtils.random(121)); + + Set> constraintViolations = validator.validate(draft); + + assertEquals(constraintViolations.size(), 1); + } + + @Test + public void validationOfDraftWithoutContentShouldPass() { + TopicDraft draft = ObjectsFactory.getDefaultTopicDraft(); + + draft.setContent(null); + + Set> constraintViolations = validator.validate(draft); + + assertEquals(constraintViolations.size(), 0); + } + + @Test + public void validationOfDraftWithTooLongContentShouldFail() { + TopicDraft draft = ObjectsFactory.getDefaultTopicDraft(); + + draft.setContent(RandomStringUtils.random(20001)); + + Set> constraintViolations = validator.validate(draft); + + assertEquals(constraintViolations.size(), 1); + } + + @Test + public void validationOfDraftWithoutPollTitleShouldPass() { + TopicDraft draft = ObjectsFactory.getDefaultTopicDraft(); + + draft.setPollTitle(null); + + Set> constraintViolations = validator.validate(draft); + + assertEquals(constraintViolations.size(), 0); + } + + @Test + public void validationOfDraftWithTooLongPollTitleShouldFail() { + TopicDraft draft = ObjectsFactory.getDefaultTopicDraft(); + + draft.setPollTitle(RandomStringUtils.random(121)); + + Set> constraintViolations = validator.validate(draft); + + assertEquals(constraintViolations.size(), 1); + } + + @Test + public void validationOfDraftWithoutPollItemsValueShouldPass() { + TopicDraft draft = ObjectsFactory.getDefaultTopicDraft(); + + draft.setPollItemsValue(null); + + Set> constraintViolations = validator.validate(draft); + + assertEquals(constraintViolations.size(), 0); + } + + @Test + public void validationOfDraftWithTooManyOfPollItemsShouldFail() { + TopicDraft draft = ObjectsFactory.getDefaultTopicDraft(); + + String pollItemsValue = ""; + for (int i = 0; i < 51; i++) { + pollItemsValue += RandomStringUtils.random(15) + "\n"; + } + draft.setPollItemsValue(pollItemsValue); + + Set> constraintViolations = validator.validate(draft); + + assertEquals(constraintViolations.size(), 1); + } + + @Test + public void updateLastSavedTimeShouldUpdateLastSavedFiled() throws Exception { + TopicDraft draft = ObjectsFactory.getDefaultTopicDraft(); + + DateTimeUtils.setCurrentMillisFixed(DateTimeUtils.currentTimeMillis() + 1000); + + draft.updateLastSavedTime(); + + assertEquals(draft.getLastSaved().getMillis(), DateTimeUtils.currentTimeMillis()); + + DateTimeUtils.setCurrentMillisSystem(); + } +} diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/TopicTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/TopicTest.java index 5b66e8e55d..e373bdc9bc 100644 --- a/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/TopicTest.java +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/entity/TopicTest.java @@ -14,12 +14,14 @@ */ package org.jtalks.jcommune.model.entity; -import org.joda.time.DateTime; -import org.testng.annotations.Test; - +import java.util.Collections; import java.util.HashSet; import java.util.Set; +import com.google.common.base.Optional; +import org.joda.time.DateTime; +import org.testng.annotations.Test; + import static org.testng.Assert.*; public class TopicTest { @@ -68,32 +70,24 @@ public void firstPostShouldReturnFirstPostOfTheTopic() { public void addPostShouldUpdateModificationDate() throws InterruptedException { Topic topic = createTopic(); DateTime prevDate = topic.getModificationDate(); - Thread.sleep(25); // millisecond precise is a kind of fiction - topic.addPost(new Post()); - + Post post = new Post(); + post.setCreationDate(new DateTime().plusHours(1)); + topic.addPost(post); assertTrue(topic.getModificationDate().isAfter(prevDate)); } - - public void updatePostShouldUpdateModificationDate() throws InterruptedException { - Topic topic = createTopic(); - DateTime prevDate = topic.getModificationDate(); - Thread.sleep(25); // millisecond precise is a kind of fiction - topic.getPosts().get(0).updateModificationDate(); - - assertTrue(topic.getModificationDate().isBefore(prevDate)); - } - @Test - public void updateModificationDateShouldChangeTheModificationDate() { + public void addPostShouldSetModificationDateToPostCreationDate() throws InterruptedException { Topic topic = createTopic(); DateTime prevDate = topic.getModificationDate(); + Post post = new Post(); + post.setCreationDate(new DateTime().plusDays(1)); + topic.addPost(post); - DateTime modDate = topic.updateModificationDate(); - - assertNotSame(modDate, prevDate); + assertEquals(topic.getModificationDate(), post.getCreationDate()); + assertFalse(prevDate.equals(topic.getModificationDate())); } - + @Test public void recalculateModificationDateShouldSetModificationDateAsTheLatestDateAmongAllPosts() { Topic topic = createTopic(); @@ -106,7 +100,6 @@ public void recalculateModificationDateShouldSetModificationDateAsTheLatestDateA topic.addPost(post3); - topic.updateModificationDate(); topic.recalculateModificationDate(); assertEquals(topic.getModificationDate(), lastModificationDate); @@ -126,6 +119,7 @@ public void hasUpdatesShouldReturnTrueInCaseOfUpdatesExist() { topic.getFirstPost().setCreationDate(creationDate); topic.getLastPost().setCreationDate(creationDate.plusDays(1)); topic.setLastReadPostDate(creationDate); + assertTrue(topic.isHasUpdates()); } @@ -191,6 +185,24 @@ public void removePostShouldRemovePostFromTheTopic() { assertFalse(topic.getPosts().contains(toRemove), "The post isn't removed from the topic"); } + @Test + public void removePostShouldSetModificationDateToLastPostInTheTopic(){ + Post post1 = new Post(); + post1.setCreationDate(new DateTime()); + Post post2 = new Post(); + post2.setCreationDate(new DateTime()); + Topic topic = new Topic(new JCUser(), "title"); + topic.addPost(post1); + topic.addPost(post2); + Post post3 = new Post(); + post3.setCreationDate(new DateTime()); + topic.addPost(post3); + topic.removePost(post3); + + assertEquals(topic.getModificationDate(), post2.getCreationDate()); + + } + @Test public void setSubscribersShouldSubscribeUserToTheTopic() { Topic topic = createTopic(); @@ -291,13 +303,226 @@ public void isPlugableShouldReturnFalseIfTopicTypeNotSet() { assertFalse(topic.isPlugable()); } + @Test + public void testIsContainsOwnerPostsOnlyReturnsTrueIfOthersDidNotRespond() { + Topic topic = createTopic(); + + assertTrue(topic.getPosts().size() > 0); + assertTrue(topic.isContainsOwnerPostsOnly()); + } + + @Test + public void testIsContainsOwnerPostsOnlyReturnsFalseIfOthersHaveResponded() { + Topic topic = createTopicWithOthersPosts(); + + assertTrue(topic.getPosts().size() > 0); + assertFalse(topic.isContainsOwnerPostsOnly()); + } + + @Test + public void testIsContainsOwnerPostsOnlyReturnsTrueOnEmptyTopic() { + Topic topic = createTopicWithOthersPosts(); + topic.setPosts(Collections.emptyList()); + + assertTrue(topic.getPosts().size() == 0); + assertTrue(topic.isContainsOwnerPostsOnly()); + } + + @Test + public void getDraftForUserShouldReturnDraftOfSpecifiedUser() { + Topic topic = new Topic(); + JCUser targetUser = new JCUser(); + PostDraft expected = new PostDraft("blahblah", targetUser); + topic.addDraft(expected); + topic.addDraft(new PostDraft("qwerr", new JCUser())); + + PostDraft actual = topic.getDraftForUser(targetUser); + + assertEquals(actual, expected); + } + + @Test + public void getDraftForUserShouldReturnNullIfDraftNotFound() { + Topic topic = new Topic(); + + PostDraft draft = topic.getDraftForUser(new JCUser()); + + assertNull(draft); + } + + @Test + public void getUserPostCountTest() { + JCUser user = new JCUser(); + Topic topic = new Topic(); + topic.addPost(new Post(user, "")); + + assertEquals(topic.getUserPostCount(user), 1); + } + + @Test + public void getUserPostCountShouldReturnZeroIfUserNotPosted() { + Topic topic = new Topic(); + + assertEquals(topic.getUserPostCount(new JCUser()), 0); + } + + @Test + public void getUserPostCountShouldReturnZeroIfOnlyOthersPosted() { + Topic topic = new Topic(); + topic.addPost(new Post(new JCUser(), "")); + + assertEquals(topic.getUserPostCount(new JCUser()), 0); + } + + @Test + public void getUserPostCountTestMoreThanOneUserPosted() { + Topic topic = new Topic(); + topic.addPost(new Post(new JCUser(), "")); + JCUser user = new JCUser(); + topic.addPost(new Post(user, "")); + topic.addPost(new Post(user, "")); + + assertEquals(topic.getUserPostCount(user), 2); + } + + @Test + public void testGetUnsubscribeLinkForSubscribersOfPostShouldReturnTopicUnsubscribeLink() + { + Topic topic = new Topic(); + topic.setId(1); + + assertEquals(topic.getUnsubscribeLinkForSubscribersOf(Post.class), "/topics/1/unsubscribe"); + } + + /** + * Case for hibernate proxies + */ + @Test + public void getUnsubscribeLinkForSubscribersOfPostSubclassShouldReturnTopicUnsubscribeLink() + { + class PostSubClass extends Post { + } + Topic topic = new Topic(); + topic.setId(1); + + assertEquals(topic.getUnsubscribeLinkForSubscribersOf(PostSubClass.class), "/topics/1/unsubscribe"); + } + + + + @Test + public void getUnsubscribeLinkForSubscribersOfTopicShouldReturnTopicUnsubscribeLink() + { + Topic topic = new Topic(); + topic.setId(1); + + assertEquals(topic.getUnsubscribeLinkForSubscribersOf(Topic.class), "/topics/1/unsubscribe"); + } + + + /** + * Case for hibernate proxies + */ + @Test + public void getUnsubscribeLinkForSubscribersOfTopicSubclassShouldReturnTopicUnsubscribeLink() { + class TopicSubClass extends Topic { + } + Topic topic = new Topic(); + topic.setId(1); + + assertEquals(topic.getUnsubscribeLinkForSubscribersOf(TopicSubClass.class), "/topics/1/unsubscribe"); + } + + + @Test + public void getUnsubscribeLinkForSubscribersOfBranchShouldReturnBranchUnsubscribeLink() { + Topic topic = new Topic(); + Branch branch = new Branch(); + branch.setId(1); + topic.setBranch(branch); + + assertEquals(topic.getUnsubscribeLinkForSubscribersOf(Branch.class), "/branches/1/unsubscribe"); + } + + /** + * Case for hibernate proxies + */ + @Test + public void getUnsubscribeLinkForSubscribersOfBranchSubclassShouldReturnBranchUnsubscribeLink() { + class BranchSubClass extends Branch { + } + Topic topic = new Topic(); + Branch branch = new Branch(); + branch.setId(1); + topic.setBranch(branch); + + assertEquals(topic.getUnsubscribeLinkForSubscribersOf(BranchSubClass.class), "/branches/1/unsubscribe"); + } + + @Test + public void markAsReadUrlIsEmptyForAnonymousUser() throws Exception { + Optional markAsReadUrl = createTopic().getMarkAsReadUrl(new AnonymousUser(), "2"); + assertFalse(markAsReadUrl.isPresent(), "URL should be absent for anonymous user"); + } + + @Test + public void markAsReadUrlPresentsForNonAnonymousUser() throws Exception { + Optional markAsReadUrl = createTopic().getMarkAsReadUrl(new JCUser(), "2"); + assertTrue(markAsReadUrl.isPresent(), "URL should be present for authorized user"); + } + + @Test + public void markAsReadUrlContainsTopicIdAndPageNumberInPath() throws Exception { + Optional markAsReadUrl = createTopic().getMarkAsReadUrl(new JCUser(), "2"); + String url = markAsReadUrl.get(); + assertTrue(url.startsWith("0/page/2/markread"), "URL should contain topicId and page"); + } + + @Test + public void markAsReadUrlContainsUserIdAsQueryParams() throws Exception { + Optional markAsReadUrl = createTopic().getMarkAsReadUrl(new JCUser(), "2"); + String url = markAsReadUrl.get(); + assertTrue(url.contains("userId=0"), "URL should have userId param"); + } + + @Test + public void markAsReadUrlContainsLastModifiedAsQueryParams() throws Exception { + Topic topic = createTopic(); + DateTime lastModificationPostDate = topic.getLastModificationPostDate(); + Optional markAsReadUrl = topic.getMarkAsReadUrl(new JCUser(), "2"); + String url = markAsReadUrl.get(); + assertTrue(url.endsWith("lastModified=" + lastModificationPostDate.getMillis()), "URL should have lastModified param"); + } + + private Topic createTopic() { - Post post1 = new Post(); + JCUser topicStarter = new JCUser(); + + Post post1 = new Post(topicStarter, "Post N1 for tests"); post1.setCreationDate(new DateTime()); - Post post2 = new Post(); - Topic topic = new Topic(new JCUser(), "title"); + Post post2 = new Post(topicStarter, "Post N2 for tests"); + post2.setCreationDate(new DateTime()); + + Topic topic = new Topic(topicStarter, "Topic title for testing"); topic.addPost(post1); topic.addPost(post2); + return topic; } + + private Topic createTopicWithOthersPosts() { + JCUser topicStarter = new JCUser(); + JCUser otherUser = new JCUser(); + + Post post1 = new Post(topicStarter, "Post N1 by topic starter"); + post1.setCreationDate(new DateTime()); + Post post2 = new Post(otherUser, "Post N2 by other user"); + post2.setCreationDate(new DateTime()); + + Topic topic = new Topic(topicStarter, "Topic title for testing"); + topic.addPost(post1); + topic.addPost(post2); + + return topic; + } } diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/utils/SpamRuleUtils.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/utils/SpamRuleUtils.java new file mode 100644 index 0000000000..d41ed1fe47 --- /dev/null +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/utils/SpamRuleUtils.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.model.utils; + +import org.jtalks.jcommune.model.dto.SpamRuleDto; +import org.jtalks.jcommune.model.entity.SpamRule; + +import java.util.ArrayList; +import java.util.List; + +import static io.qala.datagen.RandomShortApi.*; + +/** + * @author Oleg Tkachenko + */ +public class SpamRuleUtils { + + public static SpamRule randomSpamRule() { + return new SpamRule(unicode(1, 255), blankOr(unicode(1, 255)), bool()); + } + + public static SpamRuleDto randomSpamRuleDto() { + return new SpamRuleDto(0, unicode(1, 255), blankOr(unicode(1, 255)), bool()); + } + + public static List listOfRandomSpamRules(int size) { + List ruleList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + ruleList.add(randomSpamRule()); + } + return ruleList; + } +} diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/validation/AtLeastOneFieldIsNotNullValidatorTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/validation/AtLeastOneFieldIsNotNullValidatorTest.java new file mode 100644 index 0000000000..fd1a62ecf9 --- /dev/null +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/validation/AtLeastOneFieldIsNotNullValidatorTest.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.validation; + +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.math.RandomUtils; +import org.jtalks.jcommune.model.validation.annotations.AtLeastOneFieldIsNotNull; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import javax.validation.*; +import java.util.Set; + +import static org.testng.Assert.assertEquals; + +/** + * @author Dmitry S. Dolzhenko + */ +public class AtLeastOneFieldIsNotNullValidatorTest { + @AtLeastOneFieldIsNotNull(fields = { + "field1", "field2" + }) + public static class TestObject { + private String field1; + public Integer field2; + + public TestObject(String field1, Integer field2) { + this.field1 = field1; + this.field2 = field2; + } + + public String getField1() { + return field1; + } + } + + @AtLeastOneFieldIsNotNull(fields = { + "field1", "field2" + }) + public static class TestObjectWithPrimitive { + private int field1; + private String field2; + + public TestObjectWithPrimitive(int field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } + + public int getField1() { + return field1; + } + + public String getField2() { + return field2; + } + } + + private Validator validator; + + @BeforeClass + public void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + public void validationOfFullyFilledObjectShouldPassSuccessfully() { + Set> constraintViolations = + validator.validate(new TestObject(RandomStringUtils.random(5), RandomUtils.nextInt(100))); + + assertEquals(constraintViolations.size(), 0); + } + + @Test + public void validationOfPartiallyFilledObjectShouldPassSuccessfully() { + Set> constraintViolations = + validator.validate(new TestObject(null, RandomUtils.nextInt(100))); + + assertEquals(constraintViolations.size(), 0); + } + + @Test + public void validationOfInvalidObjectShouldFail() { + Set> constraintViolations = + validator.validate(new TestObject(null, null)); + + assertEquals(constraintViolations.size(), 1); + } + + @Test + public void validationOfTestObjectWithPrimitiveShouldPassSuccessFully() throws Exception { + Set> constraintViolations = + validator.validate(new TestObjectWithPrimitive(RandomUtils.nextInt(100), RandomStringUtils.random(5))); + + assertEquals(constraintViolations.size(), 0); + } +} diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/validation/IntegerRangeValidatorTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/validation/IntegerRangeValidatorTest.java new file mode 100644 index 0000000000..d5a77e5c46 --- /dev/null +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/validation/IntegerRangeValidatorTest.java @@ -0,0 +1,144 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.validation; + +import org.jtalks.jcommune.model.validation.annotations.IntegerRange; +import org.jtalks.jcommune.model.validation.validators.IntegerRangeValidator; +import org.mockito.Mock; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static io.qala.datagen.RandomShortApi.integer; +import static io.qala.datagen.RandomShortApi.positiveInteger; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * @author skythet + */ +public class IntegerRangeValidatorTest { + + private static final int DEFAULT_MIN = -100; + private static final int DEFAULT_MAX = 100; + + @Mock + private IntegerRange integerRange; + + @BeforeMethod + public void init() { + initMocks(this); + } + + @Test + public void testValidate() { + IntegerRangeValidator validator = new IntegerRangeValidator(); + + int min = integer(DEFAULT_MIN, 0); + int max = integer(0, DEFAULT_MAX); + integerRange(min, max); + + validator.initialize(integerRange); + + assertTrue(validator.isValid(integer(min, max) + "", null)); + } + + @Test + public void testValidateWhenMinGreaterThanMax() { + IntegerRangeValidator validator = new IntegerRangeValidator(); + + int min = integer(DEFAULT_MIN, 0); + int max = integer(0, DEFAULT_MAX); + integerRange(max, min); + + validator.initialize(integerRange); + + assertFalse(validator.isValid(integer(min, max) + "", null)); + } + + @Test + public void testValidateEqualsValues() { + IntegerRangeValidator validator = new IntegerRangeValidator(); + + int randomNumber = integer(DEFAULT_MAX); + integerRange(randomNumber, randomNumber); + + validator.initialize(integerRange); + + assertTrue(validator.isValid(randomNumber + "", null)); + } + + @Test + public void validationOfNullStringShouldFail() { + IntegerRangeValidator validator = new IntegerRangeValidator(); + + assertFalse(validator.isValid(null, null)); + } + + @Test + public void validationOfNotNumberStringShouldFail() { + IntegerRangeValidator validator = new IntegerRangeValidator(); + + assertFalse(validator.isValid("123v", null)); + } + + @Test + public void validationEmptyStringShouldFail() { + IntegerRangeValidator validator = new IntegerRangeValidator(); + + assertFalse(validator.isValid("", null)); + assertFalse(validator.isValid(" ", null)); + } + + @Test + public void validationContainsSpacesShouldFail() { + IntegerRangeValidator validator = new IntegerRangeValidator(); + + assertFalse(validator.isValid(" 12", null)); + assertFalse(validator.isValid("12 ", null)); + } + + @Test + public void validationNumberGreaterThanMaxValueShouldFail() { + IntegerRangeValidator validator = new IntegerRangeValidator(); + + int min = integer(DEFAULT_MIN, 0); + int max = integer(0, DEFAULT_MAX); + integerRange(min, max); + + validator.initialize(integerRange); + + assertFalse(validator.isValid(max + positiveInteger() + "", null)); + } + + @Test + public void validationNumberLessThanMinValueShouldFail() { + IntegerRangeValidator validator = new IntegerRangeValidator(); + + int min = integer(DEFAULT_MIN, 0); + int max = integer(0, DEFAULT_MAX); + integerRange(min, max); + + validator.initialize(integerRange); + + assertFalse(validator.isValid(DEFAULT_MIN + (positiveInteger() * -1) + "", null)); + } + + private void integerRange(int min, int max) { + when(integerRange.min()).thenReturn(min); + when(integerRange.max()).thenReturn(max); + } +} diff --git a/jcommune-model/src/test/java/org/jtalks/jcommune/model/validation/TopicDraftNumberOfPollItemsValidatorTest.java b/jcommune-model/src/test/java/org/jtalks/jcommune/model/validation/TopicDraftNumberOfPollItemsValidatorTest.java new file mode 100644 index 0000000000..1b01b6afd6 --- /dev/null +++ b/jcommune-model/src/test/java/org/jtalks/jcommune/model/validation/TopicDraftNumberOfPollItemsValidatorTest.java @@ -0,0 +1,96 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.model.validation; + +import org.apache.commons.lang.RandomStringUtils; +import org.jtalks.jcommune.model.validation.annotations.TopicDraftNumberOfPollItems; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +import static org.testng.Assert.*; + +/** + * @author Dmitry S. Dolzhenko + */ +public class TopicDraftNumberOfPollItemsValidatorTest { + + private static final int MIN_ITEMS_SIZE = 2; + private static final int MAX_ITEMS_SIZE = 10; + + private static class TestObject { + @TopicDraftNumberOfPollItems(min = MIN_ITEMS_SIZE, max = MAX_ITEMS_SIZE) + private String pollItemsValue; + + public TestObject(String pollItemsValue) { + this.pollItemsValue = pollItemsValue; + } + + public String getPollItemsValue() { + return pollItemsValue; + } + } + + private Validator validator; + + @BeforeClass + public void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + public void validationShouldFailWhenNumberOfPollItemsLessThanMin() { + TestObject testObject = new TestObject(RandomStringUtils.random(15)); + + Set> constraintViolations + = validator.validate(testObject); + + assertEquals(constraintViolations.size(), 1); + } + + @Test + public void validationShouldFailWhenNumberOfPollItemsGreaterThanMax() { + String pollItemsValue = ""; + for (int i = 0; i < MAX_ITEMS_SIZE + 1; i++) { + pollItemsValue += RandomStringUtils.random(15) + "\n"; + } + TestObject testObject = new TestObject(pollItemsValue); + + Set> constraintViolations + = validator.validate(testObject); + + assertEquals(constraintViolations.size(), 1); + } + + @Test + public void validationShouldPassSuccessfullyOnValidNumberOfPollItems() { + String pollItemsValue = ""; + for (int i = 0; i < MIN_ITEMS_SIZE; i++) { + pollItemsValue += RandomStringUtils.random(15) + "\n"; + } + TestObject testObject = new TestObject(pollItemsValue); + + Set> constraintViolations + = validator.validate(testObject); + + assertEquals(constraintViolations.size(), 0); + } +} diff --git a/jcommune-performance-tests/README.md b/jcommune-performance-tests/README.md new file mode 100644 index 0000000000..90e4338b85 --- /dev/null +++ b/jcommune-performance-tests/README.md @@ -0,0 +1,21 @@ +Gatling performance tests +------------------------- + +# Running Performance Tests + +1. Run your application server used for testing. +2. Configure pom.xml + ``` + + true (1) + org.jtalks.jcommune.performance.tests.OpenTopicPage (2) + + + Choose one: + (1) set **true** if you want to run all simulations sequentially. + (2) choose specific simulation to run +3. Pass your server address as argument to Maven: `-Dperformance.url=http://yourserveraddress.com` otherwise +`http://performance.jtalks.org/jcommune` will be taken. +4. Run simulations with maven: +`mvn test -pl jcommune-performance-tests -Dperformance-test.skip=false -Dperformance.url=http://yourserveraddress.com` +5. Result charts will be placed in "target" folder in your project root folder. \ No newline at end of file diff --git a/jcommune-performance-tests/pom.xml b/jcommune-performance-tests/pom.xml new file mode 100644 index 0000000000..313b05a90f --- /dev/null +++ b/jcommune-performance-tests/pom.xml @@ -0,0 +1,147 @@ + + + 4.0.0 + + + jcommune + org.jtalks.jcommune + 3.13-SNAPSHOT + + + jcommune-performance-tests + + + This module contains performance tests. + + + + true + 2.11.7 + UTF-8 + 2.2.2 + 3.2.2 + 2.2.0 + + + + + + io.gatling + gatling-app + ${gatling.version} + + + io.gatling + gatling-recorder + ${gatling.version} + + + io.gatling.highcharts + gatling-charts-highcharts + ${gatling.version} + + + org.scala-lang + scala-library + ${scala.version} + + + + + + + io.gatling.highcharts + gatling-charts-highcharts + + + io.gatling + gatling-app + + + io.gatling + gatling-recorder + + + org.scala-lang + scala-library + + + io.gatling + gatling-commons + ${gatling.version} + + + ch.qos.logback + * + + + + + + + + src/test/scala + + + + net.alchim31.maven + scala-maven-plugin + ${scala-maven-plugin.version} + + + + + + net.alchim31.maven + scala-maven-plugin + + + + testCompile + + + + -Ybackend:GenBCode + -Ydelambdafy:method + -target:jvm-1.8 + -deprecation + -feature + -unchecked + -language:implicitConversions + -language:postfixOps + + + + + + + io.gatling + gatling-maven-plugin + ${gatling-maven-plugin.version} + + true + ${performance-test.skip} + + + + test + + execute + + + + + + maven-surefire-plugin + 2.10 + + + default-test + none + + + + + + diff --git a/jcommune-performance-tests/src/test/resources/credentials.csv b/jcommune-performance-tests/src/test/resources/credentials.csv new file mode 100644 index 0000000000..860af79c07 --- /dev/null +++ b/jcommune-performance-tests/src/test/resources/credentials.csv @@ -0,0 +1,2 @@ +username,password +admin,admin \ No newline at end of file diff --git a/jcommune-performance-tests/src/test/resources/gatling.conf b/jcommune-performance-tests/src/test/resources/gatling.conf new file mode 100644 index 0000000000..8bfa0ed366 --- /dev/null +++ b/jcommune-performance-tests/src/test/resources/gatling.conf @@ -0,0 +1,127 @@ +######################### +# Gatling Configuration # +######################### + +# This file contains all the settings configurable for Gatling with their default values + +gatling { + core { + #outputDirectoryBaseName = "" # The prefix for each simulation result folder (then suffixed by the report generation timestamp) + #runDescription = "" # The description for this simulation run, displayed in each report + #encoding = "utf-8" # Encoding to use throughout Gatling for file and string manipulation + #simulationClass = "" # The FQCN of the simulation to run (when used in conjunction with noReports, the simulation for which assertions will be validated) + #mute = false # When set to true, don't ask for simulation name nor run description (currently only used by Gatling SBT plugin) + #elFileBodiesCacheMaxCapacity = 200 # Cache size for request body EL templates, set to 0 to disable + #rawFileBodiesCacheMaxCapacity = 200 # Cache size for request body Raw templates, set to 0 to disable + #rawFileBodiesInMemoryMaxSize = 1000 # Below this limit, raw file bodies will be cached in memory + + extract { + regex { + #cacheMaxCapacity = 200 # Cache size for the compiled regexes, set to 0 to disable caching + } + xpath { + #cacheMaxCapacity = 200 # Cache size for the compiled XPath queries, set to 0 to disable caching + } + jsonPath { + #cacheMaxCapacity = 200 # Cache size for the compiled jsonPath queries, set to 0 to disable caching + #preferJackson = false # When set to true, prefer Jackson over Boon for JSON-related operations + } + css { + #cacheMaxCapacity = 200 # Cache size for the compiled CSS selectors queries, set to 0 to disable caching + } + } + + directory { + #data = user-files/data # Folder where user's data (e.g. files used by Feeders) is located + #bodies = user-files/bodies # Folder where bodies are located + #simulations = user-files/simulations # Folder where the bundle's simulations are located + #reportsOnly = "" # If set, name of report folder to look for in order to generate its report + #binaries = "" # If set, name of the folder where compiles classes are located: Defaults to GATLING_HOME/target. + #results = results # Name of the folder where all reports folder are located + } + } + charting { + #noReports = false # When set to true, don't generate HTML reports + #maxPlotPerSeries = 1000 # Number of points per graph in Gatling reports + #useGroupDurationMetric = false # Switch group timings from cumulated response time to group duration. + indicators { + #lowerBound = 800 # Lower bound for the requests' response time to track in the reports and the console summary + #higherBound = 1200 # Higher bound for the requests' response time to track in the reports and the console summary + #percentile1 = 50 # Value for the 1st percentile to track in the reports, the console summary and Graphite + #percentile2 = 75 # Value for the 2nd percentile to track in the reports, the console summary and Graphite + #percentile3 = 95 # Value for the 3rd percentile to track in the reports, the console summary and Graphite + #percentile4 = 99 # Value for the 4th percentile to track in the reports, the console summary and Graphite + } + } + http { + #fetchedCssCacheMaxCapacity = 200 # Cache size for CSS parsed content, set to 0 to disable + #fetchedHtmlCacheMaxCapacity = 200 # Cache size for HTML parsed content, set to 0 to disable + #perUserCacheMaxCapacity = 200 # Per virtual user cache size, set to 0 to disable + #warmUpUrl = "http://gatling.io" # The URL to use to warm-up the HTTP stack (blank means disabled) + #enableGA = true # Very light Google Analytics, please support + ssl { + keyStore { + #type = "" # Type of SSLContext's KeyManagers store + #file = "" # Location of SSLContext's KeyManagers store + #password = "" # Password for SSLContext's KeyManagers store + #algorithm = "" # Algorithm used SSLContext's KeyManagers store + } + trustStore { + #type = "" # Type of SSLContext's TrustManagers store + #file = "" # Location of SSLContext's TrustManagers store + #password = "" # Password for SSLContext's TrustManagers store + #algorithm = "" # Algorithm used by SSLContext's TrustManagers store + } + } + ahc { + #keepAlive = true # Allow pooling HTTP connections (keep-alive header automatically added) + #connectTimeout = 60000 # Timeout when establishing a connection + #pooledConnectionIdleTimeout = 60000 # Timeout when a connection stays unused in the pool + #readTimeout = 60000 # Timeout when a used connection stays idle + #maxRetry = 2 # Number of times that a request should be tried again + #requestTimeout = 60000 # Timeout of the requests + #acceptAnyCertificate = true # When set to true, doesn't validate SSL certificates + #httpClientCodecMaxInitialLineLength = 4096 # Maximum length of the initial line of the response (e.g. "HTTP/1.0 200 OK") + #httpClientCodecMaxHeaderSize = 8192 # Maximum size, in bytes, of each request's headers + #httpClientCodecMaxChunkSize = 8192 # Maximum length of the content or each chunk + #webSocketMaxFrameSize = 10240000 # Maximum frame payload size + #sslEnabledProtocols = [TLSv1.2, TLSv1.1, TLSv1] # Array of enabled protocols for HTTPS, if empty use the JDK defaults + #sslEnabledCipherSuites = [] # Array of enabled cipher suites for HTTPS, if empty use the JDK defaults + #sslSessionCacheSize = 0 # SSLSession cache size, set to 0 to use JDK's default + #sslSessionTimeout = 0 # SSLSession timeout in seconds, set to 0 to use JDK's default (24h) + #useOpenSsl = false # if OpenSSL should be used instead of JSSE (requires tcnative jar) + #useNativeTransport = false # if native transport should be used instead of Java NIO (requires netty-transport-native-epoll, currently Linux only) + #usePooledMemory = true # if Gatling should use pooled memory + #tcpNoDelay = true + #soReuseAddress = false + #soLinger = -1 + #soSndBuf = -1 + #soRcvBuf = -1 + } + dns { + #queryTimeout = 5000 # Timeout of each DNS query in millis + #maxQueriesPerResolve = 3 # Maximum allowed number of DNS queries for a given name resolution + } + } + data { + #writers = [console, file] # The list of DataWriters to which Gatling write simulation data (currently supported : console, file, graphite, jdbc) + console { + #light = false # When set to true, displays a light version without detailed request stats + } + file { + #bufferSize = 8192 # FileDataWriter's internal data buffer size, in bytes + } + leak { + #noActivityTimeout = 30 # Period, in seconds, for which Gatling may have no activity before considering a leak may be happening + } + graphite { + #light = false # only send the all* stats + #host = "localhost" # The host where the Carbon server is located + #port = 2003 # The port to which the Carbon server listens to (2003 is default for plaintext, 2004 is default for pickle) + #protocol = "tcp" # The protocol used to send data to Carbon (currently supported : "tcp", "udp") + #rootPathPrefix = "gatling" # The common prefix of all metrics sent to Graphite + #bufferSize = 8192 # GraphiteDataWriter's internal data buffer size, in bytes + #writeInterval = 1 # GraphiteDataWriter's write interval, in seconds + } + } +} diff --git a/jcommune-performance-tests/src/test/resources/log4j.xml b/jcommune-performance-tests/src/test/resources/log4j.xml new file mode 100644 index 0000000000..59ece2d6f1 --- /dev/null +++ b/jcommune-performance-tests/src/test/resources/log4j.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/jcommune-performance-tests/src/test/resources/recorder.conf b/jcommune-performance-tests/src/test/resources/recorder.conf new file mode 100644 index 0000000000..969b9e8668 --- /dev/null +++ b/jcommune-performance-tests/src/test/resources/recorder.conf @@ -0,0 +1,53 @@ +recorder { + core { + #mode = "Proxy" + #encoding = "utf-8" # The encoding used for reading/writing request bodies and the generated simulation + #outputFolder = "" # The folder where generated simulation will we written + #package = "" # The package's name of the generated simulation + #className = "RecordedSimulation" # The name of the generated Simulation class + #thresholdForPauseCreation = 100 # The minimum time, in milliseconds, that must pass between requests to trigger a pause creation + #saveConfig = false # When set to true, the configuration from the Recorder GUI overwrites this configuration + #headless = false # When set to true, run the Recorder in headless mode instead of the GUI + #harFilePath = "" # The path of the HAR file to convert + } + filters { + #filterStrategy = "Disabled" # The selected filter resources filter strategy (currently supported : "Disabled", "BlackList", "WhiteList") + #whitelist = [] # The list of ressources patterns that are part of the Recorder's whitelist + #blacklist = [] # The list of ressources patterns that are part of the Recorder's blacklist + } + http { + #automaticReferer = true # When set to false, write the referer + enable 'disableAutoReferer' in the generated simulation + #followRedirect = true # When set to false, write redirect requests + enable 'disableFollowRedirect' in the generated simulation + #removeCacheHeaders = true # When set to true, removes from the generated requests headers leading to request caching + #inferHtmlResources = true # When set to true, add inferred resources + set 'inferHtmlResources' with the configured blacklist/whitelist in the generated simulation + #checkResponseBodies = false # When set to true, save response bodies as files and add raw checks in the generated simulation + } + proxy { + #port = 8000 # Local port used by Gatling's Proxy for HTTP/HTTPS + https { + #mode = "SelfSignedCertificate" # The selected "HTTPS mode" (currently supported : "SelfSignedCertificate", "ProvidedKeyStore", "GatlingCertificateAuthority", "CustomCertificateAuthority") + keyStore { + #path = "" # The path of the custom key store + #password = "" # The password for this key store + #type = "JKS" # The type of the key store (currently supported: "JKS") + } + certificateAuthority { + #certificatePath = "" # The path of the custom certificate + #privateKeyPath = "" # The certificate's private key path + } + } + outgoing { + #host = "" # The outgoing proxy's hostname + #username = "" # The username to use to connect to the outgoing proxy + #password = "" # The password corresponding to the user to use to connect to the outgoing proxy + #port = 0 # The HTTP port to use to connect to the outgoing proxy + #sslPort = 0 # If set, The HTTPS port to use to connect to the outgoing proxy + } + } + netty { + #maxInitialLineLength = 10000 # Maximum length of the initial line of the response (e.g. "HTTP/1.0 200 OK") + #maxHeaderSize = 20000 # Maximum size, in bytes, of each request's headers + #maxChunkSize = 8192 # Maximum length of the content or each chunk + #maxContentLength = 100000000 # Maximum length of the aggregated content of each response + } +} diff --git a/jcommune-performance-tests/src/test/scala/Engine.scala b/jcommune-performance-tests/src/test/scala/Engine.scala new file mode 100644 index 0000000000..63c501fa3e --- /dev/null +++ b/jcommune-performance-tests/src/test/scala/Engine.scala @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +import io.gatling.app.Gatling +import io.gatling.core.config.GatlingPropertiesBuilder + +object Engine extends App { + + val props = new GatlingPropertiesBuilder + props.dataDirectory(IDEPathHelper.dataDirectory.toString) + props.resultsDirectory(IDEPathHelper.resultsDirectory.toString) + props.bodiesDirectory(IDEPathHelper.bodiesDirectory.toString) + props.binariesDirectory(IDEPathHelper.mavenBinariesDirectory.toString) + + Gatling.fromMap(props.build) +} diff --git a/jcommune-performance-tests/src/test/scala/IDEPathHelper.scala b/jcommune-performance-tests/src/test/scala/IDEPathHelper.scala new file mode 100644 index 0000000000..481c891bfe --- /dev/null +++ b/jcommune-performance-tests/src/test/scala/IDEPathHelper.scala @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +import java.nio.file.Path + +import io.gatling.commons.util.PathHelper._ + +object IDEPathHelper { + + val gatlingConfUrl: Path = getClass.getClassLoader.getResource("gatling.conf").toURI + val projectRootDir = gatlingConfUrl.ancestor(3) + + val mavenSourcesDirectory = projectRootDir / "src" / "test" / "scala" + val mavenResourcesDirectory = projectRootDir / "src" / "test" / "resources" + val mavenTargetDirectory = projectRootDir / "target" + val mavenBinariesDirectory = mavenTargetDirectory / "test-classes" + + val dataDirectory = mavenResourcesDirectory / "data" + val bodiesDirectory = mavenResourcesDirectory / "bodies" + + val recorderOutputDirectory = mavenSourcesDirectory + val resultsDirectory = mavenTargetDirectory / "gatling" + + val recorderConfigFile = mavenResourcesDirectory / "recorder.conf" +} diff --git a/jcommune-performance-tests/src/test/scala/Recorder.scala b/jcommune-performance-tests/src/test/scala/Recorder.scala new file mode 100644 index 0000000000..b4248bbe59 --- /dev/null +++ b/jcommune-performance-tests/src/test/scala/Recorder.scala @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +import io.gatling.recorder.GatlingRecorder +import io.gatling.recorder.config.RecorderPropertiesBuilder + +object Recorder extends App { + + val props = new RecorderPropertiesBuilder + props.simulationOutputFolder(IDEPathHelper.recorderOutputDirectory.toString) + props.simulationPackage("org.jtalks.com") + props.bodiesFolder(IDEPathHelper.bodiesDirectory.toString) + + GatlingRecorder.fromMap(props.build, Some(IDEPathHelper.recorderConfigFile)) +} diff --git a/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/model/User.scala b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/model/User.scala new file mode 100644 index 0000000000..cd0572f856 --- /dev/null +++ b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/model/User.scala @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.performance.model + +import io.gatling.core.Predef._ +import io.gatling.core.structure.ChainBuilder +import io.gatling.http.Predef._ +import org.jtalks.jcommune.performance.model.User.Role.Role + +/** + * @author Oleg Tkachenko + */ + +object User { + + object Role extends Enumeration { + type Role = Value + val Anonymous, Registered = Value + } + + private val header = Map("Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + private val credentials = Array(Map("username" -> "admin", "password" -> "admin")) + + val performLogin = scenario("Perform Login") + .feed(credentials.circular) + .exec(http("Perform Login (" + "${username}" + ")") + .post("/login_ajax") + .formParam("userName", "${username}") + .formParam("password", "${password}") + .formParam("_spring_security_remember_me", "on") + .check(status.is(200))) + + def openForumMainPage(role: Role): ChainBuilder = exec(http("Open main forum page (" + role + ")") + .get("/") + .check(regex(".*/branches/([\\d]{1,4})").find(0).saveAs("branchId")) + .headers(header) + .check(status.is(200)) + ) + + def openRecent(role: Role): ChainBuilder = exec(http("Open recent (" + role + ")") + .get("/topics/recent") + .headers(header) + .check(regex(".*/topics/([\\d]{1,6})").find(0).saveAs("topicId")) + .check(status.is(200)) + ) + + def openBranch(role: Role): ChainBuilder = { + doIf("${branchId.exists()}") { + exec(http("Open branch (id: ${branchId}) by (" + role + ")") + .get("/branches/${branchId}") + .headers(header) + .check(status.is(200)) + .check(regex(".*/topics/([\\d]{1,6})").find(0).saveAs("topicId"))) + } + } + + def openRandomTopic(role: Role): ChainBuilder = { + doIf("${topicId.exists()}") { + exec(http("Open topic (id: ${topicId}) by (" + role + ")") + .get("/topics/${topicId}") + .headers(header) + .check(status.in(200, 304))) + } + } +} diff --git a/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/Login.scala b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/Login.scala new file mode 100644 index 0000000000..14fea2b05c --- /dev/null +++ b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/Login.scala @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.performance.tests + +import io.gatling.core.Predef._ +import org.jtalks.jcommune.performance.utils.ScnBuilder._ +import scala.concurrent.duration.FiniteDuration + +class Login extends Simulation { + + val TEST_DURATION: FiniteDuration = 60 //Simulation duration in seconds + val NUM_OF_USERS: Int = 3 // Number of users generated per second + + setUp( + scnPerformLogin + .inject( + constantUsersPerSec(NUM_OF_USERS) + .during(TEST_DURATION))) + .assertions( + global.successfulRequests.percent.is(100), + global.responseTime.percentile2.lessThan(1500)) + .protocols(httpProtocol) +} diff --git a/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenBranchPage.scala b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenBranchPage.scala new file mode 100644 index 0000000000..dd6892655c --- /dev/null +++ b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenBranchPage.scala @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.performance.tests + +import io.gatling.core.Predef._ +import org.jtalks.jcommune.performance.model.User.Role._ +import org.jtalks.jcommune.performance.utils.ScnBuilder._ +import scala.concurrent.duration.FiniteDuration + +class OpenBranchPage extends Simulation{ + + val TEST_DURATION: FiniteDuration = 60 //Simulation duration in seconds + val NUM_OF_ANONYMOUS_USERS: Int = 1 // Number of Anonymous users generated per second + val NUM_OF_REGISTERED_USERS: Int = 1 // Number of Registered users generated per second + + setUp( + scnOpenBranchPage(Anonymous) + .inject( + constantUsersPerSec(NUM_OF_ANONYMOUS_USERS) + .during(TEST_DURATION)), + + scnOpenBranchPage(Registered) + .inject( + constantUsersPerSec(NUM_OF_REGISTERED_USERS) + .during(TEST_DURATION) + )) + .assertions( + global.successfulRequests.percent.is(100), + global.responseTime.percentile1.lessThan(1500)) + .protocols(httpProtocol) +} diff --git a/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenForumMainPage.scala b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenForumMainPage.scala new file mode 100644 index 0000000000..f791eb9459 --- /dev/null +++ b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenForumMainPage.scala @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.performance.tests + +import io.gatling.core.Predef._ +import org.jtalks.jcommune.performance.model.User.Role._ +import org.jtalks.jcommune.performance.utils.ScnBuilder._ + +import scala.concurrent.duration.FiniteDuration + +class OpenForumMainPage extends Simulation{ + + val TEST_DURATION: FiniteDuration = 60 //Simulation duration in seconds + val NUM_OF_ANONYMOUS_USERS: Int = 1 // Number of Anonymous users generated per second + val NUM_OF_REGISTERED_USERS: Int = 1 // Number of Registered users generated per second + + setUp( + scnOpenForumMainPage(Anonymous) + .inject( + constantUsersPerSec(NUM_OF_ANONYMOUS_USERS) + .during(TEST_DURATION)), + + scnOpenForumMainPage(Registered) + .inject( + constantUsersPerSec(NUM_OF_REGISTERED_USERS) + .during(TEST_DURATION) + )) + .assertions( + global.successfulRequests.percent.is(100), + global.responseTime.percentile1.lessThan(1500)) + .protocols(httpProtocol) +} diff --git a/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenRecentActivityPage.scala b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenRecentActivityPage.scala new file mode 100644 index 0000000000..9582e3dbbd --- /dev/null +++ b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenRecentActivityPage.scala @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.performance.tests + +import io.gatling.core.Predef._ +import org.jtalks.jcommune.performance.model.User.Role.{Anonymous, Registered} +import org.jtalks.jcommune.performance.utils.ScnBuilder._ +import scala.concurrent.duration.FiniteDuration + +class OpenRecentActivityPage extends Simulation { + + val TEST_DURATION: FiniteDuration = 60 //Simulation duration in seconds + val NUM_OF_ANONYMOUS_USERS: Int = 1 // Number of Anonymous users generated per second + val NUM_OF_REGISTERED_USERS: Int = 1 // Number of Registered users generated per second + + setUp( + scnOpenRecentActivityPage(Anonymous) + .inject( + constantUsersPerSec(NUM_OF_ANONYMOUS_USERS) + .during(TEST_DURATION)), + + scnOpenRecentActivityPage(Registered) + .inject( + constantUsersPerSec(NUM_OF_REGISTERED_USERS) + .during(TEST_DURATION) + )) + .assertions( + global.successfulRequests.percent.is(100), + global.responseTime.percentile1.lessThan(1500)) + .protocols(httpProtocol) +} diff --git a/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenTopicPage.scala b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenTopicPage.scala new file mode 100644 index 0000000000..d6effcbf5c --- /dev/null +++ b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/tests/OpenTopicPage.scala @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.performance.tests + +import io.gatling.core.Predef._ +import org.jtalks.jcommune.performance.model.User.Role._ +import org.jtalks.jcommune.performance.utils.ScnBuilder._ +import scala.concurrent.duration.FiniteDuration + +class OpenTopicPage extends Simulation{ + + val TEST_DURATION: FiniteDuration = 60 //Simulation duration in seconds + val NUM_OF_ANONYMOUS_USERS: Int = 1 // Number of Anonymous users generated per second + val NUM_OF_REGISTERED_USERS: Int = 1 // Number of Registered users generated per second + + setUp( + scnOpenTopicPage(Anonymous) + .inject( + constantUsersPerSec(NUM_OF_ANONYMOUS_USERS) + .during(TEST_DURATION)), + + scnOpenTopicPage(Registered) + .inject( + constantUsersPerSec(NUM_OF_REGISTERED_USERS) + .during(TEST_DURATION) + )) + .assertions( + global.successfulRequests.percent.is(100), + global.responseTime.percentile1.lessThan(3000)) + .protocols(httpProtocol) +} diff --git a/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/utils/ScnBuilder.scala b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/utils/ScnBuilder.scala new file mode 100644 index 0000000000..d9df1c43ef --- /dev/null +++ b/jcommune-performance-tests/src/test/scala/org/jtalks/jcommune/performance/utils/ScnBuilder.scala @@ -0,0 +1,79 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.performance.utils + +import io.gatling.core.Predef._ +import io.gatling.core.structure.ScenarioBuilder +import io.gatling.http.Predef._ +import org.jtalks.jcommune.performance.model.User.Role.{Registered, Role} +import org.jtalks.jcommune.performance.model.User._ +/** + * @author Oleg Tkachenko + */ + +object ScnBuilder { + + val serverUrl: String = { + val url: String = System.getProperty("performance.url") + if (url == null) "http://performance.jtalks.org/jcommune" else url + } + + val scnPerformLogin: ScenarioBuilder = scenario("Perform Login").exec(performLogin) + + def scnOpenForumMainPage(role: Role): ScenarioBuilder = { + scenario(role + " User open forum main page") + .doIf(role == Registered) { + exec(performLogin) + } + .exec(openForumMainPage(role)) + } + + def scnOpenRecentActivityPage(role: Role): ScenarioBuilder = { + scenario(role + " User open recent activity page") + .doIf(role == Registered) { + exec(performLogin) + } + .exec(openRecent(role)) + } + + def scnOpenBranchPage(role: Role): ScenarioBuilder = { + scenario(role + " User open branch page") + .doIf(role == Registered) { + exec(performLogin) + } + .exec(openForumMainPage(role)) + .exec(openBranch(role)) + } + + def scnOpenTopicPage(role: Role): ScenarioBuilder = { + scenario(role + " User open topic page") + .doIf(role == Registered) { + exec(performLogin) + } + .exec(openForumMainPage(role)) + .exec(openBranch(role)) + .exec(openRandomTopic(role)) + } + + val httpProtocol = http + .baseURL(serverUrl) + .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + .doNotTrackHeader("1") + .acceptLanguageHeader("en-US,en;q=0.5") + .acceptEncodingHeader("gzip, deflate") + .userAgentHeader("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0") + +} diff --git a/jcommune-plugin-api/pom.xml b/jcommune-plugin-api/pom.xml index d0212cc37e..b84e202e48 100644 --- a/jcommune-plugin-api/pom.xml +++ b/jcommune-plugin-api/pom.xml @@ -4,7 +4,7 @@ jcommune org.jtalks.jcommune - 2.14-SNAPSHOT + 3.13-SNAPSHOT jcommune-plugin-api @@ -44,12 +44,16 @@ spring-webmvc - velocity-tools - velocity-tools-generic + org.apache.velocity + velocity-tools org.kefirsf kefirbb + + org.apache.commons + commons-lang3 + diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/PluginClassLoader.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/PluginClassLoader.java index 51c08b7897..278a694159 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/PluginClassLoader.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/PluginClassLoader.java @@ -46,7 +46,7 @@ public class PluginClassLoader extends URLClassLoader { */ public PluginClassLoader (String folder) { super(resolvePluginLocations(folder), StatefullPlugin.class.getClassLoader()); - LOGGER.debug("Plugin class loader created for folder {}"); + LOGGER.debug("Plugin class loader created for folder {}", folder); } private static URL[] resolvePluginLocations(String folder) { diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/AuthenticationPlugin.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/AuthenticationPlugin.java index c9eef21c27..bc1eebe676 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/AuthenticationPlugin.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/AuthenticationPlugin.java @@ -44,4 +44,15 @@ public interface AuthenticationPlugin extends Plugin { */ Map authenticate(String login, String password) throws UnexpectedErrorException, NoConnectionException; + + /** + * Performs user activation by username. On the registration stage two copies + * of user has been created. One user in jcommune and the other in the Poulpe + * and each user have unique UUID. So we can't use UUID as universal key to + * access to the user in both databases. When user tries to activate account we + * use UUID to activate account in jcommune and username to activate in poulpe. + * + * @param username username + */ + void activate(String username); } diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/StatefullPlugin.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/StatefullPlugin.java index a73ac56252..aa86a02240 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/StatefullPlugin.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/StatefullPlugin.java @@ -52,10 +52,10 @@ public void configure(PluginConfiguration configuration) throws UnexpectedErrorE this.applyConfiguration(configuration.getProperties()); if (configuration.isActive()){ state = State.ENABLED; - LOGGER.debug("Plugin {} is configured and activated", this.getName()); + LOGGER.trace("Plugin {} is configured and activated", this.getName()); } else { state = State.CONFIGURED; - LOGGER.debug("Plugin {} is configured", this.getName()); + LOGGER.trace("Plugin {} is configured", this.getName()); } } catch (PluginConfigurationException | RuntimeException e) { state = State.IN_ERROR; diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/SubscribersFilter.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/SubscribersFilter.java new file mode 100644 index 0000000000..90d3c89377 --- /dev/null +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/SubscribersFilter.java @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.api.core; + +import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.SubscriptionAwareEntity; + +import java.util.Collection; + +/** + * Provides possibility to have different recipients for same type of notifications in different plugins + * + * @author Mikhail Stryzhonok + */ +public interface SubscribersFilter { + + /** + * Filters collection of subscribers if specified entity belongs to plugin, otherwise ignores collection silently + * + * @param users collections of subscribers to filter + * @param entity entity to perform filtering + */ + void filter(Collection users, SubscriptionAwareEntity entity); +} diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/TopicPlugin.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/TopicPlugin.java index 6993ea6999..2e37b6a6f8 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/TopicPlugin.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/core/TopicPlugin.java @@ -48,4 +48,19 @@ public interface TopicPlugin extends PluginWithBranchPermissions { * @return topic type */ String getTopicType(); + + /** + * Gets permission which should be granted to user to allow add comments to posts from topic, provided by plugin + * This gives possibility to have different permissions for adding comments in plugins + * + * @return the permission which allows to add comments + */ + JtalksPermission getCommentPermission(); + + /** + * Gets subscribers filter of current plugin + * + * @return subscribers filter of current plugin + */ + SubscribersFilter getSubscribersFilter(); } diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/filters/TopicTypeFilter.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/filters/TopicTypeFilter.java new file mode 100644 index 0000000000..010d00c10e --- /dev/null +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/filters/TopicTypeFilter.java @@ -0,0 +1,45 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.api.filters; + +import org.apache.commons.lang.Validate; +import org.jtalks.jcommune.plugin.api.core.Plugin; +import org.jtalks.jcommune.plugin.api.core.TopicPlugin; + +import java.util.Objects; + +/** + * @author Dmitry S. Dolzhenko + */ +public class TopicTypeFilter implements PluginFilter { + + private final String topicType; + + public TopicTypeFilter(String topicType) { + Validate.notEmpty(topicType, "Could not lookup plugin by empty topicType: [" + topicType + "]"); + + this.topicType = topicType; + } + + @SuppressWarnings("SimplifiableIfStatement") + @Override + public boolean accept(Plugin plugin) { + if (!(plugin instanceof TopicPlugin)) { + return false; + } + + return Objects.equals(topicType, ((TopicPlugin) plugin).getTopicType()); + } +} diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginCommentService.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginCommentService.java new file mode 100644 index 0000000000..985e70f02c --- /dev/null +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginCommentService.java @@ -0,0 +1,57 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.api.service; + +import org.jtalks.jcommune.model.entity.Post; +import org.jtalks.jcommune.model.entity.PostComment; +import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; + +/** + * @author Mikhail Stryzhonok + */ +public interface PluginCommentService { + + /** + * Updates comment's body + * + * @param id ID of comment + * @param body new body of comment + * @param branchId ID of branch containing code review to check permissions + * + * @return updated comment entity + * @throws NotFoundException when entity not found + */ + PostComment updateComment(long id, String body, long branchId) throws NotFoundException; + + /** + * Fetch comment with specified id from database + * + * @param id id of interested comment + * + * @return comment from with specified id + * @throws NotFoundException if comment with specified id not found in database + */ + PostComment getComment(long id) throws NotFoundException; + + /** + * Marks specified comment of specified post as deleted by setting deletion date + * + * @param post post which contains comment + * @param comment comment to be delete + * + * @return comment marked as deleted + */ + public PostComment markCommentAsDeleted(Post post, PostComment comment); +} diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginLastReadPostService.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginLastReadPostService.java index e141c0b650..89ad69c6a5 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginLastReadPostService.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginLastReadPostService.java @@ -23,14 +23,12 @@ public interface PluginLastReadPostService { /** - * Marks topic page as read for the current user. - * That means all posts on this page are to marked as read. - * If paging as disabled all posts in the topic will be marked as read. + * Marks the whole topic as read for the current user. + * That means all posts there are to marked as read. *

* For anonymous user call will have no effect. * * @param topic topic to mark as read - * @param pageNum page to mark as read */ - void markTopicPageAsRead(Topic topic, int pageNum); + void markTopicAsRead(Topic topic); } diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginLocationService.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginLocationService.java index 601cabb642..5a3f00100d 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginLocationService.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginLocationService.java @@ -15,7 +15,7 @@ package org.jtalks.jcommune.plugin.api.service; import org.jtalks.common.model.entity.Entity; -import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.UserInfo; import java.util.List; @@ -34,5 +34,5 @@ public interface PluginLocationService { * @return Users, who're viewing the page for entity passed. Will return empty list if * there are no viewers or view tracking is not supported for this entity type */ - List getUsersViewing(Entity entity); + List getUsersViewing(Entity entity); } diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginPostService.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginPostService.java index 80c2f9a7ee..5637235b53 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginPostService.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginPostService.java @@ -15,8 +15,11 @@ package org.jtalks.jcommune.plugin.api.service; import org.jtalks.jcommune.model.entity.Post; +import org.jtalks.jcommune.model.entity.PostComment; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import java.util.Map; + /** * @author Andrei Alikov */ @@ -48,4 +51,18 @@ public interface PluginPostService { * when post not found */ void updatePost(Post post, String postContent) throws NotFoundException; + + /** + * Adds comment to post with specified id + * + * @param postId id of post to which comment will be added + * @param attributes list of comment attributes + * @param body text of the comment + + * @return newly created comment + * + * @throws NotFoundException if post with specified id not found + */ + PostComment addComment(Long postId, Map attributes, String body) throws NotFoundException; + } diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginTopicDraftService.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginTopicDraftService.java new file mode 100644 index 0000000000..8c64907102 --- /dev/null +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/PluginTopicDraftService.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.api.service; + +import org.jtalks.jcommune.model.entity.TopicDraft; + +/** + * @author Dmitry S. Dolzhenko + */ +public interface PluginTopicDraftService { + /** + * Returns the draft topic for current user. + * + * @return the draft topic or null + */ + TopicDraft getDraft(); + + /** + * Save or update the draft topic. + * + * @param draft the draft topic + */ + TopicDraft saveOrUpdateDraft(TopicDraft draft); + + /** + * Delete the draft topic. + */ + void deleteDraft(); +} diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/nontransactional/PluginLocationServiceImpl.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/nontransactional/PluginLocationServiceImpl.java index b3653bca3c..9bc28ca0b5 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/nontransactional/PluginLocationServiceImpl.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/nontransactional/PluginLocationServiceImpl.java @@ -15,7 +15,7 @@ package org.jtalks.jcommune.plugin.api.service.nontransactional; import org.jtalks.common.model.entity.Entity; -import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.UserInfo; import org.jtalks.jcommune.plugin.api.service.PluginLocationService; import java.util.List; @@ -47,7 +47,7 @@ public static PluginLocationService getInstance() { } @Override - public List getUsersViewing(Entity entity) { + public List getUsersViewing(Entity entity) { return locationService.getUsersViewing(entity); } diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/nontransactional/PropertiesHolder.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/nontransactional/PropertiesHolder.java new file mode 100644 index 0000000000..62646f2c32 --- /dev/null +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/nontransactional/PropertiesHolder.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.api.service.nontransactional; + +import org.jtalks.jcommune.model.entity.JCommuneProperty; + +/** + * Class that holds properties and allows access to these properties from plugins + * + * This class is singleton because we can't use spring dependency injection mechanism in plugins due plugins can be + * added or removed in runtime. + * + * + * @author Mikhail Stryzhonok + */ +public class PropertiesHolder { + + private static final PropertiesHolder INSTANCE = new PropertiesHolder(); + + private JCommuneProperty allPagesTitlePrefixProperty; + + /** + * Use {@link #getInstance()}, this class is singleton + */ + private PropertiesHolder() { + + } + + /** + * Gets instance of this class + * + * @return instance of {@link PropertiesHolder} + */ + public static PropertiesHolder getInstance() { + return INSTANCE; + } + + /** + * Gets prefix for titles of all pages + * + * @return prefix for titles of all pages + */ + public String getAllPagesTitlePrefix() { + return allPagesTitlePrefixProperty.getValue(); + } + + public void setAllPagesTitlePrefixProperty(JCommuneProperty allPagesTitlePrefixProperty) { + this.allPagesTitlePrefixProperty = allPagesTitlePrefixProperty; + } +} diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginCommentService.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginCommentService.java new file mode 100644 index 0000000000..a19be0666f --- /dev/null +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginCommentService.java @@ -0,0 +1,84 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.api.service.transactional; + +import org.jtalks.jcommune.model.entity.Post; +import org.jtalks.jcommune.model.entity.PostComment; +import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.jtalks.jcommune.plugin.api.service.PluginCommentService; + +/** + * Class for manipulating with comments from plugin.To manipulate with comments from jcommune use classes from service + * module + * + * This class is singleton because we can't use spring dependency injection mechanism in plugins due plugins can be + * added or removed in runtime. + * + * @author Mikhail Stryzhonok + */ +public class TransactionalPluginCommentService implements PluginCommentService{ + private static final TransactionalPluginCommentService INSTANCE = new TransactionalPluginCommentService(); + + private PluginCommentService commentService; + + /** Use {@link #getInstance()}, this class is singleton. */ + private TransactionalPluginCommentService() { + + } + + /** + * Gets instance of {@link TransactionalPluginCommentService class + * + * @return instance of {@link TransactionalPluginCommentService class + */ + public static PluginCommentService getInstance() { + return INSTANCE; + } + + /** + * {@inheritDoc} + */ + @Override + public PostComment updateComment(long id, String body, long branchId) throws NotFoundException { + return commentService.updateComment(id, body, branchId); + } + + /** + * {@inheritDoc} + */ + + @Override + public PostComment getComment(long id) throws NotFoundException { + return commentService.getComment(id); + } + + /** + * {@inheritDoc} + */ + @Override + public PostComment markCommentAsDeleted(Post post, PostComment comment) { + return commentService.markCommentAsDeleted(post, comment); + } + + /** + * Sets specified {@link org.jtalks.jcommune.plugin.api.service.PluginCommentService} implementation + * Should be used once, during initialization + * + * @param commentService {@link org.jtalks.jcommune.plugin.api.service.PluginCommentService} implementation to set + */ + public void setCommentService(PluginCommentService commentService) { + this.commentService = commentService; + } +} diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginLastReadPostService.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginLastReadPostService.java index f9e794b7a9..7ad04ff3ad 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginLastReadPostService.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginLastReadPostService.java @@ -58,7 +58,7 @@ public void setLastReadPostService(PluginLastReadPostService lastReadPostService * {@inheritDoc} */ @Override - public void markTopicPageAsRead(Topic topic, int pageNum) { - lastReadPostService.markTopicPageAsRead(topic, pageNum); + public void markTopicAsRead(Topic topic) { + lastReadPostService.markTopicAsRead(topic); } } diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginPostService.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginPostService.java index 20ef9b926e..8442f4e44f 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginPostService.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginPostService.java @@ -16,9 +16,12 @@ package org.jtalks.jcommune.plugin.api.service.transactional; import org.jtalks.jcommune.model.entity.Post; +import org.jtalks.jcommune.model.entity.PostComment; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.service.PluginPostService; +import java.util.Map; + /** * @author Andrei Alikov */ @@ -66,9 +69,17 @@ public void updatePost(Post post, String postContent) throws NotFoundException { } /** - * Sets post service. Should be used once, during initialization + * {@inheritDoc} + */ + @Override + public PostComment addComment(Long postId, Map attributes, String body) throws NotFoundException { + return postService.addComment(postId, attributes, body); + } + + /** + * Sets specified post service. Should be used once, during initialization * - * @param postService + * @param postService post service to set */ public void setPostService(PluginPostService postService) { this.postService = postService; diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginTopicDraftService.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginTopicDraftService.java new file mode 100644 index 0000000000..e6c4ae774c --- /dev/null +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginTopicDraftService.java @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.api.service.transactional; + +import org.jtalks.jcommune.model.entity.TopicDraft; +import org.jtalks.jcommune.plugin.api.service.PluginTopicDraftService; + +/** + * @author Dmitry S. Dolzhenko + */ +public class TransactionalPluginTopicDraftService implements PluginTopicDraftService { + private static final PluginTopicDraftService INSTANCE = + new TransactionalPluginTopicDraftService(); + + private PluginTopicDraftService topicDraftService; + + public TransactionalPluginTopicDraftService() { + } + + public static PluginTopicDraftService getInstance() { + return INSTANCE; + } + + /** + * {@inheritDoc} + */ + @Override + public TopicDraft getDraft() { + return topicDraftService.getDraft(); + } + + /** + * {@inheritDoc} + */ + @Override + public TopicDraft saveOrUpdateDraft(TopicDraft draft) { + return topicDraftService.saveOrUpdateDraft(draft); + } + + /** + * {@inheritDoc} + */ + @Override + public void deleteDraft() { + topicDraftService.deleteDraft(); + } + + public void setTopicDraftService(PluginTopicDraftService topicDraftService) { + this.topicDraftService = topicDraftService; + } +} diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/PluginHandlerMapping.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/PluginHandlerMapping.java index 5e54bad049..9e9389a987 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/PluginHandlerMapping.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/PluginHandlerMapping.java @@ -28,22 +28,22 @@ import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; -import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.List; import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; /** * Custom handler mapping. Needed to map plugin handlers separately from application handlers. It's necessary to allow - * update handlers without application restart. + * update handlers without application restart. Default Spring {@code } still needs to be + * declared as usually - it will handle usual static controllers. * * @author Mikhail Stryzhonok */ public class PluginHandlerMapping extends RequestMappingHandlerMapping { - private static final PluginHandlerMapping INSTANCE = new PluginHandlerMapping(); - private final Map pluginHandlerMethods = new HashMap<>(); + private final Map pluginHandlerMethods = new ConcurrentHashMap<>(); private PluginLoader pluginLoader; private PluginHandlerMapping() { @@ -67,8 +67,6 @@ protected boolean isContextRequired() { protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) { if (PluginController.class.isAssignableFrom(method.getDeclaringClass())) { registerPluginHandlerMethod((PluginController)handler, method, mapping); - } else { - super.registerHandlerMethod(handler, method, mapping); } } @@ -165,12 +163,12 @@ public boolean matches(Method method) { */ @Override protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { + String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); + MethodAwareKey key = new MethodAwareKey(RequestMethod.valueOf(request.getMethod()), getUniformUrl(lookupPath)); //We should clear map in case if plugin version was changed pluginHandlerMethods.clear(); //We should update Web plugins before resolving handler pluginLoader.reloadPlugins(new TypeFilter(WebControllerPlugin.class)); - String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); - MethodAwareKey key = new MethodAwareKey(RequestMethod.valueOf(request.getMethod()), getUniformUrl(lookupPath)); HandlerMethod handlerMethod = findHandlerMethod(key); if (handlerMethod != null) { RequestMappingInfo mappingInfo = getMappingForMethod(handlerMethod.getMethod(), handlerMethod.getBeanType()); diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/PostDraftDto.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/PostDraftDto.java new file mode 100644 index 0000000000..c54a533e83 --- /dev/null +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/PostDraftDto.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.api.web.dto; + +import org.jtalks.jcommune.model.entity.Post; +import org.jtalks.jcommune.plugin.api.web.validation.annotations.BbCodeAwareSize; +import org.jtalks.jcommune.plugin.api.web.validation.annotations.BbCodeNesting; + +/** + * @author Dmitry S. Dolzhenko + */ +public class PostDraftDto { + /** + * Unlike post, draft may contain only one symbol to be saved. + */ + @BbCodeAwareSize(min = 1, max = Post.MAX_LENGTH) + @BbCodeNesting + private String bodyText; + private long topicId; + + public PostDraftDto() { + } + + public String getBodyText() { + return bodyText; + } + + public void setBodyText(String bodyText) { + this.bodyText = bodyText; + } + + public long getTopicId() { + return topicId; + } + + public void setTopicId(long topicId) { + this.topicId = topicId; + } +} diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/PostDto.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/PostDto.java index dda0d6d7ad..6a816c5a2f 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/PostDto.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/PostDto.java @@ -14,8 +14,10 @@ */ package org.jtalks.jcommune.plugin.api.web.dto; -import org.hibernate.validator.constraints.NotBlank; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.jtalks.jcommune.model.entity.Post; +import org.jtalks.jcommune.model.entity.PostDraft; import org.jtalks.jcommune.plugin.api.web.validation.annotations.BbCodeAwareSize; import org.jtalks.jcommune.plugin.api.web.validation.annotations.BbCodeNesting; @@ -24,12 +26,14 @@ */ public class PostDto { - @NotBlank @BbCodeAwareSize(min = Post.MIN_LENGTH, max = Post.MAX_LENGTH) @BbCodeNesting private String bodyText; private long id; private long topicId; + private TopicDto topicDto; + private DateTime creationDate; + private DateTime modificationDate; /** * Get topic id. @@ -85,6 +89,73 @@ public void setBodyText(String bodyText) { this.bodyText = bodyText; } + /** + * Gets topic dto of the post + * + * @return topic dto + */ + public TopicDto getTopicDto() { + return topicDto; + } + + /** + * Sets specified topic dto to the post + * + * @param topicDto topic dto to set + */ + public void setTopicDto(TopicDto topicDto) { + this.topicDto = topicDto; + } + + /** + * Gets time of post creation represented by this dto + * + * @return time of post creation + */ + public DateTime getCreationDate() { + return creationDate; + } + + /** + * Sets specified time of post creation + * + * @param creationDate time of post creation + */ + public void setCreationDate(DateTime creationDate) { + this.creationDate = creationDate; + } + + /** + * Gets time of post modificatiom represented by this dto + * + * @return + */ + public DateTime getModificationDate() { + return modificationDate; + } + + /** + * Sets specified time of post modification + * + * @param modificationDate time of post modification + */ + public void setModificationDate(DateTime modificationDate) { + this.modificationDate = modificationDate; + } + + /** + * Gets millisecond representation of post creation date in UTC timezone + * + * We store dates in database in timezone of server. And to display correct time of creation draft + * we convert UTC representation ot user's timezone in javascript + * + * @return millisecond representation of post creation date in UTC timezone + */ + public long getUtcCreationTime() { + DateTimeZone zone = creationDate.getZone(); + return zone.convertLocalToUTC(creationDate.getMillis(), false); + } + /** * Create dto * @@ -96,7 +167,42 @@ public static PostDto getDtoFor(Post post) { dto.setBodyText(post.getPostContent()); dto.setId(post.getId()); dto.setTopicId(post.getTopic().getId()); + dto.setCreationDate(post.getCreationDate()); + dto.setModificationDate(post.getModificationDate()); return dto; } -} + /** + * Creates dto from draft + * + * @param postdraft draft to create dto + * + * @return dto for draft + */ + public static PostDto getDtoFor(PostDraft postdraft) { + PostDto dto = new PostDto(); + dto.setBodyText(postdraft.getContent()); + dto.setId(postdraft.getId()); + dto.setTopicId(postdraft.getTopic().getId()); + dto.setCreationDate(postdraft.getLastSaved()); + return dto; + } + + public long getDifferenceMillis() { + DateTime currentDate = new DateTime(); + long differenceTime = currentDate.getMillis() - creationDate.getMillis(); + return differenceTime > 0 ? differenceTime : creationDate.getMillis(); + } + + /** + * Fills this dto from draft + * + * @param draft draft to fill dto + */ + public void fillFrom(PostDraft draft) { + this.setBodyText(draft.getContent()); + this.setId(draft.getId()); + this.setTopicId(draft.getTopic().getId()); + this.setCreationDate(draft.getLastSaved()); + } +} \ No newline at end of file diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/TopicDto.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/TopicDto.java index 99c688f571..917c3d5f5f 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/TopicDto.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/TopicDto.java @@ -14,9 +14,7 @@ */ package org.jtalks.jcommune.plugin.api.web.dto; -import org.jtalks.jcommune.model.entity.Poll; -import org.jtalks.jcommune.model.entity.Post; -import org.jtalks.jcommune.model.entity.Topic; +import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.plugin.api.web.validation.annotations.BbCodeAwareSize; import org.jtalks.jcommune.plugin.api.web.validation.annotations.BbCodeNesting; @@ -62,6 +60,26 @@ public TopicDto(Topic topic) { } } + /** + * Create dto from {@link TopicDraft} + * + * @param topicDraft draft topic for conversion + */ + public TopicDto(TopicDraft topicDraft) { + this.topic = new Topic(topicDraft.getTopicStarter(), topicDraft.getTitle()); + this.setBodyText(topicDraft.getContent()); + + String pollTitle = topicDraft.getPollTitle(); + String pollItemsValue = topicDraft.getPollItemsValue(); + + Poll poll = new Poll(); + if (pollTitle != null || pollItemsValue != null) { + poll.setTitle(pollTitle); + poll.setPollItemsValue(pollItemsValue); + } + this.topic.setPoll(poll); + } + /** * @return topic that used as dto between controllers and services */ diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/FailJsonResponse.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/FailJsonResponse.java similarity index 96% rename from jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/FailJsonResponse.java rename to jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/FailJsonResponse.java index 7bbdc10673..941d55ce3f 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/FailJsonResponse.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/FailJsonResponse.java @@ -12,7 +12,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.dto.json; +package org.jtalks.jcommune.plugin.api.web.dto.json; /** * AJAX response class to send fail JSON response diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/FailValidationJsonResponse.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/FailValidationJsonResponse.java similarity index 93% rename from jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/FailValidationJsonResponse.java rename to jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/FailValidationJsonResponse.java index b449ceeb3e..a6d7bbd72d 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/FailValidationJsonResponse.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/FailValidationJsonResponse.java @@ -12,7 +12,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.dto.json; +package org.jtalks.jcommune.plugin.api.web.dto.json; import java.util.ArrayList; import java.util.List; @@ -37,7 +37,7 @@ public class FailValidationJsonResponse extends FailJsonResponse { public FailValidationJsonResponse(List validationErrors) { super(JsonResponseReason.VALIDATION); - List errors = new ArrayList(); + List errors = new ArrayList<>(); for (ObjectError error : validationErrors) { String field = null; if (error instanceof FieldError) { diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/JsonResponse.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/JsonResponse.java similarity index 95% rename from jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/JsonResponse.java rename to jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/JsonResponse.java index 65f7cfce47..a7dfe25c63 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/JsonResponse.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/JsonResponse.java @@ -12,7 +12,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.dto.json; +package org.jtalks.jcommune.plugin.api.web.dto.json; /** * This is a generic AJAX response class to send JSON response from server to the client. This class can be used for @@ -26,6 +26,9 @@ public class JsonResponse { private JsonResponseStatus status; private Object result; + private JsonResponse() { + } + /** * Creates new instance * diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/JsonResponseReason.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/JsonResponseReason.java similarity index 93% rename from jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/JsonResponseReason.java rename to jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/JsonResponseReason.java index c36aebb8de..535df197d2 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/JsonResponseReason.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/JsonResponseReason.java @@ -12,7 +12,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.dto.json; +package org.jtalks.jcommune.plugin.api.web.dto.json; /** * Reason of request failure @@ -31,6 +31,6 @@ public enum JsonResponseReason { ENTITY_NOT_FOUND, /** Request was failed due to some server-side error */ - INTERNAL_SERVER_ERROR; + INTERNAL_SERVER_ERROR } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/JsonResponseStatus.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/JsonResponseStatus.java similarity index 94% rename from jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/JsonResponseStatus.java rename to jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/JsonResponseStatus.java index bd648913e5..ff4c343769 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/JsonResponseStatus.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/JsonResponseStatus.java @@ -12,7 +12,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.dto.json; +package org.jtalks.jcommune.plugin.api.web.dto.json; /** * Allowed response statuses for AJAX requests. @@ -25,5 +25,5 @@ public enum JsonResponseStatus { SUCCESS, /** When some error occurred during request */ - FAIL; + FAIL } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/ValidationError.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/ValidationError.java similarity index 96% rename from jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/ValidationError.java rename to jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/ValidationError.java index c5a5f20447..c35e688f51 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/ValidationError.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/ValidationError.java @@ -12,7 +12,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.dto.json; +package org.jtalks.jcommune.plugin.api.web.dto.json; /** * Class contains information about one validation error diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/package-info.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/package-info.java similarity index 94% rename from jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/package-info.java rename to jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/package-info.java index 39409e2b6c..df5248dd9d 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/json/package-info.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/dto/json/package-info.java @@ -17,4 +17,4 @@ * Package for view layer DTOs (which use JSON). * */ -package org.jtalks.jcommune.web.dto.json; +package org.jtalks.jcommune.plugin.api.web.dto.json; diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/locale/JcLocaleResolver.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/locale/JcLocaleResolver.java similarity index 70% rename from jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/locale/JcLocaleResolver.java rename to jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/locale/JcLocaleResolver.java index eea81b7a6a..e5b209a19b 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/locale/JcLocaleResolver.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/locale/JcLocaleResolver.java @@ -12,11 +12,12 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.locale; +package org.jtalks.jcommune.plugin.api.web.locale; import org.jtalks.jcommune.model.entity.JCUser; -import org.jtalks.jcommune.service.UserService; +import org.jtalks.jcommune.plugin.api.service.UserReader; +import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.i18n.CookieLocaleResolver; import javax.servlet.http.HttpServletRequest; @@ -29,10 +30,19 @@ */ public class JcLocaleResolver extends CookieLocaleResolver { - private UserService userService; + private static final LocaleResolver INSTANCE = new JcLocaleResolver(); - public JcLocaleResolver(UserService userService) { - this.userService = userService; + private UserReader userReader; + + private JcLocaleResolver() { + } + + public static LocaleResolver getInstance() { + return INSTANCE; + } + + public void setUserReader(UserReader userReader) { + this.userReader = userReader; } /** @@ -49,7 +59,7 @@ public Locale resolveLocale(HttpServletRequest request) { return locale; } - JCUser currentUser = userService.getCurrentUser(); + JCUser currentUser = userReader.getCurrentUser(); if (currentUser.isAnonymous()) { locale = super.resolveLocale(request); } else { @@ -58,4 +68,14 @@ public Locale resolveLocale(HttpServletRequest request) { request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, locale); return locale; } + + /** + * Method delegated following DRY principle. Default locale should be determined in single place. In our case - + * in xml configuration. + * {@inheritDoc} + */ + @Override + public Locale getDefaultLocale() { + return super.getDefaultLocale(); + } } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/locale/package-info.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/locale/package-info.java similarity index 94% rename from jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/locale/package-info.java rename to jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/locale/package-info.java index 30ed0ddff9..12bdb089e8 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/locale/package-info.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/locale/package-info.java @@ -15,4 +15,4 @@ /** * Provides package with custom locale resolvers */ -package org.jtalks.jcommune.web.locale; \ No newline at end of file +package org.jtalks.jcommune.plugin.api.web.locale; \ No newline at end of file diff --git a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/velocity/tool/JodaDateTimeTool.java b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/velocity/tool/JodaDateTimeTool.java index 3dc3c6e429..888ebf47c2 100644 --- a/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/velocity/tool/JodaDateTimeTool.java +++ b/jcommune-plugin-api/src/main/java/org/jtalks/jcommune/plugin/api/web/velocity/tool/JodaDateTimeTool.java @@ -18,6 +18,7 @@ import org.joda.time.DateTimeZone; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; +import org.jtalks.jcommune.plugin.api.web.locale.JcLocaleResolver; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; @@ -38,9 +39,11 @@ public class JodaDateTimeTool { private DateTimeFormatter formatter = DateTimeFormat.forPattern(DATE_FORMAT_PATTERN); private long offset = DEFAULT_OFFSET; + private Locale locale = Locale.ENGLISH; public JodaDateTimeTool(HttpServletRequest request) { if (request != null) { + locale = JcLocaleResolver.getInstance().resolveLocale(request); Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { @@ -58,10 +61,9 @@ public JodaDateTimeTool(HttpServletRequest request) { * Example: 01 Jan 2011 05:13 * * @param dateTime Date and time to be converted to string - * @param locale locale to be used * @return dateTime string representation */ - public String format(DateTime dateTime, Locale locale) { + public String format(DateTime dateTime) { if (dateTime == null) { return ""; } diff --git a/jcommune-plugin-api/src/main/resources/org/jtalks/jcommune/jcommune-plugin-api/web/templates/bbeditor.vm b/jcommune-plugin-api/src/main/resources/org/jtalks/jcommune/jcommune-plugin-api/web/templates/bbeditor.vm index aae8ea7b73..45d148531a 100644 --- a/jcommune-plugin-api/src/main/resources/org/jtalks/jcommune/jcommune-plugin-api/web/templates/bbeditor.vm +++ b/jcommune-plugin-api/src/main/resources/org/jtalks/jcommune/jcommune-plugin-api/web/templates/bbeditor.vm @@ -17,7 +17,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #macro(jtalksBinding $result $path) ##works through magic #if($result.hasFieldErrors($path)) - + $result.getFieldError($path).getDefaultMessage() #end @@ -44,7 +44,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -  Highlight  + label.answer.highlight.button

@@ -100,26 +100,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA title="label.answer.insert_link"> - - - label.answer.font_code - + + label.answer.font_code.button - - @@ -128,7 +112,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- label.answer.indent + label.answer.indent.button
-
- label.keymaps.post -
+ label.keymaps.post +
#jtalksBinding($result ${bodyParameterName})
-
+
#if($permissionTool.hasPermission($targedId.longValue(),"BRANCH","BranchPermission.MOVE_TOPICS")) $user.username  #end diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/dto/json/FailJsonResponseTest.java b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/dto/json/FailJsonResponseTest.java similarity index 86% rename from jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/dto/json/FailJsonResponseTest.java rename to jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/dto/json/FailJsonResponseTest.java index 17bfe70373..f322f86fd8 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/dto/json/FailJsonResponseTest.java +++ b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/dto/json/FailJsonResponseTest.java @@ -12,10 +12,14 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.dto.json; +package org.jtalks.jcommune.plugin.api.dto.json; import static org.testng.AssertJUnit.assertNull; import static org.testng.AssertJUnit.assertEquals; + +import org.jtalks.jcommune.plugin.api.web.dto.json.FailJsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseReason; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.testng.annotations.Test; public class FailJsonResponseTest { diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/dto/json/FailValidationJsonResponseTest.java b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/dto/json/FailValidationJsonResponseTest.java similarity index 84% rename from jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/dto/json/FailValidationJsonResponseTest.java rename to jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/dto/json/FailValidationJsonResponseTest.java index a7b175137f..37d953a3e6 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/dto/json/FailValidationJsonResponseTest.java +++ b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/dto/json/FailValidationJsonResponseTest.java @@ -12,12 +12,16 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.dto.json; +package org.jtalks.jcommune.plugin.api.dto.json; import static org.testng.AssertJUnit.assertEquals; import java.util.ArrayList; import java.util.List; +import org.jtalks.jcommune.plugin.api.web.dto.json.FailValidationJsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseReason; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.ValidationError; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.testng.annotations.Test; @@ -32,7 +36,7 @@ public class FailValidationJsonResponseTest { @SuppressWarnings("unchecked") @Test public void testConstructorWithBindingResult() { - List allErrors = new ArrayList(); + List allErrors = new ArrayList<>(); allErrors.add(new ObjectError(OBJECT_NAME, OBJECT_MESSAGE)); allErrors.add(new FieldError(OBJECT_NAME, FIELD, FIELD_MESSAGE)); diff --git a/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/filters/TopicTypeFilterTest.java b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/filters/TopicTypeFilterTest.java new file mode 100644 index 0000000000..4eb27f38b6 --- /dev/null +++ b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/filters/TopicTypeFilterTest.java @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.api.filters; + +import org.jtalks.jcommune.plugin.api.core.Plugin; +import org.jtalks.jcommune.plugin.api.core.TopicPlugin; +import org.testng.annotations.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * @author Dmitry S. Dolzhenko + */ +public class TopicTypeFilterTest { + @Test + public void filterShouldAcceptTopicPluginWithCorrectTopicType() throws Exception { + TopicPlugin topicPlugin = getPluginWithTopicType("type"); + + TopicTypeFilter topicTypeFilter = new TopicTypeFilter("type"); + assertTrue(topicTypeFilter.accept(topicPlugin)); + } + + @Test + public void filterShouldRejectTopicPluginWithIncorrectTopicType() throws Exception { + TopicPlugin topicPlugin = getPluginWithTopicType("type"); + + TopicTypeFilter topicTypeFilter = new TopicTypeFilter("type1"); + assertFalse(topicTypeFilter.accept(topicPlugin)); + } + + @Test + public void filterShouldRejectNotTopicPlugin() { + Plugin plugin = mock(Plugin.class); + + TopicTypeFilter topicTypeFilter = new TopicTypeFilter("type"); + assertFalse(topicTypeFilter.accept(plugin)); + } + + @Test + public void filterShouldRejectTopicPlugin_IfTopicTypeCaseDoesNotMatch() { + TopicPlugin topicPlugin = getPluginWithTopicType("type"); + + TopicTypeFilter topicTypeFilter = new TopicTypeFilter("Type"); + assertFalse(topicTypeFilter.accept(topicPlugin)); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void filterShouldThrowException_IfTopicTypeIsNull() { + new TopicTypeFilter(null); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void filterShouldThrowException_IfTopicTypeIsEmpty() { + new TopicTypeFilter(""); + } + + private TopicPlugin getPluginWithTopicType(String type) { + TopicPlugin topicPlugin = mock(TopicPlugin.class); + when(topicPlugin.getTopicType()).thenReturn(type); + + return topicPlugin; + } +} \ No newline at end of file diff --git a/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/service/nontransactional/PluginLocationServiceImplTest.java b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/service/nontransactional/PluginLocationServiceImplTest.java index fefc537071..a8c8a15ca7 100644 --- a/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/service/nontransactional/PluginLocationServiceImplTest.java +++ b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/service/nontransactional/PluginLocationServiceImplTest.java @@ -16,16 +16,15 @@ import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.Topic; +import org.jtalks.jcommune.model.entity.UserInfo; import org.jtalks.jcommune.plugin.api.service.PluginLocationService; import org.mockito.Mock; -import org.testng.annotations.BeforeMethod; +import org.mockito.Mockito; import org.testng.annotations.Test; -import java.util.Arrays; +import java.util.Collections; import java.util.List; -import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; import static org.testng.Assert.assertEquals; /** @@ -35,23 +34,15 @@ public class PluginLocationServiceImplTest { @Mock private PluginLocationService locationService; - @BeforeMethod - public void init() { - initMocks(this); - PluginLocationServiceImpl service = (PluginLocationServiceImpl)PluginLocationServiceImpl.getInstance(); - service.setLocationService(locationService); - } - @Test - public void testGetUsersViewing() { - JCUser user = new JCUser("name", "mail@example.com", "password"); + public void userShouldBeInViewersList() { + locationService = Mockito.mock(PluginLocationService.class); + UserInfo user = new UserInfo(new JCUser("name", "mail@example.com", "password")); Topic topic = new Topic(); - when(locationService.getUsersViewing(topic)).thenReturn(Arrays.asList(user)); - - List users = PluginLocationServiceImpl.getInstance().getUsersViewing(topic); - + Mockito.when(locationService.getUsersViewing(topic)).thenReturn(Collections.singletonList(user)); + ((PluginLocationServiceImpl)PluginLocationServiceImpl.getInstance()).setLocationService(locationService); + List users = PluginLocationServiceImpl.getInstance().getUsersViewing(topic); assertEquals(users.size(), 1); assertEquals(users.get(0), user); } } - diff --git a/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginLastReadPostServiceTest.java b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginLastReadPostServiceTest.java index 55f83b1b07..c4e3d21362 100644 --- a/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginLastReadPostServiceTest.java +++ b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/service/transactional/TransactionalPluginLastReadPostServiceTest.java @@ -42,8 +42,8 @@ public void init() { public void markTopicPageAsReadShouldCallLastReadPostService() { Topic topic = new Topic(); - TransactionalPluginLastReadPostService.getInstance().markTopicPageAsRead(topic, 1); + TransactionalPluginLastReadPostService.getInstance().markTopicAsRead(topic); - verify(lastReadPostService).markTopicPageAsRead(topic, 1); + verify(lastReadPostService).markTopicAsRead(topic); } } diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/locale/JcLocaleResolverTest.java b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/web/locale/JcLocaleResolverTest.java similarity index 78% rename from jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/locale/JcLocaleResolverTest.java rename to jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/web/locale/JcLocaleResolverTest.java index f636dfbbf3..868c82c399 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/locale/JcLocaleResolverTest.java +++ b/jcommune-plugin-api/src/test/java/org/jtalks/jcommune/plugin/api/web/locale/JcLocaleResolverTest.java @@ -12,12 +12,12 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.locale; +package org.jtalks.jcommune.plugin.api.web.locale; import org.jtalks.jcommune.model.entity.AnonymousUser; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.Language; -import org.jtalks.jcommune.service.UserService; +import org.jtalks.jcommune.plugin.api.service.UserReader; import org.springframework.web.servlet.i18n.CookieLocaleResolver; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -38,23 +38,24 @@ */ public class JcLocaleResolverTest { @Mock - private UserService userService; + private UserReader userReader; @Mock private HttpServletRequest request; @BeforeMethod public void init() { initMocks(this); + JcLocaleResolver resolver = (JcLocaleResolver) JcLocaleResolver.getInstance(); + resolver.setUserReader(userReader); } @Test public void resolveLocaleShouldReturnUserLocaleIfUserLoggedIn() { - JcLocaleResolver localeResolver = new JcLocaleResolver(userService); JCUser currentUser = new JCUser("username", "email@mail.ru", "password"); currentUser.setLanguage(Language.ENGLISH); - when(userService.getCurrentUser()).thenReturn(currentUser); + when(userReader.getCurrentUser()).thenReturn(currentUser); - Locale result = localeResolver.resolveLocale(request); + Locale result = JcLocaleResolver.getInstance().resolveLocale(request); assertEquals(result, currentUser.getLanguage().getLocale()); verify(request).setAttribute(CookieLocaleResolver.LOCALE_REQUEST_ATTRIBUTE_NAME, Locale.ENGLISH); @@ -62,14 +63,13 @@ public void resolveLocaleShouldReturnUserLocaleIfUserLoggedIn() { @Test public void resolveLocaleShouldReturnRequestLocaleIfUserAnonymous() { - JcLocaleResolver localeResolver = new JcLocaleResolver(userService); AnonymousUser user = new AnonymousUser(); - user.setLanguage(Language.SPANISH); + user.setLanguage(Language.RUSSIAN); Locale defaultLocale = Language.ENGLISH.getLocale(); - when(userService.getCurrentUser()).thenReturn(user); + when(userReader.getCurrentUser()).thenReturn(user); when(request.getLocale()).thenReturn(defaultLocale); - Locale result = localeResolver.resolveLocale(request); + Locale result = JcLocaleResolver.getInstance().resolveLocale(request); assertEquals(result, defaultLocale); } @@ -80,13 +80,12 @@ public void resolveLocaleShouldReturnRequestLocaleIfUserAnonymous() { */ @Test public void resolveLocaleShouldNotRetrieveCurrentUserIfRequestLocaleAttributeNotNull() { - JcLocaleResolver localeResolver = new JcLocaleResolver(userService); Locale locale = Locale.ENGLISH; when(request.getAttribute(CookieLocaleResolver.LOCALE_REQUEST_ATTRIBUTE_NAME)).thenReturn(locale); - Locale result = localeResolver.resolveLocale(request); + Locale result = JcLocaleResolver.getInstance().resolveLocale(request); assertEquals(result, locale); - verify(userService, never()).getCurrentUser(); + verify(userReader, never()).getCurrentUser(); } } diff --git a/jcommune-plugins/README.md b/jcommune-plugins/README.md index 8c7c430b6b..5eec25dafd 100644 --- a/jcommune-plugins/README.md +++ b/jcommune-plugins/README.md @@ -1,7 +1,17 @@ JCommune Plugins --- -Plugins are designed to extend the functionality of JCommune forum engine. They can be installed by putting plugin jar file into special folder `JCOMMUNE_PLUGIN_FOLDER` that configured in `$TOMCAT_HOME/conf/Catalina/localhost/jcommune.xml` file. +Plugins are designed to extend the functionality of JCommune forum engine. They can be installed by putting plugin jar +file into special folder `JCOMMUNE_PLUGIN_FOLDER` that configured in `$TOMCAT_HOME/conf/Catalina/localhost/jcommune.xml` +file. By default it points to user home dir. So to install a plugin: + +- Download it [from Nexus](http://repo.jtalks.org/content/repositories/builds/org/jtalks/jcommune/) + (version should be the same as forum version) or generate its jar file: `mvn package` +- Copy it from `jcommune-plugins/plugin-name/target/plugin-name*.jar` into `JCOMMUNE_PLUGIN_FOLDER` (by default: `~/`) +- Log in into forum as admin (by default: admin/admin) +- Go into Administration -> Plugins and enabled it there. + +## Dev Info Now we provides some velocity macros for plugins: diff --git a/jcommune-plugins/jcommune-dummy-plugin/pom.xml b/jcommune-plugins/jcommune-dummy-plugin/pom.xml index 3d73b45042..1e4a6fa59c 100644 --- a/jcommune-plugins/jcommune-dummy-plugin/pom.xml +++ b/jcommune-plugins/jcommune-dummy-plugin/pom.xml @@ -5,7 +5,7 @@ jcommune-plugins org.jtalks.jcommune - 2.14-SNAPSHOT + 3.13-SNAPSHOT jcommune-dummy-plugin diff --git a/jcommune-plugins/kaptcha-plugin/pom.xml b/jcommune-plugins/kaptcha-plugin/pom.xml index c4d95e0d7a..7f2150982e 100644 --- a/jcommune-plugins/kaptcha-plugin/pom.xml +++ b/jcommune-plugins/kaptcha-plugin/pom.xml @@ -4,7 +4,7 @@ jcommune-plugins org.jtalks.jcommune - 2.14-SNAPSHOT + 3.13-SNAPSHOT kaptcha-plugin @@ -51,7 +51,7 @@ - maven-assembly-plugin + maven-shade-plugin diff --git a/jcommune-plugins/kaptcha-plugin/src/main/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginService.java b/jcommune-plugins/kaptcha-plugin/src/main/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginService.java index dc98498fdf..f69c864382 100644 --- a/jcommune-plugins/kaptcha-plugin/src/main/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginService.java +++ b/jcommune-plugins/kaptcha-plugin/src/main/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginService.java @@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableMap; import org.apache.commons.lang.StringUtils; import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.tools.generic.DateTool; import org.jtalks.jcommune.model.dto.UserDto; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.ui.velocity.VelocityEngineUtils; @@ -52,6 +53,8 @@ public class KaptchaPluginService { private static final String BASE_URL = "baseUrl"; private static final String FORM_ELEMENT_ID = "formElementId"; private static final String PLUGIN_PREFIX = "plugin-"; + private static final String DATE = "date"; + private Producer captchaProducer; public KaptchaPluginService(int width, int height, int length, String possibleSymbols) { @@ -131,6 +134,7 @@ public String getHtml(HttpServletRequest request, String pluginId, Locale locale model.put(CAPTCHA_PLUGIN_ID, pluginId); model.put(FORM_ELEMENT_ID, getFormElementId(pluginId)); model.put(BASE_URL, getDeploymentRootUrl(request)); + model.put(DATE, new DateTool()); return VelocityEngineUtils.mergeTemplateIntoString( engine, "org/jtalks/jcommune/plugin/kaptcha/template/captcha.vm", "UTF-8", model); } diff --git a/jcommune-plugins/kaptcha-plugin/src/main/resources/org/jtalks/jcommune/plugin/kaptcha/template/captcha.vm b/jcommune-plugins/kaptcha-plugin/src/main/resources/org/jtalks/jcommune/plugin/kaptcha/template/captcha.vm index 36365cee46..7baa3400c3 100644 --- a/jcommune-plugins/kaptcha-plugin/src/main/resources/org/jtalks/jcommune/plugin/kaptcha/template/captcha.vm +++ b/jcommune-plugins/kaptcha-plugin/src/main/resources/org/jtalks/jcommune/plugin/kaptcha/template/captcha.vm @@ -16,11 +16,16 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *#
- ${altCaptcha} - ${altRefreshCaptcha} +## Fix for [http://jira.jtalks.org/browse/JC-2094] issue. Query t="date" added because Firefox ignores cache control +## headers if DOM changed by JavaScript and no way to prevent image caching. + ${altCaptcha} +## Div can't recieve focus without tabindex attribute. +
+ ${altRefreshCaptcha} +
+ placeholder='${captchaLabel}' class='reg_input captcha'/>
\ No newline at end of file diff --git a/jcommune-plugins/kaptcha-plugin/src/test/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginServiceTest.java b/jcommune-plugins/kaptcha-plugin/src/test/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginServiceTest.java index eb3f664fab..fd3667b2f8 100644 --- a/jcommune-plugins/kaptcha-plugin/src/test/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginServiceTest.java +++ b/jcommune-plugins/kaptcha-plugin/src/test/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginServiceTest.java @@ -119,11 +119,11 @@ public void testGetHtml() throws Exception { String actual = service.getHtml(request, "1", Locale.ENGLISH); assertTrue(actual.contains("")); + + "alt='Captcha' src='http://localhost/plugin/1/refreshCaptcha?t=")); assertTrue(actual.contains("Refresh captcha")); assertTrue(actual.contains("")); + assertTrue(actual.contains("placeholder='Captcha text' class='reg_input captcha'/>")); } @Test diff --git a/jcommune-plugins/kaptcha-plugin/src/test/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginTest.java b/jcommune-plugins/kaptcha-plugin/src/test/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginTest.java index 9a33311a8a..23676c644e 100644 --- a/jcommune-plugins/kaptcha-plugin/src/test/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginTest.java +++ b/jcommune-plugins/kaptcha-plugin/src/test/java/org/jtalks/jcommune/plugin/kaptcha/KaptchaPluginTest.java @@ -58,7 +58,7 @@ public void testGetHtml() throws Exception { "
" + newLine + "
" + newLine + " " + newLine + + " placeholder='Captcha text' class='reg_input captcha'/>" + newLine + "
" + newLine + "
"; diff --git a/jcommune-plugins/pom.xml b/jcommune-plugins/pom.xml index 7738d32443..237fa9625e 100644 --- a/jcommune-plugins/pom.xml +++ b/jcommune-plugins/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.jtalks.jcommune jcommune-plugins - 2.14-SNAPSHOT + 3.13-SNAPSHOT pom @@ -64,9 +64,9 @@ 1.7 - velocity-tools - velocity-tools-generic - 1.4 + org.apache.velocity + velocity-tools + 2.0 @@ -99,6 +99,7 @@ org.restlet.jee org.restlet.ext.jaxb 2.2.0 + runtime com.xebialabs.restito @@ -151,7 +152,6 @@ **/.* **/empty **/lib/** - **/wro/** **/META-INF/context.xml true @@ -170,19 +170,15 @@ - maven-assembly-plugin - 2.4 - - - jar-with-dependencies - - + org.apache.maven.plugins + maven-shade-plugin + 2.3 jar-with-dependencies package - single + shade @@ -215,16 +211,12 @@ jtalks-nexus jtalks nexus - - http://repo.jtalks.org/content/repositories/releases - + http://repo.jtalks.org/content/repositories/builds jtalks-nexus jtalks nexus - - http://repo.jtalks.org/content/repositories/snapshots - + http://repo.jtalks.org/content/repositories/snapshots diff --git a/jcommune-plugins/poulpe-auth-plugin/pom.xml b/jcommune-plugins/poulpe-auth-plugin/pom.xml index cdc416e350..fe8301d2ce 100644 --- a/jcommune-plugins/poulpe-auth-plugin/pom.xml +++ b/jcommune-plugins/poulpe-auth-plugin/pom.xml @@ -4,7 +4,7 @@ jcommune-plugins org.jtalks.jcommune - 2.14-SNAPSHOT + 3.13-SNAPSHOT poulpe-auth-plugin @@ -67,7 +67,7 @@ - maven-assembly-plugin + maven-shade-plugin diff --git a/jcommune-plugins/poulpe-auth-plugin/src/main/java/org/jtalks/jcommune/plugin/auth/poulpe/PoulpeAuthPlugin.java b/jcommune-plugins/poulpe-auth-plugin/src/main/java/org/jtalks/jcommune/plugin/auth/poulpe/PoulpeAuthPlugin.java index 7109250092..d04af13ee0 100644 --- a/jcommune-plugins/poulpe-auth-plugin/src/main/java/org/jtalks/jcommune/plugin/auth/poulpe/PoulpeAuthPlugin.java +++ b/jcommune-plugins/poulpe-auth-plugin/src/main/java/org/jtalks/jcommune/plugin/auth/poulpe/PoulpeAuthPlugin.java @@ -117,6 +117,14 @@ public Map authenticate(String login, String password) } } + /** + * {@inheritDoc} + */ + @Override + public void activate(String username){ + service.activate(username); + } + @Override public boolean supportsJCommuneVersion(String version) { return true; diff --git a/jcommune-plugins/poulpe-auth-plugin/src/main/java/org/jtalks/jcommune/plugin/auth/poulpe/service/PoulpeAuthService.java b/jcommune-plugins/poulpe-auth-plugin/src/main/java/org/jtalks/jcommune/plugin/auth/poulpe/service/PoulpeAuthService.java index c3789597ab..8e6f4f8104 100644 --- a/jcommune-plugins/poulpe-auth-plugin/src/main/java/org/jtalks/jcommune/plugin/auth/poulpe/service/PoulpeAuthService.java +++ b/jcommune-plugins/poulpe-auth-plugin/src/main/java/org/jtalks/jcommune/plugin/auth/poulpe/service/PoulpeAuthService.java @@ -15,6 +15,7 @@ package org.jtalks.jcommune.plugin.auth.poulpe.service; +import org.apache.commons.lang3.StringUtils; import org.jtalks.jcommune.model.dto.UserDto; import org.jtalks.jcommune.plugin.api.exceptions.NoConnectionException; import org.jtalks.jcommune.plugin.api.exceptions.UnexpectedErrorException; @@ -48,17 +49,17 @@ public class PoulpeAuthService { private static final int CONNECTION_TIMEOUT = 5000; - public static final String DRY_RUN_PARAM = "dryRun"; - public static final String TRUE = "true"; private final Logger logger = LoggerFactory.getLogger(getClass()); private String regUrl; private String authUrl; + private String activationUrl; private String login; private String password; public PoulpeAuthService(String url, String login, String password) { this.regUrl = url + "/rest/private/user"; + this.activationUrl = url + "/rest/private/activate"; this.authUrl = url + "/rest/authenticate"; this.login = login; this.password = password; @@ -97,7 +98,18 @@ public Map authenticate(String username, String passwordHash) return result; } - + /** + * Activate user with specified username in Poulpe. + * @param username username + */ + public void activate(String username) { + ClientResource clientResource = null; + try { + clientResource = sendActivationRequest(username); + } finally { + if (clientResource != null) closeRestletConnection(clientResource); + } + } private void closeRestletConnection(ClientResource clientResource) { try { @@ -259,17 +271,15 @@ private void addHeaderAttribute(ClientResource clientResource, String attrName, */ protected ClientResource sendRegistrationRequest(User user, Boolean dryRun) { ClientResource clientResource = createClientResource(regUrl, true); - if (login != null && !login.isEmpty() && password != null && !password.isEmpty()) { - clientResource.setChallengeResponse(ChallengeScheme.HTTP_BASIC, login, password); - } + addCredentialsIfAny(clientResource); if (dryRun) { - addHeaderAttribute(clientResource, DRY_RUN_PARAM, TRUE); + addHeaderAttribute(clientResource, "dryRun", "true"); } writeRequestInfoToLog(clientResource); try { clientResource.post(user); } catch (ResourceException e) { - logger.debug("Poulpe registration request error: {}", e.getStatus()); + logger.warn("Poulpe registration request error: {}", e.getStatus()); } return clientResource; } @@ -284,25 +294,39 @@ protected ClientResource sendRegistrationRequest(User user, Boolean dryRun) { protected ClientResource sendAuthRequest(String username, String passwordHash) { String url = authUrl + "?username=" + username + "&passwordHash=" + passwordHash; ClientResource clientResource = createClientResource(url, false); - if (login != null && !login.isEmpty() && password != null && !password.isEmpty()) { - clientResource.setChallengeResponse(ChallengeScheme.HTTP_BASIC, login, password); + addCredentialsIfAny(clientResource); + writeRequestInfoToLog(clientResource); + try { + clientResource.get(); + } catch (ResourceException e) { + logger.warn("Poulpe authentication request error: {}", e.getStatus()); } + return clientResource; + } + + /** + * Send user activation request for specified username + * @param username username + * @return ClientResource result + */ + protected ClientResource sendActivationRequest(String username){ + String url = activationUrl + "?username=" + username; + ClientResource clientResource = createClientResource(url, false); + addCredentialsIfAny(clientResource); writeRequestInfoToLog(clientResource); try { clientResource.get(); } catch (ResourceException e) { - logger.debug("Poulpe authentication request error: {}", e.getStatus()); + logger.warn("Poulpe activation request error: {}", e.getStatus()); } return clientResource; } + @SuppressWarnings("unchecked") private void writeRequestInfoToLog(ClientResource clientResource) { ConcurrentMap attrs = clientResource.getRequest().getAttributes(); Series
headers = (Series
) attrs.get(HeaderConstants.ATTRIBUTE_HEADERS); - if (headers != null) { - String h = headers.toString(); - } - logger.debug("Request to Poulpe: requested URI - {}, request headers - {}, request body - {}", + logger.info("Request to Poulpe: requested URI - {}, request headers - {}, request body - {}", new Object[]{clientResource.getRequest().getResourceRef(), headers, clientResource.getRequest()}); } @@ -313,4 +337,9 @@ private ClientResource createClientResource(String url, boolean buffering) { clientResource.setEntityBuffering(buffering); return clientResource; } + + private void addCredentialsIfAny(ClientResource clientResource){ + if (!StringUtils.isAnyBlank(login, password)) + clientResource.setChallengeResponse(ChallengeScheme.HTTP_BASIC, login, password); + } } diff --git a/jcommune-plugins/poulpe-auth-plugin/src/main/resources/ValidationMessages_en.properties b/jcommune-plugins/poulpe-auth-plugin/src/main/resources/ValidationMessages_en.properties index 384499ef11..efa2410ece 100644 --- a/jcommune-plugins/poulpe-auth-plugin/src/main/resources/ValidationMessages_en.properties +++ b/jcommune-plugins/poulpe-auth-plugin/src/main/resources/ValidationMessages_en.properties @@ -1,7 +1,7 @@ #registration user.email.illegal_length=Email field should not contain more than {max} symbols -user.username.already_exists=User with the username already exists. -user.email.already_exists=User with the email already exists. +user.username.already_exists=User with this username already exists +user.email.already_exists=User with this email already exists user.username.null_constraint_violation=Please enter username user.username.length_constraint_violation=Username length must be between {min} and {max} characters validation.invalid_email_format=An email format should be like mail@mail.ru diff --git a/jcommune-plugins/questions-n-answers-plugin/pom.xml b/jcommune-plugins/questions-n-answers-plugin/pom.xml index ef16452979..0feba92073 100644 --- a/jcommune-plugins/questions-n-answers-plugin/pom.xml +++ b/jcommune-plugins/questions-n-answers-plugin/pom.xml @@ -4,7 +4,7 @@ jcommune-plugins org.jtalks.jcommune - 2.14-SNAPSHOT + 3.13-SNAPSHOT questions-n-answers-plugin @@ -37,8 +37,8 @@ velocity - velocity-tools - velocity-tools-generic + org.apache.velocity + velocity-tools org.springframework @@ -49,7 +49,7 @@ - maven-assembly-plugin + maven-shade-plugin diff --git a/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/QuestionSubscribersFilter.java b/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/QuestionSubscribersFilter.java new file mode 100644 index 0000000000..73ecdf0ea1 --- /dev/null +++ b/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/QuestionSubscribersFilter.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.questionsandanswers; + +import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.Post; +import org.jtalks.jcommune.model.entity.SubscriptionAwareEntity; +import org.jtalks.jcommune.plugin.api.core.SubscribersFilter; + +import java.util.Collection; + +/** + * Implementation of {@link SubscribersFilter} of Questions and Answers plugin + * Provides possibility to send notifications only to author of post when comment is added + * + * @author Mikhail Stryzhonok + */ +public class QuestionSubscribersFilter implements SubscribersFilter { + + /** + * {@inheritDoc} + */ + @Override + public void filter(Collection users, SubscriptionAwareEntity entity) { + if (entity instanceof Post + && ((Post)entity).getTopic().getType().equals(QuestionsAndAnswersPlugin.TOPIC_TYPE)) { + JCUser postCreator = ((Post)entity).getUserCreated(); + boolean containsCreator = users.contains(postCreator); + users.clear(); + if (containsCreator) { + users.add(((Post) entity).getUserCreated()); + } + } + } +} diff --git a/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/QuestionsAndAnswersPlugin.java b/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/QuestionsAndAnswersPlugin.java index 9663c192ca..f6db8e1edf 100644 --- a/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/QuestionsAndAnswersPlugin.java +++ b/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/QuestionsAndAnswersPlugin.java @@ -14,8 +14,10 @@ */ package org.jtalks.jcommune.plugin.questionsandanswers; +import org.jtalks.common.model.permissions.BranchPermission; import org.jtalks.common.model.permissions.JtalksPermission; import org.jtalks.jcommune.model.entity.PluginProperty; +import org.jtalks.jcommune.plugin.api.core.SubscribersFilter; import org.jtalks.jcommune.plugin.api.core.TopicPlugin; import org.jtalks.jcommune.plugin.api.core.WebControllerPlugin; import org.jtalks.jcommune.plugin.api.web.dto.CreateTopicBtnDto; @@ -169,8 +171,27 @@ public PluginController getController() { return new QuestionsAndAnswersController(); } + /** + * {@inheritDoc} + */ @Override public String getTopicType() { return TOPIC_TYPE; } + + /** + * {@inheritDoc} + */ + @Override + public JtalksPermission getCommentPermission() { + return BranchPermission.CREATE_POSTS; + } + + /** + * {@inheritDoc} + */ + @Override + public SubscribersFilter getSubscribersFilter() { + return new QuestionSubscribersFilter(); + } } diff --git a/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/controller/QuestionsAndAnswersController.java b/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/controller/QuestionsAndAnswersController.java index d1de009bfc..b7e35d057f 100644 --- a/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/controller/QuestionsAndAnswersController.java +++ b/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/controller/QuestionsAndAnswersController.java @@ -14,31 +14,32 @@ */ package org.jtalks.jcommune.plugin.questionsandanswers.controller; +import com.google.common.annotations.VisibleForTesting; import com.google.common.io.ByteStreams; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang.time.DateFormatUtils; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.exception.VelocityException; import org.apache.velocity.tools.generic.EscapeTool; -import org.jtalks.jcommune.model.entity.Branch; -import org.jtalks.jcommune.model.entity.JCUser; -import org.jtalks.jcommune.model.entity.Post; -import org.jtalks.jcommune.model.entity.Topic; +import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.service.*; import org.jtalks.jcommune.plugin.api.service.nontransactional.BbToHtmlConverter; import org.jtalks.jcommune.plugin.api.service.nontransactional.PluginLocationServiceImpl; -import org.jtalks.jcommune.plugin.api.service.transactional.TransactionalPluginBranchService; -import org.jtalks.jcommune.plugin.api.service.transactional.TransactionalPluginLastReadPostService; -import org.jtalks.jcommune.plugin.api.service.transactional.TransactionalPluginPostService; -import org.jtalks.jcommune.plugin.api.service.transactional.TransactionalTypeAwarePluginTopicService; +import org.jtalks.jcommune.plugin.api.service.nontransactional.PropertiesHolder; +import org.jtalks.jcommune.plugin.api.service.transactional.*; import org.jtalks.jcommune.plugin.api.web.PluginController; import org.jtalks.jcommune.plugin.api.web.dto.PostDto; import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; +import org.jtalks.jcommune.plugin.api.web.dto.json.*; +import org.jtalks.jcommune.plugin.api.web.locale.JcLocaleResolver; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; import org.jtalks.jcommune.plugin.api.web.velocity.tool.JodaDateTimeTool; import org.jtalks.jcommune.plugin.api.web.velocity.tool.PermissionTool; import org.jtalks.jcommune.plugin.questionsandanswers.QuestionsAndAnswersPlugin; -import org.springframework.beans.BeansException; +import org.jtalks.jcommune.plugin.questionsandanswers.dto.CommentDto; +import org.kefirsf.bb.EscapeXmlProcessorFactory; +import org.kefirsf.bb.TextProcessor; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.domain.PageImpl; @@ -47,6 +48,7 @@ import org.springframework.ui.velocity.VelocityEngineUtils; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.LocaleResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -74,6 +76,7 @@ public class QuestionsAndAnswersController implements ApplicationContextAware, P private static final String QUESTION_TEMPLATE_PATH = TEMPLATE_PATH + "question.vm"; private static final String BREADCRUMB_LIST = "breadcrumbList"; private static final String TOPIC_DTO = "topicDto"; + private static final String TOPIC_DRAFT = "topicDraft"; private static final String POST_DTO = "postDto"; private static final String EDIT_MODE = "edit"; private static final String RESULT = "result"; @@ -82,11 +85,19 @@ public class QuestionsAndAnswersController implements ApplicationContextAware, P private static final String SUBSCRIBED = "subscribed"; private static final String CONVERTER = "converter"; private static final String VIEW_LIST = "viewList"; + private static final String LIMIT_OF_POSTS_ATTRIBUTE = "postLimit"; + public static final int LIMIT_OF_POSTS_VALUE = 50; public static final String PLUGIN_VIEW_NAME = "plugin/plugin"; public static final String BRANCH_ID = "branchId"; public static final String CONTENT = "content"; private static final String QEUSTION_TITLE = "questionTitle"; + public static final String POST_ID = "postId"; + public static final String COMMENT_ID = "commentId"; + private static final String HTML_ESCAPER = "htmlEscaper"; + // custom processor is used for escaping of HTML because + // standard Velocity escaping utility not correct displays emoji. + private TextProcessor htmlEscaper = EscapeXmlProcessorFactory.getInstance().create(); private BreadcrumbBuilder breadcrumbBuilder = new BreadcrumbBuilder(); @@ -108,13 +119,21 @@ public String showNewQuestionPage(@RequestParam(BRANCH_ID) Long branchId, Model throws NotFoundException { VelocityEngine engine = new VelocityEngine(getProperties()); engine.init(); + + TopicDraft draft = ObjectUtils.defaultIfNull( + getPluginTopicDraftService().getDraft(), new TopicDraft()); + + TopicDto dto = new TopicDto(draft); + dto.getTopic().setType(QuestionsAndAnswersPlugin.TOPIC_TYPE); + Branch branch = getPluginBranchService().get(branchId); - Topic topic = new Topic(); - topic.setBranch(branch); - TopicDto dto = new TopicDto(topic); + dto.getTopic().setBranch(branch); + Map data = getDefaultModel(request); - data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); + data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(dto.getTopic())); data.put(TOPIC_DTO, dto); + data.put(TOPIC_DRAFT, draft); + data.put(HTML_ESCAPER, htmlEscaper); data.put(EDIT_MODE, false); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_FORM_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; @@ -141,16 +160,16 @@ public String createQuestion(@Valid @ModelAttribute TopicDto topicDto, BindingRe engine.init(); Branch branch = getPluginBranchService().get(branchId); Map data = getDefaultModel(request); + topicDto.getTopic().setBranch(branch); + topicDto.getTopic().setType(QuestionsAndAnswersPlugin.TOPIC_TYPE); if (result.hasErrors()) { - topicDto.getTopic().setBranch(branch); - data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topicDto.getTopic())); + data.put(BREADCRUMB_LIST, breadcrumbBuilder.getNewTopicBreadcrumb(branch)); data.put(TOPIC_DTO, topicDto); + data.put(HTML_ESCAPER, htmlEscaper); data.put(RESULT, result); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_FORM_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } - topicDto.getTopic().setBranch(branch); - topicDto.getTopic().setType(QuestionsAndAnswersPlugin.TOPIC_TYPE); Topic createdQuestion = getTypeAwarePluginTopicService().createTopic(topicDto.getTopic(), topicDto.getBodyText()); return "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + createdQuestion.getId(); @@ -163,7 +182,7 @@ public String createQuestion(@Valid @ModelAttribute TopicDto topicDto, BindingRe * @param response HttpServletResponse * @param name name of icon */ - @RequestMapping(value = "icon/{name}", method = RequestMethod.GET) + @RequestMapping(value = "/resources/icon/{name}", method = RequestMethod.GET) public void getIcon(HttpServletRequest request, HttpServletResponse response, @PathVariable("name") String name) { try { processIconRequest(request, response, PATH_TO_IMAGES + name); @@ -187,21 +206,50 @@ public String showQuestion(HttpServletRequest request, Model model, @PathVariabl throws NotFoundException { Topic topic = getTypeAwarePluginTopicService().get(id, QuestionsAndAnswersPlugin.TOPIC_TYPE); getTypeAwarePluginTopicService().checkViewTopicPermission(topic.getBranch().getId()); + + JCUser currentUser = getUserReader().getCurrentUser(); + PostDto postDto = new PostDto(); + PostDraft draft = topic.getDraftForUser(currentUser); + if (draft != null) { + postDto = PostDto.getDtoFor(draft); + } + Map data = getDefaultModel(request); data.put(QUESTION, topic); - data.put(POST_PAGE, new PageImpl<>(topic.getPosts())); + data.put(POST_PAGE, new PageImpl<>(getSortedPosts(topic.getPosts()))); data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); data.put(SUBSCRIBED, false); - data.put(CONVERTER, BbToHtmlConverter.getInstance()); + data.put(CONVERTER, getPluginBbCodeService()); + data.put(HTML_ESCAPER, htmlEscaper); data.put(VIEW_LIST, getLocationService().getUsersViewing(topic)); - data.put(POST_DTO, new PostDto()); - getPluginLastReadPostService().markTopicPageAsRead(topic, 1); + data.put(POST_DTO, postDto); + data.put(LIMIT_OF_POSTS_ATTRIBUTE, LIMIT_OF_POSTS_VALUE); + getPluginLastReadPostService().markTopicAsRead(topic); VelocityEngine engine = new VelocityEngine(getProperties()); engine.init(); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } + /** + * Check if user can answer for question with given id. Yer can answer if question has less than + * {@link #LIMIT_OF_POSTS_VALUE} answers + * + * @param questionId id of question to be checked + * + * @return Success response in JSON form if user can leave answer and fail response in JSON form otherwise + * @throws NotFoundException if question with given id not found + */ + @RequestMapping(value = "{id}/canpost", method = RequestMethod.GET) + @ResponseBody + public JsonResponse canPost(@PathVariable("id") Long questionId) throws NotFoundException { + Topic topic = getTypeAwarePluginTopicService().get(questionId, QuestionsAndAnswersPlugin.TOPIC_TYPE); + if (topic.getPostCount() - 1 >= LIMIT_OF_POSTS_VALUE) { + return new JsonResponse(JsonResponseStatus.FAIL); + } + return new JsonResponse(JsonResponseStatus.SUCCESS); + } + /** * Shows answer * @@ -235,6 +283,7 @@ public String editQuestionPage(HttpServletRequest request, Model model, @PathVar Map data = getDefaultModel(request); data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); data.put(TOPIC_DTO, topicDto); + data.put(HTML_ESCAPER, htmlEscaper); data.put(EDIT_MODE, true); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_FORM_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; @@ -266,6 +315,7 @@ public String updateQuestion(@Valid @ModelAttribute TopicDto topicDto, BindingRe engine.init(); data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); data.put(TOPIC_DTO, topicDto); + data.put(HTML_ESCAPER, htmlEscaper); data.put(EDIT_MODE, true); data.put(RESULT, result); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_FORM_TEMPLATE_PATH, "UTF-8", data)); @@ -297,6 +347,7 @@ public String editAnswerPage(HttpServletRequest request, Model model, @PathVaria Map data = getDefaultModel(request); data.put(QEUSTION_TITLE, answer.getTopic().getTitle()); data.put(POST_DTO, answerDto); + data.put(HTML_ESCAPER, htmlEscaper); model.addAttribute(CONTENT, getMergedTemplate(engine, ANSWER_FORM_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } @@ -316,8 +367,7 @@ public String editAnswerPage(HttpServletRequest request, Model model, @PathVaria */ @RequestMapping(value = "post/{id}/edit", method = RequestMethod.POST) public String updateAnswer(@Valid @ModelAttribute PostDto postDto, BindingResult result, Model model, - @PathVariable("id") Long id, HttpServletRequest request) - throws NotFoundException { + @PathVariable("id") Long id, HttpServletRequest request) throws NotFoundException { Post answer = getPluginPostService().get(id); Map data = getDefaultModel(request); if (result.hasErrors()) { @@ -326,9 +376,9 @@ public String updateAnswer(@Valid @ModelAttribute PostDto postDto, BindingResult data.put(QEUSTION_TITLE, answer.getTopic().getTitle()); data.put(POST_DTO, postDto); data.put(RESULT, result); + data.put(HTML_ESCAPER, htmlEscaper); model.addAttribute(CONTENT, getMergedTemplate(engine, ANSWER_FORM_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; - } getPluginPostService().updatePost(answer, postDto.getBodyText()); return "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + answer.getTopic().getId() + "#" + id; @@ -338,34 +388,49 @@ public String updateAnswer(@Valid @ModelAttribute PostDto postDto, BindingResult * Process the answer form. Adds new answer to the specified question and redirects to the * question view page. * + * @param questionId id of question to which answer will be added * @param postDto dto that contains data entered in form * @param result validation result + * @param model model for transferring to template + * @param request HttpServletRequest + * * @return redirect to the answer or back to answer page if validation failed * @throws NotFoundException when question or branch not found */ - @RequestMapping(value = "{id}", method = RequestMethod.POST) - public String create(@PathVariable("id") Long questionId, @Valid @ModelAttribute PostDto postDto, + @RequestMapping(value = "{questionId}", method = RequestMethod.POST) + public String create(@PathVariable("questionId") Long questionId, @Valid @ModelAttribute PostDto postDto, BindingResult result, Model model, HttpServletRequest request) throws NotFoundException { postDto.setTopicId(questionId); Topic topic = getTypeAwarePluginTopicService().get(questionId, QuestionsAndAnswersPlugin.TOPIC_TYPE); - if (result.hasErrors()) { + //We can't provide limitation properly without database-level locking + if (result.hasErrors() || LIMIT_OF_POSTS_VALUE <= topic.getPostCount() - 1) { + JCUser currentUser = getUserReader().getCurrentUser(); + PostDraft draft = topic.getDraftForUser(currentUser); + if (draft != null) { + // If we create new dto object instead of using already existing + // we lose error messages linked with it + postDto.fillFrom(draft); + } + Map data = getDefaultModel(request); VelocityEngine engine = new VelocityEngine(getProperties()); engine.init(); data.put(QUESTION, topic); - data.put(POST_PAGE, new PageImpl<>(topic.getPosts())); + data.put(POST_PAGE, new PageImpl<>(getSortedPosts(topic.getPosts()))); data.put(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); data.put(SUBSCRIBED, false); data.put(RESULT, result); data.put(CONVERTER, BbToHtmlConverter.getInstance()); data.put(VIEW_LIST, getLocationService().getUsersViewing(topic)); data.put(POST_DTO, postDto); + data.put(LIMIT_OF_POSTS_ATTRIBUTE, LIMIT_OF_POSTS_VALUE); model.addAttribute(CONTENT, getMergedTemplate(engine, QUESTION_TEMPLATE_PATH, "UTF-8", data)); return PLUGIN_VIEW_NAME; } - Post newbie = getTypeAwarePluginTopicService().replyToTopic(questionId, postDto.getBodyText(), topic.getBranch().getId()); - getPluginLastReadPostService().markTopicPageAsRead(newbie.getTopic(), 1); + Post newbie = getTypeAwarePluginTopicService().replyToTopic(questionId, + postDto.getBodyText(), topic.getBranch().getId()); + getPluginLastReadPostService().markTopicAsRead(newbie.getTopic()); return "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + questionId + "#" + newbie.getId(); } @@ -417,7 +482,105 @@ public String deleteAnswer(@PathVariable Long answerId) } /** - * Writes icon to response and set apropriate response geaders + * Adds new comment to post + * + * @param dto dto populated in form + * @param result validation result + * @param request http servlet request + * + * @return result in JSON format + */ + @RequestMapping(method = RequestMethod.POST, value = "newcomment") + @ResponseBody + JsonResponse addComment(@Valid @RequestBody CommentDto dto, BindingResult result, HttpServletRequest request) { + if (result.hasErrors()) { + return new FailValidationJsonResponse(result.getAllErrors()); + } + PostComment comment; + try { + Post targetPost = getPluginPostService().get(dto.getPostId()); + //We can't provide limitation properly without database-level locking + if (targetPost.getNotRemovedComments().size() >= LIMIT_OF_POSTS_VALUE) { + return new JsonResponse(JsonResponseStatus.FAIL); + } + comment = getPluginPostService().addComment(dto.getPostId(), Collections.EMPTY_MAP, dto.getBody()); + } catch (NotFoundException ex) { + return new FailJsonResponse(JsonResponseReason.ENTITY_NOT_FOUND); + } + JodaDateTimeTool dateTimeTool = new JodaDateTimeTool(request); + return new JsonResponse(JsonResponseStatus.SUCCESS, + new CommentDto(comment, dateTimeTool) + .withRenderBody(getPluginBbCodeService().convertBbToHtml(comment.getBody()))); + } + + /** + * Edits existence comment + * + * @param dto dto populated in form + * @param result validation result + * @param branchId id of a branch to check permission + * + * @return result in JSON format + */ + @RequestMapping(method = RequestMethod.POST, value = "editcomment") + @ResponseBody + JsonResponse editComment(@Valid @RequestBody CommentDto dto, BindingResult result, + @RequestParam("branchId") long branchId) { + if (result.hasErrors()) { + return new FailValidationJsonResponse(result.getAllErrors()); + } + PostComment updatedComment; + try { + updatedComment = getCommentService().updateComment(dto.getId(), dto.getBody(), branchId); + } catch (NotFoundException ex) { + return new FailJsonResponse(JsonResponseReason.ENTITY_NOT_FOUND); + } + CommentDto commentDto = new CommentDto(updatedComment.getBody(), + getPluginBbCodeService().convertBbToHtml(updatedComment.getBody())); + return new JsonResponse(JsonResponseStatus.SUCCESS, commentDto); + } + + /** + * Deletes comment + * + * @param commentId id of comment to delete + * @param postId id of a post to which this comment belong + * + * @return result in JSON format + */ + @RequestMapping(method = RequestMethod.GET, value = "deletecomment") + @ResponseBody + JsonResponse deleteComment(@RequestParam(COMMENT_ID) Long commentId, @RequestParam(POST_ID) Long postId) { + Post post; + PostComment postComment; + try { + post = getPluginPostService().get(postId); + postComment = getCommentService().getComment(commentId); + } catch (NotFoundException ex) { + return new FailJsonResponse(JsonResponseReason.ENTITY_NOT_FOUND); + } + getCommentService().markCommentAsDeleted(post, postComment); + return new JsonResponse(JsonResponseStatus.SUCCESS); + } + + /** + * Gets copy of specified collection of posts sorted by rating and creation date + * + * @param posts collection of posts to sort + * + * @return collection of posts sorted by rating and creation date + */ + @VisibleForTesting + List getSortedPosts(List posts) { + List result = new ArrayList<>(posts); + Post question = result.remove(0); + Collections.sort(result, new PostComparator()); + result.add(0, question); + return result; + } + + /** + * Writes icon to response and set apropriate response headers * * @param request HttpServletRequest * @param response HttpServletResponse @@ -439,9 +602,7 @@ private void processIconRequest(HttpServletRequest request, HttpServletResponse response.setHeader("Cache-Control", "public"); response.addHeader("Cache-Control", "must-revalidate"); response.addHeader("Cache-Control", "max-age=0"); - String formattedDateExpires = DateFormatUtils.format( - new Date(System.currentTimeMillis()), - HTTP_HEADER_DATETIME_PATTERN, Locale.US); + String formattedDateExpires = DateFormatUtils.format(new Date(), HTTP_HEADER_DATETIME_PATTERN, Locale.US); response.setHeader("Expires", formattedDateExpires); Date lastModificationDate = new Date(0); response.setHeader("Last-Modified", DateFormatUtils.format(lastModificationDate, HTTP_HEADER_DATETIME_PATTERN, @@ -451,19 +612,19 @@ private void processIconRequest(HttpServletRequest request, HttpServletResponse /** * Gets resource bundle with locale of current user * - * @param currentUser current user + * @param request http servlet request * * @return resource bundle with locale of current user */ - private ResourceBundle getLocalizedMessagesBundle(JCUser currentUser) { - return ResourceBundle.getBundle(MESSAGE_PATH, currentUser.getLanguage().getLocale()); + private ResourceBundle getLocalizedMessagesBundle(HttpServletRequest request) { + return ResourceBundle.getBundle(MESSAGE_PATH, getLocaleResolver().resolveLocale(request)); } /** * {@inheritDoc} */ @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @@ -499,8 +660,9 @@ private Map getDefaultModel(HttpServletRequest request) { JCUser currentUser = getUserReader().getCurrentUser(); PermissionTool tool = new PermissionTool(applicationContext); model.put("currentUser", currentUser); - model.put("messages", getLocalizedMessagesBundle(currentUser)); + model.put("messages", getLocalizedMessagesBundle(request)); model.put("permissionTool", tool); + model.put("propertiesHolder", PropertiesHolder.getInstance()); return model; } @@ -513,6 +675,17 @@ public void setApiPath(String apiPath) { /** * Needed for mocking + * + * @return locale resolver used in plugin + */ + LocaleResolver getLocaleResolver() { + return JcLocaleResolver.getInstance(); + } + + /** + * Needed for mocking + * + * @return service for manipulating with posts */ PluginPostService getPluginPostService() { return TransactionalPluginPostService.getInstance(); @@ -520,6 +693,17 @@ PluginPostService getPluginPostService() { /** * Needed for mocking + * + * @return service for manipulating draft topics + */ + PluginTopicDraftService getPluginTopicDraftService() { + return TransactionalPluginTopicDraftService.getInstance(); + } + + /** + * Needed for mocking + * + * @return service for manipulating with branches */ PluginBranchService getPluginBranchService() { return TransactionalPluginBranchService.getInstance(); @@ -527,6 +711,8 @@ PluginBranchService getPluginBranchService() { /** * Needed for mocking + * + * @return service for marking topic as read */ PluginLastReadPostService getPluginLastReadPostService() { return TransactionalPluginLastReadPostService.getInstance(); @@ -534,6 +720,17 @@ PluginLastReadPostService getPluginLastReadPostService() { /** * Needed for mocking + * + * @return Service which convert BBCode and URL to HTML + */ + PluginBbCodeService getPluginBbCodeService() { + return BbToHtmlConverter.getInstance(); + } + + /** + * Needed for mocking + * + * @return service for manipulating with topics */ TypeAwarePluginTopicService getTypeAwarePluginTopicService() { return TransactionalTypeAwarePluginTopicService.getInstance(); @@ -541,29 +738,63 @@ TypeAwarePluginTopicService getTypeAwarePluginTopicService() { /** * Needed for mocking + * + * @return service for fetching information about users */ UserReader getUserReader() { return ReadOnlySecurityService.getInstance(); } /** - * Needed for mocking + * + * Merge the specified Velocity template with the given model into a String. + * Needed for mocking. + * + * @param velocityEngine VelocityEngine to work with + * @param templateLocation the location of template, relative to Velocity's resource loader path + * @param encoding the encoding of the template file + * @param model the Map that contains model names as keys and model objects as values + * + * @return the result as String */ String getMergedTemplate(VelocityEngine velocityEngine, String templateLocation, String encoding, Map model) throws VelocityException { return VelocityEngineUtils.mergeTemplateIntoString(velocityEngine, templateLocation, encoding, model); } + /** * Needed for mocking + * + * @return service for manipulating with user location */ PluginLocationService getLocationService() { return PluginLocationServiceImpl.getInstance(); } /** + * Needed for mocking + * + * @return service for manipulating with comments + */ + PluginCommentService getCommentService() { + return TransactionalPluginCommentService.getInstance(); + } + + /** + * Sets specified {@link BreadcrumbBuilder} * Needed for tests + * + * @param breadcrumbBuilder {@link BreadcrumbBuilder} to set */ void setBreadcrumbBuilder(BreadcrumbBuilder breadcrumbBuilder) { this.breadcrumbBuilder = breadcrumbBuilder; } + + private static class PostComparator implements Comparator { + @Override + public int compare(Post o1, Post o2) { + return o1.getRating() == o2.getRating() ? o2.getCreationDate().compareTo(o1.getCreationDate()) : + o2.getRating() - o1.getRating(); + } + } } diff --git a/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/dto/CommentDto.java b/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/dto/CommentDto.java new file mode 100644 index 0000000000..f38c673190 --- /dev/null +++ b/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/dto/CommentDto.java @@ -0,0 +1,117 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.questionsandanswers.dto; + +import org.jtalks.jcommune.model.entity.PostComment; +import org.jtalks.jcommune.model.validation.annotations.NotBlankSized; +import org.jtalks.jcommune.plugin.api.web.velocity.tool.JodaDateTimeTool; + +/** + * @author Mikhail Stryzhonok + */ +public class CommentDto { + + private long id; + + private long postId; + + @NotBlankSized(min = PostComment.BODY_MIN_LENGTH, max = PostComment.BODY_MAX_LENGTH) + private String body; + + private String authorUsername; + + private long authorId; + + private String formattedCreationDate; + + private String renderBody; + + public CommentDto() { + } + + public CommentDto(PostComment postComment, JodaDateTimeTool dateTimeTool) { + this.id = postComment.getId(); + this.authorUsername = postComment.getAuthor().getUsername(); + this.body = postComment.getBody(); + this.formattedCreationDate = dateTimeTool.format(postComment.getCreationDate()); + this.authorId = postComment.getAuthor().getId(); + } + + public CommentDto(String body, String renderBody) { + this.body = body; + this.renderBody = renderBody; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public long getPostId() { + return postId; + } + + public void setPostId(long postId) { + this.postId = postId; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getRenderBody() { + return renderBody; + } + + public void setRenderBody(String renderBody) { + this.renderBody = renderBody; + } + + public String getAuthorUsername() { + return authorUsername; + } + + public void setAuthorUsername(String authorUsername) { + this.authorUsername = authorUsername; + } + + public long getAuthorId() { + return authorId; + } + + public void setAuthorId(long authorId) { + this.authorId = authorId; + } + + public String getFormattedCreationDate() { + return formattedCreationDate; + } + + public void setFormattedCreationDate(String formattedCreationDate) { + this.formattedCreationDate = formattedCreationDate; + } + + public CommentDto withRenderBody(String renderBody) { + this.renderBody = renderBody; + return this; + } +} diff --git a/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/dto/package-info.java b/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/dto/package-info.java new file mode 100644 index 0000000000..121655a64b --- /dev/null +++ b/jcommune-plugins/questions-n-answers-plugin/src/main/java/org/jtalks/jcommune/plugin/questionsandanswers/dto/package-info.java @@ -0,0 +1,18 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +/** + * Package with DTO objects for questions-n-answers plugin + */ +package org.jtalks.jcommune.plugin.questionsandanswers.dto; \ No newline at end of file diff --git a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_en.properties b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_en.properties index 968cc3e355..cf3f76419d 100644 --- a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_en.properties +++ b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_en.properties @@ -2,7 +2,8 @@ label.addQuestion=Ask Question label.addQuestion.tip=Do you need an answer or explanation? label.order=Create topic button order in dropdown. label.order.hint=Buttons in dropdown will be sorted by this value in in ascending order. Core topic types have order value 100 and 101 -label.answer=Answer +label.answer=Answer the Question +label.own.answer=Answer to your own Question label.answer.placeholder=Type your answer here label.edit=Edit label.delete=Delete @@ -15,11 +16,11 @@ permissions.moderators=Moderators: label.answers.count=Answers: label.tips.view_profile=Click to view profile label.tips.edit.question=Edit question -label.tips.edit.answer=Edit answer +label.tips.edit.answer=Save answer label.tips.remove.question=Delete question label.tips.remove.answer=Delete answer label.tips.answer=Do you know answer? -label.confirm.delete.question=Are you sure you want to delete the question (topic would be deleted too)? +label.confirm.delete.question=Are you sure you want to delete the question (topic will be deleted too)? label.confirm.delete.answer=Are you sure you want to delete the answer? label.edit.comment=Edit comment label.delete.comment=Delete comment @@ -34,3 +35,16 @@ label.ask=Ask label.vote.error.not.registered=Voting is available for registered users only label.vote.error.no.permissions=You do not have sufficient permissions to vote in this branch label.vote.error.own.post=You can't vote for your own question/answer +label.comment.add.error.not.found=The post which you are trying to comment, has been removed +label.comment.edit.error.not.found=The comment which you are trying to edit, has been removed +label.comment.remove.error.not.found=The comment or post with this comment has been removed +label.comment.limit.reached=Sorry, the limit for comments is reached. The maximum number of comments can be 50. +label.answer.limit.reached=Your answer can't be saved because limit of answers for this question has been reached. +label.question.voteUp.tooltip=The question is clear, useful and it's hard to find a duplicate at forum +label.question.voteDown.tooltip=The question is vague, offensive, there are plenty of duplicates at forum that are easy to find +label.answer.voteUp.tooltip=The answer is useful +label.answer.voteDown.tooltip=The answer is not useful, offensive, redirects to external sources without additional explanations +label.confirm.self.answer.title=Are you sure you want to answer your question? +label.confirm.self.answer.message=Use comments if you want to respond to someone's answer.
Or edit your question if you want to add more details. +label.confirm.multi.answer.title=Are you sure you want to leave another answer? +label.confirm.multi.answer.message=Use comments if you want to respond to someone's answer.
Or edit your previous answer if you want to add more details. diff --git a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_es.properties b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_es.properties index a9e4e2a20f..f328f0de68 100644 --- a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_es.properties +++ b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_es.properties @@ -12,7 +12,7 @@ permissions.moderators=Moderadores: label.answers.count=Respuestas: label.tips.view_profile=Click para ver el perfil label.tips.edit.question=Editar la cuesti\u00F3n -label.tips.edit.answer=Editar la respuesta +label.tips.edit.answer=Guarde la respuesta label.tips.remove.question=Eliminar la cuesti\u00F3n label.tips.remove.answer=Eliminar la respuesta label.tips.answer=\u00BFSabe la respuesta? @@ -32,3 +32,12 @@ label.answer.placeholder=Escriba su respuesta aqu\u00ED label.vote.error.not.registered=S\u00F3lo los usuarios registrados pueden votar label.vote.error.no.permissions=Usted no est\u00E1 autorizado a votar en esta rama label.vote.error.own.post=Usted no puede votar por su propia pregunta/respuesta +label.comment.add.error.not.found=El mensaje que usted est\u00E1 tratando de comentar, se ha eliminado +label.comment.edit.error.not.found=El comentario que usted est\u00E1 tratando de editar, se ha eliminado +label.comment.remove.error.not.found=El comentario o post con este comentario ha sido eliminado +label.comment.limit.reached=Lo sentimos, se alcanza el l\u00EDmite para los comentarios. El n\u00FAmero m\u00E1ximo de comentarios puede ser 50. +label.answer.limit.reached=Su respuesta no se puede guardar porque se ha alcanzado el limite de respuestas para esta pregunta. +label.question.voteUp.tooltip=La pregunta es clara, \u00FAtil y es dif\u00EDcil encontrarla duplicada en el foro +label.question.voteDown.tooltip=La pregunta es imprecisa, ofensiva, hay muchas similares en el foro que son f\u00E1cil de encontrar +label.answer.voteUp.tooltip=La respuesta es \u00FAtil +label.answer.voteDown.tooltip=La respuesta no es \u00FAtil, es ofensiva o redirige a contenidos externos sin explicaciones adicionales diff --git a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_ru.properties b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_ru.properties index 18a78c32d4..e029b0d0ac 100644 --- a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_ru.properties +++ b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_ru.properties @@ -2,7 +2,8 @@ label.addQuestion=\u0417\u0430\u0434\u0430\u0442\u044C \u0412\u043E\u043F\u0440\ label.addQuestion.tip=\u041D\u0443\u0436\u0435\u043D \u043E\u0442\u0432\u0435\u0442 \u0438\u043B\u0438 \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u0438\u0435 \u043F\u043E \u0438\u043D\u0442\u0435\u0440\u0435\u0441\u0443\u044E\u0449\u0435\u043C\u0443 \u0432\u043E\u043F\u0440\u043E\u0441\u0443? label.order=\u041E\u0447\u0435\u0440\u0451\u0434\u043D\u043E\u0441\u0442\u044C \u043A\u043D\u043E\u043F\u043A\u0438 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F \u0442\u0435\u043C\u044B \u0432 \u0432\u044B\u043F\u0430\u0434\u0430\u044E\u0449\u0435\u043C \u0441\u043F\u0438\u0441\u043A\u0435. label.order.hint=\u041A\u043D\u043E\u043F\u043A\u0438 \u0432 \u0432\u044B\u043F\u0430\u0434\u0430\u044E\u0449\u0435\u043C \u0441\u043F\u0438\u0441\u043A\u0435 \u0431\u0443\u0434\u0443\u0442 \u043E\u0442\u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u044B \u043F\u043E \u0432\u043E\u0437\u0440\u0430\u0441\u0442\u0430\u043D\u0438\u044E \u0434\u0430\u043D\u043D\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F. \u0412\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0435 \u0442\u0435\u043C\u044B \u0438\u043C\u0435\u044E\u0442 \u043E\u0447\u0435\u0440\u0451\u0434\u043D\u043E\u0441\u0442\u044C 100 \u0438 101 -label.answer=\u041E\u0442\u0432\u0435\u0442 +label.answer=\u041E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0430 \u0412\u043E\u043F\u0440\u043E\u0441 +label.own.answer=\u041E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0430 \u0441\u0432\u043E\u0439 \u0412\u043E\u043F\u0440\u043E\u0441 label.edit=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C label.delete=\u0423\u0434\u0430\u043B\u0438\u0442\u044C label.subscribe=\u041F\u043E\u0434\u043F\u0438\u0441\u0430\u0442\u044C\u0441\u044F @@ -14,7 +15,7 @@ permissions.moderators=\u041C\u043E\u0434\u0435\u0440\u0430\u0442\u043E\u0440\u0 label.answers.count=\u041E\u0442\u0432\u0435\u0442\u043E\u0432: label.tips.view_profile=\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0434\u043B\u044F \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430 \u043F\u0440\u043E\u0444\u0438\u043B\u044F label.tips.edit.question=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u043E\u043F\u0440\u043E\u0441 -label.tips.edit.answer=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043E\u0442\u0432\u0435\u0442 +label.tips.edit.answer=\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043E\u0442\u0432\u0435\u0442 label.tips.remove.question=\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0432\u043E\u043F\u0440\u043E\u0441 label.tips.remove.answer=\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043E\u0442\u0432\u0435\u0442 label.tips.answer=\u0417\u043D\u0430\u0435\u0442\u0435 \u043E\u0442\u0432\u0435\u0442? @@ -31,8 +32,20 @@ label.question=\u0412\u043E\u043F\u0440\u043E\u0441 label.question.details=\u041F\u043E\u0434\u0440\u043E\u0431\u043D\u0435\u0435 label.ask=\u0421\u043F\u0440\u043E\u0441\u0438\u0442\u044C label.answer.placeholder=\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043E\u0442\u0432\u0435\u0442 \u0437\u0434\u0435\u0441\u044C -label.vote.error.not.registered=\u0413\u043E\u043B\u043E\u0441\u043E\u0432\u0430\u043D\u0438\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439\ +label.vote.error.not.registered=\u0413\u043E\u043B\u043E\u0441\u043E\u0432\u0430\u043D\u0438\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439 label.vote.error.no.permissions=\u0423 \u0412\u0430\u0441 \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043E\u0447\u043D\u043E \u043F\u0440\u0430\u0432 \u0434\u043B\u044F \u0433\u043E\u043B\u043E\u0441\u043E\u0432\u0430\u043D\u0438\u044F \u0432 \u044D\u0442\u043E\u0439 \u0432\u0435\u0442\u043A\u0435 label.vote.error.own.post=\u0412\u044B \u043D\u0435 \u043C\u043E\u0436\u0435\u0442\u0435 \u0433\u043E\u043B\u043E\u0441\u043E\u0432\u0430\u0442\u044C \u0437\u0430 \u0441\u0432\u043E\u0439 \u0432\u043E\u043F\u0440\u043E\u0441/\u043E\u0442\u0432\u0435\u0442 -label.vote.error.no.permissions=\u0423 \u0412\u0430\u0441 \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043E\u0447\u043D\u043E \u043F\u0440\u0430\u0432 \u0434\u043B\u044F \u0433\u043E\u043B\u043E\u0441\u043E\u0432\u0430\u043D\u0438\u044F \u0432 \u044D\u0442\u043E\u0439 \u0432\u0435\u0442\u043A\u0435\ +label.comment.add.error.not.found=\u041F\u043E\u0441\u0442, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0412\u044B \u043F\u044B\u0442\u0430\u0435\u0442\u0435\u0441\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C, \u0431\u044B\u043B \u0443\u0434\u0430\u043B\u0435\u043D +label.comment.edit.error.not.found=\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0412\u044B \u043F\u044B\u0442\u0430\u0435\u0442\u0435\u0441\u044C \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u044C, \u0431\u044B\u043B \u0443\u0434\u0430\u043B\u0435\u043D +label.comment.remove.error.not.found=\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u0438\u043B\u0438 \u043F\u043E\u0441\u0442 \u0441 \u044D\u0442\u0438\u043C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0435\u043C \u0431\u044B\u043B \u0443\u0434\u0430\u043B\u0435\u043D \u0440\u0430\u043D\u0435\u0435 +label.comment.limit.reached=\u0418\u0437\u0432\u0438\u043D\u0438\u0442\u0435, \u0434\u043E\u0441\u0442\u0438\u0433\u043D\u0443\u0442 \u043B\u0438\u043C\u0438\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0435\u0432. \u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u043E \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0435\u0432 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C 50. +label.answer.limit.reached=\u0412\u0430\u0448 \u043E\u0442\u0432\u0435\u0442 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D, \u0442\u0430\u043A \u043A\u0430\u043A \u043D\u0430 \u044D\u0442\u043E\u0442 \u0432\u043E\u043F\u0440\u043E\u0441 \u0434\u043E\u0441\u0442\u0438\u0433\u043D\u0443\u0442 \u043B\u0438\u043C\u0438\u0442 \u043E\u0442\u0432\u0435\u0442\u043E\u0432. +label.question.voteUp.tooltip=\u0412\u043E\u043F\u0440\u043E\u0441 \u043F\u043E\u043D\u044F\u0442\u0435\u043D, \u043F\u043E\u043B\u0435\u0437\u0435\u043D \u0438 \u043D\u0430 \u0444\u043E\u0440\u0443\u043C\u0435 \u0441\u043B\u043E\u0436\u043D\u043E \u043D\u0430\u0439\u0442\u0438 \u0435\u043C\u0443 \u0434\u0443\u0431\u043B\u0438\u043A\u0430\u0442 +label.question.voteDown.tooltip=\u0412\u043E\u043F\u0440\u043E\u0441 \u043D\u0435\u0447\u0435\u0442\u043E\u043A, \u043E\u0441\u043A\u043E\u0440\u0431\u0438\u0442\u0435\u043B\u0435\u043D, \u043D\u0430 \u0444\u043E\u0440\u0443\u043C\u0435 \u043C\u043E\u0436\u043D\u043E \u043B\u0435\u0433\u043A\u043E \u043D\u0430\u0439\u0442\u0438 \u043C\u043D\u043E\u0436\u0435\u0441\u0442\u0432\u043E \u043F\u043E\u0445\u043E\u0436\u0438\u0445 +label.answer.voteUp.tooltip=\u041E\u0442\u0432\u0435\u0442 \u043F\u043E\u043B\u0435\u0437\u0435\u043D +label.answer.voteDown.tooltip=\u041E\u0442\u0432\u0435\u0442 \u043D\u0435 \u043F\u043E\u043B\u0435\u0437\u0435\u043D, \u043E\u0441\u043A\u043E\u0440\u0431\u0438\u0442\u0435\u043B\u0435\u043D, \u0441\u0441\u044B\u043B\u0430\u0435\u0442\u0441\u044F \u043D\u0430 \u0432\u043D\u0435\u0448\u043D\u0438\u0439 \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0431\u0435\u0437 \u043A\u0430\u043A\u0438\u0445-\u043B\u0438\u0431\u043E \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u0438\u0439 +label.confirm.self.answer.title=\u0412\u044B \u0442\u043E\u0447\u043D\u043E \u0445\u043E\u0442\u0438\u0442\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0430 \u0441\u0432\u043E\u0439 \u0436\u0435 \u0432\u043E\u043F\u0440\u043E\u0441? +label.confirm.self.answer.message=\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438, \u0435\u0441\u043B\u0438 \u0445\u043E\u0442\u0438\u0442\u0435 \u043F\u0440\u043E\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0447\u0435\u0439-\u0442\u043E \u043E\u0442\u0432\u0435\u0442.
\u041B\u0438\u0431\u043E \u043E\u0442\u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0441\u0432\u043E\u0439 \u0432\u043E\u043F\u0440\u043E\u0441, \u0435\u0441\u043B\u0438 \u0445\u043E\u0442\u0438\u0442\u0435 \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0431\u043E\u043B\u044C\u0448\u0435 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u0438. +label.confirm.multi.answer.title=\u0412\u044B \u0442\u043E\u0447\u043D\u043E \u0445\u043E\u0442\u0438\u0442\u0435 \u0434\u0430\u0442\u044C \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442? +label.confirm.multi.answer.message=\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438, \u0435\u0441\u043B\u0438 \u0445\u043E\u0442\u0438\u0442\u0435 \u043F\u0440\u043E\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0447\u0435\u0439-\u0442\u043E \u043E\u0442\u0432\u0435\u0442.
\u041B\u0438\u0431\u043E \u043E\u0442\u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0441\u0432\u043E\u0439 \u043F\u0435\u0440\u0432\u043E\u043D\u0430\u0447\u0430\u043B\u044C\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442, \u0435\u0441\u043B\u0438 \u0445\u043E\u0442\u0438\u0442\u0435 \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0431\u043E\u043B\u044C\u0448\u0435 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u0438. diff --git a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_uk.properties b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_uk.properties index f706f63957..f45bc9af7b 100644 --- a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_uk.properties +++ b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/messages_uk.properties @@ -2,7 +2,8 @@ label.addQuestion=\u0417\u0430\u0434\u0430\u0442\u0438 \u041F\u0438\u0442\u0430\ label.addQuestion.tip=\u0428\u0443\u043A\u0430\u0454\u0442\u0435 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C \u0430\u0431\u043E \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u043D\u044F? label.order=\u0412\u0438\u0437\u043D\u0430\u0447\u0430\u0454 \u043F\u043E\u0440\u044F\u0434\u043E\u043A \u043A\u043D\u043E\u043F\u043A\u0438 \u0443 \u0432\u0438\u043F\u0430\u0434\u0430\u044E\u0447\u043E\u043C\u0443 \u0441\u043F\u0438\u0441\u043A\u0443. label.order.hint=\u041A\u043D\u043E\u043F\u043A\u0438 \u0437 \u0440\u0456\u0437\u043D\u0438\u0445 \u043F\u043B\u0430\u0433\u0456\u043D\u0456\u0432 \u0431\u0443\u0434\u0443\u0442 \u0440\u043E\u0437\u0442\u0430\u0448\u043E\u0432\u0430\u043D\u0456 \u0443 \u0437\u0440\u043E\u0441\u0442\u0430\u044E\u0447\u043E\u043C\u0443 \u043F\u043E\u0440\u044F\u0434\u043A\u0443. \u0421\u0442\u0430\u043D\u0434\u0430\u0440\u0442\u043D\u0456 \u0432\u0438\u0434\u0438 \u0442\u0435\u043C \u043C\u0430\u044E\u0442\u044C \u043F\u043E\u0440\u044F\u0434\u043E\u043A 100 \u0434\u043B\u044F \u0414\u0438\u0441\u043A\u0443\u0441\u0456\u0456 \u0456 101 \u0434\u043B\u044F \u0420\u0435\u0446\u0435\u043D\u0437\u0456\u0457 \u041A\u043E\u0434\u0443. -label.answer=\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C +label.answer=\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0441\u0442\u0438 \u043D\u0430 \u041F\u0438\u0442\u0430\u043D\u043D\u044F +label.own.answer=\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0441\u0442\u0438 \u043D\u0430 \u0441\u0432\u043E\u0454 \u041F\u0438\u0442\u0430\u043D\u043D\u044F label.edit=\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438 label.delete=\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 label.subscribe=\u041F\u0456\u0434\u043F\u0438\u0441\u0430\u0442\u0438\u0441\u044F @@ -14,7 +15,7 @@ permissions.moderators=\u041C\u043E\u0434\u0435\u0440\u0430\u0442\u043E\u0440\u0 label.answers.count=\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u0435\u0439: label.tips.view_profile=\u041D\u0430\u0442\u0438\u0441\u043D\u0456\u0442\u044C \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0433\u043B\u044F\u0434\u0443 \u043F\u0440\u043E\u0444\u0456\u043B\u044E label.tips.edit.question=\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438 \u043F\u0438\u0442\u0430\u043D\u043D\u044F -label.tips.edit.answer=\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C +label.tips.edit.answer=\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438 \u0432i\u0434\u043F\u043E\u0432i\u0434\u044C label.tips.remove.question=\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u043F\u0438\u0442\u0430\u043D\u043D\u044F label.tips.remove.answer=\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C label.tips.answer=\u0417\u043D\u0430\u0454\u0442\u0435 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C? @@ -25,7 +26,7 @@ label.delete.comment=\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u043A\u04 label.add.comment=\u0414\u043E\u0434\u0430\u0442\u0438 \u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440 label.expand.comments=\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u0438 \u0432\u0441\u0456 \u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u0456 label.hide.comments=C\u0445\u043E\u0432\u0430\u0442\u0438 -label.cancel=\u0412\u0456\u0434\u043C\u0456\u043D\u0438\u0442\u0438 +label.cancel=\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438 label.comment=\u041A\u043E\u043C\u0435\u043D\u0442\u0443\u0432\u0430\u0442\u0438 label.question=\u041F\u0438\u0442\u0430\u043D\u043D\u044F label.question.details=\u0414\u0435\u0442\u0430\u043B\u044C\u043D\u0456\u0448\u0435 @@ -34,3 +35,16 @@ label.answer.placeholder=\u0412\u0432\u0435\u0434\u0456\u0442\u044C \u0432\u0456 label.vote.error.not.registered=\u0413\u043E\u043B\u043E\u0441\u0443\u0432\u0430\u0442\u0438 \u043C\u043E\u0436\u0443\u0442\u044C \u043B\u0438\u0448\u0435 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043E\u0432\u0430\u043D\u0456 \u043A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456 label.vote.error.no.permissions=\u0423 \u0412\u0430\u0441 \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043D\u044C\u043E \u043F\u0440\u0430\u0432 \u0434\u043B\u044F \u0433\u043E\u043B\u043E\u0441\u0443\u0432\u0430\u043D\u043D\u044F \u0432 \u0446\u0456\u0439 \u0433\u0456\u043B\u0446\u0456 label.vote.error.own.post=\u0412\u0438 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442\u0435 \u0433\u043E\u043B\u043E\u0441\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0432\u043B\u0430\u0441\u043D\u0435 \u043F\u0438\u0442\u0430\u043D\u043D\u044F/\u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C +label.comment.add.error.not.found=\u041F\u043E\u0441\u0442, \u044F\u043A\u0438\u0439 \u0412\u0438 \u043D\u0430\u043C\u0430\u0433\u0430\u0454\u0442\u0435\u0441\u044C \u043A\u043E\u043C\u0435\u043D\u0442\u0443\u0432\u0430\u0442\u0438, \u0431\u0443\u0432 \u0432\u0438\u0434\u0430\u043B\u0435\u043D\u0438\u0439 +label.comment.edit.error.not.found=\u041A\u043E\u043C\u0435\u043D\u0442\u0430\u0440, \u044F\u043A\u0438\u0439 \u0412\u0438 \u043D\u0430\u043C\u0430\u0433\u0430\u0454\u0442\u0435\u0441\u044C \u0440\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438, \u0431\u0443\u0432 \u0432\u0438\u0434\u0430\u043B\u0435\u043D\u0438\u0439 +label.comment.remove.error.not.found=\u041A\u043E\u043C\u0435\u043D\u0442\u0430\u0440 \u0430\u0431\u043E \u043F\u043E\u0441\u0442 \u0456\u0437 \u0446\u0438\u043C \u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u0435\u043C \u0431\u0443\u0432 \u0432\u0438\u0434\u0430\u043B\u0435\u043D\u0438\u0439 \u0440\u0430\u043D\u0456\u0448\u0435 +label.comment.limit.reached=\u0412\u0438\u0431\u0430\u0447\u0442\u0435, \u0434\u043E\u0441\u044F\u0433\u043D\u0443\u0442\u043E \u043B\u0456\u043C\u0456\u0442 \u0434\u043B\u044F \u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u0456\u0432. \u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0430 \u043A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u0456\u0432 \u043C\u043E\u0436\u0435 \u0431\u0443\u0442\u0438 50. +label.answer.limit.reached=\u041D\u0435\u043C\u043E\u0436\u043B\u0438\u0432\u043E \u0437\u0431\u0435\u0440\u0435\u0433\u0442\u0438 \u0412\u0430\u0448\u0443 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C, \u0442\u0430\u043A \u044F\u043A \u043B\u0456\u043C\u0456\u0442 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u0435\u0439 \u043D\u0430 \u0446\u0435 \u043F\u0438\u0442\u0430\u043D\u043D\u044F \u0432\u0438\u0447\u0435\u0440\u043F\u0430\u043D\u043E. +label.question.voteUp.tooltip=\u041F\u0438\u0442\u0430\u043D\u043D\u044F \u0437\u0440\u043E\u0437\u0443\u043C\u0456\u043B\u0435, \u043A\u043E\u0440\u0438\u0441\u043D\u0435 \u0456 \u043D\u0430 \u0444\u043E\u0440\u0443\u043C\u0456 \u0432\u0430\u0436\u043A\u043E \u0437\u043D\u0430\u0439\u0442\u0438 \u0434\u0443\u0431\u043B\u0456\u043A\u0430\u0442 +label.question.voteDown.tooltip=\u041F\u0438\u0442\u0430\u043D\u043D\u044F \u043D\u0435\u0437\u0440\u043E\u0437\u0443\u043C\u0456\u043B\u0435, \u043E\u0431\u0440\u0430\u0437\u043B\u0438\u0432\u0435, \u043D\u0430 \u0444\u043E\u0440\u0443\u043C\u0456 \u043C\u043E\u0436\u043D\u0430 \u0437 \u043B\u0435\u0433\u043A\u0456\u0441\u0442\u044E \u0437\u043D\u0430\u0439\u0442\u0438 \u0431\u0430\u0433\u0430\u0442\u043E \u0441\u0445\u043E\u0436\u0438\u0445 +label.answer.voteUp.tooltip=\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C \u043A\u043E\u0440\u0438\u0441\u043D\u0430 +label.answer.voteDown.tooltip=\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C \u043D\u0435 \u043A\u043E\u0440\u0438\u0441\u043D\u0430, \u043E\u0431\u0440\u0430\u0437\u043B\u0438\u0432\u0430, \u043F\u0440\u043E\u043F\u043E\u043D\u0443\u0454 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F \u043D\u0430 \u0437\u043E\u0432\u043D\u0456\u0448\u043D\u0456 \u0440\u0435\u0441\u0443\u0440\u0441\u0438 \u0431\u0435\u0437 \u0434\u043E\u0434\u0430\u0442\u043A\u043E\u0432\u0438\u0445 \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u044C +label.confirm.self.answer.title=\u0412\u0438 \u0442\u043E\u0447\u043D\u043E \u0431\u0430\u0436\u0430\u0454\u0442\u0435 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0441\u0442\u0438 \u043D\u0430 \u0441\u0432\u043E\u0454 \u043F\u0438\u0442\u0430\u043D\u043D\u044F? +label.confirm.self.answer.message=\u041A\u043E\u0440\u0438\u0441\u0442\u0443\u0439\u0442\u0435\u0441\u044F \u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u044F\u043C\u0438, \u044F\u043A\u0449\u043E \u0431\u0430\u0436\u0430\u0454\u0442\u0435 \u043F\u0440\u043E\u043A\u043E\u043C\u0435\u043D\u0442\u0443\u0432\u0430\u0442\u0438 \u0447\u0438\u044E\u0441\u044C \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C.
\u0410\u0431\u043E \u0432i\u0434\u0440\u0435\u0434\u0430\u0433\u0443\u0439\u0442\u0435 \u0441\u0432\u043E\u0454 \u043F\u0438\u0442\u0430\u043D\u043D\u044F, \u044F\u043A\u0449\u043E \u0445\u043E\u0447\u0435\u0442\u0435 \u0434\u043E\u0434\u0430\u0442\u0438 \u0431\u0456\u043B\u044C\u0448\u0435 \u0456\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0456\u0457. +label.confirm.multi.answer.title=\u0412\u0438 \u0442\u043E\u0447\u043D\u043E \u0431\u0430\u0436\u0430\u0454\u0442\u0435 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0441\u0442\u0438 \u043D\u0430 \u043F\u0438\u0442\u0430\u043D\u043D\u044F \u0449\u0435 \u0440\u0430\u0437? +label.confirm.multi.answer.message=\u041A\u043E\u0440\u0438\u0441\u0442\u0443\u0439\u0442\u0435\u0441\u044F \u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u044F\u043C\u0438, \u044F\u043A\u0449\u043E \u0431\u0430\u0436\u0430\u0454\u0442\u0435 \u043F\u0440\u043E\u043A\u043E\u043C\u0435\u043D\u0442\u0443\u0432\u0430\u0442\u0438 \u0447\u0438\u044E\u0441\u044C \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C.
\u0410\u0431\u043E \u0432\u0456\u0434\u0440\u0435\u0434\u0430\u0433\u0443\u0439\u0442\u0435 \u0441\u0432\u043E\u044E \u043F\u0435\u0440\u0448\u0443 \u0432\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C, \u044F\u043A\u0449\u043E \u0445\u043E\u0447\u0435\u0442\u0435 \u0434\u043E\u0434\u0430\u0442\u0438 \u0431\u0456\u043B\u044C\u0448\u0435 \u0456\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0456\u0457. diff --git a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/template/answerForm.vm b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/template/answerForm.vm index 743c4bba43..6fc3c8159e 100644 --- a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/template/answerForm.vm +++ b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/template/answerForm.vm @@ -16,27 +16,25 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *# #parse("org/jtalks/jcommune/jcommune-plugin-api/web/templates/bbeditor.vm") - + - ${esc.html(${questionTitle})} - ${messages.getString("label.tips.edit.answer")} + ${propertiesHolder.allPagesTitlePrefix} + ${htmlEscaper.process($questionTitle)} - ${messages.getString("label.tips.edit.answer")}
-

- ${questionTitle} +

+ ${htmlEscaper.process($questionTitle)}

- #bbeditor(${messages.getString("label.tips.edit.answer")} "" "${esc.html(${postDto.bodyText})}" "bodyText" true ${messages.getString("label.answer.placeholder")} ${result}) + #bbeditor(${messages.getString("label.tips.edit.answer")} "" "${htmlEscaper.process($postDto.bodyText)}" "bodyText" true ${messages.getString("label.answer.placeholder")} ${result})
- - - label.back - +

+#end + + + #set($targedId = ${question.branch.id}) + #set($canLeavePosts=false) + #if((!${question.closed} || ${permissionTool.hasPermission($targedId.longValue(),"BRANCH","BranchPermission.CLOSE_TOPICS")}) + && ${permissionTool.hasPermission($targedId.longValue(),"BRANCH","BranchPermission.CREATE_POSTS")}) + #set($canLeavePosts=true) + #end + + #if ($canLeavePosts) + ## determines if the post editor is show or not - depends on whether user has already left some posts + ## Also we shouldn't hide the area if there is some text already (draft or validation error returned after + ## submitting invalid form) + #set($formVisibilityClass = "") + ## determines if the button that prevents users to leave multiple answers is shown + #set($antiMultianswerBtnVisibilityClass = "hide-element") + #if(${question.topicStarter.id} == ${currentUser.id} && !${postDto.bodyText}) + #initAnswerConfirmationParams(${messages.getString("label.confirm.self.answer.title")} ${messages.getString("label.confirm.self.answer.message")}) + #elseif(${question.getUserPostCount(${currentUser})} > 0 && !${postDto.bodyText}) + #initAnswerConfirmationParams(${messages.getString("label.confirm.multi.answer.title")} ${messages.getString("label.confirm.multi.answer.message")}) + #end + #end - ${esc.html(${question.title})} + + ${propertiesHolder.allPagesTitlePrefix} + ${htmlEscaper.process(${question.title})} + - #set($targedId = ${question.branch.id}) - #set($canLeavePosts=false) - #if((!${question.closed} || ${permissionTool.hasPermission($targedId.longValue(),"BRANCH","BranchPermission.CLOSE_TOPICS")}) - && ${permissionTool.hasPermission($targedId.longValue(),"BRANCH","BranchPermission.CREATE_POSTS")}) - #set($canLeavePosts=true) - #end

- ${esc.html(${question.title})} + ${htmlEscaper.process(${question.title})}

@@ -73,6 +115,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #topicActions($targedId ${request} ${question} ${permissionTool})
+ #set($labelAnswerType = "label.answer") + #if(!${currentUser.anonymous} && ${question.topicStarter.id} == ${currentUser.id}) + #set($labelAnswerType = "label.own.answer") + #end #set($answersCount = ${postPage.getContent().size()} - 1) #foreach(${post} in ${postPage.content}) #set($voteUpClass = "vote-up-unpressed") @@ -100,14 +146,27 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #set($postClass = "") #set($postEditUrl = "") #set($postDeleteUrl = "") + #macro(voteTooltips $voteUpTooltipText $voteDownTooltipText) + #if(!${currentUser.anonymous}) + #set($voteUpTooltip = $voteUpTooltipText) + #set($voteDownTooltip = $voteDownTooltipText) + #else + #set($voteUpTooltip = $messages.getString('label.vote.error.not.registered')) + #set($voteDownTooltip = $messages.getString('label.vote.error.not.registered')) + #end + #end #if($isFirstPost) #set($postClass = "question") #set($postEditUrl = "${request.contextPath}/topics/question/${question.id.longValue()}/edit") #set($postDeleteUrl = "${request.contextPath}/topics/${question.id.longValue()}") + #voteTooltips($messages.getString('label.question.voteUp.tooltip'), + $messages.getString('label.question.voteDown.tooltip')) #else #set($postEditUrl = "${request.contextPath}/topics/question/post/${post.id.longValue()}/edit") #set($postDeleteUrl = "${request.contextPath}/topics/question/post/${post.id.longValue()}") #set($postClass = "answer") + #voteTooltips($messages.getString('label.answer.voteUp.tooltip'), + $messages.getString('label.answer.voteDown.tooltip')) #end #if($velocityCount == 2)
@@ -121,13 +180,15 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#topicActions($targedId ${request} ${question} ${permissionTool}) - #if($canLeavePosts) + #if($canLeavePosts && ${postLimit} > ${postPage.getContent().size()} - 1) #if(!${postDto.bodyText}) #set(${postDto.bodyText} = "") #end + + + #if(${postDto.id} != 0) + + #end
+ method="POST" class='well anti-multipost submit-form $formVisibilityClass'> - #bbeditor(${messages.getString("label.answer")} "" "${esc.html(${postDto.bodyText})}" "bodyText" true ${messages.getString("label.answer.placeholder")}) + #bbeditor(${messages.getString($labelAnswerType)} "" "${htmlEscaper.process($postDto.bodyText)}" "bodyText" true ${messages.getString("label.answer.placeholder")})
+
+ +
#end
@@ -236,4 +297,4 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- \ No newline at end of file + diff --git a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/template/questionForm.vm b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/template/questionForm.vm index ef2e4a8eb3..52094a509d 100644 --- a/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/template/questionForm.vm +++ b/jcommune-plugins/questions-n-answers-plugin/src/main/resources/org/jtalks/jcommune/plugin/questionsandanswers/template/questionForm.vm @@ -19,7 +19,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - ${esc.html(${topicDto.topic.branch.name})} - + ${propertiesHolder.allPagesTitlePrefix} + #if(!${result.hasErrors()}) + ${esc.html(${topicDto.topic.branch.name})} - + #end #if(${edit}) ${messages.getString("label.tips.edit.question")} #else @@ -37,6 +40,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA <form action="${request.contextPath}/topics/question/new?branchId=${topicDto.topic.branch.id}" method="POST" id="topicDto" class="well anti-multipost submit-form" enctype="multipart/form-data"> #end + #if(${topicDraft.lastSaved}) + <input id="topicDraftLastSavedMillis" type="hidden" value="${topicDraft.lastSaved.millis}"/> + #end + <input id="branchId" type="hidden" value="${topicDto.topic.branch.id}"/> + <input id="topicType" type="hidden" value="${topicDto.topic.type}"/> <div class='control-group hide-on-preview'> <div class='controls'> #if(!${topicDto.topic.title}) @@ -47,18 +55,15 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #end <input name="topic.title" id="subject" type="text" size="45" maxlength="255" tabindex="100" - class="span11 script-confirm-unsaved" placeholder="${messages.getString("label.question")}" - value="${esc.html(${topicDto.topic.title})}"/> + class="full-width script-confirm-unsaved" placeholder="${messages.getString("label.question")}" + value="${htmlEscaper.process($topicDto.topic.title)}"/> #jtalksBinding(${result} "topic.title") </div> </div> - #bbeditor(${messages.getString("label.ask")} "" "${esc.html(${topicDto.bodyText})}" "bodyText" true ${messages.getString("label.question.details")} ${result}) + #bbeditor(${messages.getString("label.ask")} "" "${htmlEscaper.process($topicDto.bodyText)}" "bodyText" true ${messages.getString("label.question.details")} ${result}) </form> - <a href="${request.contextPath}/branches/${topicDto.topic.branch.id}" tabindex="1000" class='back-btn'> - <i class="icon-arrow-left"></i> - <jcommune:message>label.back</jcommune:message> - </a> + </div> <script> Utils.focusFirstEl('#subject'); diff --git a/jcommune-plugins/questions-n-answers-plugin/src/test/java/org/jtalks/jcommune/plugin/questionsandanswers/QuestionSubscribersFilterTest.java b/jcommune-plugins/questions-n-answers-plugin/src/test/java/org/jtalks/jcommune/plugin/questionsandanswers/QuestionSubscribersFilterTest.java new file mode 100644 index 0000000000..7c472bba1b --- /dev/null +++ b/jcommune-plugins/questions-n-answers-plugin/src/test/java/org/jtalks/jcommune/plugin/questionsandanswers/QuestionSubscribersFilterTest.java @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.plugin.questionsandanswers; + +import org.jtalks.jcommune.model.entity.Branch; +import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.Post; +import org.jtalks.jcommune.model.entity.Topic; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * @author Mikhail Stryzhonok + */ +public class QuestionSubscribersFilterTest { + + @Test + public void questionSubscribersFilterShouldNotChangeRecepientsIfBranchSubscribersFilters() { + Branch branch = new Branch("name", "description"); + JCUser user1 = new JCUser("name1", "email1@example.com", "pwd1"); + JCUser user2 = new JCUser("name2", "email2@example.com", "pwd2"); + QuestionSubscribersFilter filter = new QuestionSubscribersFilter(); + List<JCUser> users = new ArrayList<>(Arrays.asList(user1, user2)); + + filter.filter(users, branch); + + assertEquals(users.size(), 2); + assertTrue(users.contains(user1)); + assertTrue(users.contains(user2)); + } + + @Test + public void questionSubscribersFilterShouldNotChangeRecepientsIfTopicSubscribersFilters() { + Topic topic = new Topic(); + JCUser user1 = new JCUser("name1", "email1@example.com", "pwd1"); + JCUser user2 = new JCUser("name2", "email2@example.com", "pwd2"); + QuestionSubscribersFilter filter = new QuestionSubscribersFilter(); + List<JCUser> users = new ArrayList<>(Arrays.asList(user1, user2)); + + + filter.filter(users, topic); + + assertEquals(users.size(), 2); + assertTrue(users.contains(user1)); + assertTrue(users.contains(user2)); + } + + @Test + public void questionSubscribersFilterShouldNotChangeRecepientsIfPostFomTopicWithOtherType() { + Post post = getInTopicWithType("Some amazing topic"); + JCUser user1 = new JCUser("name1", "email1@example.com", "pwd1"); + JCUser user2 = new JCUser("name2", "email2@example.com", "pwd2"); + QuestionSubscribersFilter filter = new QuestionSubscribersFilter(); + List<JCUser> users = new ArrayList<>(Arrays.asList(user1, user2)); + + filter.filter(users, post); + + assertEquals(users.size(), 2); + assertTrue(users.contains(user1)); + assertTrue(users.contains(user2)); + } + + @Test + public void questionSubscribersFilterShouldLeaveOnlyPostAuthorInIfPostSubscribersFilters() { + Post post = getInTopicWithType(QuestionsAndAnswersPlugin.TOPIC_TYPE); + JCUser user1 = new JCUser("name1", "email1@example.com", "pwd1"); + JCUser user2 = new JCUser("name2", "email2@example.com", "pwd2"); + QuestionSubscribersFilter filter = new QuestionSubscribersFilter(); + List<JCUser> users = new ArrayList<>(Arrays.asList(user1, user2, post.getUserCreated())); + + filter.filter(users, post); + + assertEquals(users.size(), 1); + assertTrue(users.contains(post.getUserCreated())); + } + + private Post getInTopicWithType(String type) { + JCUser author = new JCUser("authorName", "authoremail@example.com", "authorpwd"); + Post post = new Post(author, "text"); + Topic topic = new Topic(); + topic.setType(type); + post.setTopic(topic); + return post; + } +} diff --git a/jcommune-plugins/questions-n-answers-plugin/src/test/java/org/jtalks/jcommune/plugin/questionsandanswers/controller/QuestionsAndAnswersControllerTest.java b/jcommune-plugins/questions-n-answers-plugin/src/test/java/org/jtalks/jcommune/plugin/questionsandanswers/controller/QuestionsAndAnswersControllerTest.java index 78b014ba56..7a1845b031 100644 --- a/jcommune-plugins/questions-n-answers-plugin/src/test/java/org/jtalks/jcommune/plugin/questionsandanswers/controller/QuestionsAndAnswersControllerTest.java +++ b/jcommune-plugins/questions-n-answers-plugin/src/test/java/org/jtalks/jcommune/plugin/questionsandanswers/controller/QuestionsAndAnswersControllerTest.java @@ -16,16 +16,19 @@ import org.apache.velocity.app.VelocityEngine; import org.jtalks.common.model.entity.Entity; -import org.jtalks.jcommune.model.entity.Branch; -import org.jtalks.jcommune.model.entity.JCUser; -import org.jtalks.jcommune.model.entity.Post; -import org.jtalks.jcommune.model.entity.Topic; +import org.jtalks.common.model.entity.Section; +import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.service.*; +import org.jtalks.jcommune.plugin.api.web.dto.Breadcrumb; import org.jtalks.jcommune.plugin.api.web.dto.PostDto; import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; +import org.jtalks.jcommune.plugin.api.web.dto.json.*; +import org.jtalks.jcommune.plugin.api.web.locale.JcLocaleResolver; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; import org.jtalks.jcommune.plugin.questionsandanswers.QuestionsAndAnswersPlugin; +import org.jtalks.jcommune.plugin.api.service.PluginBbCodeService; +import org.jtalks.jcommune.plugin.questionsandanswers.dto.CommentDto; import org.mockito.Mock; import org.mockito.Spy; import org.springframework.context.ApplicationContext; @@ -34,14 +37,13 @@ import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; +import org.springframework.web.servlet.LocaleResolver; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.util.Collections; -import java.util.Date; -import java.util.Properties; +import java.util.*; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyLong; @@ -51,14 +53,18 @@ import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; /** * @author Mikhail Stryzhonok */ public class QuestionsAndAnswersControllerTest { + @Mock private PluginBranchService branchService; @Mock + private PluginTopicDraftService topicDraftService; + @Mock private PluginLastReadPostService lastReadPostService; @Mock private TypeAwarePluginTopicService topicService; @@ -76,6 +82,13 @@ public class QuestionsAndAnswersControllerTest { private BindingResult result; @Mock private PluginLocationService locationService; + @Mock + private LocaleResolver localeResolver; + @Mock + private PluginCommentService commentService; + @Mock + private PluginBbCodeService pluginBbCodeService; + @Spy private QuestionsAndAnswersController controller = new QuestionsAndAnswersController(); private String content = "some html"; @@ -85,19 +98,26 @@ public class QuestionsAndAnswersControllerTest { public void init() { initMocks(this); when(controller.getPluginBranchService()).thenReturn(branchService); + when(controller.getPluginTopicDraftService()).thenReturn(topicDraftService); when(controller.getPluginLastReadPostService()).thenReturn(lastReadPostService); when(controller.getTypeAwarePluginTopicService()).thenReturn(topicService); when(controller.getPluginPostService()).thenReturn(postService); when(controller.getProperties()).thenReturn(new Properties()); when(controller.getUserReader()).thenReturn(userReader); when(controller.getLocationService()).thenReturn(locationService); + when(controller.getLocaleResolver()).thenReturn(localeResolver); + when(controller.getCommentService()).thenReturn(commentService); + when(controller.getPluginBbCodeService()).thenReturn(pluginBbCodeService); + when(localeResolver.resolveLocale(any(HttpServletRequest.class))).thenReturn(Locale.ENGLISH); when(userReader.getCurrentUser()).thenReturn(new JCUser("name", "example@mail.ru", "pwd")); controller.setApplicationContext(context); controller.setBreadcrumbBuilder(breadcrumbBuilder); - when(breadcrumbBuilder.getForumBreadcrumb()).thenReturn(Collections.EMPTY_LIST); - when(locationService.getUsersViewing(any(Entity.class))).thenReturn(Collections.EMPTY_LIST); + when(breadcrumbBuilder.getForumBreadcrumb()).thenReturn(Collections.<Breadcrumb>emptyList()); + when(locationService.getUsersViewing(any(Entity.class))).thenReturn(Collections.<UserInfo>emptyList()); doReturn(content).when(controller).getMergedTemplate(any(VelocityEngine.class), anyString(), anyString(), anyMap()); + when(userReader.getCurrentUser()).thenReturn(new JCUser("name", "example@mail.ru", "pwd")); + ((JcLocaleResolver)JcLocaleResolver.getInstance()).setUserReader(userReader); } @Test @@ -140,6 +160,7 @@ public void createQuestionSuccessTest() throws Exception { @Test public void createQuestionValidationErrorsTest() throws Exception { Branch branch = new Branch("name", "description"); + branch.setSection(new Section("namesection")); Topic createdQuestion = new Topic(); createdQuestion.setId(1); TopicDto topicDto = new TopicDto(new Topic()); @@ -148,9 +169,13 @@ public void createQuestionValidationErrorsTest() throws Exception { when(result.hasErrors()).thenReturn(true); when(branchService.get(anyLong())).thenReturn(branch); + List<Breadcrumb> breadcrumbs = new BreadcrumbBuilder().getNewTopicBreadcrumb(branch); + when(breadcrumbBuilder.getNewTopicBreadcrumb(branch)).thenReturn(breadcrumbs); String actual = controller.createQuestion(topicDto, result , model, 1L, request); + assertEquals(breadcrumbs.get(2).getValue(), branch.getName()); + assertEquals(breadcrumbs.get(1).getValue(), branch.getSection().getName()); assertEquals(actual, QuestionsAndAnswersController.PLUGIN_VIEW_NAME); assertEquals(model.asMap().get(QuestionsAndAnswersController.CONTENT), content); } @@ -164,7 +189,7 @@ public void createQuestionShouldThrowExceptionIfBranchNotFound() throws Exceptio @Test public void showQuestionSuccessTest() throws Exception { Branch branch = new Branch("name", "description"); - Topic topic = new Topic(); + Topic topic = createTopic(); topic.setBranch(branch); Model model = new ExtendedModelMap(); @@ -439,12 +464,28 @@ public void createAnswerShouldUpdateAnswerIfValidationSuccess() throws Exception verify(topicService).replyToTopic(42L, answerContent, topic.getBranch().getId()); } + + @Test + public void createAnswerMustReturnDraft_ifValidationFails() throws Exception { + Topic topic = createTopic(); + topic.addDraft(new PostDraft("blah", userReader.getCurrentUser())); + when(topicService.get(anyLong(), anyString())).thenReturn(topic); + when(result.hasErrors()).thenReturn(true); + + Model model = new ExtendedModelMap(); + PostDto postDto = new PostDto(); + controller.create(42L, postDto, result, model, request); + + assertEquals(postDto.getBodyText(), "blah"); + } + private Topic createTopic() { Branch branch = new Branch("name", "description"); branch.setId(1); Topic topic = new Topic(); topic.setId(42L); topic.setBranch(branch); + topic.addPost(new Post(null, null)); return topic; } @@ -472,5 +513,209 @@ public void deleteAnswerPageShouldRedirectToNeighborAnswer() throws Exception { assertEquals(result, "redirect:" + QuestionsAndAnswersPlugin.CONTEXT + "/" + answer.getTopic().getId() + "#" + neighborPostId); } + + @Test + public void testAddCommentSuccess() throws Exception { + PostComment comment = getComment(); + CommentDto dto = new CommentDto(); + dto.setPostId(1); + dto.setBody(comment.getBody()); + + when(postService.get(anyLong())).thenReturn(new Post(null, null)); + when(postService.addComment(eq(dto.getPostId()), anyMap(), eq(dto.getBody()))).thenReturn(comment); + + JsonResponse response = controller.addComment(dto, result, request); + + assertEquals(response.getStatus(), JsonResponseStatus.SUCCESS); + assertTrue(response.getResult() instanceof CommentDto); + assertEquals(((CommentDto)response.getResult()).getBody(), dto.getBody()); + } + + @Test + public void addCommentShouldReturnFailResponseIfValidationErrorOccurred() { + CommentDto dto = new CommentDto(); + + when(result.hasErrors()).thenReturn(true); + + JsonResponse response = controller.addComment(dto, result, request); + + assertEquals(response.getStatus(), JsonResponseStatus.FAIL); + assertTrue(response instanceof FailJsonResponse); + assertEquals(((FailJsonResponse)response).getReason(), JsonResponseReason.VALIDATION); + + } + + @Test + public void addCommentShouldReturnFailResponseIfPostNotFound() throws Exception { + CommentDto dto = new CommentDto(); + + when(postService.get(anyLong())).thenReturn(new Post(null, null)); + when(postService.addComment(anyLong(), anyMap(), anyString())).thenThrow(new NotFoundException()); + + JsonResponse response = controller.addComment(dto, result, request); + + assertEquals(response.getStatus(), JsonResponseStatus.FAIL); + assertTrue(response instanceof FailJsonResponse); + assertEquals(((FailJsonResponse)response).getReason(), JsonResponseReason.ENTITY_NOT_FOUND); + } + + @Test + public void testEditCommentSuccess() throws Exception { + PostComment comment = getComment(); + CommentDto dto = new CommentDto(); + dto.setBody(comment.getBody()); + + when(commentService.updateComment(eq(dto.getId()), eq(dto.getBody()), anyLong())).thenReturn(comment); + + JsonResponse response = controller.editComment(dto, result, 1); + + assertEquals(response.getStatus(), JsonResponseStatus.SUCCESS); + assertTrue(response.getResult() instanceof CommentDto); + assertEquals(((CommentDto) response.getResult()).getBody(), dto.getBody()); + } + + @Test + public void editCommentShouldReturnFailResponseIfValidationErrorOccurred() { + CommentDto dto = new CommentDto(); + + when(result.hasErrors()).thenReturn(true); + + JsonResponse response = controller.editComment(dto, result, 1); + + assertEquals(response.getStatus(), JsonResponseStatus.FAIL); + assertTrue(response instanceof FailJsonResponse); + assertEquals(((FailJsonResponse)response).getReason(), JsonResponseReason.VALIDATION); + } + + @Test + public void editCommentShouldReturnFailResponseIfPostNotFound() throws Exception { + CommentDto dto = new CommentDto(); + + when(commentService.updateComment(anyLong(), anyString(), anyLong())).thenThrow(new NotFoundException()); + + JsonResponse response = controller.editComment(dto, result, 1); + + assertEquals(response.getStatus(), JsonResponseStatus.FAIL); + assertTrue(response instanceof FailJsonResponse); + assertEquals(((FailJsonResponse)response).getReason(), JsonResponseReason.ENTITY_NOT_FOUND); + } + + @Test + public void testDeleteCommentSuccess() throws Exception { + Post post = new Post(null, null); + PostComment comment = new PostComment(); + + when(postService.get(1L)).thenReturn(post); + when(commentService.getComment(1)).thenReturn(comment); + + JsonResponse response = controller.deleteComment(1L, 1L); + + assertEquals(response.getStatus(), JsonResponseStatus.SUCCESS); + verify(commentService).markCommentAsDeleted(post, comment); + } + + @Test + public void testDeleteCommentShouldReturnFailResponseIfPostNotFound() throws Exception { + when(postService.get(anyLong())).thenThrow(new NotFoundException()); + + JsonResponse response = controller.deleteComment(1L, 1L); + + assertEquals(response.getStatus(), JsonResponseStatus.FAIL); + assertTrue(response instanceof FailJsonResponse); + assertEquals(((FailJsonResponse)response).getReason(), JsonResponseReason.ENTITY_NOT_FOUND); + } + + @Test + public void testDeleteCommentShouldReturnFailResponseIfCommentNotFound() throws Exception { + when(commentService.getComment(anyLong())).thenThrow(new NotFoundException()); + + JsonResponse response = controller.deleteComment(1L, 1L); + + assertEquals(response.getStatus(), JsonResponseStatus.FAIL); + assertTrue(response instanceof FailJsonResponse); + assertEquals(((FailJsonResponse)response).getReason(), JsonResponseReason.ENTITY_NOT_FOUND); + } + + @Test + public void shouldBeImpossibleToAddAnswerIfAnswersLimitReached() throws Exception { + Topic topic = getTopicWithPosts(QuestionsAndAnswersController.LIMIT_OF_POSTS_VALUE + 1); + Model model = new ExtendedModelMap(); + PostDto postDto = new PostDto(); + postDto.setBodyText(answerContent); + + when(topicService.get(1L, QuestionsAndAnswersPlugin.TOPIC_TYPE)).thenReturn(topic); + when(result.hasErrors()).thenReturn(false); + + String methodResult = controller.create(1L, postDto, result, model, request); + + assertEquals(methodResult, QuestionsAndAnswersController.PLUGIN_VIEW_NAME); + verify(topicService, never()).replyToTopic(anyLong(), anyString(), anyLong()); + } + + @Test + public void shouldBeImpossibleToAddCommentIfCommentsLimitReached() throws Exception { + Post post = getPostWithNotRemovedComments(QuestionsAndAnswersController.LIMIT_OF_POSTS_VALUE + 1); + + when(result.hasErrors()).thenReturn(false); + when(postService.get(anyLong())).thenReturn(post); + + JsonResponse response = controller.addComment(new CommentDto(), result, request); + + assertEquals(response.getStatus(), JsonResponseStatus.FAIL); + verify(postService, never()).addComment(anyLong(), anyMap(), anyString()); + } + + @Test + public void canPostShouldReturnSuccessResponseIfLimitOfAnswersNotReached() throws Exception { + Topic topic = getTopicWithPosts(QuestionsAndAnswersController.LIMIT_OF_POSTS_VALUE); + + when(topicService.get(1L, QuestionsAndAnswersPlugin.TOPIC_TYPE)).thenReturn(topic); + + JsonResponse response = controller.canPost(1L); + + assertEquals(response.getStatus(), JsonResponseStatus.SUCCESS); + } + + @Test + public void canPostShouldReturnFailResponseIfLimitOfAnswersNotReached() throws Exception { + Topic topic = getTopicWithPosts(QuestionsAndAnswersController.LIMIT_OF_POSTS_VALUE + 1); + + when(topicService.get(1L, QuestionsAndAnswersPlugin.TOPIC_TYPE)).thenReturn(topic); + + JsonResponse response = controller.canPost(1L); + + assertEquals(response.getStatus(), JsonResponseStatus.FAIL); + } + + @Test(expectedExceptions = NotFoundException.class) + public void canPostShouldThrowExceptionIfTopicNotFound() throws Exception { + when(topicService.get(anyLong(), anyString())).thenThrow(new NotFoundException()); + + controller.canPost(1L); + } + + private Post getPostWithNotRemovedComments(int numberOfComments) { + Post post = new Post(null, null); + for (int i = 0; i < numberOfComments; i ++) { + post.addComment(new PostComment()); + } + return post; + } + + private Topic getTopicWithPosts(int numberOfPosts) { + Topic topic = new Topic(); + for (int i = 0; i < numberOfPosts; i ++) { + topic.addPost(new Post(null, null)); + } + return topic; + } + + private PostComment getComment() { + PostComment comment = new PostComment(); + comment.setAuthor(new JCUser("test", "example@test.com", "pwd")); + comment.setBody("test"); + comment.setId(1); + return comment; + } } diff --git a/jcommune-service/pom.xml b/jcommune-service/pom.xml index 0383240566..851cbe4715 100644 --- a/jcommune-service/pom.xml +++ b/jcommune-service/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>jcommune</artifactId> <groupId>org.jtalks.jcommune</groupId> - <version>2.14-SNAPSHOT</version> + <version>3.13-SNAPSHOT</version> </parent> <artifactId>jcommune-service</artifactId> <name>${project.artifactId}</name> @@ -51,10 +51,24 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>org.unitils</groupId> + <artifactId>unitils-core</artifactId> + <exclusions> + <exclusion> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> </dependency> + <dependency> + <groupId>io.qala.datagen</groupId> + <artifactId>qala-datagen</artifactId> + </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> @@ -68,12 +82,12 @@ <artifactId>spring-security-acl</artifactId> </dependency> <dependency> - <groupId>net.sf.ehcache</groupId> - <artifactId>ehcache-core</artifactId> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-web</artifactId> </dependency> <dependency> <groupId>net.sf.ehcache</groupId> - <artifactId>ehcache-jgroupsreplication</artifactId> + <artifactId>ehcache-core</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> @@ -96,8 +110,8 @@ <artifactId>commons-codec</artifactId> </dependency> <dependency> - <groupId>commons-lang</groupId> - <artifactId>commons-lang</artifactId> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.apache.velocity</groupId> @@ -120,12 +134,6 @@ <groupId>org.kefirsf</groupId> <artifactId>kefirbb</artifactId> </dependency> - <dependency> - <groupId>org.jtalks.common</groupId> - <artifactId>jtalks-common-security</artifactId> - </dependency> - - <dependency> <groupId>org.jclarion</groupId> <artifactId>image4j</artifactId> diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/Authenticator.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/Authenticator.java index 9cc67c4075..64e2399988 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/Authenticator.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/Authenticator.java @@ -15,8 +15,9 @@ package org.jtalks.jcommune.service; import org.jtalks.jcommune.model.dto.RegisterUserDto; -import org.jtalks.jcommune.plugin.api.exceptions.NoConnectionException; -import org.jtalks.jcommune.plugin.api.exceptions.UnexpectedErrorException; +import org.jtalks.jcommune.plugin.api.exceptions.*; +import org.jtalks.jcommune.service.exceptions.UserTriesActivatingAccountAgainException; +import org.jtalks.jcommune.service.util.AuthenticationStatus; import org.springframework.validation.BindingResult; import javax.servlet.http.HttpServletRequest; @@ -24,7 +25,7 @@ import org.jtalks.jcommune.model.dto.LoginUserDto; /** - * Serves for authentication and registration user. + * Serves for user authentication,registration and activation. * * @author Andrey Pogorelov */ @@ -36,12 +37,12 @@ public interface Authenticator { * @param loginUserDto DTO which represent information about user * @param request HTTP request * @param response HTTP response - * @return true if user was logged in. false if there were any errors during + * @return AAUTHENTICATED if user was logged in. AUTHENTICATION_FAIL if there were any errors during * logging in. * @throws UnexpectedErrorException if external service returns unexpected result * @throws NoConnectionException if we can't connect for any reason to external authentication service */ - public boolean authenticate(LoginUserDto loginUserDto, HttpServletRequest request, HttpServletResponse response) + public AuthenticationStatus authenticate(LoginUserDto loginUserDto, HttpServletRequest request, HttpServletResponse response) throws UnexpectedErrorException, NoConnectionException; /** @@ -55,5 +56,13 @@ public boolean authenticate(LoginUserDto loginUserDto, HttpServletRequest reques public BindingResult register(RegisterUserDto registerUserDto) throws UnexpectedErrorException, NoConnectionException; - + /** + * Activates user account based on uuid passed. + * We use UUID's to be sure activation link cannot be autogenerated from username. + * + * @param uuid unique entity identifier to locate user account + * @throws NotFoundException if there is no user matching username given + * @throws UserTriesActivatingAccountAgainException if user tries to activate account for the second time + */ + void activateAccount(String uuid) throws NotFoundException, UserTriesActivatingAccountAgainException; } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/GroupService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/GroupService.java new file mode 100644 index 0000000000..0e6af63ec9 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/GroupService.java @@ -0,0 +1,91 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.service; + +import org.jtalks.common.model.entity.Group; +import org.jtalks.common.service.EntityService; +import org.jtalks.common.service.exceptions.NotFoundException; +import org.jtalks.jcommune.model.dto.GroupAdministrationDto; +import org.jtalks.jcommune.model.dto.PageRequest; +import org.jtalks.jcommune.model.dto.SecurityGroupList; +import org.jtalks.jcommune.model.dto.UserDto; +import org.springframework.data.domain.Page; + +import java.util.List; + +/** + * Service for dealing with {@link Group} objects + * + * @author unascribed + */ +public interface GroupService extends EntityService<Group> { + + /** + * @return list of all {@link Group} objects + */ + List<Group> getAll(); + + /** + * @return {@link SecurityGroupList} with all {@link Group} objects + */ + SecurityGroupList getSecurityGroups(); + + /** + * @param name to look up + * @return list of groups which names contains given name + */ + List<Group> getByNameContains(String name); + + /** + * Returns list of group which name is equal ignoring case with given name + * @param name to look up + * @return list of groups which names exactly match the given name + */ + List<Group> getByName(String name); + + Page<UserDto> getPagedGroupUsers(long id, PageRequest pageRequest); + + /** + * Delete group + * + * @param group to be delete + * @throws IllegalArgumentException if group is null + * @throws NotFoundException if current user have no sid(not activated) + */ + void deleteGroup(Group group) throws NotFoundException; + + /** + * Save or update group. + * + * @param selectedGroup instance to save + * @throws IllegalArgumentException if group is null + */ + void saveGroup(Group selectedGroup); + + /** + * Save new group if id is null. + * Update group if id is not null. + * + * @param dto + * @throws NotFoundException + * @throws org.jtalks.common.validation.ValidationException in case of duplicate group name + */ + void saveOrUpdate(GroupAdministrationDto dto) throws NotFoundException; + + /** + * @return list of GroupAdministrationDto + */ + List<GroupAdministrationDto> getGroupNamesWithCountOfUsers(); +} \ No newline at end of file diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/PostCommentService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/PostCommentService.java index 43c22941d7..28eebfe418 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/PostCommentService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/PostCommentService.java @@ -14,6 +14,7 @@ */ package org.jtalks.jcommune.service; +import org.jtalks.jcommune.model.entity.Post; import org.jtalks.jcommune.model.entity.PostComment; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; @@ -34,4 +35,13 @@ public interface PostCommentService extends EntityService<PostComment> { */ PostComment updateComment(long id, String body, long branchId) throws NotFoundException; + /** + * Delete comment from post + * + * @param post post which contains comment + * @param comment comment to be delete + * + * @return comment marked as deleted + */ + public PostComment markCommentAsDeleted(Post post, PostComment comment); } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/PostService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/PostService.java index ad2dbba105..5b538c0169 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/PostService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/PostService.java @@ -124,4 +124,25 @@ public interface PostService extends EntityService<Post> { * @return post with vote */ Post vote(Post post, PostVote vote); + + /** + * Creates new or updates existence draft of current user with specified content in specified topic + * + * @param topic target topic for draft + * @param content content of draft + * + * @return newly created or updated draft + */ + PostDraft saveOrUpdateDraft(Topic topic, String content); + + /** + * Deletes draft with specified id. + * + * @param draftId id of draft to delete + * + * @throws NotFoundException if draft not found + * @throws org.springframework.security.access.AccessDeniedException if user who try to delete draft is no author of + * th draft + */ + void deleteDraft(Long draftId) throws NotFoundException; } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/PrivateMessageService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/PrivateMessageService.java index 679dffc001..f8bf0ee2f4 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/PrivateMessageService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/PrivateMessageService.java @@ -72,14 +72,12 @@ public interface PrivateMessageService extends EntityService<PrivateMessage> { * Save message as draft. If message exist it will be updated. * * @param id message id. - * @param recipient addressee. + * @param userTo receiver of the message * @param title the title of the message. * @param body the body of the message. * @param userFrom sender. - * @throws NotFoundException if the receiver does not exist. */ - void saveDraft(long id, String recipient, String title, String body, JCUser userFrom) - throws NotFoundException; + void saveDraft(long id, JCUser userTo, String title, String body, JCUser userFrom); /** * Get count of new messages for current user. diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/SpamProtectionService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/SpamProtectionService.java new file mode 100644 index 0000000000..963d52ef77 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/SpamProtectionService.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service; + +import org.jtalks.common.service.exceptions.NotFoundException; +import org.jtalks.jcommune.model.entity.SpamRule; + +import java.util.List; + +public interface SpamProtectionService extends EntityService<SpamRule>{ + boolean isEmailInBlackList(String email); + void saveOrUpdate(SpamRule rule) throws NotFoundException; + void deleteRule(long id); + List<SpamRule> getAllRules(); +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/SubscriptionService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/SubscriptionService.java index acd1747f11..0d5a6e5b19 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/SubscriptionService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/SubscriptionService.java @@ -80,4 +80,12 @@ public interface SubscriptionService { */ Collection<JCUser> getAllowedSubscribers(SubscriptionAwareEntity entity); + /** + * Subscribe subscription state for the {@link SubscriptionAwareEntity} given. + * Subscription will be applied to the current user logged in. + * + * @param entityToSubscribe object to subscribe or unsubscribe current user to + */ + void subscribe(SubscriptionAwareEntity entityToSubscribe); + } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/TopicDraftService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/TopicDraftService.java new file mode 100644 index 0000000000..7128cead33 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/TopicDraftService.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.service; + +import org.jtalks.jcommune.model.entity.TopicDraft; + +/** + * The interface to manipulate with draft topic of current user + * + * @author Dmitry S. Dolzhenko + */ +public interface TopicDraftService { + /** + * Returns the draft topic for current user. + * + * @return the draft topic or null + */ + TopicDraft getDraft(); + + /** + * Save or update the draft topic. + * + * @param draft the draft topic + */ + TopicDraft saveOrUpdateDraft(TopicDraft draft); + + /** + * Delete the draft topic. + */ + void deleteDraft(); +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/UserService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/UserService.java index 83ad9b5b37..673746feab 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/UserService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/UserService.java @@ -15,22 +15,24 @@ package org.jtalks.jcommune.service; import org.jtalks.common.model.entity.User; +import org.jtalks.jcommune.model.dto.LoginUserDto; +import org.jtalks.jcommune.model.dto.UserDto; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.Language; import org.jtalks.jcommune.model.entity.Post; import org.jtalks.jcommune.plugin.api.exceptions.NoConnectionException; +import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.exceptions.UnexpectedErrorException; import org.jtalks.jcommune.service.dto.UserInfoContainer; import org.jtalks.jcommune.service.dto.UserNotificationsContainer; import org.jtalks.jcommune.service.dto.UserSecurityContainer; import org.jtalks.jcommune.service.exceptions.MailingFailedException; -import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.exceptions.UserTriesActivatingAccountAgainException; +import org.jtalks.jcommune.service.util.AuthenticationStatus; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.List; -import org.jtalks.jcommune.model.dto.LoginUserDto; /** * This interface should have methods which give us more abilities in manipulating User persistent entity. @@ -113,17 +115,6 @@ JCUser saveEditedUserNotifications(long editedUserId, UserNotificationsContainer */ void restorePassword(String email) throws MailingFailedException; - /** - * Activates user account based on uuid passed. - * We use UUID's to be sure activation link cannot be autogenerated from username. - * - * @param uuid unique entity identifier to locate user account - * @throws NotFoundException if there is no user matching username given - * @throws UserTriesActivatingAccountAgainException - * if user tries to activate account for the second time - */ - void activateAccount(String uuid) throws NotFoundException, UserTriesActivatingAccountAgainException; - /** * Get user by UUID * @@ -179,10 +170,12 @@ JCUser saveEditedUserNotifications(long editedUserId, UserNotificationsContainer * @param loginUserDto DTO object which represent authentication information * @param request HTTP request * @param response HTTP response - * @return true if user was logged in. false if there were any errors during - * logging in. + * @return AuthenticationStatus.AUTHENTICATED if user's account + * is enabled and he was logged in. AuthenticationStatus.NOT_ENABLED if + * user's account was not activated. AuthenticationStatus.AUTHENTICATION_FAIL + * if there were any errors during logging in. */ - boolean loginUser(LoginUserDto loginUserDto, HttpServletRequest request, HttpServletResponse response) + AuthenticationStatus loginUser(LoginUserDto loginUserDto, HttpServletRequest request, HttpServletResponse response) throws UnexpectedErrorException, NoConnectionException; /** @@ -213,4 +206,20 @@ boolean loginUser(LoginUserDto loginUserDto, HttpServletRequest request, HttpSer */ void changeLanguage(JCUser jcUser, Language newLang); + /** + * Searches users with email or username matching specified key + * + * @param forumComponentId id of forum (for security checking) + * @param searchKey key to search users + * @return first 20 users with email or username matching key + */ + List<JCUser> findByUsernameOrEmail(long forumComponentId, String searchKey); + + List<UserDto> findByUsernameOrEmailNotInGroup(String pattern, long groupId, int count); + + List<Long> getUserGroupIDs(long forumComponentId, long userID) throws NotFoundException; + + void addUserToGroup(long forumId, long userID, long groupID) throws NotFoundException; + + void deleteUserFromGroup(long forumId, long userID, long groupID) throws NotFoundException; } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/bb2htmlprocessors/BBForeignLinksPostprocessor.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/bb2htmlprocessors/BBForeignLinksPostprocessor.java index 95150d5f61..f1a34c3046 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/bb2htmlprocessors/BBForeignLinksPostprocessor.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/bb2htmlprocessors/BBForeignLinksPostprocessor.java @@ -40,19 +40,18 @@ public class BBForeignLinksPostprocessor implements TextPostProcessor { private static final String URL_PATTERN = "(<a .*?href=(\"|').*?(\"|')|<img .*?src=(\"|').*?(\"|'))"; /** - * Process incoming text with adding prefix "/out" to foreign links. This prefix - * will be excluded from indexing by search engines (robots.txt) + * Process incoming text with adding attribute rel="nofollow" to foreign links. * * @return resultant text */ @Override public String postProcess(String bbDecodedText) { HttpServletRequest httpServletRequest = getServletRequest(); - return addPrefixToForeignLinks(bbDecodedText, httpServletRequest.getServerName()); + return addRelNoFollowToForeignLinks(bbDecodedText, httpServletRequest.getServerName()); } - private String addPrefixToForeignLinks(String decodedText, String serverName) { + private String addRelNoFollowToForeignLinks(String decodedText, String serverName) { Pattern linkPattern = Pattern.compile(URL_PATTERN, Pattern.DOTALL); Matcher linkMatcher = linkPattern.matcher(decodedText); String href; @@ -63,7 +62,7 @@ private String addPrefixToForeignLinks(String decodedText, String serverName) { if (!href.contains(serverName) && href.split("(http|ftp|https)://", 2).length == 2 && href.startsWith("<a")) { decodedText = decodedText.replace(href, - encoded.replaceFirst("<a.*href=\"", "<a rel=\"nofollow\" href=\"" + getHrefPrefix())); + encoded.replaceFirst("<a.*href=\"", "<a rel=\"nofollow\" href=\"")); } else if(href.startsWith("<a")){ decodedText = decodedText.replace(href, encoded.replaceFirst("<a.*href=\"", "<a href=\"")); @@ -87,14 +86,4 @@ protected HttpServletRequest getServletRequest() { return ((ServletRequestAttributes) attributes).getRequest(); } - /** - * Gets prefix to add href - * - * @return prefix - */ - @VisibleForTesting - protected String getHrefPrefix() { - return "/out?url="; - } - } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/bb2htmlprocessors/UrlToLinkConvertPostProcessor.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/bb2htmlprocessors/UrlToLinkConvertPostProcessor.java new file mode 100644 index 0000000000..6565114fcf --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/bb2htmlprocessors/UrlToLinkConvertPostProcessor.java @@ -0,0 +1,116 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.service.bb2htmlprocessors; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This text postprocessor searches URLs in text and wrap them in {@code <a>} tag to highlight. + * Links which are already inside {@code <a>, <img>} tag are skipped. + * Created by Alexey Usharovskiy on 25.12.16. + */ +public class UrlToLinkConvertPostProcessor implements TextPostProcessor { + + private static final Pattern htmlTagsToSkip = Pattern.compile( + "(<a.*?>.*?</a>|<img.*?>.*?</img>)", + Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + private static final Pattern urlInText = Pattern.compile( + "(\\b((?:https?|ftp|file):\\/\\/|www\\.|ftp\\.)[-A-ZА-Я0-9+&@#\\/%?=~_|!:,.;]*[-A-ZА-Я0-9+&@#\\/%=~_|])", + Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.UNICODE_CHARACTER_CLASS | Pattern.UNICODE_CASE); + private static final String htmlLinkTemplate = "<a href=\"%s\">%s</a>"; + + @Override + public String postProcess(String bbDecodedText) { + return processLinks(bbDecodedText); + } + + /** + * Search for text blocks outside {@code <a>, <img>} tags, + * pass them to URL process method and build resulting post text + * with highlighted URLs + * + * @param bbEncodedText bb encoded text to process + * @return processed text + */ + private static String processLinks(String bbEncodedText) { + StringBuilder stringBuilder = new StringBuilder(); + int prevPos = 0; + Matcher matcher = htmlTagsToSkip.matcher(bbEncodedText); + while (matcher.find()) { + String substring = bbEncodedText.substring(prevPos, matcher.start()); + if (!substring.isEmpty()) { + stringBuilder.append(processUrlInText(substring)); + } + stringBuilder.append(matcher.group()); + prevPos = matcher.end(); + } + if (prevPos == 0) { + return processUrlInText(bbEncodedText); + } + String substring = bbEncodedText.substring(prevPos, bbEncodedText.length()); + if (!substring.isEmpty()) { + stringBuilder.append(processUrlInText(substring)); + } + return stringBuilder.toString(); + } + + /** + * Search by URLs in textBlock and wrap them inside tags to highlight + * + * @param textBlock text for URL search and wrap + * @return text with wrapped URLs + */ + private static String processUrlInText(String textBlock) { + Matcher matcher = urlInText.matcher(textBlock); + StringBuilder stringBuilder = new StringBuilder(); + int prevPos = 0; + while (matcher.find()) { + stringBuilder.append(textBlock.substring(prevPos, matcher.start())); + stringBuilder.append(complementUrlWithProtocol(matcher.group(1), matcher.group(2))); + prevPos = matcher.end(); + } + if (prevPos == 0) { + return textBlock; + } + String substring = textBlock.substring(prevPos, textBlock.length()); + if (!substring.isEmpty()) { + stringBuilder.append(substring); + } + return stringBuilder.toString(); + } + + /** + * Complement URL with protocol if URL begin from www. or ftp. + * + * @param url URL text + * @param protocol protocol from URL + * @return URL which is completed with protocol if needs + */ + private static String complementUrlWithProtocol(String url, String protocol) { + String htmlLink; + switch (protocol) { + case "www.": + htmlLink = "http://" + url; + break; + case "ftp.": + htmlLink = "ftp://" + url; + break; + default: + htmlLink = url; + } + return String.format(htmlLinkTemplate, htmlLink, url); + } +} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/EntityToDtoConverter.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/dto/EntityToDtoConverter.java similarity index 81% rename from jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/EntityToDtoConverter.java rename to jcommune-service/src/main/java/org/jtalks/jcommune/service/dto/EntityToDtoConverter.java index ff5b559ff5..82ab465581 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/EntityToDtoConverter.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/dto/EntityToDtoConverter.java @@ -12,15 +12,17 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.dto; +package org.jtalks.jcommune.service.dto; import org.jtalks.jcommune.model.dto.PageRequest; +import org.jtalks.jcommune.model.entity.Post; import org.jtalks.jcommune.model.entity.Topic; import org.jtalks.jcommune.plugin.api.PluginLoader; import org.jtalks.jcommune.plugin.api.core.Plugin; import org.jtalks.jcommune.plugin.api.core.TopicPlugin; import org.jtalks.jcommune.plugin.api.filters.StateFilter; import org.jtalks.jcommune.plugin.api.filters.TypeFilter; +import org.jtalks.jcommune.plugin.api.web.dto.PostDto; import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -36,10 +38,13 @@ public class EntityToDtoConverter { public static final String PREFIX = "/topics/"; - public static final String PLUGABLE_UNREAD = "/icon/unread.png"; - public static final String PLUGABLE_READ = "/icon/read.png"; - public static final String PLUGABLE_CLOSED_UNREAD = "/icon/closed_unread.png"; - public static final String PLUGABLE_CLOSED_READ = "/icon/closed_unread.png"; + public static final String PLUGABLE_UNREAD = "/resources/icon/unread.png"; + public static final String PLUGABLE_READ = "/resources/icon/read.png"; + /** + * Now we have no icons for closed pluggable topics and will use same as for open topics + */ + public static final String PLUGABLE_CLOSED_UNREAD = PLUGABLE_UNREAD; + public static final String PLUGABLE_CLOSED_READ = PLUGABLE_READ; public static final String CODE_REVIEW_NEW_POSTS = "/resources/images/code-review-new-posts.png"; public static final String CODE_REVIEW_NO_NEW_POSTS = "/resources/images/code-review-no-new-posts.png"; public static final String DISCUSSION_CLOSED_NEW_POSTS = "/resources/images/closed-new-posts.png"; @@ -60,7 +65,7 @@ public EntityToDtoConverter(PluginLoader pluginLoader) { * @param source page of {@link Topic} * @return page of {@link TopicDto} */ - public Page<TopicDto> convertToDtoPage(Page<Topic> source) { + public Page<TopicDto> convertTopicPageToTopicDtoPage(Page<Topic> source) { List<Plugin> plugins = pluginLoader.getPlugins(new TypeFilter(TopicPlugin.class), new StateFilter(Plugin.State.ENABLED)); List<TopicDto> dtos = new ArrayList<>(); @@ -70,6 +75,25 @@ public Page<TopicDto> convertToDtoPage(Page<Topic> source) { return new PageImpl<>(dtos, PageRequest.fetchFromPage(source), source.getTotalElements()); } + /** + * Converts page of {@link Post} to page of {@link PostDto} + * + * @param source page of {@link Post} + * + * @return page of {@link PostDto} + */ + public Page<PostDto> convertPostPageToPostDtoPage(Page<Post> source) { + List<Plugin> plugins = pluginLoader.getPlugins(new TypeFilter(TopicPlugin.class), + new StateFilter(Plugin.State.ENABLED)); + List<PostDto> dtos = new ArrayList<>(); + for (Post post : source) { + PostDto dto = PostDto.getDtoFor(post); + dto.setTopicDto(createTopicDto(post.getTopic(), plugins)); + dtos.add(dto); + } + return new PageImpl<>(dtos, PageRequest.fetchFromPage(source), source.getTotalElements()); + } + /** * Converts {@link Topic} to {@link TopicDto} * diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/exceptions/OperationIsNotAllowedException.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/exceptions/OperationIsNotAllowedException.java new file mode 100644 index 0000000000..0352b5a50b --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/exceptions/OperationIsNotAllowedException.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.service.exceptions; + +/** + * Thrown in the case of not allowed or not possible operation, + * like modification of pre-defined user group. + * + * @author Pavel Vervenko + */ +public class OperationIsNotAllowedException extends RuntimeException { + + public OperationIsNotAllowedException(String message) { + super(message); + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/BBCodeService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/BBCodeService.java index cd4877df45..6b596107f2 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/BBCodeService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/BBCodeService.java @@ -14,6 +14,7 @@ */ package org.jtalks.jcommune.service.nontransactional; +import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.plugin.api.service.PluginBbCodeService; @@ -73,13 +74,26 @@ public String convertBbToHtml(String bbEncodedText) { for (TextProcessor preprocessor : preprocessors) { bbEncodedText = preprocessor.process(bbEncodedText); } - bbEncodedText = processor.process(bbEncodedText); + if (isUserbbCode(bbEncodedText)){ + bbEncodedText = processUserbbCode(bbEncodedText); + } else { + bbEncodedText = processor.process(bbEncodedText); + } for (TextPostProcessor postpreprocessor : postprocessors) { bbEncodedText = postpreprocessor.postProcess(bbEncodedText); } return bbEncodedText; } + /** + * if it's [user]-tag processing, use method + * "processUserbbCode" + * @param bbEncodedText line with bb-codes + */ + private boolean isUserbbCode(String bbEncodedText) { + return bbEncodedText.startsWith("[user=/") && bbEncodedText.endsWith("[/user]"); + } + /** @param preprocessors objects that process input text from users post before the actual bb-converting is * started */ public void setPreprocessors(List<TextProcessor> preprocessors) { @@ -105,4 +119,24 @@ public void setPostprocessors(List<TextPostProcessor> postprocessors) { public String stripBBCodes(String bbCode) { return stripBBCodesProcessor.process(bbCode); } + + /** + * In case when bb code contains tag [user], text processing execute with help + * this method because KefirrBB does not work properly + * with eg "[user=/jcommune/users/16][user]user[/user][/user]" i.e user name contains tag [user] + * @param bbEncodedText line with nested [user] bb-codes + * @return formatted text with link + */ + private String processUserbbCode(String bbEncodedText) { + String openTag = "<a href=\""; + String closeTag = "</a>"; + String classInfo = "\" class=\"mentioned-user\" >"; + + String[] array = StringUtils.substringsBetween(bbEncodedText, "[", "]"); + String userLink = array[0]; + String userName = bbEncodedText.substring(userLink.length() + 2, bbEncodedText.length() - 7); + String result = openTag + userLink.substring(5, userLink.length()) + classInfo + userName + closeTag; + + return result; + } } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/LocationService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/LocationService.java index 8f4ada7836..40e2f2f1a0 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/LocationService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/LocationService.java @@ -16,9 +16,9 @@ package org.jtalks.jcommune.service.nontransactional; import org.jtalks.common.model.entity.Entity; -import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.UserInfo; import org.jtalks.jcommune.plugin.api.service.PluginLocationService; -import org.jtalks.jcommune.service.UserService; +import org.jtalks.jcommune.service.security.SecurityService; import org.springframework.security.core.session.SessionRegistry; import org.springframework.stereotype.Component; @@ -33,18 +33,17 @@ * * @author Andrey Kluev */ -@Component public class LocationService implements PluginLocationService { - private UserService userService; - private SessionRegistry sessionRegistry; - private Map<JCUser, String> registerUserMap = new ConcurrentHashMap<>(); + private final SecurityService securityService; + private final SessionRegistry sessionRegistry; + private final Map<UserInfo, String> registerUserMap = new ConcurrentHashMap<>(); /** - * @param userService to figure out the current user + * @param securityService for retrieving basic info about current user * @param sessionRegistry session registry to get all the users logged in */ - public LocationService(UserService userService, SessionRegistry sessionRegistry) { - this.userService = userService; + public LocationService(SecurityService securityService, SessionRegistry sessionRegistry) { + this.securityService = securityService; this.sessionRegistry = sessionRegistry; } @@ -57,18 +56,18 @@ public LocationService(UserService userService, SessionRegistry sessionRegistry) * @return Users, who're viewing the page for entity passed. Will return empty list if * there are no viewers or view tracking is not supported for this entity type */ - public List<JCUser> getUsersViewing(Entity entity) { - List<JCUser> viewList = new ArrayList<>(); - JCUser currentUser = userService.getCurrentUser(); - /** + public List<UserInfo> getUsersViewing(Entity entity) { + List<UserInfo> viewList = new ArrayList<>(); + UserInfo currentUser = securityService.getCurrentUserBasicInfo(); + /* * This condition does not allow Anonymous add to the map of active users. */ - if (!currentUser.isAnonymous()) { + if (currentUser != null) { registerUserMap.put(currentUser, entity.getUuid()); } for (Object o : sessionRegistry.getAllPrincipals()) { - JCUser user = (JCUser) o; + UserInfo user = (UserInfo) o; if (entity.getUuid().equals(registerUserMap.get(user))) { viewList.add(user); } @@ -82,6 +81,19 @@ public List<JCUser> getUsersViewing(Entity entity) { * topic/branch viewer's list until explicitly added */ public void clearUserLocation() { - registerUserMap.remove(userService.getCurrentUser()); + UserInfo currentUser = securityService.getCurrentUserBasicInfo(); + clearUserLocation(currentUser); + } + + /** + * Clears forum location for the given user. After the call, user + * passed as param will be excluded from all the topic/branch viewer's list + * + * @param user to clean location. + */ + public void clearUserLocation(Object user) { + if (user instanceof UserInfo) { + registerUserMap.remove(user); + } } } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/MailService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/MailService.java index 258467cc8b..babad6c83c 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/MailService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/MailService.java @@ -18,6 +18,7 @@ import org.apache.velocity.tools.generic.EscapeTool; import org.jtalks.common.model.entity.Entity; import org.jtalks.jcommune.model.entity.*; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.jtalks.jcommune.service.exceptions.MailingFailedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,6 +74,7 @@ public class MailService { private final MessageSource messageSource; private final JCommuneProperty notificationsEnabledProperty; private final EscapeTool escapeTool; + private final EntityToDtoConverter converter; /** * Creates a mailing service with a default template message autowired. @@ -92,13 +94,15 @@ public MailService(JavaMailSender sender, VelocityEngine engine, MessageSource source, JCommuneProperty notificationsEnabledProperty, - EscapeTool escapeTool) { + EscapeTool escapeTool, + EntityToDtoConverter converter) { this.mailSender = sender; this.from = from; this.velocityEngine = engine; this.messageSource = source; this.notificationsEnabledProperty = notificationsEnabledProperty; this.escapeTool = escapeTool; + this.converter = converter; } /** @@ -136,15 +140,15 @@ public void sendPasswordRecoveryMail(JCUser user, String newPassword) throws Mai */ public void sendUpdatesOnSubscription(JCUser recipient, SubscriptionAwareEntity entity) { try { - String urlSuffix = entity.prepareUrlSuffix(); + String urlSuffix = entity.getUrlSuffix(); String url = this.getDeploymentRootUrl() + urlSuffix; Locale locale = recipient.getLanguage().getLocale(); Map<String, Object> model = new HashMap<>(); model.put(LINK, url); model.put(LINK_LABEL, getDeploymentRootUrlWithoutPort() + urlSuffix); - if (entity instanceof Branch) { - model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl() + getUnsubscribeBranchLink(entity)); - } + model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl() + + entity.getUnsubscribeLinkForSubscribersOf(entity.getClass())); + sendEmailOnForumUpdates(recipient, model, locale, (Entity) entity, "subscriptionNotification.subject", "subscriptionNotification.vm"); } catch (MailingFailedException e) { @@ -218,30 +222,6 @@ public void sendAccountActivationMail(JCUser recipient) { } } - /** - * Sends email to topic starter that his or her topic was moved - * - * @param recipient user to send notification - * @param topic relocated topic - */ - public void sendTopicMovedMail(JCUser recipient, Topic topic) { - String urlSuffix = "/topics/" + topic.getId(); - String url = this.getDeploymentRootUrl() + urlSuffix; - Locale locale = recipient.getLanguage().getLocale(); - Map<String, Object> model = new HashMap<>(); - model.put(NAME, recipient.getUsername()); - model.put(LINK, url); - model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl() + getUnsubscribeBranchLink(topic.getBranch())); - model.put(LINK_LABEL, getDeploymentRootUrlWithoutPort() + urlSuffix); - model.put(RECIPIENT_LOCALE, locale); - try { - this.sendEmail(recipient.getEmail(), messageSource.getMessage("moveTopic.subject", - new Object[]{}, locale), model, "moveTopic.vm"); - } catch (MailingFailedException e) { - LOGGER.error("Failed to sent activation mail for user: " + recipient.getUsername()); - } - } - /** * Sends email to topic starter that his or her topic was moved * @@ -249,15 +229,17 @@ public void sendTopicMovedMail(JCUser recipient, Topic topic) { * @param topic relocated topic * @param curUser User that moved topic */ - public void sendTopicMovedMail(JCUser recipient, Topic topic, String curUser) { - String urlSuffix = "/topics/" + topic.getId(); + public <T extends SubscriptionAwareEntity> void sendTopicMovedMail( + JCUser recipient, Topic topic, String curUser, Class<T> subsсriptionTargetClass) { + String urlSuffix = getTopicUrlSuffix(topic); String url = this.getDeploymentRootUrl() + urlSuffix; Locale locale = recipient.getLanguage().getLocale(); Map<String, Object> model = new HashMap<>(); model.put(NAME, recipient.getUsername()); model.put(CUR_USER, curUser); model.put(LINK, url); - model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl() + getUnsubscribeBranchLink(topic.getBranch())); + model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl() + + topic.getUnsubscribeLinkForSubscribersOf(subsсriptionTargetClass)); model.put(LINK_LABEL, getDeploymentRootUrlWithoutPort() + urlSuffix); model.put(RECIPIENT_LOCALE, locale); try { @@ -399,44 +381,14 @@ private String getTitleName(Entity entity) { } else if (entity instanceof Branch) { Branch branch = (Branch) entity; return ": " + branch.getName(); + } else if (entity instanceof Post) { + Post post = (Post) entity; + return ": " + post.getTopic().getTitle(); } else { return ""; } } - /** - * Set mail about removing topic. - * - * @param recipient Recipient for which send notification - * @param topic Current topic - */ - public void sendRemovingTopicMail(JCUser recipient, Topic topic) { - Locale locale = recipient.getLanguage().getLocale(); - Map<String, Object> model = new HashMap<>(); - model.put(USER, recipient); - model.put(RECIPIENT_LOCALE, locale); - model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl() + getUnsubscribeBranchLink(topic.getBranch())); - model.put(TOPIC, topic); - - try { - - String subjectTemplate = REMOVE_TOPIC_SUBJECT_TEMPLATE; - String messageBodyTemplate = REMOVE_TOPIC_MESSAGE_BODY_TEMPLATE; - - if (topic.isCodeReview()) { - subjectTemplate = REMOVE_CODE_REVIEW_SUBJECT_TEMPLATE; - messageBodyTemplate = REMOVE_CODE_REVIEW_MESSAGE_BODY_TEMPLATE; - } - - String subject = messageSource.getMessage(subjectTemplate, new Object[]{}, locale); - this.sendEmail(recipient.getEmail(), subject, model, messageBodyTemplate); - - } catch (MailingFailedException e) { - LOGGER.error("Failed to sent mail about removing topic or code review for user: " - + recipient.getUsername()); - } - } - /** * Set mail about removing topic. * @@ -450,7 +402,8 @@ public void sendRemovingTopicMail(JCUser recipient, Topic topic, String curUser) model.put(USER, recipient); model.put(RECIPIENT_LOCALE, locale); model.put(CUR_USER, curUser); - model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl() + getUnsubscribeBranchLink(topic.getBranch())); + //Topic not exist more and user not subscribed to branch, so simply redirect to branch + model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl() + "/branches/" + topic.getBranch().getId()); model.put(TOPIC, topic); try { @@ -480,13 +433,13 @@ public void sendRemovingTopicMail(JCUser recipient, Topic topic, String curUser) */ void sendTopicCreationMail(JCUser subscriber, Topic topic) { try { - String urlSuffix = "/topics/" + topic.getId(); + String urlSuffix = getTopicUrlSuffix(topic); String url = this.getDeploymentRootUrl() + urlSuffix; Locale locale = subscriber.getLanguage().getLocale(); Map<String, Object> model = new HashMap<>(); model.put(LINK, url); model.put(LINK_UNSUBSCRIBE, this.getDeploymentRootUrl() - + getUnsubscribeBranchLink(topic.getBranch())); + + topic.getBranch().getUnsubscribeLinkForSubscribersOf(Branch.class)); model.put(LINK_LABEL, getDeploymentRootUrlWithoutPort() + urlSuffix); sendEmailOnForumUpdates(subscriber, model, locale, topic.getBranch(), "subscriptionNotification.subject", "branchSubscriptionNotification.vm"); @@ -495,17 +448,14 @@ void sendTopicCreationMail(JCUser subscriber, Topic topic) { } } - private String getUnsubscribeBranchLink(SubscriptionAwareEntity entity) { - String result = "/branches/{0}/unsubscribe"; - if (entity instanceof Branch) { - return result.replace("{0}", "" + ((Branch) entity).getId()); - } - if (entity instanceof Topic) { - return result.replace("{0}", "" + ((Topic) entity).getBranch().getId()); - } - if (entity instanceof Post) { - return result.replace("{0}", "" + ((Post) entity).getTopic().getBranch().getId()); - } - return null; + /** + * Gets url suffix of specified topic. Urls of topics provided by plugins can differ + * + * @param topic topic to get url + * + * @return url of specified topic + */ + private String getTopicUrlSuffix(Topic topic) { + return converter.convertTopicToDto(topic).getTopicUrl(); } } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/MentionedUsers.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/MentionedUsers.java index 32797780f4..c7e04418b9 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/MentionedUsers.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/MentionedUsers.java @@ -26,9 +26,6 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; import javax.servlet.http.HttpServletRequest; import java.util.*; import java.util.regex.Matcher; @@ -51,9 +48,9 @@ public class MentionedUsers { public static final String USER_WITH_LINK_TO_PROFILE_TEMPLATE = "[user=%s]%s[/user]"; private static final Logger LOGGER = LoggerFactory.getLogger(MentionedUsers.class); private static final Pattern ALL_MENTIONED_USERS_PATTERN = - Pattern.compile("\\[user\\].*?\\[/user\\]|\\[user notified=true\\].*?\\[/user\\]"); + Pattern.compile("\\[user\\].+?(\\[/user\\])+|\\[user notified=true\\].+?(\\[/user\\])+"); private static final Pattern MENTIONED_AND_NOT_NOTIFIED_USERS_PATTERN = - Pattern.compile("\\[user\\].*?\\[/user\\]"); + Pattern.compile("\\[user\\].+?(\\[/user\\])+"); private static final String CLOSE_BRACKET_CODE_PLACEHOLDER = "@w0956756wo@"; private static final String OPEN_BRACKET_CODE_PLACEHOLDER = "@ywdffgg434y@"; private static final String SLASH_CODE_PLACEHOLDER = "14@123435vggv4f"; @@ -188,60 +185,32 @@ private Set<String> extractMentionedUsers(String canContainMentionedUsers, Patte Set<String> mentionedUsernames = new HashSet<>(); while (matcher.find()) { String userBBCode = matcher.group(); - String mentionedUser = userBBCode.replaceAll("\\[.*?\\]", StringUtils.EMPTY); - mentionedUsernames.add(decodeUsername(mentionedUser)); + String mentionedUser; + if (userBBCode.contains("[user notified=true]")) { + mentionedUser = StringUtils.substring(userBBCode, 20, userBBCode.length() - 7); + } else { + mentionedUser = StringUtils.substring(userBBCode, 6, userBBCode.length() - 7); + } + mentionedUsernames.add(replacePlaceholdersWithChars(mentionedUser)); } return mentionedUsernames; } return Collections.emptySet(); } - private String decodeUsername(String encodedUsername) { - String decodeUserName = encodedUsername; - - Object jsDecodedName = invokeJavaScript("decodeURI", encodedUsername.replace("\\", "\\\\")); - if (jsDecodedName != null) { - decodeUserName = jsDecodedName.toString(); - } - + private String replacePlaceholdersWithChars(String userNameWithPlaceholders) { + String formattedUserName = userNameWithPlaceholders; for (Map.Entry<String, String> decodeEntry : CHARS_PLACEHOLDERS.entrySet()) { - decodeUserName = decodeUserName.replace(decodeEntry.getValue(), decodeEntry.getKey()); + formattedUserName = formattedUserName.replace(decodeEntry.getValue(), decodeEntry.getKey()); } - encodedUserNames.put(decodeUserName, encodedUsername); - return decodeUserName; + encodedUserNames.put(formattedUserName, userNameWithPlaceholders); + return formattedUserName; } private String encodeUsername(String decodedUsername) { return encodedUserNames.get(decodedUsername); } - /** - * Invokes JavaScript function via built-in JavaScript engine - * - * @param functionName name of the function to be invoked - * @param arguments arguments of the function joined in one string - * @return result of the invocation or null if some error happened - */ - private Object invokeJavaScript(String functionName, String arguments) { - Object result = null; - - ScriptEngineManager factory = new ScriptEngineManager(); - ScriptEngine engine = factory.getEngineByName("JavaScript"); - - if (engine == null) { - LOGGER.error("JavaScript engige was not found."); - return result; - } - - try { - result = engine.eval(functionName + "('" + arguments + "')").toString(); - } catch (ScriptException e) { - LOGGER.error("Error while invoking JavaScript function.", e); - } - - return result; - } - /** * Gets list of users which should be notified * @@ -269,7 +238,7 @@ private List<JCUser> getNewUsersToNotify(Set<String> mentionedUsernames, UserDao * @return true if we need to send notification and false otherwise */ private boolean shouldNotificationBeSent(JCUser mentionedUser) { - boolean isOtherNotificationAlreadySent = post.getTopicSubscribers().contains(mentionedUser); + boolean isOtherNotificationAlreadySent = post.getSubscribers().contains(mentionedUser); return !isOtherNotificationAlreadySent && mentionedUser.isMentioningNotificationsEnabled(); } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/NotificationService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/NotificationService.java index e94ef39f5f..f5f4023c8c 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/NotificationService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/nontransactional/NotificationService.java @@ -14,15 +14,20 @@ */ package org.jtalks.jcommune.service.nontransactional; +import org.jtalks.jcommune.model.entity.Branch; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.SubscriptionAwareEntity; import org.jtalks.jcommune.model.entity.Topic; +import org.jtalks.jcommune.plugin.api.PluginLoader; +import org.jtalks.jcommune.plugin.api.core.Plugin; +import org.jtalks.jcommune.plugin.api.core.TopicPlugin; +import org.jtalks.jcommune.plugin.api.filters.StateFilter; +import org.jtalks.jcommune.plugin.api.filters.TypeFilter; import org.jtalks.jcommune.service.SubscriptionService; import org.jtalks.jcommune.service.UserService; import java.util.Collection; -import java.util.HashSet; -import java.util.Set; +import java.util.List; /** * Send email notifications to the users subscribed. @@ -41,19 +46,23 @@ public class NotificationService { SubscriptionService subscriptionService; private UserService userService; private MailService mailService; + private final PluginLoader pluginLoader; /** * @param userService to determine the update author * @param mailService to perform actual email notifications * @param subscriptionService to get the subscribers of the entity + * @param pluginLoader to get different subscribers for plugable topics */ public NotificationService( UserService userService, MailService mailService, - SubscriptionService subscriptionService) { + SubscriptionService subscriptionService, + PluginLoader pluginLoader) { this.userService = userService; this.mailService = mailService; this.subscriptionService = subscriptionService; + this.pluginLoader = pluginLoader; } /** @@ -65,30 +74,13 @@ public NotificationService( */ public void subscribedEntityChanged(SubscriptionAwareEntity entity) { Collection<JCUser> subscribers = subscriptionService.getAllowedSubscribers(entity); - subscribers.remove(userService.getCurrentUser()); + filterSubscribers(subscribers, entity); for (JCUser user : subscribers) { mailService.sendUpdatesOnSubscription(user, entity); } } - /** - * Overload for skipping topic subscribers - * - * @param entity - * @param topicSubscribers - */ - public void subscribedEntityChanged(SubscriptionAwareEntity entity, Collection<JCUser> topicSubscribers) { - Collection<JCUser> subscribers = subscriptionService.getAllowedSubscribers(entity); - subscribers.remove(userService.getCurrentUser()); - - for (JCUser user : subscribers) { - if (!topicSubscribers.contains(user)) { - mailService.sendUpdatesOnSubscription(user, entity); - } - } - } - /** * Notifies topic starter by email that his or her topic * was moved to another sections and also notifies all branch @@ -101,29 +93,38 @@ public void sendNotificationAboutTopicMoved(Topic topic) { //send notification to topic subscribers Collection<JCUser> topicSubscribers = subscriptionService.getAllowedSubscribers(topic); - this.filterSubscribers(topicSubscribers); + this.filterSubscribers(topicSubscribers, topic); for (JCUser subscriber : topicSubscribers) { - mailService.sendTopicMovedMail(subscriber, topic, curUser); + mailService.sendTopicMovedMail(subscriber, topic, curUser, Topic.class); } //send notification to branch subscribers - Set<JCUser> branchSubscribers = new HashSet<>(topic.getBranch().getSubscribers()); - - this.filterSubscribers(branchSubscribers); + Collection<JCUser> branchSubscribers = subscriptionService.getAllowedSubscribers(topic.getBranch()); + branchSubscribers.removeAll(topicSubscribers); + this.filterSubscribers(branchSubscribers, topic.getBranch()); for (JCUser subscriber : branchSubscribers) { if (!topicSubscribers.contains(subscriber)) { - mailService.sendTopicMovedMail(subscriber, topic, curUser); + mailService.sendTopicMovedMail(subscriber, topic, curUser, Branch.class); } } } /** - * Filter collection - remove current user from subscribers + * Filter collection - remove current user from subscribers and performs plugin filtering * * @param subscribers collection of subscribers + * @see org.jtalks.jcommune.plugin.api.core.SubscribersFilter */ - private void filterSubscribers(Collection<JCUser> subscribers) { + private void filterSubscribers(Collection<JCUser> subscribers, SubscriptionAwareEntity entity) { + List<Plugin> plugins = pluginLoader.getPlugins(new StateFilter(Plugin.State.ENABLED), + new TypeFilter(TopicPlugin.class)); + for (Plugin plugin : plugins) { + TopicPlugin topicPlugin = (TopicPlugin)plugin; + topicPlugin.getSubscribersFilter().filter(subscribers, entity); + } + // Current user should be removed after filtering by plugin because filter don't know anything + // about current user subscribers.remove(userService.getCurrentUser()); } @@ -131,11 +132,14 @@ private void filterSubscribers(Collection<JCUser> subscribers) { * Send notification to subscribers about removing topic or code review. * * @param topic Current topic - * @param subscribers Collection of subscribers */ - public void sendNotificationAboutRemovingTopic(Topic topic, Collection<JCUser> subscribers) { + public void sendNotificationAboutRemovingTopic(Topic topic) { + subscribedEntityChanged(topic.getBranch()); + Collection<JCUser> subscribers = subscriptionService.getAllowedSubscribers(topic); + Collection<JCUser> branchSubscribers = subscriptionService.getAllowedSubscribers(topic.getBranch()); + subscribers.removeAll(branchSubscribers); String curUser = userService.getCurrentUser().getUsername(); - this.filterSubscribers(subscribers); + this.filterSubscribers(subscribers, topic); for (JCUser subscriber : subscribers) { mailService.sendRemovingTopicMail(subscriber, topic, curUser); } @@ -148,7 +152,7 @@ public void sendNotificationAboutRemovingTopic(Topic topic, Collection<JCUser> s */ public void sendNotificationAboutTopicCreated(Topic topic) { Collection<JCUser> branchSubscribers = subscriptionService.getAllowedSubscribers(topic.getBranch()); - this.filterSubscribers(branchSubscribers); + this.filterSubscribers(branchSubscribers, topic); for (JCUser subscriber : branchSubscribers) { mailService.sendTopicCreationMail(subscriber, topic); } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AdministrationGroup.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AdministrationGroup.java index c1ea5dbd0d..611cda5a25 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AdministrationGroup.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AdministrationGroup.java @@ -14,6 +14,9 @@ */ package org.jtalks.jcommune.service.security; +import java.util.ArrayList; +import java.util.List; + /** * Elements contains IDs predefined user groups. * More information see in V21__Add_predefined_groups.sql migration. @@ -36,6 +39,14 @@ public enum AdministrationGroup { private String name; + public static final List<String> PREDEFINED_GROUP_NAMES = new ArrayList<>(); + + static { + for (AdministrationGroup group : values()) { + PREDEFINED_GROUP_NAMES.add(group.getName()); + } + } + /** * @param name group database name, hardcoded in initial SQL migrations */ @@ -49,4 +60,11 @@ private AdministrationGroup(String name) { public String getName() { return name; } + + /** + * @return true if provided name is the one from pre-defined groups + */ + public static boolean isPredefinedGroup(String groupName) { + return PREDEFINED_GROUP_NAMES.contains(groupName); + } } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/JCPermissionFactory.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/JCPermissionFactory.java index bbcc822162..b5d652d4ac 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/JCPermissionFactory.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/JCPermissionFactory.java @@ -15,7 +15,6 @@ package org.jtalks.jcommune.service.security; import org.jtalks.common.model.permissions.JtalksPermission; -import org.jtalks.common.security.acl.JtalksPermissionFactory; import org.jtalks.jcommune.plugin.api.PluginsPermissionFactory; import org.springframework.security.acls.domain.PermissionFactory; import org.springframework.security.acls.model.Permission; diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/JtalksPermissionFactory.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/JtalksPermissionFactory.java new file mode 100644 index 0000000000..bed519ef1c --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/JtalksPermissionFactory.java @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security; + +import org.jtalks.common.model.permissions.BranchPermission; +import org.jtalks.common.model.permissions.GeneralPermission; +import org.jtalks.common.model.permissions.JtalksPermission; +import org.jtalks.common.model.permissions.ProfilePermission; +import org.springframework.security.acls.domain.PermissionFactory; +import org.springframework.security.acls.model.Permission; + +import java.util.*; + +/** + * Can return {@link JtalksPermission}s by its name or mask. Use {@link #init()} method to initialize the class. + * Internally it contains a list of permissions which it loads from classes like {@link BranchPermission}, so if you + * need to add extra permissions, you should change this class to include them. See {@link #init()} method for these + * purposes. + * + * @author stanislav bashkirtsev + */ +public class JtalksPermissionFactory implements PermissionFactory { + private final Map<Integer, JtalksPermission> permissionsByMask = new HashMap<Integer, JtalksPermission>(); + private final Map<String, JtalksPermission> permissionsByName = new HashMap<String, JtalksPermission>(); + + /** + * {@inheritDoc} + */ + @Override + public Permission buildFromMask(int mask) { + return permissionsByMask.get(mask); + } + + /** + * {@inheritDoc} + */ + @Override + public Permission buildFromName(String name) { + return permissionsByName.get(name); + } + + /** + * {@inheritDoc} + */ + @Override + public List<Permission> buildFromNames(List<String> names) { + List<Permission> resultingPermissions = new ArrayList<Permission>(); + for (String name : names) { + resultingPermissions.add(buildFromName(name)); + } + return resultingPermissions; + } + + /** + * Initializes the class by loading lists of the permissions from classes like {@link BranchPermission}. + * + * @return this + */ + public JtalksPermissionFactory init() { + List<JtalksPermission> permissions = new LinkedList<JtalksPermission>(); + permissions.addAll(BranchPermission.getAllAsList()); + permissions.addAll(GeneralPermission.getAllAsList()); + permissions.addAll(ProfilePermission.getAllAsList()); + for (JtalksPermission permission : permissions) { + permissionsByMask.put(permission.getMask(), permission); + permissionsByName.put(permission.getName(), permission); + } + return this; + } + + /** + * Returns a view of the list of all available permissions. + * + * @return a view of the list of all available permissions + */ + public Collection<? extends JtalksPermission> getAllPermissions() { + return permissionsByMask.values(); + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/ObjectIdentityRetrievalStrategyImpl.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/ObjectIdentityRetrievalStrategyImpl.java index c1101670f5..05169beb9e 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/ObjectIdentityRetrievalStrategyImpl.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/ObjectIdentityRetrievalStrategyImpl.java @@ -15,7 +15,7 @@ package org.jtalks.jcommune.service.security; import org.jtalks.common.model.entity.Entity; -import org.jtalks.common.security.acl.AclUtil; +import org.jtalks.jcommune.service.security.acl.AclUtil; import org.springframework.security.acls.model.ObjectIdentity; import org.springframework.security.acls.model.ObjectIdentityRetrievalStrategy; diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/PermissionManager.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/PermissionManager.java index 2a75e8867b..030549da85 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/PermissionManager.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/PermissionManager.java @@ -24,17 +24,17 @@ import org.jtalks.common.model.permissions.GeneralPermission; import org.jtalks.common.model.permissions.JtalksPermission; import org.jtalks.common.model.permissions.ProfilePermission; -import org.jtalks.common.security.acl.AclManager; -import org.jtalks.common.security.acl.AclUtil; -import org.jtalks.common.security.acl.GroupAce; -import org.jtalks.common.security.acl.builders.AclBuilders; -import org.jtalks.common.security.acl.sids.UniversalSid; -import org.jtalks.common.security.acl.sids.UserSid; import org.jtalks.jcommune.model.dao.GroupDao; import org.jtalks.jcommune.model.dto.GroupsPermissions; import org.jtalks.jcommune.model.dto.PermissionChanges; import org.jtalks.jcommune.model.entity.AnonymousGroup; import org.jtalks.jcommune.plugin.api.PluginPermissionManager; +import org.jtalks.jcommune.service.security.acl.AclManager; +import org.jtalks.jcommune.service.security.acl.AclUtil; +import org.jtalks.jcommune.service.security.acl.GroupAce; +import org.jtalks.jcommune.service.security.acl.builders.AclBuilders; +import org.jtalks.jcommune.service.security.acl.sids.UniversalSid; +import org.jtalks.jcommune.service.security.acl.sids.UserSid; import org.springframework.security.acls.model.AccessControlEntry; import org.springframework.security.acls.model.Permission; import org.springframework.security.acls.model.Sid; @@ -58,7 +58,7 @@ public class PermissionManager { /** * Constructs {@link org.jtalks.jcommune.service.security.PermissionManager} with given - * {@link org.jtalks.common.security.acl.AclManager} and {@link GroupDao} + * {@link AclManager} and {@link GroupDao} * * @param aclManager manager instance * @param groupDao group dao instance @@ -235,7 +235,7 @@ public List<Group> getGroupsByIds(List<Long> groupIds) { /** * @param groupAce from which if of group should be extracted - * @return {@link org.jtalks.common.model.entity.Group} extracted from {@link org.jtalks.common.security.acl.GroupAce} + * @return {@link Group} extracted from {@link GroupAce} */ private Group getGroup(GroupAce groupAce) { return groupDao.get(groupAce.getGroupId()); diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/PermissionService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/PermissionService.java index 76a9c59046..b3696fe85d 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/PermissionService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/PermissionService.java @@ -20,6 +20,8 @@ import org.jtalks.jcommune.model.dto.GroupsPermissions; import org.jtalks.jcommune.model.dto.PermissionChanges; import org.jtalks.jcommune.model.entity.Branch; +import org.jtalks.jcommune.service.security.acl.AclClassName; +import org.jtalks.jcommune.service.security.acl.AclGroupPermissionEvaluator; import java.util.List; diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/SecurityConstants.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/SecurityConstants.java new file mode 100644 index 0000000000..7a398908b1 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/SecurityConstants.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security; + +/** + * Contains constants specific to security. + * + * @author Kirill Afonin + */ +public final class SecurityConstants { + /** + * Username of anonymous user (not logged in user). + * If you want to change it you should change it here and in security-context.xml + */ + public static final String ANONYMOUS_USERNAME = "anonymousUser"; + /** + * Role name of administrators. + */ + public static final String ROLE_ADMIN = "ROLE_ADMIN"; + /** + * Role name of user. Every registered user have this role by default. + */ + public static final String ROLE_USER = "ROLE_USER"; + + /** + * You can't create instance of this class. + */ + private SecurityConstants() { + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/SecurityService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/SecurityService.java new file mode 100644 index 0000000000..ddef0f5d5c --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/SecurityService.java @@ -0,0 +1,189 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security; + + +import org.jtalks.common.model.entity.Entity; +import org.jtalks.common.model.entity.User; +import org.jtalks.common.service.security.SecurityContextFacade; +import org.jtalks.jcommune.model.dao.UserDao; +import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.UserInfo; +import org.jtalks.jcommune.service.security.acl.AclManager; +import org.jtalks.jcommune.service.security.acl.builders.AclAction; +import org.jtalks.jcommune.service.security.acl.builders.AclBuilders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + + +/** + * Abstract layer for Spring Security. Contains methods for authentication and authorization. This service + * + * @author Kirill Afonin + * @author Max Malakhov + * @author Dmitry Sokolov + */ +public class SecurityService implements UserDetailsService { + private final UserDao userDao; + private final AclManager aclManager; + private final AclBuilders aclBuilders = new AclBuilders(); + private final SecurityContextFacade securityContextFacade; + private final static ThreadLocal<JCUser> CACHED_USER = new ThreadLocal<>(); + + /** + * Constructor creates an instance of service. + * @param userDao {@link UserDao} to be injected + * @param aclManager manager for actions with ACLs + * @param securityContextFacade for access to security context that contain {@link Authentication} object. + */ + public SecurityService(UserDao userDao, AclManager aclManager, SecurityContextFacade securityContextFacade) { + this.userDao = userDao; + this.aclManager = aclManager; + this.securityContextFacade = securityContextFacade; + } + + /** + * Returns object that contains basic information about authenticated user. + * @return {@link UserInfo} if {@link Authentication} contain it, otherwise null. + */ + public UserInfo getCurrentUserBasicInfo() { + Object principal = extractPrincipalFromAuthentication(); + return principal instanceof UserInfo ? (UserInfo) principal : null; + } + + /** + * Returns copy of persistent user. + * + * @param authentication to get principal + * @return copy of persistent user associated with authentication principal, if authentication + * or principal is null - returns null. + */ + public JCUser getFullUserInfoFrom(Authentication authentication){ + if (authentication == null) return null; + Object principal = authentication.getPrincipal(); + if (!(principal instanceof UserInfo)) return null; + return updateCacheAndGet(((UserInfo) principal).getId()); + } + + /** + * Get current authenticated {@link User} username. + * + * @return current authenticated {@link User} username or {@code null} if there is no {@link User} authenticated + * or if no authentication information is available (request not went through spring security filters). + */ + public String getCurrentUserUsername() { + Object principal = extractPrincipalFromAuthentication(); + if (principal == null) return null; + String username = extractUsername(principal); + return isAnonymous(username) ? null : username; + } + + public Authentication getAuthentication(){ + return securityContextFacade.getContext().getAuthentication(); + } + + /** + * Creates the builder to work with the permissions. + * + * @param <T> entity that should be the receiver of the permission (SID) + * @return the builder to work with the permissions + */ + public <T extends Entity> AclAction<T> createAclBuilder() { + return aclBuilders.newBuilder(aclManager); + } + + /** + * Get username from principal. + * + * @param principal principal + * @return username + */ + private String extractUsername(Object principal) { + // if principal is spring security user, cast it and get username + // else it is javax.security principal with toString() that return username + if (principal instanceof UserDetails) { + return ((UserDetails) principal).getUsername(); + } + return principal.toString(); + } + + /** + * @param username username + * @return {@code true} if user is anonymous + */ + private boolean isAnonymous(String username) { + return username.equals(SecurityConstants.ANONYMOUS_USERNAME); + } + + /** + * Delete object from acl. All permissions will be removed. + * + * @param securedObject a removed secured object. + */ + public void deleteFromAcl(Entity securedObject) { + deleteFromAcl(securedObject.getClass(), securedObject.getId()); + } + + /** + * Delete object from acl. All permissions will be removed. + * + * @param clazz object {@code Class} + * @param id object id + */ + public void deleteFromAcl(Class clazz, long id) { + aclManager.deleteFromAcl(clazz, id); + } + + /** + * {@inheritDoc} + */ + @Override + public UserDetails loadUserByUsername(String username) { + User user = userDao.getByUsername(username); + if (user == null) { + throw new UsernameNotFoundException("User not found: " + username); + } + return new UserInfo(user); + } + + private JCUser updateCacheAndGet(long principalId){ + JCUser cached = CACHED_USER.get(); + if (cached == null || cached.getId() != principalId){ + cached = JCUser.copyUser(userDao.loadById(principalId)); + CACHED_USER.set(cached); + } + return cached; + } + + /** + * Returns the principal encapsulated by Authentication. + * + * @return <code>Principal</code> if {@link Authentication} contain any, otherwise null. + */ + private Object extractPrincipalFromAuthentication() { + Authentication auth = securityContextFacade.getContext().getAuthentication(); + return auth != null ? auth.getPrincipal() : null; + } + + /** + * Removes JCUser object from threadlocal storage + */ + public void cleanThreadLocalStorage() { + CACHED_USER.remove(); + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/SidRetrievalStrategyImpl.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/SidRetrievalStrategyImpl.java index 779fa787b7..52bdb1cc95 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/SidRetrievalStrategyImpl.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/SidRetrievalStrategyImpl.java @@ -15,8 +15,10 @@ package org.jtalks.jcommune.service.security; import org.jtalks.common.model.entity.Group; -import org.jtalks.common.security.acl.sids.JtalksSidFactory; import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.service.security.acl.sids.JtalksSidFactory; +import org.jtalks.jcommune.service.security.acl.sids.UserGroupSid; +import org.jtalks.jcommune.service.security.acl.sids.UserSid; import org.springframework.security.acls.model.Sid; import org.springframework.security.acls.model.SidRetrievalStrategy; import org.springframework.security.core.Authentication; @@ -28,7 +30,7 @@ /** * JCommune implementation of {@link SidRetrievalStrategy} that creates a {@link Sid} * for the principal by {@link JtalksSidFactory}. Created sids may be - * {@link org.jtalks.common.security.acl.sids.UserSid} or {@link org.jtalks.common.security.acl.sids.UserGroupSid} type. + * {@link UserSid} or {@link UserGroupSid} type. * * @author Elena Lepaeva */ diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AclAuthorizationStrategyImpl.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclAuthorizationStrategyImpl.java similarity index 97% rename from jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AclAuthorizationStrategyImpl.java rename to jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclAuthorizationStrategyImpl.java index 389f595dcf..c5b15d5ba9 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AclAuthorizationStrategyImpl.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclAuthorizationStrategyImpl.java @@ -12,7 +12,8 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.service.security; + +package org.jtalks.jcommune.service.security.acl; import org.springframework.security.access.AccessDeniedException; diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AclClassName.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclClassName.java similarity index 85% rename from jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AclClassName.java rename to jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclClassName.java index 0a1f9cc337..0a4a980dd4 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AclClassName.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclClassName.java @@ -12,10 +12,11 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.service.security; + +package org.jtalks.jcommune.service.security.acl; /** - * Contains acl class names that use by {@link org.jtalks.common.security.acl.TypeConvertingObjectIdentityGenerator} + * Contains acl class names that use by {@link TypeConvertingObjectIdentityGenerator} * * @author Elena Lepaeva */ diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AclGroupPermissionEvaluator.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclGroupPermissionEvaluator.java similarity index 72% rename from jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AclGroupPermissionEvaluator.java rename to jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclGroupPermissionEvaluator.java index c33d5092ca..4bdc27c4fe 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/AclGroupPermissionEvaluator.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclGroupPermissionEvaluator.java @@ -12,44 +12,33 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.service.security; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -import javax.annotation.Nonnull; +package org.jtalks.jcommune.service.security.acl; import org.apache.commons.lang.Validate; -//import org.jtalks.common.model.dao.GroupDao; -//import org.jtalks.common.model.entity.Group; -//import org.jtalks.common.model.entity.User; -import org.jtalks.common.model.dao.GroupDao; import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.permissions.BranchPermission; import org.jtalks.common.model.permissions.GeneralPermission; +import org.jtalks.common.model.permissions.JtalksPermission; import org.jtalks.common.model.permissions.ProfilePermission; -import org.jtalks.common.security.acl.AclManager; -import org.jtalks.common.security.acl.AclUtil; -import org.jtalks.common.security.acl.ExtendedMutableAcl; -import org.jtalks.common.security.acl.GroupAce; -import org.jtalks.common.security.acl.sids.JtalksSidFactory; - -import org.jtalks.common.security.acl.sids.UniversalSid; -import org.jtalks.jcommune.model.dao.UserDao; import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.UserInfo; import org.jtalks.jcommune.plugin.api.PluginPermissionManager; +import org.jtalks.jcommune.service.security.SecurityService; +import org.jtalks.jcommune.service.security.acl.sids.JtalksSidFactory; +import org.jtalks.jcommune.service.security.acl.sids.UniversalSid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.acls.jdbc.JdbcMutableAclService; -import org.springframework.security.acls.model.AccessControlEntry; -import org.springframework.security.acls.model.NotFoundException; -import org.springframework.security.acls.model.ObjectIdentity; -import org.springframework.security.acls.model.Permission; -import org.springframework.security.acls.model.Sid; +import org.springframework.security.acls.model.*; import org.springframework.security.core.Authentication; +import javax.annotation.Nonnull; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + /** * This evaluator is used to process the annotations of the Spring Security like {@link * org.springframework.security.access.prepost.PreAuthorize}. In order to be able to use it, you need to specify the id @@ -65,33 +54,30 @@ public class AclGroupPermissionEvaluator implements PermissionEvaluator { private final AclManager aclManager; private final AclUtil aclUtil; - private final GroupDao groupDao; private final JtalksSidFactory sidFactory; private final JdbcMutableAclService mutableAclService; - private final UserDao userDao; private final PluginPermissionManager pluginPermissionManager; + private final SecurityService securityService; /** * @param aclManager for getting permissions on object indentity * @param aclUtil utilities to work with Spring ACL - * @param groupDao dao for user group getting * @param sidFactory factory to work with principals * @param mutableAclService for checking existing of sids + * @param securityService to get current user from SecurityContext */ - public AclGroupPermissionEvaluator(@Nonnull org.jtalks.common.security.acl.AclManager aclManager, + public AclGroupPermissionEvaluator(@Nonnull AclManager aclManager, @Nonnull AclUtil aclUtil, - @Nonnull GroupDao groupDao, @Nonnull JtalksSidFactory sidFactory, @Nonnull JdbcMutableAclService mutableAclService, - @Nonnull UserDao userDao, - @Nonnull PluginPermissionManager pluginPermissionManager) { + @Nonnull PluginPermissionManager pluginPermissionManager, + @Nonnull SecurityService securityService) { this.aclManager = aclManager; this.aclUtil = aclUtil; this.sidFactory = sidFactory; this.mutableAclService = mutableAclService; - this.userDao = userDao; - this.groupDao = groupDao; this.pluginPermissionManager = pluginPermissionManager; + this.securityService = securityService; } /** @@ -105,51 +91,41 @@ public boolean hasPermission(Authentication authentication, Object targetId, Obj } /** - * TODO In runtime authentication object contains clear user password (not the hashed one). - * May be potential security issue. - * <p/> * {@inheritDoc} */ @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { - boolean result = false; Long id = parseTargetId(targetId); - - ObjectIdentity objectIdentity = aclUtil.createIdentity(id, targetType); - Permission jtalksPermission; - if (permission instanceof Permission) { - jtalksPermission = (Permission) permission; - } else { - jtalksPermission = getPermission(permission); + JtalksPermission jtalksPermission = parseJtalksPermissionFrom(permission); + if (jtalksPermission == ProfilePermission.EDIT_OWN_PROFILE && + ((UserInfo) authentication.getPrincipal()).getId() != id) { + return false; } + ObjectIdentity objectIdentity = aclUtil.createIdentity(id, targetType); Sid sid = sidFactory.createPrincipal(authentication); List<AccessControlEntry> aces; List<GroupAce> controlEntries; try { aces = ExtendedMutableAcl.castAndCreate(mutableAclService.readAclById(objectIdentity)).getEntries(); - controlEntries = aclManager.getGroupPermissionsOn(objectIdentity); + controlEntries = aclManager.getGroupPermissionsFilteredByPermissionOn(objectIdentity, jtalksPermission); } catch (NotFoundException nfe) { aces = new ArrayList<>(); controlEntries = new ArrayList<>(); } - - if (permission == ProfilePermission.EDIT_OWN_PROFILE && - ((JCUser) authentication.getPrincipal()).getId() != id) { - return false; + if (jtalksPermission instanceof ProfilePermission && authentication.getPrincipal() instanceof UserInfo){ + if (isRestrictedPersonalPermission(authentication, jtalksPermission)) return false; + else if (isAllowedPersonalPermission(authentication, jtalksPermission)) return true; } - if (isRestrictedForSid(sid, aces, jtalksPermission) || - isRestrictedForGroup(controlEntries, authentication, jtalksPermission) || - isRestrictedPersonalPermission(authentication, jtalksPermission)) { + isRestrictedForGroup(controlEntries, authentication, jtalksPermission)) { return false; } else if (isAllowedForSid(sid, aces, jtalksPermission) || - isAllowedForGroup(controlEntries, authentication, jtalksPermission) || - isAllowedPersonalPermission(authentication, jtalksPermission)) { + isAllowedForGroup(controlEntries, authentication, jtalksPermission)) { return true; } - return result; + return false; } /** @@ -159,20 +135,14 @@ public boolean hasPermission(Authentication authentication, Serializable targetI * @return targetId as Long. */ private Long parseTargetId(Serializable targetId) { - Long result = 0L; - Validate.isTrue(targetId instanceof String || targetId instanceof Long); - if (targetId instanceof String) { - result = Long.parseLong((String) targetId); - } else if (targetId instanceof Long) { - result = (Long) targetId; - } - return result; + Validate.isTrue(targetId instanceof String || targetId instanceof Long, "Can't parse targetId, value=[" + targetId + "]"); + if (targetId instanceof String) return Long.parseLong((String) targetId); + return (Long) targetId; } /** * Check if this <tt>personal permission</tt> is allowed for groups of user from authentication * - * @param authentication authentication to check permission for it * @return <code>true</code> if this permission is allowed */ private boolean isAllowedPersonalPermission(Authentication authentication, Permission permission) { @@ -182,7 +152,6 @@ private boolean isAllowedPersonalPermission(Authentication authentication, Permi /** * Check if this <tt>personal permission</tt> is restricted for groups of user from authentication * - * @param authentication authentication to check permission for it * @return <code>true</code> if this permission is restricted */ private boolean isRestrictedPersonalPermission(Authentication authentication, Permission permission) { @@ -248,15 +217,14 @@ private boolean isGrantedForSid(Sid sid, List<AccessControlEntry> controlEntries */ private boolean isGrantedForSid(Sid sid, AccessControlEntry ace, Permission permission, boolean isCheckAllowedGrant) { - return ((UniversalSid)sid).getSidId().equals(((UniversalSid)ace.getSid()).getSidId()) + return ace.isGranting() == isCheckAllowedGrant && permission.equals(ace.getPermission()) - && (ace.isGranting() == isCheckAllowedGrant); + && ((UniversalSid)sid).getSidId().equals(((UniversalSid)ace.getSid()).getSidId()); } /** * Check if this <tt>permission</tt> is granted for groups of user from authentication * - * @param authentication authentication to check permission for it * @param permission permission to check * @param isCheckAllowedGrant flag that indicates what type of grant need to * be checked - 'allowed' (true) or 'restricted' (false) @@ -265,30 +233,21 @@ private boolean isGrantedForSid(Sid sid, AccessControlEntry ace, */ private boolean isGrantedPersonalPermission(Authentication authentication, Permission permission, boolean isCheckAllowedGrant) { - if (authentication.getPrincipal() instanceof JCUser) { - JCUser storedUser = (JCUser) authentication.getPrincipal(); - // retriev user with replicated groups from EhCache - JCUser actualUser = userDao.get(storedUser.getId()); - if (actualUser == null) { - LOGGER.warn("{} : User #{} not found", - this.getClass().getCanonicalName(), - storedUser.getId()); - return !isCheckAllowedGrant; + JCUser jcUser = securityService.getFullUserInfoFrom(authentication); + if (jcUser == null) return false; + List<Group> groups = jcUser.getGroups(); + for (Group group : groups) { + ObjectIdentity groupIdentity = aclUtil.createIdentity(group.getId(), "GROUP"); + Sid groupSid = sidFactory.create(group); + List<AccessControlEntry> groupAces; + try { + groupAces = ExtendedMutableAcl.castAndCreate( + mutableAclService.readAclById(groupIdentity)).getEntries(); + } catch (NotFoundException nfe) { + groupAces = new ArrayList<>(); } - List<Group> groups = actualUser.getGroups(); - for (Group group : groups) { - ObjectIdentity groupIdentity = aclUtil.createIdentity(group.getId(), "GROUP"); - Sid groupSid = sidFactory.create(group); - List<AccessControlEntry> groupAces; - try { - groupAces = ExtendedMutableAcl.castAndCreate( - mutableAclService.readAclById(groupIdentity)).getEntries(); - } catch (NotFoundException nfe) { - groupAces = new ArrayList<>(); - } - if (isGrantedForSid(groupSid, groupAces, permission, isCheckAllowedGrant)) { - return true; - } + if (isGrantedForSid(groupSid, groupAces, permission, isCheckAllowedGrant)) { + return true; } } return false; @@ -300,7 +259,6 @@ private boolean isGrantedPersonalPermission(Authentication authentication, Permi * * @param controlEntries list of entries with security information for groups * to loop through - * @param authentication authentication to check permission for it * @param permission permission to check * @return <code>true</code> if this permission is allowed. */ @@ -315,7 +273,6 @@ private boolean isAllowedForGroup(List<GroupAce> controlEntries, * * @param controlEntries list of entries with security information for groups * to loop through - * @param authentication authentication to check permission for it * @param permission permission to check * @return <code>true</code> if this permission is restricted. */ @@ -330,7 +287,6 @@ private boolean isRestrictedForGroup(List<GroupAce> controlEntries, * * @param controlEntries list of entries with security information for groups * to loop through - * @param authentication authentication to check permission for it * @param permission permission to check * @param isCheckAllowedGrant flag that indicates what type of grant need to * be checked - 'allowed' (true) or 'restricted' (false) @@ -340,11 +296,12 @@ private boolean isRestrictedForGroup(List<GroupAce> controlEntries, private boolean isGrantedForGroup(List<GroupAce> controlEntries, Authentication authentication, Permission permission, boolean isCheckAllowedGrant) { - if (authentication.getPrincipal() instanceof JCUser) { - for (GroupAce ace : controlEntries) { - if (isGrantedForGroup(ace, authentication, permission, isCheckAllowedGrant)) { - return true; - } + JCUser jcUser = securityService.getFullUserInfoFrom(authentication); + if (jcUser == null) return false; + List<Long> groupsIDs = jcUser.getGroupsIDs(); + for (GroupAce ace : controlEntries) { + if (groupsIDs.contains(ace.getGroupId())) { + if (isGrantedForGroup(ace, permission, isCheckAllowedGrant)) return true; } } return false; @@ -355,26 +312,23 @@ private boolean isGrantedForGroup(List<GroupAce> controlEntries, * group. * * @param ace entry with security information (for groups) - * @param authentication authentication to check permission for it * @param permission permission to check * @param isCheckAllowedGrant flag that indicates what type of grant need to * be checked - 'allowed' (true) or 'restricted' (false) * @return <code>true</code> if this entry has specified <tt>permission</tt> * and type of grant. */ - private boolean isGrantedForGroup(GroupAce ace, Authentication authentication, - Permission permission, boolean isCheckAllowedGrant) { + private boolean isGrantedForGroup(GroupAce ace, Permission permission, boolean isCheckAllowedGrant) { Permission permissionToComapare = ace.getPermission(); if (permissionToComapare == null) { permissionToComapare = pluginPermissionManager.findPluginsBranchPermissionByMask(ace.getPermissionMask()); } return ace.isGranting() == isCheckAllowedGrant - && permission.equals(permissionToComapare) - && ace.getGroup(groupDao).getUsers(). - contains(authentication.getPrincipal()); + && permission.equals(permissionToComapare); } - private Permission getPermission(Object permission) { + private JtalksPermission parseJtalksPermissionFrom(Object permission) { + if (permission instanceof Permission) return (JtalksPermission) permission; String permissionName = (String) permission; if ((permissionName).startsWith(GeneralPermission.class.getSimpleName())) { diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclManager.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclManager.java new file mode 100644 index 0000000000..9b90db2fa3 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclManager.java @@ -0,0 +1,214 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl; + +import org.jtalks.common.model.dao.GroupDao; +import org.jtalks.common.model.entity.Branch; +import org.jtalks.common.model.entity.Entity; +import org.jtalks.common.model.entity.Group; +import org.jtalks.common.model.entity.User; +import org.jtalks.common.model.permissions.JtalksPermission; +import org.jtalks.jcommune.service.security.acl.sids.UserGroupSid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.model.*; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; + +/** + * Contains coarse-grained operations with Spring ACL to manage the permissions of Groups & Users for the actions on + * entities like Branch or Topic. + * + * @author Kirill Afonin + */ +public class AclManager { + private final Logger logger = LoggerFactory.getLogger(getClass()); + private final MutableAclService mutableAclService; + private GroupDao groupDao; + private AclUtil aclUtil; + + public AclManager(@Nonnull MutableAclService mutableAclService) { + this.mutableAclService = mutableAclService; + aclUtil = new AclUtil(mutableAclService); + } + + /** + * Gets only group permissions (where sid is {@link UserGroupSid}) and returns them for the specified entity (object + * identity). Note, that if there are other records with sids different than {@link UserGroupSid}, they will be + * filtered out. + * + * @param entity an object for which the permissions were given + * @return permissions assigned on {@link Group}s without any other permissions. Returns empty collection if there + * are no group permissions given on the specified object identity + */ + public List<GroupAce> getGroupPermissionsOn(@Nonnull Entity entity) { + MutableAcl branchAcl = aclUtil.getAclFor(entity); + return getGroupPermissions(branchAcl); + } + + /** + * Gets only group permissions (where sid is {@link UserGroupSid}) and returns them for the specified entity (object + * identity). Note, that if there are other records with sids different than {@link UserGroupSid}, they will be + * filtered out. + * + * @param entity an object identity for which the permissions were given + * @return permissions assigned on {@link Group}s without any other permissions. Returns empty collection if there + * are no group permissions given on the specified object identity + */ + public List<GroupAce> getGroupPermissionsOn(@Nonnull ObjectIdentity entity) { + MutableAcl branchAcl = aclUtil.getAclFor(entity); + return getGroupPermissions(branchAcl); + } + + public List<GroupAce> getGroupPermissionsFilteredByPermissionOn(@Nonnull ObjectIdentity entity, JtalksPermission permission) { + MutableAcl branchAcl = aclUtil.getAclFor(entity); + return getGroupPermissionsFilteredByPermission(branchAcl, permission); + } + + private List<GroupAce> getGroupPermissions(MutableAcl branchAcl) { + List<AccessControlEntry> originalAces = branchAcl.getEntries(); + List<GroupAce> resultingAces = new ArrayList<GroupAce>(originalAces.size()); + for (AccessControlEntry originalAce : originalAces) { + if (originalAce.getSid() instanceof UserGroupSid) { + resultingAces.add(new GroupAce(originalAce)); + } + } + return resultingAces; + } + + private List<GroupAce> getGroupPermissionsFilteredByPermission(MutableAcl branchAcl, JtalksPermission permission) { + List<AccessControlEntry> originalAces = branchAcl.getEntries(); + List<GroupAce> resultingAces = new ArrayList<GroupAce>(originalAces.size()); + int permissionMask = permission.getMask(); + for (AccessControlEntry originalAce : originalAces) { + if (originalAce.getSid() instanceof UserGroupSid + && originalAce.getPermission().getMask() == permissionMask) { + resultingAces.add(new GroupAce(originalAce)); + } + } + return resultingAces; + } + + /** + * @deprecated use {@link #getGroupPermissionsOn} + */ + @Deprecated() + public List<GroupAce> getBranchPermissions(Branch branch) { + MutableAcl branchAcl = aclUtil.getAclFor(branch); + List<AccessControlEntry> originalAces = branchAcl.getEntries(); + List<GroupAce> resultingAces = new ArrayList<GroupAce>(originalAces.size()); + for (AccessControlEntry entry : originalAces) { + resultingAces.add(new GroupAce(entry)); + } + return resultingAces; + } + + /** + * TODO: NOT FINISHED! TO BE IMPLEMENTED + * + * @param user + * @param branch + * @return + */ + public List<Permission> getPermissions(User user, Branch branch) { + throw new UnsupportedOperationException(); +// List<Permission> permissions = new ArrayList<Permission>(); +// +// List<Group> groups = groupDao.getGroupsOfUser(user); +// +// MutableAcl branchAcl = aclUtil.getAclFor(branch); +// List<AccessControlEntry> originalAces = branchAcl.getEntries(); +// +// for (AccessControlEntry entry : originalAces) { +// GroupAce groupAce = new GroupAce(entry); +// if (groups.contains(groupAce.getGroup(groupDao))) { +// permissions.add(groupAce.getBranchPermission()); +// } +// } +// +// return permissions; + } + + /** + * Grant permissions from list to every sid in list on {@code target} object. + * + * @param sids list of sids + * @param permissions list of permissions + * @param target secured object + */ + public void grant(List<? extends Sid> sids, List<Permission> permissions, Entity target) { + MutableAcl acl = aclUtil.grant(sids, permissions, target); + mutableAclService.updateAcl(acl); + } + + /** + * Revoke permissions from lists for every sid in list on {@code target} entity + * + * @param sids list of sids + * @param permissions list of permissions + * @param target secured object + */ + public void restrict(List<? extends Sid> sids, List<Permission> permissions, Entity target) { + MutableAcl acl = aclUtil.restrict(sids, permissions, target); + mutableAclService.updateAcl(acl); + } + + /** + * Delete permissions from list for every sid in list on {@code target} object. + * + * @param sids list of sids + * @param permissions list of permissions + * @param target secured object + */ + public void delete(List<? extends Sid> sids, List<Permission> permissions, Entity target) { + MutableAcl acl = aclUtil.delete(sids, permissions, target); + mutableAclService.updateAcl(acl); + } + + /** + * Deletes all ACEs defined in the acl_entry table, wired with the presented SID, also wires owner_sid of OID + * belongs to SID to another SID, deletes given SID defined in acl_sid. + * + * @param sid to ACL delete + * @param sidHeir will became the owner of ObjectIdentities belongs to sid + */ + public void deleteSid(Sid sid, Sid sidHeir){ + mutableAclService.deleteEntriesForSid(sid, sidHeir); + } + + /** + * Delete object from acl. All permissions will be removed. + * + * @param clazz object {@code Class} + * @param id object id + */ + public void deleteFromAcl(Class clazz, long id) { + if (id <= 0) { + throw new IllegalStateException("Object id must be greater then 0."); + } + ObjectIdentity oid = new ObjectIdentityImpl(clazz, id); + mutableAclService.deleteAcl(oid, true); + logger.debug("Deleted securedObject" + clazz.getSimpleName() + " with id:" + id); + } + + + public void setAclUtil(AclUtil aclUtil) { + this.aclUtil = aclUtil; + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclUtil.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclUtil.java new file mode 100644 index 0000000000..f354751d1c --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/AclUtil.java @@ -0,0 +1,187 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl; + +import com.google.common.base.Predicate; +import org.jtalks.common.model.entity.Entity; +import org.springframework.security.acls.model.*; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.validation.constraints.Min; +import java.util.List; + +import static com.google.common.collect.Iterables.filter; +import static com.google.common.collect.Lists.newArrayList; +import static org.jtalks.jcommune.service.security.acl.TypeConvertingObjectIdentityGenerator.createDefaultGenerator; + +/** + * The fine grained utilities to work with Spring ACL. + * + * @author stanislav bashkirtsev + */ +public class AclUtil { + private TypeConvertingObjectIdentityGenerator objectIdentityGenerator = createDefaultGenerator(); + private final MutableAclService mutableAclService; + + /** + * Use this constructor if you need a full blown ACL utilities. + * + * @param mutableAclService the acl service that is required for some operations related to DB. If you use factory + * methods, then you don't need to specify it working with object identities only for + * example. + */ + public AclUtil(@Nonnull MutableAclService mutableAclService) { + this.mutableAclService = mutableAclService; + } + + /** + * Need to be able to create it without {@link MutableAclService} if it's not required. This was created mainly for + * the purpose of factory methods. + */ + AclUtil() { + this.mutableAclService = null; + } + + + /** + * {@inheritDoc} + */ + public ExtendedMutableAcl getAclFor(Entity entity) { + ObjectIdentity oid = createIdentityFor(entity); + return getAclFor(oid); + } + + /** + * {@inheritDoc} + */ + public ExtendedMutableAcl getAclFor(ObjectIdentity oid) { + try { + return ExtendedMutableAcl.castAndCreate(mutableAclService.readAclById(oid)); + } catch (NotFoundException nfe) { + return ExtendedMutableAcl.castAndCreate(mutableAclService.createAcl(oid)); + } + } + + /** + * {@inheritDoc} + */ + public ObjectIdentity createIdentityFor(Entity securedObject) { + return objectIdentityGenerator.getObjectIdentity(securedObject); + } + + /** + * {@inheritDoc} + */ + public ObjectIdentity createIdentity(long id, @Nonnull String type) { + return objectIdentityGenerator.createObjectIdentity(id, type); + } + + /** + * {@inheritDoc} + */ + public ExtendedMutableAcl grant(List<? extends Sid> sids, List<Permission> permissions, Entity target) { + return applyPermissionsToSids(sids, permissions, target, true); + } + + /** + * {@inheritDoc} + */ + public ExtendedMutableAcl restrict(List<? extends Sid> sids, List<Permission> permissions, Entity target) { + return applyPermissionsToSids(sids, permissions, target, false); + } + + /** + * {@inheritDoc} + */ + public ExtendedMutableAcl delete(List<? extends Sid> sids, List<Permission> permissions, Entity target) { + ObjectIdentity oid = createIdentityFor(target); + ExtendedMutableAcl acl = ExtendedMutableAcl.castAndCreate(mutableAclService.readAclById(oid)); + deletePermissionsFromAcl(acl, sids, permissions); + return acl; + } + + /** + * {@inheritDoc} + */ + public void deletePermissionsFromAcl( + ExtendedMutableAcl acl, List<? extends Sid> sids, List<Permission> permissions) { + List<AccessControlEntry> allEntries = acl.getEntries(); // it's a copy + List<AccessControlEntry> filtered = newArrayList(filter(allEntries, new BySidAndPermissionFilter(sids, permissions))); + acl.delete(filtered); + } + + public Acl aclFromObjectIdentity(@Min(1) long id, @Nonnull String type) { + ObjectIdentity identity = this.objectIdentityGenerator.createObjectIdentity(id, type); + return getAclFor(identity); + } + + + public void setObjectIdentityGenerator(TypeConvertingObjectIdentityGenerator objectIdentityGenerator) { + this.objectIdentityGenerator = objectIdentityGenerator; + } + + + /** + * Apply every permission from list to every sid from list. + * + * @param sids list of sids + * @param permissions list of permissions + * @param target securable object + * @param granting grant if true, restrict if false + * @return the ACL that manages the specified {@code target} and its Sids & Permissions + */ + private ExtendedMutableAcl applyPermissionsToSids( + List<? extends Sid> sids, List<Permission> permissions, Entity target, boolean granting) { + ExtendedMutableAcl acl = getAclFor(target); + deletePermissionsFromAcl(acl, sids, permissions); + acl.addPermissions(sids, permissions, granting); + return acl; + } + + + /** + * Gets the list of Sids and Permissions into the constructor and filters out those {@link AccessControlEntry} whose + * Sid & Permission is not in the specified lists. + * + * @see com.google.common.collect.Iterators#filter(java.util.Iterator, Predicate) + */ + private static class BySidAndPermissionFilter implements Predicate<AccessControlEntry> { + private final List<? extends Sid> sids; + private final List<Permission> permissions; + + /** + * @param sids to find {@link AccessControlEntry}s that contain them + * @param permissions to find the {@link AccessControlEntry}s where specified {@code sids} have these + * permissions + */ + private BySidAndPermissionFilter(@Nonnull List<? extends Sid> sids, @Nonnull List<Permission> permissions) { + this.sids = sids; + this.permissions = permissions; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean apply(@Nullable AccessControlEntry input) { + if (input == null) { + return false; + } + return sids.contains(input.getSid()) && permissions.contains(input.getPermission()); + } + } +} \ No newline at end of file diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/ExtendedMutableAcl.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/ExtendedMutableAcl.java new file mode 100644 index 0000000000..c7e39f69c1 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/ExtendedMutableAcl.java @@ -0,0 +1,265 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl; + +import org.apache.commons.lang.Validate; +import org.springframework.security.acls.model.*; + +import javax.annotation.Nonnull; +import java.io.Serializable; +import java.util.List; + +/** + * This implementations of {@link MutableAcl} adds additional handy methods like {@link #delete(AccessControlEntry)}. + * It's actually a wrapper that delegates all the implemented methods to the internal {@link MutableAcl} that is + * accepted in the constructor. + * + * @author stanislav bashkirtsev + */ +public class ExtendedMutableAcl implements MutableAcl { + /** + * This is a mock instance that is used usually just for testing purposes, most of its methods will throw {@link + * NullPointerException} if you invoke them. + */ + public final static ExtendedMutableAcl NULL_ACL = new ExtendedMutableAcl(); + + private final MutableAcl acl; + + /** + * Use factory methods like {@link #create(MutableAcl)} to instantiate the objects. + * + * @param acl the internal delegate-instance + */ + private ExtendedMutableAcl(@Nonnull MutableAcl acl) { + Validate.notNull(acl, "Acl can't be null while creating ExtendedMutableAcl!"); + this.acl = acl; + } + + /** + * An empty construct that shouldn't be ever used :) It's purpose is only to serve for {@link #NULL_ACL}. + */ + private ExtendedMutableAcl() { + acl = null; + } + + /** + * Finds the specified {@link AccessControlEntry} and removes it from the entry list of the {@link Acl}. + * + * @param entryToDelete the entry to remove from the {@link Acl} + * @return the id of the removed entry or {@code -1} if no such entry was found + */ + public int delete(@Nonnull AccessControlEntry entryToDelete) { + List<AccessControlEntry> aclEntries = acl.getEntries(); + for (int i = 0; i < aclEntries.size(); i++) { + if (aclEntries.get(i).equals(entryToDelete)) { + acl.deleteAce(i); + return i; + } + } + return -1; + } + + /** + * Deletes all the specified entries from the {@link Acl#getEntries()} list. If some or all entries were not found + * in the list, those elements are not removed and nothing happens. + * + * @param entriesToDelete the list of entries to remove from the {@link Acl#getEntries()} + */ + public void delete(@Nonnull List<AccessControlEntry> entriesToDelete) { + for (AccessControlEntry next : entriesToDelete) { + delete(next); + } + } + + /** + * Adds all the permissions to the specified sid. Note, that it doesn't check whether there is already such Sid with + * such Permission in the ACL, so you should ensures this on your own. + * + * @param sid sid to grant the permissions to + * @param permissions the list of permissions that should be granted to the Sid + * @param granting specify {@code true} if you want to grant the permission or {@code false} if you want to + * restrict all the permissions + * @see #addPermissions(List, List, boolean) + */ + public void addPermissions(@Nonnull Sid sid, @Nonnull List<Permission> permissions, boolean granting) { + int entriesAmount = acl.getEntries().size(); + for (Permission permission : permissions) { + acl.insertAce(entriesAmount++, permission, sid, granting); + } + } + + /** + * Adds all the permissions to all the specified sids. Note, that it doesn't check whether there are already such + * Sids with such Permission in the ACL, so you should check it on your own. + * + * @param sids the sids to grant the permissions to + * @param permissions the list of permissions that should be granted to the Sid + * @param granting specify {@code true} if you want to grant the permission or {@code false} if you want to + * restrict all the permissions + * @see #addPermissions(Sid, List, boolean) + */ + public void addPermissions(@Nonnull List<? extends Sid> sids, @Nonnull List<Permission> permissions, boolean granting) { + for (Sid recipient : sids) { + addPermissions(recipient, permissions, granting); + } + } + + /** + * Wraps the specified {@link MutableAcl} with the instance of {@link ExtendedMutableAcl} and returns the latter. + * + * @param acl the acl to be wrapped with the {@link ExtendedMutableAcl} + * @return a new instance of {@link ExtendedMutableAcl} that wraps the specified {@code acl} + */ + public static ExtendedMutableAcl create(@Nonnull MutableAcl acl) { + return new ExtendedMutableAcl(acl); + } + + /** + * Wraps the specified {@link MutableAcl} with the instance of {@link ExtendedMutableAcl} and returns the latter. + * Throws exception if the specified parameter is not an instance of {@link MutableAcl}. + * + * @param acl the acl to be wrapped with the {@link ExtendedMutableAcl} + * @return a new instance of {@link ExtendedMutableAcl} that wraps the specified {@code acl} + * @throws ClassCastException if the specified parameter is not an instance of {@link MutableAcl} + */ + public static ExtendedMutableAcl castAndCreate(@Nonnull Acl acl) { + return new ExtendedMutableAcl((MutableAcl) acl); + } + + /** + * Gets the wrapped instance that was specified into the factory methods. + * + * @return the wrapped instance that was specified into the factory methods + */ + public MutableAcl getAcl() { + return acl; + } + + /** + * {@inheritDoc} + */ + @Override + public void deleteAce(int aceIndex) throws NotFoundException { + acl.deleteAce(aceIndex); + } + + /** + * {@inheritDoc} + */ + @Override + public void insertAce( + int atIndexLocation, Permission permission, Sid sid, boolean granting) throws NotFoundException { + acl.insertAce(atIndexLocation, permission, sid, granting); + } + + /** + * {@inheritDoc} + */ + @Override + public List<AccessControlEntry> getEntries() { + return acl.getEntries(); + } + + /** + * {@inheritDoc} + */ + @Override + public Serializable getId() { + return acl.getId(); + } + + /** + * {@inheritDoc} + */ + @Override + public ObjectIdentity getObjectIdentity() { + return acl.getObjectIdentity(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEntriesInheriting() { + return acl.isEntriesInheriting(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isGranted(List<Permission> permission, List<Sid> sids, + boolean administrativeMode) throws NotFoundException, UnloadedSidException { + return acl.isGranted(permission, sids, administrativeMode); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isSidLoaded(List<Sid> sids) { + return acl.isSidLoaded(sids); + } + + /** + * {@inheritDoc} + */ + @Override + public void setEntriesInheriting(boolean entriesInheriting) { + acl.setEntriesInheriting(entriesInheriting); + } + + /** + * {@inheritDoc} + */ + @Override + public void setOwner(Sid newOwner) { + acl.setOwner(newOwner); + } + + /** + * {@inheritDoc} + */ + @Override + public Sid getOwner() { + return acl.getOwner(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setParent(Acl newParent) { + acl.setParent(newParent); + } + + /** + * {@inheritDoc} + */ + @Override + public Acl getParentAcl() { + return acl.getParentAcl(); + } + + /** + * {@inheritDoc} + */ + @Override + public void updateAce(int aceIndex, Permission permission) throws NotFoundException { + acl.updateAce(aceIndex, permission); + } + +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/GroupAce.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/GroupAce.java new file mode 100644 index 0000000000..1a7a763610 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/GroupAce.java @@ -0,0 +1,100 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl; + +import org.jtalks.common.model.dao.GroupDao; +import org.jtalks.common.model.entity.Group; +import org.jtalks.common.model.permissions.BranchPermission; +import org.jtalks.common.model.permissions.GeneralPermission; +import org.jtalks.common.model.permissions.JtalksPermission; +import org.jtalks.jcommune.service.security.acl.sids.UserGroupSid; +import org.springframework.security.acls.model.AccessControlEntry; + +/** + * @author stanislav bashkirtsev + */ +public class GroupAce { + private final AccessControlEntry ace; + + public GroupAce(AccessControlEntry ace) { + if (!(ace.getSid() instanceof UserGroupSid)) { + throw new IllegalArgumentException("The specified ACE has sid which is not of type: " + UserGroupSid.class); + } + this.ace = ace; + } + + public Group getGroup(GroupDao groupDao) { + long groupId = getGroupId(); + Group group = groupDao.get(groupId); + throwIfNull(groupId, group); + return group; + } + + /** + * @return id of associated {@link UserGroupSid} and its {@link Group} + */ + public long getGroupId() { + String groupIdString = ((UserGroupSid) ace.getSid()).getGroupId(); + return Long.parseLong(groupIdString); + } + + public JtalksPermission getPermission() { + JtalksPermission permission = BranchPermission.findByMask(getPermissionMask()); + if (permission == null) { + permission = GeneralPermission.findByMask(getPermissionMask()); + } + return permission; + } + + public int getPermissionMask() { + return ace.getPermission().getMask(); + } + + public boolean isGranting() { + return ace.isGranting(); + } + + /** + * Defines whether the ACE is restricting and SID is not allowed to perform action. + * + * @return true if the permission is restricted or false if it's granted + */ + public boolean isRestricting() { + return !isGranting(); + } + + public AccessControlEntry getOriginalAce() { + return ace; + } + + private void throwIfNull(long groupId, Group group) { + if (group == null) { + throw new ObsoleteAclException(groupId); + } + } + + @SuppressWarnings("serial") + public static class ObsoleteAclException extends RuntimeException { + + public ObsoleteAclException(long groupId) { + super(new StringBuilder("A group with ID [").append(groupId).append("] was removed") + .append("but this ID is still registered as a Permission owner (SID) in ACL tables. ") + .append("To resolve this issue you should manually remove records from ACL tables ") + .append("Note, that this is a bug and this issue should be reported to be corrected in ") + .append("future versions.").toString()); + } + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/JtalksLookupStrategy.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/JtalksLookupStrategy.java new file mode 100644 index 0000000000..6e5bae4cfb --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/JtalksLookupStrategy.java @@ -0,0 +1,56 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl; + +import org.jtalks.jcommune.service.security.acl.sids.SidFactory; +import org.springframework.security.acls.domain.AclAuthorizationStrategy; +import org.springframework.security.acls.domain.AuditLogger; +import org.springframework.security.acls.jdbc.BasicLookupStrategy; +import org.springframework.security.acls.model.AclCache; +import org.springframework.security.acls.model.Sid; + +import javax.sql.DataSource; + +/** + * Gives possibility to implement custom Sid + * @author Mikhail Stryzhonok + * @see Sid + */ +public class JtalksLookupStrategy extends BasicLookupStrategy { + + private SidFactory sidFactory; + + public JtalksLookupStrategy(DataSource dataSource, AclCache aclCache, + AclAuthorizationStrategy aclAuthorizationStrategy, AuditLogger auditLogger) { + super(dataSource, aclCache, aclAuthorizationStrategy, auditLogger); + } + + /** + * {@inheritDoc} + */ + @Override + protected Sid createSid(boolean isPrincipal, String sid) { + return sidFactory.create(sid, isPrincipal); + } + + public SidFactory getSidFactory() { + return sidFactory; + } + + public void setSidFactory(SidFactory sidFactory) { + this.sidFactory = sidFactory; + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/JtalksMutableAcService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/JtalksMutableAcService.java new file mode 100644 index 0000000000..0786e21543 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/JtalksMutableAcService.java @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl; + +import org.jtalks.jcommune.service.security.acl.sids.SidFactory; +import org.jtalks.jcommune.service.security.acl.sids.UniversalSid; +import org.springframework.security.acls.jdbc.JdbcMutableAclService; +import org.springframework.security.acls.jdbc.LookupStrategy; +import org.springframework.security.acls.model.*; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; + +import javax.sql.DataSource; + +/** + * Gives possibility to implement custom Sid + * @author Mikhail Stryzhonok + * @see Sid + * @see UniversalSid + */ +public class JtalksMutableAcService extends JdbcMutableAclService { + + private SidFactory sidFactory; + + public JtalksMutableAcService(DataSource dataSource, LookupStrategy lookupStrategy, AclCache aclCache) { + super(dataSource, lookupStrategy, aclCache); + } + + /** + * {@inheritDoc} + */ + @Override + public MutableAcl createAcl(ObjectIdentity objectIdentity) throws AlreadyExistsException { + Assert.notNull(objectIdentity, "Object Identity required"); + + // Check this object identity hasn't already been persisted + if (retrieveObjectIdentityPrimaryKey(objectIdentity) != null) { + throw new AlreadyExistsException("Object identity '" + objectIdentity + "' already exists"); + } + + // Need to retrieve the current principal, in order to know who "owns" this ACL (can be changed later on) + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + Sid sid = sidFactory.createPrincipal(auth); + createObjectIdentity(objectIdentity, sid); + + // Retrieve the ACL via superclass (ensures cache registration, proper retrieval etc) + Acl acl = readAclById(objectIdentity); + Assert.isInstanceOf(MutableAcl.class, acl, "MutableAcl should be been returned"); + + return (MutableAcl) acl; + } + + /** + *{@inheritDoc} + */ + @Override + protected Long createOrRetrieveSidPrimaryKey(Sid sid, boolean allowCreate) { + Assert.notNull(sid, "Sid required"); + Assert.isInstanceOf(UniversalSid.class, sid, "Unsupported sid implementation"); + + String sidId = ((UniversalSid) sid).getSidId(); + boolean isPrinciple = ((UniversalSid) sid).isPrincipal(); + return createOrRetrieveSidPrimaryKey(sidId, isPrinciple, allowCreate); + } + + /** + * {@inheritDoc} + */ + @Override + protected String getSidId(Sid sid) { + Assert.notNull(sid, "Sid required"); + Assert.isInstanceOf(UniversalSid.class, sid, "Unsupported sid implementation"); + return ((UniversalSid) sid).getSidId(); + } + + public SidFactory getSidFactory() { + return sidFactory; + } + + public void setSidFactory(SidFactory sidFactory) { + this.sidFactory = sidFactory; + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/TypeConvertingObjectIdentityGenerator.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/TypeConvertingObjectIdentityGenerator.java new file mode 100644 index 0000000000..82f4dc3b95 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/TypeConvertingObjectIdentityGenerator.java @@ -0,0 +1,156 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl; + +import org.apache.commons.lang.Validate; +import org.jtalks.common.model.entity.*; +import org.springframework.security.acls.domain.ObjectIdentityImpl; +import org.springframework.security.acls.model.ObjectIdentity; + +import javax.annotation.Nonnull; +import javax.annotation.concurrent.ThreadSafe; +import javax.validation.constraints.Min; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * If we don't want to have Object Identity (OID) in the database having the same type as the entity class, we can have + * this generator being initialized with conversion rules so that some other value goes to database. <br/><br/> + * <b>Example</b>: You're saving an instance of {@link Branch} as an Object Identity to the database into ACL tables. If + * you use usual {@link org.springframework.security.acls.model.ObjectIdentityRetrievalStrategy}, then you'll have in + * your {@code acl_class} table a record with {@link Branch} value. But if you want to + * have a custom type being saved there, let's say {@code BRANCH_CLASS}, you can use this generator and set a conversion + * rule <u>{@code Branch.class -> BRANCH_CLASS}</u> and the latter will be saved into database. <br/><br/><b>When we + * need this</b>: Since we have a number of projects working with the same database (like Poulpe and JCommune), they all + * have their own custom classes for instance for {@link Branch} and thus if we use a default OID generator, we'll end + * up having Poulpe saving its own Branch with its own class to the database, and JCommune can't read it since it has + * its own Branch and it expects a different value stored in {@code acl_class} table. But if we use different classes + * and we covert their class name to the same {@code acl_class}, then we can use the same ACL records in Poulpe and + * JCommune. This is required because Poulpe gives the permissions to Branches and JCommune needs to read those + * permissions. <br/><br/><b>Other notes:</b> <ul><li>This class will be empty without any rules if created with default + * constructor, thus if you want to ensure you use the same string constants for all the projects, you should use {@link + * #createDefaultGenerator()} which contains mapping for entities in common modules and fill it with additional rules + * specific to your project.</li> <li>While specifying the conversion rules for the class, not only the very specified + * class is going to be converted to the specific string, but also if the object is of assignable class ({@code object + * instanceof Class or any of its ancestors} ), then this conversion rule still is going to be applied. Thus if the rule + * states Branch->BRANCH, then PoulpeBranch is also going to be converted to BRANCH. This is also important because of + * Hibernate that creates proxies </li></ul> + * + * @author stanislav bashkirtsev + */ +@ThreadSafe +public class TypeConvertingObjectIdentityGenerator { + /** + * Creates an instance of {@link ObjectIdentity} from the specified id of the entity and the type of it (it's not + * necessary a class name). + * + * @param id a database id of the entity we want to create an object identity for, must be greater than 0 + * @param type a type of the entity, it can be either a class or a result of conversion rule (e.g. either + * {@link Branch} or <u>BRANCH_CLASS</u> or anything else, see class + * JavaDocs for more details. Note, that if a conversion rules are applied for this entity and you're + * specifying the wrong type here, you'll end-up with an invalid Object Identity that can't be found in + * database because its type is absent in {@code acl_class}. + * @return an object identity constructed from the specified id of the entity and the entity type (either a simple + * class name or a converted value) + * @throws IllegalArgumentException if the specified id is zero or less (aka not persisted yet) + * @see #addConversionRule(Class, String) + * @see #setAdditionalConversionRules + */ + public ObjectIdentity createObjectIdentity(@Min(1) long id, String type) { + Validate.isTrue(id > 0, "Entity must be persisted before creating Secured OID for it!"); + return new ObjectIdentityImpl(type, id); + } + + /** + * Creates an Object Identity (OID) for the specified entity using the conversion rules set into generator. + * + * @param domainObject an entity that is stored into a database and which is going to be an Object Identity + * (permissions will be assigned to SID to do something with this object). Must be persisted + * already and have its id. + * @return an object identity with the ID of specified entity and a type according to the conversion rules. If no + * conversion rule was found for the class of specified object, then it's {@link + * Object#getClass()#getSimpleName} will be used as the OID type). + * @throws IllegalArgumentException if the specified domain object is not persistent (it's ID is not a positive + * number) + * @see #addConversionRule(Class, String) + * @see #setAdditionalConversionRules + */ + public ObjectIdentity getObjectIdentity(@Nonnull Entity domainObject) { + String type = getType(domainObject); + return createObjectIdentity(domainObject.getId(), type); + } + + /** + * You don't usually need an empty OID generator since there are common entities with common names that are shared + * between projects within JTalks, that's why you can use this factory method that creates a generator with already + * filled values, see the method body to understand for what classes the conversion rules are set. Afterwards you + * can fill the class with your custom classes specific only to your project. Note, that if an entity was placed in + * the common modules of JTalks, it might make sense to add it to this method. + * + * @return an OID generator with already filled type conversion rules + */ + public static TypeConvertingObjectIdentityGenerator createDefaultGenerator() { + return new TypeConvertingObjectIdentityGenerator() + .addConversionRule(Group.class, "GROUP") + .addConversionRule(Branch.class, "BRANCH") + .addConversionRule(Section.class, "SECTION") + .addConversionRule(Component.class, "COMPONENT"); + } + + /** + * Adds additional conversion rule to the generator. + * + * @param entityClass a class to covert its name to the string + * @param convertTo a string the specified class will be converted while creating an Object Identity + * @return this + * @see #setAdditionalConversionRules + */ + public TypeConvertingObjectIdentityGenerator addConversionRule(Class entityClass, String convertTo) { + oidClassToTypeMap.put(entityClass, convertTo); + return this; + } + + /** + * Iterates through all the conversion rules and searches for the one applicable to the specified entity. If a + * conversion rule was set via {@link #addConversionRule(Class, String)} or {@link #setAdditionalConversionRules}, + * then the string will be returned representing the type of specified object, otherwise a {@link + * Class#getSimpleName()} of the specified object will be returned. + * + * @param domainObject an object to find its conversion rule or to generate it from its class's simple name + * @return a type of the object identity according to the conversion rules or the class's simple name if no rule was + * found for this entity + */ + private String getType(Object domainObject) { + for (Map.Entry<Class, String> nextConversionPair : oidClassToTypeMap.entrySet()) { + if (nextConversionPair.getKey().isInstance(domainObject)) { + return nextConversionPair.getValue(); + } + } + return domainObject.getClass().getSimpleName(); + } + + /** + * Adds the specified conversion rules to the generator. <b>Note</b>, that it doesn't remove old rules, but it can + * override them if the key value of the map will be the same. + * + * @param oidClassToTypeMap see {@link #addConversionRule(Class, String)} for what each entry of the map means + */ + public void setAdditionalConversionRules(@Nonnull Map<Class, String> oidClassToTypeMap) { + this.oidClassToTypeMap.putAll(oidClassToTypeMap); + } + + private final Map<Class, String> oidClassToTypeMap = new ConcurrentHashMap<Class, String>(); +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclAction.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclAction.java new file mode 100644 index 0000000000..8aa5901a28 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclAction.java @@ -0,0 +1,82 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.builders; + +import org.jtalks.common.model.entity.Entity; +import org.jtalks.common.model.permissions.JtalksPermission; + +import javax.annotation.Nonnull; + +/** + * Represents a set of actions that can be made with permissions like grant, delete and restrict. This interface should + * be used along with other interfaces in the package in order to create a finished chain of methods to finish granting + * the permissions. + * + * @author stanislav bashkirtsev + * @see AclTo + * @see AclFrom + * @see AclOn + * @see AclFlush + * @since 0.13 + */ +public interface AclAction<T extends Entity> { + /** + * Gives the access for the action represented by the specified permission. + * + * @param permissions the permissions to be given to some Sid + * @return next interface to continue the chain of ACL methods to be able to specify the Sid you're going to grant + * access to + */ + AclTo<T> grant(@Nonnull JtalksPermission... permissions); + + /** + * Restricts the access for the action represented by the specified permission. + * + * @param permissions the permissions to be restricted to some Sid provided further in the chain of methods + * @return next interface to continue the chain of acl to be able to specify the Sid to restrict the permission to + */ + AclTo<T> restrict(@Nonnull JtalksPermission... permissions); + + /** + * Removes the access for the action represented by the specified permission. No matter whether this permission was + * granted before or it was restricted, the removal of the permission will treat them both as just ACL permissions. + * + * @param permissions the permissions to be removed from some Sid + * @return next interface to continue the chain of acl related methods to be able to specify the Sid to remove its + * permissions + */ + AclFrom<T> delete(@Nonnull JtalksPermission... permissions); + + /** + * Represents all the actions that can be undertaken with the permissions (effectively the are the same as the + * methods in this interface). This might be useful to the implementations of the interface to make their + * code/performance more effective. + */ + enum Actions { + /** + * @see AclAction#grant(JtalksPermission...) + */ + GRANT, + /** + * @see AclAction#delete(JtalksPermission...) + */ + DELETE, + /** + * @see AclAction#restrict(JtalksPermission...) + */ + RESTRICT + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclBuilders.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclBuilders.java new file mode 100644 index 0000000000..1233f2da12 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclBuilders.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.builders; + +import org.jtalks.common.model.entity.Entity; +import org.jtalks.jcommune.service.security.acl.AclManager; + +/** + * @author stanislav bashkirtsev + */ +public class AclBuilders { + public <T extends Entity> AclAction<T> newBuilder(AclManager aclManager){ + return new CompoundAclBuilder<T>(aclManager); + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclFlush.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclFlush.java new file mode 100644 index 0000000000..1f8c0b060f --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclFlush.java @@ -0,0 +1,33 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.builders; + +/** + * Represents an action of flushing changes to the ACL structure to the database. + * + * @author stanislav bashkirtsev + * @see AclAction + * @see AclTo + * @see AclFrom + * @see AclOn + */ +public interface AclFlush { + /** + * Flushes the changes made during the construction of ACL to the database and cache. This is the last action in the + * building ACL structure. + */ + void flush(); +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclFrom.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclFrom.java new file mode 100644 index 0000000000..c2eeb72d7e --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclFrom.java @@ -0,0 +1,44 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.builders; + +import org.jtalks.common.model.entity.Entity; + +import javax.annotation.Nonnull; + +/** + * A step to set from what SIDs the permissions will be removed. This class only affects the removal of permissions, for + * granting or restricting you had to choose the appropriate method in {@link AclAction} which would lead you to the + * {@link AclTo}. + * + * @author stanislav bashkirtsev + * @see AclAction + * @see AclTo + * @see AclFrom + * @see AclOn + * @see AclFlush + * @since 0.13 + */ +public interface AclFrom<T extends Entity> { + /** + * Defines the SIDs (the object that had permissions) to remove their permissions from the ACL records. The + * permission record will be removed from database at all. + * + * @param sids the objects like users or user groups to remove the permissions from them + * @return the next step - setting on what object identity the permission being removing was previously set + */ + AclOn from(@Nonnull T... sids); +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclOn.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclOn.java new file mode 100644 index 0000000000..42a8419edd --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclOn.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.builders; + +import org.jtalks.common.model.entity.Entity; + +import javax.annotation.Nonnull; + +/** + * A step while creating/modifying ACL structure to assign the permissions on some object identity (like branch, topic, + * post). + * + * @author stanislav bashkirtsev + * @see AclTo + * @see AclFrom + * @see AclAction + * @see AclFlush + */ +public interface AclOn { + /** + * This method states for what object the SID will get a permission. Object Identity (or secured object) is always + * some object SIDs can do something with, e.g. it can be a branch, or a topic, or a post, or anything else. + * + * @param objectIdentity the secured object to set permissions to make actions on it + * @return the next step of the chain - flushing the changes to the database + */ + AclFlush on(@Nonnull Entity objectIdentity); +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclTo.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclTo.java new file mode 100644 index 0000000000..10cb2d5b94 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/AclTo.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.builders; + +import org.jtalks.common.model.entity.Entity; + +import javax.annotation.Nonnull; + +/** + * A step while creating/modifying ACL structure to assign permissions to some sid entity. Sid is an object that can + * undertake some actions, like user or group of users. + * + * @author stanislav bashkirtsev + * @see AclAction + * @see AclFrom + * @see AclOn + * @see AclFlush + * @since 0.13 + */ +public interface AclTo<T extends Entity> { + /** + * Assigns (or restrict) the permission to the specified SIDs. + * + * @param sids the objects (users, groups of users) to get permissions to undertake some action on object identity + * @return the next action to be performed - choosing the object identity to assign permissions for + */ + AclOn to(@Nonnull T... sids); +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/CompoundAclBuilder.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/CompoundAclBuilder.java new file mode 100644 index 0000000000..04e29e2981 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/CompoundAclBuilder.java @@ -0,0 +1,140 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.builders; + +import org.jtalks.common.model.entity.Entity; +import org.jtalks.common.model.permissions.JtalksPermission; +import org.jtalks.jcommune.service.security.acl.AclManager; +import org.jtalks.jcommune.service.security.acl.sids.JtalksSidFactory; +import org.springframework.security.acls.model.Permission; +import org.springframework.security.acls.model.Sid; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * The class that implements all the AclBuilder related interfaces like {@link AclTo} or {@link AclFlush}, so it + * actually is a combination of ACL operations. Don't use it directly, use {@link AclBuilders} instead to upcast it to + * the respective interfaces. The only operation that actually pushes the changes to the data store is {@link + * #flush()}. + * + * @author stanislav bashkirtsev + * @see AclBuilders + */ +public class CompoundAclBuilder<T extends Entity> implements AclAction<T>, AclTo<T>, AclFrom<T>, AclOn, AclFlush { + private final List<Permission> permissions = new ArrayList<Permission>(); + private final List<Sid> sids = new ArrayList<Sid>(); + private final AclManager aclManager; + private JtalksSidFactory sidFactory = new JtalksSidFactory(); + private Entity objectIdentity; + private Actions action; + + /** + * Constructs the full blown acl builder, usually you shouldn't use this constructor and instead work with {@link + * AclBuilders}, but if you're writing tests or creating your own builders API, this might be useful for you. + * + * @param aclManager acl builder works with the ACL Manager in order to access the data store and actually work with + * permissions + */ + public CompoundAclBuilder(@Nonnull AclManager aclManager) { + this.aclManager = aclManager; + } + + /** + * {@inheritDoc} + */ + @Override + public AclTo<T> grant(@Nonnull JtalksPermission... permissions) { + addPermissions(Actions.GRANT, permissions); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public AclTo<T> restrict(@Nonnull JtalksPermission... permissions) { + addPermissions(Actions.RESTRICT, permissions); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public AclFrom<T> delete(@Nonnull JtalksPermission... permissions) { + addPermissions(Actions.DELETE, permissions); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public AclOn from(@Nonnull T... sids) { + this.sids.addAll(sidFactory.create(Arrays.asList(sids))); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public AclOn to(@Nonnull T... sids) { + this.sids.addAll(sidFactory.create(Arrays.asList(sids))); + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public AclFlush on(@Nonnull Entity objectIdentity) { + this.objectIdentity = objectIdentity; + return this; + } + + /** + * {@inheritDoc} + */ + @Override + public void flush() { + if (action == Actions.GRANT) { + aclManager.grant(clone(sids), clone(permissions), objectIdentity); + } else if (action == Actions.RESTRICT) { + aclManager.restrict(clone(sids), clone(permissions), objectIdentity); + } else { + aclManager.delete(clone(sids), clone(permissions), objectIdentity); + } + } + + /** + * {@inheritDoc} + */ + private void addPermissions(Actions action, JtalksPermission... permissions) { + this.permissions.addAll(Arrays.asList(permissions)); + this.action = action; + } + + /** + * {@inheritDoc} + */ + private <T> List<T> clone(List<T> list) { + return new ArrayList<T>(list); + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/package-info.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/package-info.java new file mode 100644 index 0000000000..609bc799d6 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/builders/package-info.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * Classes within this package relate to Spring ACL and granting/restricting/deleting permissions with handy classes + * and methods. The main class here is {@link AclBuilders} which can create ACL builders to be used in order to + * construct granting or any other operation on the permissions. Note, that there are several small interfaces like + * {@link AclAction} or {@link AclTo} that contain methods related to some particular step in creating full-blown ACL. + * You should deal with these interfaces again through {@link AclBuilders}. + */ +package org.jtalks.jcommune.service.security.acl.builders; \ No newline at end of file diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/AnonymousUserSid.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/AnonymousUserSid.java new file mode 100644 index 0000000000..1076411e33 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/AnonymousUserSid.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.sids; + +import javax.annotation.Nullable; + +/** + * An implementation of the sid particularly for anonymous users. This class is package-private, use {@link + * UserSid#isAnonymous(String)} and {@link UserSid#createAnonymous()} to work with + * this class outside of the package. + * + * @author stanislav bashkirtsev + */ +class AnonymousUserSid extends UserSid { + /** + * This is the string representation of all the anonymous users in Spring Security. + */ + public static final String ANONYMOUS_USER_NAME = "anonymousUser"; + /** + * The sid id of all of the anonymous users is the constant. + * + * @see UniversalSid#getSidId() + */ + public static final String ANONYMOUS_USER_SID_ID = SID_PREFIX + SID_NAME_SEPARATOR + ANONYMOUS_USER_NAME; + /** + * We don't need to create each time a new anonymous user since it always will be the same, so we can always work + * with this instance. + */ + private static final AnonymousUserSid ANONYMOUS_USER = new AnonymousUserSid(); + + /** + * Creates a sid with the sid id equals to {@link #ANONYMOUS_USER_SID_ID}. + */ + private AnonymousUserSid() { + super(ANONYMOUS_USER_SID_ID); + } + + /** + * Defines whether the specified string is the name of the anonymous user which is equal to {@link + * #ANONYMOUS_USER_NAME}. + * + * @param principal the principal to understand whether it's an anonymous user or not + * @return {@code true} if the specified principal is an anonymous user, {@code false} if it's {@code null} or + * doesn't equal to {@link #ANONYMOUS_USER_NAME} + */ + public static boolean isAnonymous(@Nullable String principal) { + return ANONYMOUS_USER_NAME.equals(principal); + } + + /** + * Returns all the time the same instance of the anonymous user. Since they shouldn't ever differ and this instance + * is immutable, this is pretty enough. + * + * @return the instance of anonymous user + */ + public static AnonymousUserSid create() { + return ANONYMOUS_USER; + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/JtalksSidFactory.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/JtalksSidFactory.java new file mode 100644 index 0000000000..36c310d259 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/JtalksSidFactory.java @@ -0,0 +1,154 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.sids; + +import org.jtalks.common.model.entity.Entity; +import org.jtalks.common.model.entity.Group; +import org.jtalks.common.model.entity.User; +import org.jtalks.jcommune.model.entity.UserInfo; +import org.springframework.security.acls.domain.GrantedAuthoritySid; +import org.springframework.security.acls.domain.PrincipalSid; +import org.springframework.security.acls.model.Sid; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Decides what implementation of {@link Sid} should be created by the string representation of the sid name (or sid id) + * or its class or whatever. There are might be either standard {@link Sid}s or custom sids like {@link UserGroupSid}. + * If you want to add another possible implementation, take a look at the methods {@link #create(Entity)} and {@link + * #parseCustomSid(String)}. + * + * @author stanislav bashkirtsev + * @see Sid + * @see UniversalSid + */ +public class JtalksSidFactory implements SidFactory { + /** + * This is a static factory, it shouldn't be instantiated. + */ + public JtalksSidFactory() { + } + + /** + * {@inheritDoc} + */ + @Override + public Sid createPrincipal(Authentication authentication) { + Object principal = authentication.getPrincipal(); + if (principal instanceof UserInfo) { + return new UserSid((UserInfo) principal); + } else if (UserSid.isAnonymous(principal.toString())) { + return UserSid.createAnonymous(); + } else { + return new UserSid(principal.toString()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public List<? extends Sid> createGrantedAuthorities(Collection<? extends GrantedAuthority> grantedAuthorities) { + List<Sid> sids = new ArrayList<Sid>(); + for (GrantedAuthority authority : grantedAuthorities) { + sids.add(new GrantedAuthoritySid(authority)); + } + return sids; + } + + /** + * Creates a list of sids by the underlying classes that were specified. + * + * @param receivers the list of receivers to be wrapped with the respective implementations of {@link Sid}s + * @return the list of sids that wrap the specified receivers. The list might contain {@code null}s if some of + * specified receivers don't have the matching {@link Sid} implementation. + * @see #create(Entity) + */ + public List<Sid> create(List<? extends Entity> receivers) { + List<Sid> sids = new ArrayList<Sid>(receivers.size()); + for (Entity next : receivers) { + sids.add(create(next)); + } + return sids; + } + + /** + * Creates the instance of custom sid that works with specified {@code receiver}. E.g. if the {@link User} or one of + * its children was specified, then a {@link UserSid} instance will be returned. + * + * @param receiver the object to be wrapped with the Sid to become a real receiver from the Spring Security + * perspective + * @return the instance of custom sid that works with specified {@code receiver} or {@code null} if no respective + * sid class was found + */ + public Sid create(Entity receiver) { + if (User.class.isAssignableFrom(receiver.getClass())) { + return new UserSid(receiver.getId()); + } else if (Group.class.isAssignableFrom(receiver.getClass())) { + return new UserGroupSid((Group) receiver); + } else { + return null; + } + } + + /** + * Looks at the format of the {@code sidName} and finds out what sid implementation should be created. If the + * specified name doesn't comply with the format of custom sids (prefix + {@link UniversalSid#SID_NAME_SEPARATOR} + + * entity id), then ordinary Spring Security implementations are used (either {@link PrincipalSid} or {@link + * GrantedAuthoritySid} which is defined by the second parameter {@code principal}. + * + * @param sidName the name of the sid (its id) to look at its format and decide what implementation of sid should + * be created + * @param principal pass {@code true} if it's some kind of entity ID (like user or group), or {@code false} if it's + * some standard role ({@link GrantedAuthoritySid} + * @return created instance of sid that has the {@code sidName} as the sid id inside + */ + @Override + public Sid create(@Nonnull String sidName, boolean principal) { + Sid toReturn = parseCustomSid(sidName); + if (toReturn == null) { + if (principal) { + toReturn = new PrincipalSid(sidName); + } else { + toReturn = new GrantedAuthoritySid(sidName); + } + } + return toReturn; + } + + /** + * Iterates through all the available sid prefixes and finds out what of them suites more to the specified sid + * name. + * + * @param sidName the name of the sid to find the respective sid implementation + * @return the instantiated sid implementation that complies with the pattern of specified sid name or {@code null} + * if no mapping for that name was found and there are no appropriate custom implementations of sid + */ + private static Sid parseCustomSid(String sidName) { + if (sidName.startsWith(UserGroupSid.SID_PREFIX)) { + return new UserGroupSid(sidName); + } else if (sidName.startsWith(UserSid.SID_PREFIX)) { + return new UserSid(sidName); + } else { + return null; + } + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/SidFactory.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/SidFactory.java new file mode 100644 index 0000000000..43d1959ac4 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/SidFactory.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.sids; + +import org.springframework.security.acls.model.Sid; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; +import java.util.List; + +/** + * Typical application that uses Spring ACL won't need anything but {@link org.springframework.security.acls.domain.PrincipalSid} + * or {@link org.springframework.security.acls.domain.GrantedAuthoritySid}, + * but sometimes we need to extend this list of implementations or replace it for more complicated scenarios when + * default capabilities of Spring ACL is not enough. You can implement this factory and inject it into different classes + * that work with {@link Sid}s in order them to create <i>your</i> SIDs. + * + * @author stanislav bashkirtsev + */ +public interface SidFactory { + /** + * The Factory Method that creates a particular implementation of {@link Sid} depending on the arguments. + * + * @param sidName the name of the sid representing its unique identifier. In typical ACL database schema it's + * located in table {@code acl_sid} table, {@code sid} column. + * @param principal whether it's a user or granted authority like role + * @return the instance of Sid with the {@code sidName} as an identifier + */ + Sid create(String sidName, boolean principal); + + /** + * Creates a principal-like sid from the authentication information. + * + * @param authentication the authentication information that can provide principal and thus the sid's id will be + * dependant on the value inside + * @return a sid with the ID taken from the authentication information + */ + Sid createPrincipal(Authentication authentication); + + List<? extends Sid> createGrantedAuthorities(Collection<? extends GrantedAuthority> grantedAuthorities); +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/UniversalSid.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/UniversalSid.java new file mode 100644 index 0000000000..6826047ed4 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/UniversalSid.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.sids; + +import org.springframework.security.acls.model.Sid; + +/** + * This interface is dedicated to join all the custom {@link Sid}s into one group that can be accessed with unified + * method to obtain their identifier (it will be saved into the ACL table). + * IMPORTANT: Every custom Sid should implement this interface + * @author stanislav bashkirtsev + */ +public interface UniversalSid extends Sid { + + /** + * Gets the unique identifier of the SID (usually a database ID of the entity). It is string since the ACL tables + * require this. + * + * @return the unique identifier of the SID (usually a database ID of the entity) + */ + boolean isPrincipal(); + + public String getSidId(); + /** + * All the custom Sids, when they implement the {@link UniversalSid} should obey some pattern since they are + * saved as string to the DB. This pattern usually will be: some string identifier of the Sid implementation + ":" + + * the database id of the entity which is a Sid. Like in case of user groups: {@code usergroup:2123}. + */ + static final String SID_NAME_SEPARATOR = ":"; + + /** + * Since some of Sid implementations accept different arguments while creation, like String, and these arguments + * have to be of some particular pattern, we need such exception that is thrown when the passed argument is of wrong + * format. + * + * @author stanislav bashkirtsev + */ + public static class WrongFormatException extends RuntimeException { + /** + * @param sidName the name of the sid (actually the value which is of wrong format) + */ + public WrongFormatException(String sidName) { + super("Sid name is of incorrect format: " + sidName); + } + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/UserGroupSid.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/UserGroupSid.java new file mode 100644 index 0000000000..99dc53e145 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/UserGroupSid.java @@ -0,0 +1,150 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.sids; + +import org.jtalks.common.model.entity.Group; + +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.Immutable; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * This class does the same as {@link org.springframework.security.acls.domain.PrincipalSid} does for users. More + * precisely it contains some security ID that is used by Spring ACL to associate the owner to the permissions it owns. + * Thus this class contains an ID that will be saved into database ({@code acl_sids#sid, acl_object_identity#owner_sid, + * acl_entry#sid}). + * + * @author stanislav bashkirstev + */ +@Immutable +public class UserGroupSid implements UniversalSid { + public static final String SID_PREFIX = "usergroup"; + private final String groupId; + + /** + * @param sidId passes the direct sid id which should obey the format "usergroup:[group_id]" + * @throws UniversalSid.WrongFormatException + * if the format of the passed string is wrong + */ + public UserGroupSid(@Nonnull String sidId) { + this.groupId = parseGroupId(sidId); + } + + /** + * Constructs SID by the group id. The {@link #getSidId()} will consist of word 'usrgroup:[specified id]'. + * + * @param groupId the id of the group that will own permissions for some actions on some objects + * @see Group#getId() + */ + public UserGroupSid(@Nonnegative long groupId) { + this.groupId = String.valueOf(groupId); + } + + /** + * Constructs a SID by retrieving the group id from the {@link Group} object. + * + * @param group a group to take its database id + * @see Group#getId() + */ + public UserGroupSid(@Nonnull Group group) { + this.groupId = String.valueOf(group.getId()); + } + + private String parseGroupId(String sidId) { + String[] splitted = sidId.split(Pattern.quote(":")); + if (splitted.length != 2) { + throw new WrongFormatException(sidId); + } + return splitted[1]; + } + + /** + * Gets the id of the {@link Group} which this SID is actually is. + * + * @return the id of the {@link Group} which this SID is actually is + */ + public String getGroupId() { + return groupId; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isPrincipal() { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public String getSidId() { + return SID_PREFIX + UniversalSid.SID_NAME_SEPARATOR + groupId; + } + + /** + * Creates a list of sids from the specified list of groups. + * + * @param groups the array of groups that should be turned into the list of sids, might be empty + * @return the list of sids with the same size as it was specified + * @see #UserGroupSid(Group) + */ + public static List<UserGroupSid> create(@Nonnull Group... groups) { + List<UserGroupSid> userGroupSids = new ArrayList<UserGroupSid>(); + for (Group group : groups) { + userGroupSids.add(new UserGroupSid(group)); + } + return userGroupSids; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserGroupSid that = (UserGroupSid) o; + if (!groupId.equals(that.groupId)) { + return false; + } + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return groupId.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return getSidId(); + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/UserSid.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/UserSid.java new file mode 100644 index 0000000000..df29f93005 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/security/acl/sids/UserSid.java @@ -0,0 +1,130 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.security.acl.sids; + +import org.jtalks.jcommune.model.entity.UserInfo; +import org.springframework.security.acls.domain.PrincipalSid; + +import javax.annotation.Nonnegative; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.regex.Pattern; + +/** + * @author stanislav bashkirstev + */ +public class UserSid extends PrincipalSid implements UniversalSid { + public final static String SID_PREFIX = "user"; + private final String userId; + + public UserSid(@Nonnull String sidId) { + super(sidId); + this.userId = parseUserId(sidId); + } + + public UserSid(@Nonnegative long userId) { + super(SID_PREFIX + String.valueOf(userId)); + this.userId = String.valueOf(userId); + } + + public UserSid(@Nonnull UserInfo user) { + this(user.getId()); + } + + private String parseUserId(String sidId) { + String[] splitted = sidId.split(Pattern.quote(":")); + if (splitted.length != 2) { + throw new WrongFormatException(sidId); + } + return splitted[1]; + } + + public String getUserId() { + return userId; + } + + @Override + public String getPrincipal() { + return getSidId(); + } + + /** + * Defines whether the specified principal is an anonymous user (the one that's name is {@code anonymousUser}). + * + * @param principal the principal to decide whether it's an anonymous user + * @return {@code true} if the specified principal is an anonymous user + */ + public static boolean isAnonymous(@Nullable String principal) { + return AnonymousUserSid.isAnonymous(principal); + } + + /** + * Creates an anonymous user which actually is an instance of {@link AnonymousUserSid} and has a sid id equal to + * {@link AnonymousUserSid#ANONYMOUS_USER_SID_ID}. + * + * @return the instance of the user sid that represents an anonymous user + */ + public static UserSid createAnonymous() { + return AnonymousUserSid.create(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getSidId() { + return SID_PREFIX + UniversalSid.SID_NAME_SEPARATOR + userId; + } + + @Override + public boolean isPrincipal() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || !(o instanceof PrincipalSid)) { + return false; + } + PrincipalSid that = (PrincipalSid) o; + if (!getPrincipal().equals(that.getPrincipal())) { + return false; + } + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return userId.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return getSidId(); + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalAuthenticator.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalAuthenticator.java index 62ec033758..116557f021 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalAuthenticator.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalAuthenticator.java @@ -21,28 +21,33 @@ import org.jtalks.common.model.dao.hibernate.GenericDao; import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.entity.User; -import org.jtalks.common.service.security.SecurityContextHolderFacade; +import org.jtalks.common.service.security.SecurityContextFacade; import org.jtalks.jcommune.model.dao.UserDao; +import org.jtalks.jcommune.model.dto.LoginUserDto; import org.jtalks.jcommune.model.dto.RegisterUserDto; import org.jtalks.jcommune.model.dto.UserDto; import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.UserInfo; +import org.jtalks.jcommune.plugin.api.PluginLoader; import org.jtalks.jcommune.plugin.api.core.AuthenticationPlugin; import org.jtalks.jcommune.plugin.api.core.Plugin; import org.jtalks.jcommune.plugin.api.core.RegistrationPlugin; import org.jtalks.jcommune.plugin.api.exceptions.NoConnectionException; +import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.exceptions.UnexpectedErrorException; import org.jtalks.jcommune.service.Authenticator; import org.jtalks.jcommune.service.PluginService; -import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.jtalks.jcommune.service.exceptions.UserTriesActivatingAccountAgainException; import org.jtalks.jcommune.service.nontransactional.EncryptionService; import org.jtalks.jcommune.service.nontransactional.ImageService; import org.jtalks.jcommune.service.nontransactional.MailService; -import org.jtalks.jcommune.plugin.api.PluginLoader; import org.jtalks.jcommune.service.security.AdministrationGroup; +import org.jtalks.jcommune.service.util.AuthenticationStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.aop.framework.Advised; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -59,7 +64,6 @@ import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; -import org.jtalks.jcommune.model.dto.LoginUserDto; /** * Serves for authentication and registration user. @@ -85,7 +89,7 @@ public class TransactionalAuthenticator extends AbstractTransactionalEntityServi private PluginLoader pluginLoader; private EncryptionService encryptionService; private AuthenticationManager authenticationManager; - private SecurityContextHolderFacade securityFacade; + private SecurityContextFacade securityFacade; private RememberMeServices rememberMeServices; private SessionAuthenticationStrategy sessionStrategy; private Validator validator; @@ -112,7 +116,7 @@ public TransactionalAuthenticator(PluginLoader pluginLoader, UserDao dao, GroupD MailService mailService, ImageService avatarService, PluginService pluginService, - SecurityContextHolderFacade securityFacade, + SecurityContextFacade securityFacade, RememberMeServices rememberMeServices, SessionAuthenticationStrategy sessionStrategy, Validator validator, @@ -135,9 +139,9 @@ public TransactionalAuthenticator(PluginLoader pluginLoader, UserDao dao, GroupD * {@inheritDoc} */ @Override - public boolean authenticate(LoginUserDto loginUserDto, HttpServletRequest request, + public AuthenticationStatus authenticate(LoginUserDto loginUserDto, HttpServletRequest request, HttpServletResponse response) throws UnexpectedErrorException, NoConnectionException { - boolean result; + AuthenticationStatus result; JCUser user; try { user = getByUsername(loginUserDto.getUserName()); @@ -146,6 +150,10 @@ public boolean authenticate(LoginUserDto loginUserDto, HttpServletRequest reques LOGGER.info("User was not found during login process, username = {}, IP={}", loginUserDto.getUserName(), loginUserDto.getClientIp()); result = authenticateByPluginAndUpdateUserInfo(loginUserDto, true, request, response); + } catch(DisabledException e) { + LOGGER.info("DisabledException: username = {}, IP={}, message={}", + new String[]{loginUserDto.getUserName(), loginUserDto.getClientIp(), e.getMessage()}); + result = AuthenticationStatus.NOT_ENABLED; } catch (AuthenticationException e) { LOGGER.info("AuthenticationException: username = {}, IP={}, message={}", new String[]{loginUserDto.getUserName(), loginUserDto.getClientIp(), e.getMessage()}); @@ -159,11 +167,11 @@ public boolean authenticate(LoginUserDto loginUserDto, HttpServletRequest reques * * @param loginUserDto DTO object which represent authentication information * @param newUser is new user or not - * @return true if authentication was successful, otherwise false + * @return AUTHENTICATED if authentication was successful, otherwise AUTHENTICATION_FAIL * @throws UnexpectedErrorException if some unexpected error occurred * @throws NoConnectionException if some connection error occurred */ - private boolean authenticateByPluginAndUpdateUserInfo(LoginUserDto loginUserDto, boolean newUser, + private AuthenticationStatus authenticateByPluginAndUpdateUserInfo(LoginUserDto loginUserDto, boolean newUser, HttpServletRequest request, HttpServletResponse response) throws UnexpectedErrorException, NoConnectionException { @@ -179,13 +187,13 @@ private boolean authenticateByPluginAndUpdateUserInfo(LoginUserDto loginUserDto, Map<String, String> authInfo = authenticateByAvailablePlugin(encodedUsername, passwordHash); if (authInfo.isEmpty() || !authInfo.containsKey("email") || !authInfo.containsKey("username")) { LOGGER.info("Could not authenticate user '{}' by plugin.", loginUserDto.getUserName()); - return false; + return AuthenticationStatus.AUTHENTICATION_FAIL; } JCUser user = saveUser(authInfo, passwordHash, newUser); try { return authenticateDefault(user, loginUserDto.getUserName(), loginUserDto.isRememberMe(), request, response); } catch (AuthenticationException e) { - return false; + return AuthenticationStatus.AUTHENTICATION_FAIL; } } @@ -197,15 +205,15 @@ private boolean authenticateByPluginAndUpdateUserInfo(LoginUserDto loginUserDto, * @param rememberMe remember this user or not * @param request HTTP request * @param response HTTP response - * @return true if authentication was successful, otherwise false + * @return AUTHENTICATED if authentication was successful, otherwise AUTHENTICATION_FAIL * @throws AuthenticationException */ - private boolean authenticateDefault(JCUser user, String password, boolean rememberMe, + private AuthenticationStatus authenticateDefault(JCUser user, String password, boolean rememberMe, HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), password); - token.setDetails(user); + token.setDetails(new UserInfo(user)); Authentication auth = authenticationManager.authenticate(token); securityFacade.getContext().setAuthentication(auth); if (auth.isAuthenticated()) { @@ -214,9 +222,9 @@ private boolean authenticateDefault(JCUser user, String password, boolean rememb rememberMeServices.loginSuccess(request, response, auth); } user.updateLastLoginTime(); - return true; + return AuthenticationStatus.AUTHENTICATED; } - return false; + return AuthenticationStatus.AUTHENTICATION_FAIL; } /** @@ -318,8 +326,9 @@ public BindingResult register(RegisterUserDto registerUserDto) BindingResult jcErrors = new BeanPropertyBindingResult(registerUserDto, "newUser"); validator.validate(registerUserDto, jcErrors); UserDto userDto = registerUserDto.getUserDto(); - String encodedPassword = (userDto.getPassword() == null || userDto.getPassword().isEmpty()) ? "" - : encryptionService.encryptPassword(userDto.getPassword()); + String notEncodedPassword = userDto.getPassword(); + String encodedPassword = (notEncodedPassword == null || notEncodedPassword.isEmpty()) ? "" + : encryptionService.encryptPassword(notEncodedPassword); userDto.setPassword(encodedPassword); registerByPlugin(userDto, true, result); mergeValidationErrors(jcErrors, result); @@ -330,10 +339,46 @@ public BindingResult register(RegisterUserDto registerUserDto) if (!result.hasErrors()) { storeRegisteredUser(userDto); } - } + } + + if(result.hasErrors()) { + userDto.setPassword(notEncodedPassword); + } + return result; } + /** + * {@inheritDoc} + */ + @Override + public void activateAccount(String uuid) throws NotFoundException, UserTriesActivatingAccountAgainException { + JCUser user = this.getDao().getByUuid(uuid); + if (user == null) { + LOGGER.warn("Could not activate user with UUID[{}] because it doesn't exist. Either it was removed from DB " + + "because too much time passed between registration and activation, or there is an error in link" + + ", might be possible the user searches for vulnerabilities in the forum.", uuid); + throw new NotFoundException(); + } else if (!user.isEnabled()) { + Group group = groupDao.getGroupByName(AdministrationGroup.USER.getName()); + user.addGroup(group); + user.setEnabled(true); + this.getDao().saveOrUpdate(user); + activateByPlugin(user.getUsername()); + LOGGER.info("User [{}] successfully activated", user.getUsername()); + } else { + LOGGER.warn("User [{}] tried to activate his account again, but that's impossible. Either he clicked the " + + "link again, or someone looks for vulnerabilities in the forum.", user.getUsername()); + throw new UserTriesActivatingAccountAgainException(); + } + } + + + private void activateByPlugin(String username) { + AuthenticationPlugin authPlugin = (AuthenticationPlugin) pluginLoader.getPluginByClassName(AuthenticationPlugin.class); + if (authPlugin != null && authPlugin.isEnabled()) authPlugin.activate(username); + } + /** * Performs registration or validation by available registration plugins * diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalComponentService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalComponentService.java index d3117e6d25..15084168ea 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalComponentService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalComponentService.java @@ -45,6 +45,9 @@ public class TransactionalComponentService extends AbstractTransactionalEntitySe public static final String COPYRIGHT_PROPERTY = "jcommune.copyright"; public static final String COMPONENT_FAVICON_ICO_PARAM = "jcommune.favicon.ico"; public static final String COMPONENT_FAVICON_PNG_PARAM = "jcommune.favicon.png"; + public static final String COMPONENT_SESSION_TIMEOUT = "jcommune.session_timeout"; + public static final String COMPONENT_EMAIL_NOTIFICATION = "jcommune.sending_notifications_enabled"; + public static final String COMPONENT_AVATAR_MAX_SIZE = "jcommune.avatar_max_size"; protected static final String COMPONENT_INFO_CHANGE_DATE_PROPERTY = "jcommune.info_change_date"; @@ -87,6 +90,10 @@ public void setComponentInformation(ComponentInformation componentInformation) { forumComponent.setProperty(LOGO_TOOLTIP_PROPERTY, componentInformation.getLogoTooltip()); forumComponent.setProperty(TITLE_PREFIX_PROPERTY, componentInformation.getTitlePrefix()); forumComponent.setProperty(COPYRIGHT_PROPERTY, componentInformation.getCopyright()); + + forumComponent.setProperty(COMPONENT_AVATAR_MAX_SIZE, componentInformation.getAvatarMaxSize()); + forumComponent.setProperty(COMPONENT_EMAIL_NOTIFICATION, String.valueOf(componentInformation.isEmailNotification())); + forumComponent.setProperty(COMPONENT_SESSION_TIMEOUT, componentInformation.getSessionTimeout()); if (!StringUtils.isEmpty(componentInformation.getLogo())) { forumComponent.setProperty(LOGO_PROPERTY, componentInformation.getLogo()); diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalGroupService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalGroupService.java new file mode 100644 index 0000000000..d7f9aa2c02 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalGroupService.java @@ -0,0 +1,200 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.service.transactional; + +import com.google.common.collect.Sets; +import org.jtalks.common.model.entity.Group; +import org.jtalks.common.model.entity.User; +import org.jtalks.common.service.exceptions.NotFoundException; +import org.jtalks.common.service.transactional.AbstractTransactionalEntityService; +import org.jtalks.common.validation.ValidationError; +import org.jtalks.common.validation.ValidationException; +import org.jtalks.jcommune.model.dao.GroupDao; +import org.jtalks.jcommune.model.dao.UserDao; +import org.jtalks.jcommune.model.dto.GroupAdministrationDto; +import org.jtalks.jcommune.model.dto.PageRequest; +import org.jtalks.jcommune.model.dto.SecurityGroupList; +import org.jtalks.jcommune.model.dto.UserDto; +import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.UserInfo; +import org.jtalks.jcommune.service.GroupService; +import org.jtalks.jcommune.service.exceptions.OperationIsNotAllowedException; +import org.jtalks.jcommune.service.security.AdministrationGroup; +import org.jtalks.jcommune.service.security.SecurityService; +import org.jtalks.jcommune.service.security.acl.AclManager; +import org.jtalks.jcommune.service.security.acl.sids.UserGroupSid; +import org.jtalks.jcommune.service.security.acl.sids.UserSid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import ru.javatalks.utils.general.Assert; + +import java.util.List; + + +/** + * @author alexander afanasiev + * @author stanislav bashkirtsev + */ +public class TransactionalGroupService extends AbstractTransactionalEntityService<Group, GroupDao> + implements GroupService { + + private final AclManager manager; + private final UserDao userDao; + private final SecurityService securityService; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * Create an instance of entity based service + * + * @param groupDao - data access object, which should be able do all CRUD + * operations. + * @param manager - ACL manager to operate with sids + * @param userDao - to perform all CRUD operations with users + * @param securityService - to get current user information. + * + */ + public TransactionalGroupService(GroupDao groupDao, + AclManager manager, + UserDao userDao, + SecurityService securityService) { + this.dao = groupDao; + this.manager = manager; + this.userDao = userDao; + this.securityService = securityService; + } + + /** + * {@inheritDoc} + */ + @Override + public List<Group> getAll() { + return dao.getAll(); + } + + @Override + public SecurityGroupList getSecurityGroups() { + return new SecurityGroupList(dao.getAll()).withAnonymousGroup(); + } + + /** + * {@inheritDoc} + */ + @Override + public List<Group> getByNameContains(String name) { + return dao.getByNameContains(name); + } + + /** + * {@inheritDoc} + */ + @Override + public List<Group> getByName(String name) { + return dao.getByName(name); + } + + @Override + public Page<UserDto> getPagedGroupUsers(long id, PageRequest pageRequest) { + int totalCount = dao.getGroupUserCount(id); + pageRequest.adjustPageNumber(totalCount); + return new PageImpl<>(dao.getGroupUsersPage(id, pageRequest), pageRequest, totalCount); + } + + /** + * {@inheritDoc} + */ + @Override + public void deleteGroup(Group group) throws NotFoundException { + Assert.throwIfNull(group, "group"); + if (!isGroupEditable(group.getName())) { + logger.warn("Attempt to delete pre-defined usergoup {}", group.getName()); + throw new OperationIsNotAllowedException("Pre-defined usergoup " + group.getName() + " cannot be deleted"); + } + for (User user : group.getUsers()) { + user.getGroups().remove(group); + userDao.saveOrUpdate((JCUser) user); + } + dao.delete(group); + + UserInfo currentUser = securityService.getCurrentUserBasicInfo(); + UserGroupSid sid = new UserGroupSid(group); + UserSid sidHeier = new UserSid(currentUser); + try { + manager.deleteSid(sid, sidHeier); + } catch (EmptyResultDataAccessException noSidError) { + throw new NotFoundException(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void saveGroup(Group group) { + Assert.throwIfNull(group, "group"); + group.setName(group.getName().trim()); + dao.saveOrUpdate(group); + } + + /** + * {@inheritDoc} + */ + @Override + public void saveOrUpdate(GroupAdministrationDto dto) throws NotFoundException { + assertGroupNameUnique(dto); + Group group = dto.getId() != null ? dao.get(dto.getId()) : new Group(); + if (group == null) { + throw new NotFoundException("Group with id " + dto.getId() + " is not found"); + } + if (!isGroupEditable(group.getName())) { + logger.warn("Attempt to edit pre-defined usergoup {}", group.getName()); + throw new OperationIsNotAllowedException("Pre-defined usergoup " + group.getName() + " is not editable"); + } + dao.saveOrUpdate(dto.fillEntity(group)); + } + + /** + * {@inheritDoc} + */ + @Override + public List<GroupAdministrationDto> getGroupNamesWithCountOfUsers() { + List<GroupAdministrationDto> groupNamesWithCountOfUsers = dao.getGroupNamesWithCountOfUsers(); + setEditableFlag(groupNamesWithCountOfUsers); + return groupNamesWithCountOfUsers; + } + + private void setEditableFlag(List<GroupAdministrationDto> groups) { + for (GroupAdministrationDto dto : groups) { + dto.setEditable(isGroupEditable(dto.getName())); + } + } + + private boolean isGroupEditable(String groupName) { + return !AdministrationGroup.isPredefinedGroup(groupName); + } + + /** + * Checks group name for uniqueness. + * @throws ValidationException if group name is already used + */ + private void assertGroupNameUnique(GroupAdministrationDto dto) { + Group existingGroup = dao.getGroupByName(dto.getName()); + if (!(existingGroup == null || (dto.getId() != null && existingGroup.getId() == dto.getId()))) { + throw new ValidationException(Sets.newHashSet((new ValidationError("name", "group.already_exists")))); + } + } +} \ No newline at end of file diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPermissionService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPermissionService.java index 8b924bcfad..cff5769b86 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPermissionService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPermissionService.java @@ -17,7 +17,7 @@ import org.jtalks.common.model.entity.Component; import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.permissions.JtalksPermission; -import org.jtalks.common.service.security.SecurityContextHolderFacade; +import org.jtalks.common.service.security.SecurityContextFacade; import org.jtalks.jcommune.model.dto.GroupsPermissions; import org.jtalks.jcommune.model.dto.PermissionChanges; import org.jtalks.jcommune.model.entity.Branch; @@ -25,10 +25,9 @@ import org.jtalks.jcommune.plugin.api.core.Plugin; import org.jtalks.jcommune.plugin.api.core.TopicPlugin; import org.jtalks.jcommune.plugin.api.filters.TypeFilter; -import org.jtalks.jcommune.service.security.AclClassName; -import org.jtalks.jcommune.service.security.AclGroupPermissionEvaluator; -import org.jtalks.jcommune.service.security.PermissionManager; -import org.jtalks.jcommune.service.security.PermissionService; +import org.jtalks.jcommune.service.security.*; +import org.jtalks.jcommune.service.security.acl.AclClassName; +import org.jtalks.jcommune.service.security.acl.AclGroupPermissionEvaluator; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; @@ -45,7 +44,7 @@ public class TransactionalPermissionService implements PermissionService { * {@code BranchPermissions.EDIT_OWN_POSTS} */ private static final String PERMISSION_FULLNAME_PATTERN = "%s.%s"; - private SecurityContextHolderFacade contextFacade; + private SecurityContextFacade contextFacade; private AclGroupPermissionEvaluator aclEvaluator; private PermissionManager permissionManager; private PluginLoader pluginLoader; @@ -54,7 +53,7 @@ public class TransactionalPermissionService implements PermissionService { * @param contextFacade to get {@link Authentication} object from security context * @param aclEvaluator to evaluate permissions */ - public TransactionalPermissionService(SecurityContextHolderFacade contextFacade, + public TransactionalPermissionService(SecurityContextFacade contextFacade, AclGroupPermissionEvaluator aclEvaluator, PermissionManager permissionManager) { this.contextFacade = contextFacade; diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPollService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPollService.java index aa525f9a71..31db58f167 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPollService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPollService.java @@ -18,12 +18,12 @@ import org.jtalks.common.model.dao.Crud; import org.jtalks.common.model.dao.GroupDao; import org.jtalks.common.model.permissions.GeneralPermission; -import org.jtalks.common.security.SecurityService; import org.jtalks.jcommune.model.entity.Poll; import org.jtalks.jcommune.model.entity.PollItem; import org.jtalks.jcommune.service.PollService; import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.service.security.AdministrationGroup; +import org.jtalks.jcommune.service.security.SecurityService; import org.springframework.security.access.prepost.PreAuthorize; import java.util.List; diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPostCommentService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPostCommentService.java index c7ccc807ff..53a23127ca 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPostCommentService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPostCommentService.java @@ -14,15 +14,19 @@ */ package org.jtalks.jcommune.service.transactional; +import org.joda.time.DateTime; import org.jtalks.common.model.dao.Crud; import org.jtalks.common.model.permissions.BranchPermission; -import org.jtalks.jcommune.model.entity.PostComment; import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.Post; +import org.jtalks.jcommune.model.entity.PostComment; +import org.jtalks.jcommune.plugin.api.service.PluginCommentService; import org.jtalks.jcommune.service.PostCommentService; import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.security.PermissionService; import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.prepost.PreAuthorize; /** * The implementation of {@link org.jtalks.jcommune.service.PostCommentService} @@ -31,7 +35,7 @@ */ public class TransactionalPostCommentService extends AbstractTransactionalEntityService<PostComment, Crud<PostComment>> implements - PostCommentService { + PostCommentService, PluginCommentService { private PermissionService permissionService; private UserService userService; @@ -53,13 +57,13 @@ public TransactionalPostCommentService(Crud<PostComment> dao, /** * {@inheritDoc} */ - @Override public PostComment updateComment(long id, String body, long branchId) throws NotFoundException { PostComment comment = get(id); checkHasUpdatePermission(comment, branchId); - comment.setBody(body); + comment.setUserChanged(userService.getCurrentUser()); + comment.setModificationDate(DateTime.now()); getDao().saveOrUpdate(comment); return comment; @@ -67,7 +71,7 @@ public PostComment updateComment(long id, String body, long branchId) throws Not /** * Checks if current user can edit review comments - * + * * @param comment * - comment to check permissions on * @param branchId @@ -81,7 +85,31 @@ private void checkHasUpdatePermission(PostComment comment, long branchId) { if (!(canEditOthersPosts && !comment.isCreatedBy(currentUser)) && !(canEditOwnPosts && comment.isCreatedBy(currentUser))) { - throw new AccessDeniedException("No permission to edit review comment"); + throw new AccessDeniedException("No permission to edit comments"); } } + + /** + * Another implementation needed to be accessed from plugin-api + * + * {@inheritDoc} + */ + @Override + public PostComment getComment(long id) throws NotFoundException { + return getDao().get(id); + } + + /** + * {@inheritDoc} + */ + @Override + @PreAuthorize("(hasPermission(#post.topic.branch.id, 'BRANCH', 'BranchPermission.DELETE_OWN_POSTS') and " + + "#comment.author.username == principal.username) or " + + "(hasPermission(#post.topic.branch.id, 'BRANCH', 'BranchPermission.DELETE_OTHERS_POSTS') and " + + "#comment.author.username != principal.username)") + public PostComment markCommentAsDeleted(Post post, PostComment comment) { + comment.setDeletionDate(new DateTime()); + getDao().saveOrUpdate(comment); + return comment; + } } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPostService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPostService.java index 0b823d4276..be2d5876bb 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPostService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPostService.java @@ -15,21 +15,26 @@ package org.jtalks.jcommune.service.transactional; import org.joda.time.DateTime; +import org.jtalks.common.model.dao.Crud; import org.jtalks.common.model.permissions.BranchPermission; -import org.jtalks.common.security.SecurityService; import org.jtalks.jcommune.model.dao.PostDao; import org.jtalks.jcommune.model.dao.TopicDao; import org.jtalks.jcommune.model.dto.PageRequest; import org.jtalks.jcommune.model.entity.*; +import org.jtalks.jcommune.plugin.api.PluginLoader; +import org.jtalks.jcommune.plugin.api.core.Plugin; +import org.jtalks.jcommune.plugin.api.core.TopicPlugin; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.jtalks.jcommune.plugin.api.filters.StateFilter; +import org.jtalks.jcommune.plugin.api.filters.TypeFilter; import org.jtalks.jcommune.plugin.api.service.PluginPostService; import org.jtalks.jcommune.service.BranchLastPostService; -import org.jtalks.jcommune.service.LastReadPostService; import org.jtalks.jcommune.service.PostService; import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.service.nontransactional.NotificationService; -import org.jtalks.jcommune.service.security.AclClassName; +import org.jtalks.jcommune.service.security.acl.AclClassName; import org.jtalks.jcommune.service.security.PermissionService; +import org.jtalks.jcommune.service.security.SecurityService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -42,7 +47,6 @@ import java.util.List; import java.util.Map; - /** * Post service class. This class contains method needed to manipulate with Post persistent entity. * @@ -57,10 +61,11 @@ public class TransactionalPostService extends AbstractTransactionalEntityService private TopicDao topicDao; private SecurityService securityService; private NotificationService notificationService; - private LastReadPostService lastReadPostService; private UserService userService; private BranchLastPostService branchLastPostService; private PermissionService permissionService; + private PluginLoader pluginLoader; + private Crud<PostDraft> postDraftDao; /** * Create an instance of Post entity based service @@ -69,27 +74,31 @@ public class TransactionalPostService extends AbstractTransactionalEntityService * @param topicDao this dao used for checking branch existance * @param securityService service for authorization * @param notificationService to send email updates for subscribed users - * @param lastReadPostService to modify last read post information when topic structure is changed * @param userService to get current user * @param branchLastPostService to refresh the last post of the branch + * @param permissionService service for cheking permissions + * @param pluginLoader loader of pluinf + * @param postDraftDao data access object for manipulating with drafts */ public TransactionalPostService( PostDao dao, TopicDao topicDao, SecurityService securityService, NotificationService notificationService, - LastReadPostService lastReadPostService, UserService userService, BranchLastPostService branchLastPostService, - PermissionService permissionService) { + PermissionService permissionService, + PluginLoader pluginLoader, + Crud<PostDraft> postDraftDao) { super(dao); this.topicDao = topicDao; this.securityService = securityService; this.notificationService = notificationService; - this.lastReadPostService = lastReadPostService; this.userService = userService; this.branchLastPostService = branchLastPostService; this.permissionService = permissionService; + this.pluginLoader = pluginLoader; + this.postDraftDao = postDraftDao; } /** @@ -129,8 +138,8 @@ public void updatePost(Post post, String postContent) { "(hasPermission(#post.topic.branch.id, 'BRANCH', 'BranchPermission.DELETE_OTHERS_POSTS') and " + "#post.userCreated.username != principal.username)") public void deletePost(Post post) { - JCUser user = post.getUserCreated(); - user.setPostCount(user.getPostCount() - 1); + JCUser postCreator = post.getUserCreated(); + postCreator.setPostCount(postCreator.getPostCount() - 1); Topic topic = post.getTopic(); topic.removePost(post); Branch branch = topic.getBranch(); @@ -139,14 +148,15 @@ public void deletePost(Post post) { branch.clearLastPost(); } - if (post.getCreationDate().equals(topic.getModificationDate())) { - topic.recalculateModificationDate(); - } - // todo: event API? topicDao.saveOrUpdate(topic); securityService.deleteFromAcl(post); - notificationService.subscribedEntityChanged(topic); + + /* + only the creator of the post should be notified when it's removed. + */ + notificationService.subscribedEntityChanged(post); + if (deletedPostIsLastPostInBranch) { branchLastPostService.refreshLastPostInBranch(branch); } @@ -154,6 +164,26 @@ public void deletePost(Post post) { logger.debug("Deleted post id={}", post.getId()); } + @Override + @PreAuthorize("hasPermission(#topic.branch.id, 'BRANCH', 'BranchPermission.CREATE_POSTS')") + public PostDraft saveOrUpdateDraft(Topic topic, String content) { + JCUser currentUser = userService.getCurrentUser(); + PostDraft draft = topic.getDraftForUser(currentUser); + if (draft == null) { + draft = new PostDraft(content, currentUser); + topic.addDraft(draft); + } else { + draft.setContent(content); + draft.updateLastSavedTime(); + } + topicDao.saveOrUpdate(topic); + + logger.debug("Draft saved in topic. Topic id={}, Post id={}, Post author={}", + new Object[]{topic.getId(), draft.getId(), currentUser.getUsername()}); + + return draft; + } + /** * {@inheritDoc} */ @@ -219,10 +249,7 @@ public List<Post> getLastPostsFor(Branch branch, int postCount) { public PostComment addComment(Long postId, Map<String, String> attributes, String body) throws NotFoundException { Post targetPost = get(postId); JCUser currentUser = userService.getCurrentUser(); - permissionService.checkPermission( - targetPost.getTopic().getBranch().getId(), - AclClassName.BRANCH, - BranchPermission.LEAVE_COMMENTS_IN_CODE_REVIEW); + assertCommentAllowed(targetPost.getTopic()); PostComment comment = new PostComment(); comment.putAllAttributes(attributes); comment.setBody(body); @@ -233,6 +260,9 @@ public PostComment addComment(Long postId, Map<String, String> attributes, Strin } targetPost.addComment(comment); getDao().saveOrUpdate(targetPost); + /** + * Notify subscribers of topic if comment added + */ notificationService.subscribedEntityChanged(targetPost.getTopic()); return comment; @@ -261,7 +291,9 @@ public void deleteComment(Post post, PostComment comment) { public Post vote(Post post, PostVote vote) { JCUser currentUser = userService.getCurrentUser(); if (!post.canBeVotedBy(currentUser, vote.isVotedUp())) { - throw new AccessDeniedException("User cant vote in same direction more than one time"); + logger.info("User [{}] tries to vote for post with id={} in same direction more than one time", + currentUser.getUsername(), post.getId()); + throw new AccessDeniedException("User can't vote in same direction more than one time"); } vote.setUser(currentUser); int ratingChanges = post.calculateRatingChanges(vote); @@ -270,4 +302,69 @@ public Post vote(Post post, PostVote vote) { getDao().changeRating(post.getId(), ratingChanges); return post; } + + /** + * {@inheritDoc} + */ + @Override + public void deleteDraft(Long draftId) throws NotFoundException{ + if (!postDraftDao.isExist(draftId)) { + throw new NotFoundException("Draft with id=" + draftId + " not found"); + } + PostDraft draft = postDraftDao.get(draftId); + if (!draft.getAuthor().equals(userService.getCurrentUser())) { + throw new AccessDeniedException("Only author can delete draft"); + } + Topic topic = draft.getTopic(); + topic.getDrafts().remove(draft); + topicDao.saveOrUpdate(topic); + + logger.debug("Deleted draft id={}", draft.getId()); + } + + /** + * Checks if current user can create comments in specified topic + * + * @param topic topic to check permission + * @throws AccessDeniedException if user can't create comments in specified topic + */ + private void assertCommentAllowed(Topic topic) { + if (topic.isCodeReview()) { + permissionService.checkPermission(topic.getBranch().getId(), AclClassName.BRANCH, + BranchPermission.LEAVE_COMMENTS_IN_CODE_REVIEW); + } else if (topic.isPlugable()) { + assertCommentsAllowedForPlugableTopic(topic); + } else { + throw new AccessDeniedException("Adding comments not allowed for core topic types"); + } + if (topic.isClosed()) { + permissionService.checkPermission(topic.getBranch().getId(), AclClassName.BRANCH, + BranchPermission.CLOSE_TOPICS); + } + } + + /** + * Checks if current user can create comments in specified plugable topic + * + * @param topic plugable topic to check permission + * @throws AccessDeniedException if user not granted to create comments in specified topic type or if type of + * current topic is unknown + */ + private void assertCommentsAllowedForPlugableTopic(Topic topic) { + List<Plugin> topicPlugins = pluginLoader.getPlugins(new TypeFilter(TopicPlugin.class), + new StateFilter(Plugin.State.ENABLED)); + boolean pluginFound = false; + for (Plugin plugin : topicPlugins) { + TopicPlugin topicPlugin = (TopicPlugin)plugin; + if (topicPlugin.getTopicType().equals(topic.getType())) { + pluginFound = true; + permissionService.checkPermission(topic.getBranch().getId(), AclClassName.BRANCH, + topicPlugin.getCommentPermission()); + break; + } + } + if (!pluginFound) { + throw new AccessDeniedException("Creation of comments not allowed for unknown topic type"); + } + } } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPrivateMessageService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPrivateMessageService.java index bb9b3eae5c..6bc410d4d5 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPrivateMessageService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalPrivateMessageService.java @@ -15,7 +15,6 @@ package org.jtalks.jcommune.service.transactional; import org.jtalks.common.model.permissions.GeneralPermission; -import org.jtalks.common.security.SecurityService; import org.jtalks.jcommune.model.dao.PrivateMessageDao; import org.jtalks.jcommune.model.dto.PageRequest; import org.jtalks.jcommune.model.entity.JCUser; @@ -27,6 +26,7 @@ import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.nontransactional.MailService; import org.jtalks.jcommune.service.nontransactional.UserDataCacheService; +import org.jtalks.jcommune.service.security.SecurityService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; @@ -148,14 +148,17 @@ public Page<PrivateMessage> getDraftsForCurrentUser(String page) { */ @Override @PreAuthorize("hasPermission(#userFrom.id, 'USER', 'ProfilePermission.SEND_PRIVATE_MESSAGES')") - public void saveDraft(long id, String recipient, String title, String body, JCUser userFrom) - throws NotFoundException { - - JCUser userTo = recipient != null ? userService.getByUsername(recipient) : null; - - PrivateMessage pm = new PrivateMessage(userTo, userFrom, title, body); - pm.setId(id); - pm.setStatus(PrivateMessageStatus.DRAFT); + public void saveDraft(long id, JCUser userTo, String title, String body, JCUser userFrom) { + PrivateMessage pm; + if (this.getDao().isExist(id)) { + pm = this.getDao().get(id); + pm.setUserTo(userTo); + pm.setTitle(title); + pm.setBody(body); + } else { + pm = new PrivateMessage(userTo, userFrom, title, body); + pm.setStatus(PrivateMessageStatus.DRAFT); + } this.getDao().saveOrUpdate(pm); JCUser user = userService.getCurrentUser(); diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalSimplePageService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalSimplePageService.java index 7ff8a949e6..aaf008f489 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalSimplePageService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalSimplePageService.java @@ -18,7 +18,6 @@ import org.jtalks.common.model.dao.GroupDao; import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.permissions.GeneralPermission; -import org.jtalks.common.security.SecurityService; import org.jtalks.jcommune.model.dao.SimplePageDao; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.SimplePage; @@ -26,6 +25,7 @@ import org.jtalks.jcommune.service.dto.SimplePageInfoContainer; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.security.AdministrationGroup; +import org.jtalks.jcommune.service.security.SecurityService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.prepost.PreAuthorize; diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalSpamProtectionService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalSpamProtectionService.java new file mode 100644 index 0000000000..2e477ea26d --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalSpamProtectionService.java @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.transactional; + +import com.google.common.collect.Sets; +import org.jtalks.common.service.exceptions.NotFoundException; +import org.jtalks.common.validation.ValidationError; +import org.jtalks.common.validation.ValidationException; +import org.jtalks.jcommune.model.dao.SpamRuleDao; +import org.jtalks.jcommune.model.entity.SpamRule; +import org.jtalks.jcommune.service.SpamProtectionService; + +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import static java.util.regex.Pattern.matches; + +/** + * @author Oleg Tkachenko + */ +public class TransactionalSpamProtectionService extends AbstractTransactionalEntityService<SpamRule, SpamRuleDao> implements SpamProtectionService { + + public TransactionalSpamProtectionService(SpamRuleDao dao) { + super(dao); + } + + @Override + public boolean isEmailInBlackList(String email) { + if (email == null) return false; + List<SpamRule> enabledRules = getDao().getEnabledRules(); + for (SpamRule enabledRule : enabledRules) { + if (matches(enabledRule.getRegex(), email)) return true; + } + return false; + } + + @Override + public void saveOrUpdate(SpamRule rule) throws NotFoundException { + try { + Pattern.compile(rule.getRegex()); + } catch (PatternSyntaxException ex){ + throw new ValidationException(Sets.newHashSet(new ValidationError("regex", ex.getDescription() + " near index " + ex.getIndex()))); + } + getDao().saveOrUpdate(rule); + } + + @Override + public void deleteRule(long id) { + getDao().delete(id); + } + + @Override + public List<SpamRule> getAllRules() { + return getDao().getAllRules(); + } + + @Override + public SpamRule get(Long id) throws org.jtalks.jcommune.plugin.api.exceptions.NotFoundException { + SpamRule spamRule = getDao().get(id); + if (spamRule == null) throw new org.jtalks.jcommune.plugin.api.exceptions.NotFoundException("Spam rule with id = " + id + " is not found."); + return spamRule; + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalSubscriptionService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalSubscriptionService.java index e936974968..fce28a2607 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalSubscriptionService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalSubscriptionService.java @@ -21,7 +21,7 @@ import org.jtalks.jcommune.service.UserService; import org.springframework.security.access.prepost.PreAuthorize; -import java.util.Collection; +import java.util.*; /** * Implements database-backed durable subscriptions on forum object's updates. @@ -110,6 +110,19 @@ public void toggleSubscription(SubscriptionAwareEntity entityToSubscribe) { public Collection<JCUser> getAllowedSubscribers(SubscriptionAwareEntity entity){ if (entity instanceof Topic) { return this.topicDao.getAllowedSubscribers(entity); + } else if (entity instanceof Post) { + Post post = (Post) entity; + Collection<JCUser> subscribers = topicDao.getAllowedSubscribers(post.getTopic()); + /* Can't return Collections.emptyList() or Arrays.asList(...) + may be problems with removing elements + */ + if (subscribers.contains(post.getUserCreated())) { + List<JCUser> arrayList = new ArrayList<>(1); + arrayList.add(post.getUserCreated()); + return arrayList; + } else { + return new ArrayList<>(); + } } else{ return this.branchDao.getAllowedSubscribers(entity); } @@ -122,4 +135,13 @@ private void saveChanges(SubscriptionAwareEntity entityToSubscribe) { branchDao.saveOrUpdate((Branch) entityToSubscribe); } } + + @Override + public void subscribe(SubscriptionAwareEntity entityToSubscribe) { + JCUser current = userService.getCurrentUser(); + if (!(entityToSubscribe.getSubscribers().contains(current))) { + entityToSubscribe.getSubscribers().add(current); + saveChanges(entityToSubscribe); + } + } } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalTopicDraftService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalTopicDraftService.java new file mode 100644 index 0000000000..c8e4952cfa --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalTopicDraftService.java @@ -0,0 +1,149 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.service.transactional; + +import org.jtalks.common.service.security.SecurityContextFacade; +import org.jtalks.jcommune.model.dao.TopicDraftDao; +import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.TopicDraft; +import org.jtalks.jcommune.plugin.api.PluginLoader; +import org.jtalks.jcommune.plugin.api.core.Plugin; +import org.jtalks.jcommune.plugin.api.core.TopicPlugin; +import org.jtalks.jcommune.plugin.api.filters.StateFilter; +import org.jtalks.jcommune.plugin.api.filters.TopicTypeFilter; +import org.jtalks.jcommune.plugin.api.filters.TypeFilter; +import org.jtalks.jcommune.plugin.api.service.PluginTopicDraftService; +import org.jtalks.jcommune.service.TopicDraftService; +import org.jtalks.jcommune.service.UserService; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; + +import java.util.List; + +/** + * The implementation of TopicDraftService interface. + * + * @author Dmitry S. Dolzhenko + */ +public class TransactionalTopicDraftService implements TopicDraftService, PluginTopicDraftService { + + private final UserService userService; + private final TopicDraftDao topicDraftDao; + private final SecurityContextFacade securityContextFacade; + private final PluginLoader pluginLoader; + private final PermissionEvaluator permissionEvaluator; + + public TransactionalTopicDraftService(UserService userService, + TopicDraftDao topicDraftDao, + SecurityContextFacade securityContextFacade, + PermissionEvaluator permissionEvaluator, + PluginLoader pluginLoader) { + this.userService = userService; + this.topicDraftDao = topicDraftDao; + this.securityContextFacade = securityContextFacade; + this.permissionEvaluator = permissionEvaluator; + this.pluginLoader = pluginLoader; + } + + /** + * {@inheritDoc} + */ + @Override + @PreAuthorize("isAuthenticated()") + public TopicDraft getDraft() { + JCUser user = userService.getCurrentUser(); + return topicDraftDao.getForUser(user); + } + + /** + * {@inheritDoc} + */ + @Override + @PreAuthorize("( (not #draft.codeReview) and (not #draft.plugable) " + + "and hasPermission(#draft.branchId, 'BRANCH', 'BranchPermission.CREATE_POSTS')) " + + "or (#draft.codeReview " + + "and hasPermission(#draft.branchId, 'BRANCH', 'BranchPermission.CREATE_CODE_REVIEW')) " + + " or #draft.plugable") + public TopicDraft saveOrUpdateDraft(TopicDraft draft) { + assertCreationAllowedForPlugableTopic(draft); + + JCUser user = userService.getCurrentUser(); + + TopicDraft currentDraft = topicDraftDao.getForUser(user); + if (currentDraft == null) { + currentDraft = draft; + currentDraft.setTopicStarter(user); + } else { + currentDraft.setContent(draft.getContent()); + currentDraft.setTitle(draft.getTitle()); + currentDraft.setBranchId(draft.getBranchId()); + currentDraft.setTopicType(draft.getTopicType()); + + /* When we save draft that does not contain pollTitle and pollItemsValue (e.g. code review), + we should not overwrite already existing values of these fields. */ + if (draft.getPollTitle() != null || draft.getPollItemsValue() != null) { + currentDraft.setPollTitle(draft.getPollTitle()); + currentDraft.setPollItemsValue(draft.getPollItemsValue()); + } + } + + currentDraft.updateLastSavedTime(); + topicDraftDao.saveOrUpdate(currentDraft); + + return currentDraft; + } + + /** + * {@inheritDoc} + */ + @Override + @PreAuthorize("isAuthenticated()") + public void deleteDraft() { + JCUser user = userService.getCurrentUser(); + topicDraftDao.deleteByUser(user); + } + + /** + * Checks for draft of plugable topic if current user is granted to create topics with type. + * + * @param draft draft topic to be checked + * @throws AccessDeniedException if user not granted to create current topic type + * or if type of current topic is unknown + */ + private void assertCreationAllowedForPlugableTopic(TopicDraft draft) { + if (!draft.isPlugable()) { + return; + } + + Authentication auth = securityContextFacade.getContext().getAuthentication(); + List<Plugin> topicPlugins = pluginLoader.getPlugins(new TypeFilter(TopicPlugin.class), + new StateFilter(Plugin.State.ENABLED), new TopicTypeFilter(draft.getTopicType())); + + if (topicPlugins.size() == 0) { + throw new AccessDeniedException("Creating of unknown (" + draft.getTopicType() + ") topic type is forbidden"); + } else { + for (Plugin plugin : topicPlugins) { + TopicPlugin topicPlugin = (TopicPlugin) plugin; + + if (!permissionEvaluator.hasPermission(auth, draft.getBranchId(), + "BRANCH", topicPlugin.getCreateTopicPermission())) { + throw new AccessDeniedException("Creating of draft topic with type " + draft.getTopicType() + " is forbidden"); + } + } + } + } +} diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalTopicFetchService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalTopicFetchService.java index 4e9dd5d342..d78e729a78 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalTopicFetchService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalTopicFetchService.java @@ -23,6 +23,7 @@ import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.Topic; import org.jtalks.jcommune.plugin.api.service.PluginTopicFetchService; +import org.jtalks.jcommune.service.ComponentService; import org.jtalks.jcommune.service.TopicFetchService; import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; @@ -40,16 +41,19 @@ public class TransactionalTopicFetchService extends AbstractTransactionalEntityService<Topic, TopicDao> implements TopicFetchService, PluginTopicFetchService { + private ComponentService componentService; private UserService userService; private TopicSearchDao searchDao; /** * @param dao topic dao for database manipulations + * @param componentService to checking user permissions * @param userService to get current user and his preferences * @param searchDao for search index access */ - public TransactionalTopicFetchService(TopicDao dao, UserService userService, TopicSearchDao searchDao) { + public TransactionalTopicFetchService(TopicDao dao, ComponentService componentService, UserService userService, TopicSearchDao searchDao) { super(dao); + this.componentService = componentService; this.userService = userService; this.searchDao = searchDao; } @@ -126,6 +130,8 @@ public Page<Topic> searchByTitleAndContent(String phrase, String page) { */ @Override public void rebuildSearchIndex() { + long componentId = componentService.getComponentOfForum().getId(); + componentService.checkPermissionsForComponent(componentId); searchDao.rebuildIndex(); } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalTopicModificationService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalTopicModificationService.java index 23dbe2a7d2..f392199e6e 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalTopicModificationService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalTopicModificationService.java @@ -15,7 +15,6 @@ package org.jtalks.jcommune.service.transactional; import org.jtalks.common.model.permissions.GeneralPermission; -import org.jtalks.common.security.SecurityService; import org.jtalks.common.service.security.SecurityContextFacade; import org.jtalks.jcommune.model.dao.BranchDao; import org.jtalks.jcommune.model.dao.PostDao; @@ -30,6 +29,7 @@ import org.jtalks.jcommune.service.*; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.nontransactional.NotificationService; +import org.jtalks.jcommune.service.security.SecurityService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; @@ -37,9 +37,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; -import java.util.Collection; import java.util.List; -import java.util.Set; /** @@ -70,13 +68,14 @@ public class TransactionalTopicModificationService implements TopicModificationS private BranchLastPostService branchLastPostService; private LastReadPostService lastReadPostService; private TopicFetchService topicFetchService; + private TopicDraftService topicDraftService; private PluginLoader pluginLoader; /** * Create an instance of User entity based service. * * @param dao data access object, which should be able do all CRUD operations with topic entity - * @param securityService {@link org.jtalks.common.security.SecurityService} for retrieving current user + * @param securityService used for retrieving current user * @param branchDao used for checking branch existence * @param notificationService to send email notifications on topic updates to subscribed users * @param subscriptionService for subscribing user on topic if notification enabled @@ -100,6 +99,7 @@ public TransactionalTopicModificationService(TopicDao dao, SecurityService secur LastReadPostService lastReadPostService, PostDao postDao, TopicFetchService topicFetchService, + TopicDraftService topicDraftService, PluginLoader pluginLoader) { this.dao = dao; this.securityService = securityService; @@ -114,6 +114,7 @@ public TransactionalTopicModificationService(TopicDao dao, SecurityService secur this.lastReadPostService = lastReadPostService; this.postDao = postDao; this.topicFetchService = topicFetchService; + this.topicDraftService = topicDraftService; this.pluginLoader = pluginLoader; } @@ -131,21 +132,21 @@ public Post replyToTopic(long topicId, String answerBody, long branchId) throws Post answer = new Post(currentUser, answerBody); topic.addPost(answer); - if (currentUser.isAutosubscribe()) { - Set<JCUser> topicSubscribers = topic.getSubscribers(); - topicSubscribers.add(currentUser); + topic.removeDraftOfUser(currentUser); + if (currentUser.isAutosubscribe()){ + subscriptionService.subscribe(topic); } + postDao.saveOrUpdate(answer); Branch branch = topic.getBranch(); branch.setLastPost(answer); branchDao.saveOrUpdate(branch); + dao.saveOrUpdate(topic); securityService.createAclBuilder().grant(GeneralPermission.WRITE).to(currentUser).on(answer).flush(); notificationService.subscribedEntityChanged(topic); - userService.notifyAndMarkNewlyMentionedUsers(answer); - logger.debug("New post in topic. Topic id={}, Post id={}, Post author={}", new Object[]{topicId, answer.getId(), currentUser.getUsername()}); @@ -198,19 +199,16 @@ public Topic createTopic(Topic topicDto, String bodyText) throws NotFoundExcepti dao.saveOrUpdate(topic); branchDao.saveOrUpdate(branch); - JCUser user = userService.getCurrentUser(); - securityService.createAclBuilder().grant(GeneralPermission.WRITE).to(user).on(topic).flush(); - securityService.createAclBuilder().grant(GeneralPermission.WRITE).to(user).on(first).flush(); - + securityService.createAclBuilder().grant(GeneralPermission.WRITE).to(currentUser).on(topic).flush(); + securityService.createAclBuilder().grant(GeneralPermission.WRITE).to(currentUser).on(first).flush(); notificationService.sendNotificationAboutTopicCreated(topic); - - subscribeOnTopicIfNotificationsEnabled(topic, currentUser); + if (currentUser.isAutosubscribe()){ + subscriptionService.subscribe(topic); + } createPoll(topicDto.getPoll(), topic); - userService.notifyAndMarkNewlyMentionedUsers(topic.getFirstPost()); - lastReadPostService.markTopicAsRead(topic); - + topicDraftService.deleteDraft(); logger.debug("Created new topic id={}, branch id={}, author={}", new Object[]{topic.getId(), topic.getBranch().getId(), currentUser.getUsername()}); return topic; @@ -302,41 +300,21 @@ public void updateTopic(Topic topic, Poll poll) { topic.getPoll().setEndingDate(poll.getEndingDate()); } dao.saveOrUpdate(topic); - JCUser currentUser = userService.getCurrentUser(); - subscribeOnTopicIfNotificationsEnabled(topic, currentUser); logger.debug("Topic id={} updated", topic.getId()); } - /** - * Subscribes topic starter on created topic if notifications enabled("Notify me about the answer" checkbox). - * Subscribes and unsubscribes do if autoSubscribe enabled/disabled. - * - * @param topic topic to subscription - * @param currentUser current user - */ - private void subscribeOnTopicIfNotificationsEnabled(Topic topic, JCUser currentUser) { - boolean subscribed = topic.userSubscribed(currentUser); - if (currentUser.isAutosubscribe() ^ subscribed) { - subscriptionService.toggleTopicSubscription(topic); - } - } - - /** + /** * {@inheritDoc} */ @PreAuthorize("(hasPermission(#topic.branch.id, 'BRANCH', 'BranchPermission.DELETE_OWN_POSTS') and " + - "#topic.topicStarter.username == principal.username and " + - "#topic.postCount == 1) or " + + "#topic.containsOwnerPostsOnly and #topic.topicStarter.id == principal.id) or " + "(hasPermission(#topic.branch.id, 'BRANCH', 'BranchPermission.DELETE_OTHERS_POSTS') and " + "hasPermission(#topic.branch.id, 'BRANCH', 'BranchPermission.DELETE_OWN_POSTS'))") @Override public void deleteTopic(Topic topic) throws NotFoundException { + deleteTopicSilent(topic); - Collection<JCUser> subscribers = subscriptionService.getAllowedSubscribers(topic); - - Branch branch = deleteTopicSilent(topic); - notificationService.sendNotificationAboutRemovingTopic(topic, subscribers); - notificationService.subscribedEntityChanged(branch, subscribers); + notificationService.sendNotificationAboutRemovingTopic(topic); logger.info("Deleted topic \"{}\". Topic id: {}", topic.getTitle(), topic.getId()); } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalUserContactsService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalUserContactsService.java index d95eab37b0..89d99e683b 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalUserContactsService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalUserContactsService.java @@ -66,21 +66,23 @@ public JCUser saveEditedUserContacts(long editedUserId, List<UserContactContaine List<UserContact> tmpContactList = Lists.newArrayList(user.getContacts()); // Remove deleted contacts for (UserContact contact : tmpContactList) { - if (!contactExistInList(contact, contacts)) { + if (!contactExistInListWithNotEmptyValue(contact, contacts)) { user.removeContact(contact); } } // Add new and edit existing contacts for (UserContactContainer contactContainer: contacts) { - UserContactType actualType = get(contactContainer.getTypeId()); - UserContact contact = contactContainer.getId() == null ? null - : this.getDao().getContactById(contactContainer.getId()); - if (contact != null && contact.getOwner().getId() == user.getId()) { - contact.setValue(contactContainer.getValue()); - contact.setType(actualType); - } else { - contact = new UserContact(contactContainer.getValue(), actualType); - user.addContact(contact); + if (contactContainer.getValue() != null) { + UserContactType actualType = get(contactContainer.getTypeId()); + UserContact contact = contactContainer.getId() == null ? null + : this.getDao().getContactById(contactContainer.getId()); + if (contact != null && contact.getOwner().getId() == user.getId()) { + contact.setValue(contactContainer.getValue()); + contact.setType(actualType); + } else { + contact = new UserContact(contactContainer.getValue(), actualType); + user.addContact(contact); + } } } return user; @@ -92,9 +94,9 @@ public JCUser saveEditedUserContacts(long editedUserId, List<UserContactContaine * @param contacts list of edited contacts * @return contact exist in list */ - private boolean contactExistInList(UserContact userContact, List<UserContactContainer> contacts) { + private boolean contactExistInListWithNotEmptyValue(UserContact userContact, List<UserContactContainer> contacts) { for (UserContactContainer contact : contacts) { - if (contact.getId() != null && userContact.getId() == contact.getId()) { + if (contact.getId() != null && contact.getValue() != null && userContact.getId() == contact.getId()) { return true; } } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalUserService.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalUserService.java index 8c0b54518a..374897b83b 100644 --- a/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalUserService.java +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/transactional/TransactionalUserService.java @@ -18,16 +18,14 @@ import org.joda.time.DateTime; import org.joda.time.Period; import org.jtalks.common.model.dao.GroupDao; -import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.entity.User; -import org.jtalks.common.security.SecurityService; import org.jtalks.jcommune.model.dao.PostDao; import org.jtalks.jcommune.model.dao.UserDao; -import org.jtalks.jcommune.model.entity.AnonymousUser; -import org.jtalks.jcommune.model.entity.JCUser; -import org.jtalks.jcommune.model.entity.Language; -import org.jtalks.jcommune.model.entity.Post; +import org.jtalks.jcommune.model.dto.LoginUserDto; +import org.jtalks.jcommune.model.dto.UserDto; +import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.plugin.api.exceptions.NoConnectionException; +import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.exceptions.UnexpectedErrorException; import org.jtalks.jcommune.plugin.api.service.UserReader; import org.jtalks.jcommune.service.Authenticator; @@ -36,23 +34,20 @@ import org.jtalks.jcommune.service.dto.UserNotificationsContainer; import org.jtalks.jcommune.service.dto.UserSecurityContainer; import org.jtalks.jcommune.service.exceptions.MailingFailedException; -import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; -import org.jtalks.jcommune.service.exceptions.UserTriesActivatingAccountAgainException; import org.jtalks.jcommune.service.nontransactional.Base64Wrapper; import org.jtalks.jcommune.service.nontransactional.EncryptionService; import org.jtalks.jcommune.service.nontransactional.MailService; import org.jtalks.jcommune.service.nontransactional.MentionedUsers; -import org.jtalks.jcommune.service.security.AdministrationGroup; +import org.jtalks.jcommune.service.security.SecurityService; +import org.jtalks.jcommune.service.util.AuthenticationStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.access.prepost.PreAuthorize; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Arrays; import java.util.List; -import org.jtalks.jcommune.model.dto.LoginUserDto; /** * User service class. This class contains method needed to manipulate with User persistent entity. @@ -71,6 +66,8 @@ public class TransactionalUserService extends AbstractTransactionalEntityService implements UserService, UserReader { private static final Logger LOGGER = LoggerFactory.getLogger(TransactionalUserService.class); + protected static final int MAX_SEARCH_USER_COUNT=20; + private final PostDao postDao; private final Authenticator authenticator; private final GroupDao groupDao; @@ -152,13 +149,8 @@ public List<String> getUsernames(String pattern) { */ @Override public JCUser getCurrentUser() { - String name = securityService.getCurrentUserUsername(); - if (name == null) { - return new AnonymousUser(); - } else { - return this.getDao().getByUsername(name); - } - + UserInfo userInfo = securityService.getCurrentUserBasicInfo(); + return userInfo != null ? this.getDao().loadById(userInfo.getId()) : new AnonymousUser(); } /** @@ -250,30 +242,6 @@ public void restorePassword(String email) throws MailingFailedException { LOGGER.info("New random password was set for user {}", user.getUsername()); } - /** - * {@inheritDoc} - */ - @Override - public void activateAccount(String uuid) throws NotFoundException, UserTriesActivatingAccountAgainException { - JCUser user = this.getDao().getByUuid(uuid); - if (user == null) { - LOGGER.info("Could not activate user with UUID[{}] because it doesn't exist. Either it was removed from DB " - + "because too much time passed between registration and activation, or there is an error in link" - + ", might be possible the user searches for vulnerabilities in the forum.", uuid); - throw new NotFoundException(); - } else if (!user.isEnabled()) { - Group group = groupDao.getGroupByName(AdministrationGroup.USER.getName()); - user.addGroup(group); - user.setEnabled(true); - this.getDao().saveOrUpdate(user); - LOGGER.info("User [{}] successfully activated", user.getUsername()); - } else { - LOGGER.info("User [{}] tried to activate his account again, but that's impossible. Either he clicked the " + - "link again, or someone looks for vulnerabilities in the forum.", user.getUsername()); - throw new UserTriesActivatingAccountAgainException(); - } - } - /** * {@inheritDoc} */ @@ -291,7 +259,8 @@ public JCUser getByUuid(String uuid) throws NotFoundException { * {@inheritDoc} */ @Override - @Scheduled(cron = "0 * * * * *") // cron expression: invoke every hour at :00 min, e.g. 11:00, 12:00 and so on +// Temporarily disabled. Until we find the bug due to which activated users become not activated. +// @Scheduled(cron = "0 * * * * *") // cron expression: invoke every hour at :00 min, e.g. 11:00, 12:00 and so on public void deleteUnactivatedAccountsByTimer() { DateTime today = new DateTime(); for (JCUser user : this.getDao().getNonActivatedUsers()) { @@ -333,7 +302,7 @@ public void checkPermissionToCreateAndEditSimplePage(Long userId) { * {@inheritDoc} */ @Override - public boolean loginUser(LoginUserDto loginUserDto, HttpServletRequest request, HttpServletResponse response) + public AuthenticationStatus loginUser(LoginUserDto loginUserDto, HttpServletRequest request, HttpServletResponse response) throws UnexpectedErrorException, NoConnectionException { return authenticator.authenticate(loginUserDto, request, response); } @@ -370,4 +339,40 @@ public void changeLanguage(JCUser jcUser, Language newLang) { jcUser.setLanguage(newLang); this.getDao().saveOrUpdate(jcUser); } + + @Override + @PreAuthorize("hasPermission(#forumComponentId, 'COMPONENT', 'GeneralPermission.ADMIN')") + public List<JCUser> findByUsernameOrEmail(long forumComponentId, String searchKey) { + return getDao().findByUsernameOrEmail(searchKey, MAX_SEARCH_USER_COUNT); + } + + @Override + public List<UserDto> findByUsernameOrEmailNotInGroup(String pattern, long groupId, int count) { + return getDao().findByUsernameOrEmailNotInGroup(pattern, groupId, count); + } + + @Override + @PreAuthorize("hasPermission(#forumComponentId, 'COMPONENT', 'GeneralPermission.ADMIN')") + public List<Long> getUserGroupIDs(long forumComponentId, long userID) throws NotFoundException { + JCUser jcUser = getDao().get(userID); + return jcUser.getGroupsIDs(); + } + + @Override + @PreAuthorize("hasPermission(#forumComponentId, 'COMPONENT', 'GeneralPermission.ADMIN')") + public void addUserToGroup(long forumComponentId, long userID, long groupID) throws NotFoundException { + JCUser jcUser = getDao().get(userID); + jcUser.addGroup(groupDao.get(groupID)); + + this.getDao().saveOrUpdate(jcUser); + } + + @Override + @PreAuthorize("hasPermission(#forumComponentId, 'COMPONENT', 'GeneralPermission.ADMIN')") + public void deleteUserFromGroup(long forumComponentId, long userID, long groupID) throws NotFoundException { + JCUser jcUser = getDao().get(userID); + jcUser.deleteGroup(groupDao.get(groupID)); + + this.getDao().saveOrUpdate(jcUser); + } } diff --git a/jcommune-service/src/main/java/org/jtalks/jcommune/service/util/AuthenticationStatus.java b/jcommune-service/src/main/java/org/jtalks/jcommune/service/util/AuthenticationStatus.java new file mode 100644 index 0000000000..74dccf7e20 --- /dev/null +++ b/jcommune-service/src/main/java/org/jtalks/jcommune/service/util/AuthenticationStatus.java @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.service.util; + +public enum AuthenticationStatus { + AUTHENTICATED, NOT_ENABLED, AUTHENTICATION_FAIL +} diff --git a/jcommune-service/src/main/resources/kefirbb-strip-config.xml b/jcommune-service/src/main/resources/kefirbb-strip-config.xml index b5cf02a906..e9f7db05cc 100644 --- a/jcommune-service/src/main/resources/kefirbb-strip-config.xml +++ b/jcommune-service/src/main/resources/kefirbb-strip-config.xml @@ -23,7 +23,7 @@ get XSS attacks.</p>--> <!--See more docs on official site: http://kefir-bb.sourceforge.net/--> <configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://kefirsf.org/kefirbb/schema" - xsi:schemaLocation="http://kefirsf.org/kefirbb/schema http://kefirsf.org/kefirbb/schema/kefirbb-1.0.xsd"> + xsi:schemaLocation="http://kefirsf.org/kefirbb/schema http://kefirsf.org/kefirbb/schema/kefirbb-1.2.xsd"> <!-- XML escape symbols --> <scope name="escapeXml"> <code priority="100"> @@ -248,8 +248,8 @@ get XSS attacks.</p>--> <!-- Insert image --> <code name="img1" priority="2"> - <pattern ignoreCase="true">[img]<var name="protocol" regex="((ht|f)tps?:|\.{1,2})?"/>/<var name="addr" scope="escapeXmlAndBr"/>[/img]</pattern> - <template><var name="protocol"/>/<var name="addr"/></template> + <pattern ignoreCase="true">[img]<var name="addr" regex=".*" inherit="true" scope="escapeXmlAndBr"/>[/img]</pattern> + <template><var name="addr"/></template> </code> @@ -278,15 +278,15 @@ get XSS attacks.</p>--> </code> <code name="url2" priority="2"> <pattern ignoreCase="true">[url]<var name="protocol" regex="((ht|f)tps?:|\.{1,2})?"/>/<var name="url" scope="escapeXmlAndBr"/>[/url]</pattern> - <template><a href="<var name="protocol"/>/<var name="url"/>"><var name="protocol"/>/<var name="url"/></a></template> + <template><var name="protocol"/>/<var name="url"/></template> </code> <code name="url3" priority="1"> <pattern ignoreCase="true">[url=<var name="url" scope="escapeXmlAndBr"/>]<var name="text" scope="url"/>[/url]</pattern> - <template><a href="http://<var name="url"/>"><var name="text"/></a></template> + <template><var name="text"/></template> </code> <code name="url4" priority="1"> <pattern ignoreCase="true">[url]<var name="url" scope="escapeXmlAndBr"/>[/url]</pattern> - <template><a href="http://<var name="url"/>"><var name="url"/></a></template> + <template><var name="url"/></template> </code> @@ -313,7 +313,7 @@ get XSS attacks.</p>--> <!-- Code block --> <code name="code"> - <pattern ignoreCase="true">[code=<var name="lang"/>]<var name="code" scope="escapeXml"/>[/code]</pattern> + <pattern ignoreCase="true">[code<var name="lang"/>]<var name="code" scope="escapeXml"/>[/code]</pattern> <template><var name="code"/></template> </code> diff --git a/jcommune-service/src/main/resources/kefirbb.xml b/jcommune-service/src/main/resources/kefirbb.xml index 9da026b5e6..ab13d7e884 100644 --- a/jcommune-service/src/main/resources/kefirbb.xml +++ b/jcommune-service/src/main/resources/kefirbb.xml @@ -23,7 +23,7 @@ get XSS attacks.</p>--> <!--See more docs on official site: http://kefir-bb.sourceforge.net/--> <configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://kefirsf.org/kefirbb/schema" - xsi:schemaLocation="http://kefirsf.org/kefirbb/schema http://kefirsf.org/kefirbb/schema/kefirbb-1.0.xsd"> + xsi:schemaLocation="http://kefirsf.org/kefirbb/schema http://kefirsf.org/kefirbb/schema/kefirbb-1.2.xsd"> <!-- XML escape symbols --> <scope name="escapeXml"> <code priority="100"> @@ -143,6 +143,7 @@ get XSS attacks.</p>--> <!-- Images --> <coderef name="img1"/> + <coderef name="img2"/> <!-- links --> <coderef name="url1"/> @@ -251,7 +252,12 @@ get XSS attacks.</p>--> <!-- Insert image --> <code name="img1" priority="2"> <pattern ignoreCase="true">[img]<var name="addr" regex=".*"/>[/img]</pattern> - <template><![CDATA[<a title="" href="]]><var name="addr"/><![CDATA[" class="pretty-photo"><img class="thumbnail" alt="" src="]]><var name="addr"/><![CDATA[" onError="imgError(this)" /></a>]]></template> + <template><a title="" href="<var name="addr"/>" class="pretty-photo"><img class="thumbnail" alt="" src="<var name="addr"/>" onError="imgError(this)" /></a></template> + </code> + + <code name="img2" priority="2"> + <pattern ignoreCase="true">[img][/img]</pattern> + <template><a title="" href="/a" class="pretty-photo"><img class="thumbnail-default" alt="" src="/a" onError="imgError(this)" /></a></template> </code> <!-- Links. http, https, mailto protocols --> @@ -270,6 +276,7 @@ get XSS attacks.</p>--> <coderef name="indent"/> <coderef name="list"/> <coderef name="img1"/> + <coderef name="img2"/> </scope> <!-- HTTP --> @@ -304,24 +311,24 @@ get XSS attacks.</p>--> <!-- Quote block --> <code name="quote"> <pattern ignoreCase="true">[quote]<var inherit="true"/>[/quote]</pattern> - <template><![CDATA[<div class="quote bb_quote_container"><span class="bb_quote_title">Quote:</span><div class='bb_quote_content'>]]><var/><![CDATA[</div></div>]]></template> + <template><div class="quote bb_quote_container"><span class="bb_quote_title">Quote:</span><div class='bb_quote_content'><var/></div></div></template> </code> <code name="quote_named"> <pattern ignoreCase="true">[quote="<var name="author" inherit="true"/>"]<var name="content" inherit="true"/>[/quote]</pattern> - <template><![CDATA[<div class="quote bb_quote_container"><span class="bb_quote_title">]]><var name="author"/><![CDATA[:</span><div class='bb_quote_content'>]]><var name="content"/><![CDATA[</div></div>]]></template> + <template><div class="quote bb_quote_container"><span class="bb_quote_title"><var name="author"/>:</span><div class='bb_quote_content'><var name="content"/></div></div></template> </code> <!-- Code block --> <code name="code"> - <pattern ignoreCase="true">[code=<var name="lang"/>]<var name="code" scope="escapeXml"/>[/code]</pattern> + <pattern ignoreCase="true">[code<var name="lang"/>]<var name="code" scope="escapeXml"/>[/code]</pattern> <template><pre class="prettyprint linenums <var name="lang"/>"><var name="code"/></pre></template> </code> <!-- Offtopic block --> <code name="offtop"> <pattern ignoreCase="true">[offtop]<var scope="basic"/>[/offtop]</pattern> - <template><![CDATA[<div class="offtop"><p>]]><var/><![CDATA[</p></div>]]></template> + <template><div class="offtop"><p><var/></p></div></template> </code> <!-- Simple table --> diff --git a/jcommune-service/src/main/resources/org/jtalks/jcommune/service/applicationContext-service.xml b/jcommune-service/src/main/resources/org/jtalks/jcommune/service/applicationContext-service.xml index 62a81376ca..3628e8206e 100644 --- a/jcommune-service/src/main/resources/org/jtalks/jcommune/service/applicationContext-service.xml +++ b/jcommune-service/src/main/resources/org/jtalks/jcommune/service/applicationContext-service.xml @@ -62,6 +62,8 @@ <entry key="jcommune:name=hibernateStatistics" value-ref="hibernateStatisticsMBean"/> </map> </property> + <!-- Ignores newly created configuration --> + <property name="registrationPolicy" value="IGNORE_EXISTING"/> </bean> <bean id="managementService" class="net.sf.ehcache.management.ManagementService" @@ -135,8 +137,8 @@ </bean> <bean id="locationService" class="org.jtalks.jcommune.service.nontransactional.LocationService"> + <constructor-arg name="securityService" ref="securityService"/> <constructor-arg name="sessionRegistry" ref="sessionRegistry"/> - <constructor-arg name="userService" ref="userService"/> </bean> <bean id="abstractUserService" abstract="true"> @@ -207,10 +209,11 @@ <constructor-arg ref="topicDao"/> <constructor-arg ref="securityService"/> <constructor-arg ref="notificationService"/> - <constructor-arg ref="lastReadPostService"/> <constructor-arg ref="userService"/> <constructor-arg ref="branchLastPostService"/> <constructor-arg ref="permissionService"/> + <constructor-arg ref="pluginLoader"/> + <constructor-arg ref="postDraftDao"/> </bean> <bean id="topicModificationService" @@ -228,12 +231,14 @@ <constructor-arg ref="lastReadPostService"/> <constructor-arg ref="postDao"/> <constructor-arg ref="topicFetchService"/> + <constructor-arg ref="topicDraftService"/> <constructor-arg ref="pluginLoader"/> </bean> <bean id="topicFetchService" class="org.jtalks.jcommune.service.transactional.TransactionalTopicFetchService"> <constructor-arg ref="topicDao"/> + <constructor-arg ref="componentService"/> <constructor-arg ref="userService"/> <constructor-arg ref="topicSearchDao"/> </bean> @@ -290,10 +295,10 @@ <constructor-arg ref="topicDao"/> </bean> - <bean id="securityService" - class="org.jtalks.common.security.SecurityService"> + <bean id="securityService" class="org.jtalks.jcommune.service.security.SecurityService"> <constructor-arg name="userDao" ref="userDao"/> - <constructor-arg ref="aclManager"/> + <constructor-arg name="aclManager" ref="aclManager"/> + <constructor-arg name="securityContextFacade" ref="securityContextFacade"/> </bean> <bean id="forumStatisticsService" @@ -315,6 +320,15 @@ <constructor-arg ref="securityService"/> </bean> + <bean id="topicDraftService" + class="org.jtalks.jcommune.service.transactional.TransactionalTopicDraftService"> + <constructor-arg ref="userService"/> + <constructor-arg ref="topicDraftDao"/> + <constructor-arg ref="securityContextFacade"/> + <constructor-arg ref="aclGroupPermissionEvaluator"/> + <constructor-arg ref="pluginLoader"/> + </bean> + <bean id="bbCodeReviewProcessor" class="org.jtalks.jcommune.service.bb2htmlprocessors.BbCodeReviewProcessor"/> <bean id="bbForeignLinksPostprocessor" class="org.jtalks.jcommune.service.bb2htmlprocessors.BBForeignLinksPostprocessor"/> @@ -323,6 +337,8 @@ <constructor-arg ref="userService"/> </bean> + <bean id="urlToLinkConvertPostProcessor" class="org.jtalks.jcommune.service.bb2htmlprocessors.UrlToLinkConvertPostProcessor"/> + <bean id="bbCodeService" class="org.jtalks.jcommune.service.nontransactional.BBCodeService"> <property name="preprocessors"> <list> @@ -334,12 +350,13 @@ <property name="postprocessors"> <list> <ref local="bbCodeReviewProcessor"/> + <ref local="urlToLinkConvertPostProcessor"/> <ref local="bbForeignLinksPostprocessor"/> </list> </property> </bean> - <bean id="postReviewCommentService" + <bean id="postCommentService" class="org.jtalks.jcommune.service.transactional.TransactionalPostCommentService"> <constructor-arg ref="postCommentDao"/> <constructor-arg ref="permissionService"/> @@ -397,12 +414,21 @@ <constructor-arg name="pluginLoader" ref="pluginLoader"/> </bean> + <bean id="spamProtectionService" class="org.jtalks.jcommune.service.transactional.TransactionalSpamProtectionService"> + <constructor-arg ref="spamRuleDao"/> + </bean> + <!-- Cache beans for user related data. Now used for storing new private messages count. --> <bean id="localCache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> - <property name="configLocation" value="classpath:/org/jtalks/jcommune/model/entity/ehcache.xml"/> + <property name="configLocation" value="classpath:/org/jtalks/jcommune/model/entity/localCache.xml"/> + </bean> + + <!--Used for caching acl items--> + <bean id="distributedChace" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"> + <property name="configLocation" value="${EH_CACHE_CONFIG:classpath:/org/jtalks/jcommune/model/entity/ehcache.xml}"/> </bean> <bean id="userDataCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean"> @@ -439,6 +465,11 @@ <property name="postService" ref="postService"/> </bean> + <bean class="org.jtalks.jcommune.plugin.api.service.transactional.TransactionalPluginTopicDraftService" + factory-method="getInstance"> + <property name="topicDraftService" ref="topicDraftService"/> + </bean> + <bean class="org.jtalks.jcommune.plugin.api.service.transactional.TransactionalPluginLastReadPostService" factory-method="getInstance"> <property name="lastReadPostService" ref="lastReadPostService"/> @@ -449,6 +480,26 @@ <property name="locationService" ref="locationService"/> </bean> + <bean class="org.jtalks.jcommune.plugin.api.service.transactional.TransactionalPluginCommentService" + factory-method="getInstance"> + <property name="commentService" ref="postCommentService"/> + </bean> + + <bean class="org.jtalks.jcommune.plugin.api.service.nontransactional.PropertiesHolder" factory-method="getInstance"> + <property name="allPagesTitlePrefixProperty" ref="componentAllPagesTitlePrefixProperty"/> + </bean> + + <bean id="entityToDtoConverter" class="org.jtalks.jcommune.service.dto.EntityToDtoConverter"> + <constructor-arg ref="pluginLoader"/> + </bean> + + <bean id="groupService" class="org.jtalks.jcommune.service.transactional.TransactionalGroupService"> + <constructor-arg index="0" ref="groupDao"/> + <constructor-arg index="1" ref="aclManager"/> + <constructor-arg index="2" ref="userDao"/> + <constructor-arg index="3" ref="securityService"/> + </bean> + <beans profile="performance"> <!-- JETM implementation using bean --> <bean id="etmMonitor" class="etm.core.monitor.NestedMonitor" init-method="start" destroy-method="stop"/> diff --git a/jcommune-service/src/main/resources/org/jtalks/jcommune/service/email-context.xml b/jcommune-service/src/main/resources/org/jtalks/jcommune/service/email-context.xml index e5e901165e..b233937966 100644 --- a/jcommune-service/src/main/resources/org/jtalks/jcommune/service/email-context.xml +++ b/jcommune-service/src/main/resources/org/jtalks/jcommune/service/email-context.xml @@ -43,7 +43,7 @@ <prop key="mail.smtp.auth">true</prop> </util:properties> - <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> + <bean id="messageSourceMail" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basenames"> <list> <value>classpath:/org/jtalks/jcommune/service/bundle/TemplatesMessages</value> @@ -67,15 +67,17 @@ <!--the following argument should match real mailbox we use to send mails--> <constructor-arg name="from" value="${MAIL_FROM:jtalks@inbox.ru}"/> <constructor-arg ref="velocityEngine"/> - <constructor-arg ref="messageSource"/> + <constructor-arg ref="messageSourceMail"/> <constructor-arg ref="sendingNotificationsProperty"/> <constructor-arg ref="velocityEscapeTool"/> + <constructor-arg ref="entityToDtoConverter"/> </bean> <bean id="notificationService" class="org.jtalks.jcommune.service.nontransactional.NotificationService"> <constructor-arg ref="userService"/> <constructor-arg ref="mailService"/> <constructor-arg ref="subscriptionService"/> + <constructor-arg ref="pluginLoader"/> </bean> <bean id="velocityEscapeTool" class="org.apache.velocity.tools.generic.EscapeTool"/> diff --git a/jcommune-service/src/main/resources/org/jtalks/jcommune/service/security-service-context.xml b/jcommune-service/src/main/resources/org/jtalks/jcommune/service/security-service-context.xml index 94e0a292d6..607f9fe563 100644 --- a/jcommune-service/src/main/resources/org/jtalks/jcommune/service/security-service-context.xml +++ b/jcommune-service/src/main/resources/org/jtalks/jcommune/service/security-service-context.xml @@ -19,7 +19,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:security="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd - http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd"> + http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> <!-- Spring Security authentication manager --> <security:authentication-manager alias="authenticationManager"> @@ -41,23 +41,22 @@ </bean> <!-- Evaluates hasPermission expression --> - <bean id="aclGroupPermissionEvaluator" class="org.jtalks.jcommune.service.security.AclGroupPermissionEvaluator"> + <bean id="aclGroupPermissionEvaluator" class="org.jtalks.jcommune.service.security.acl.AclGroupPermissionEvaluator"> <!-- Link to actual ACL Service --> <constructor-arg name="aclManager" ref="aclManager"/> <constructor-arg name="aclUtil" ref="aclUtils"/> - <constructor-arg name="groupDao" ref="groupDao"/> <constructor-arg name="sidFactory" ref="jtalksSidFactory"/> <constructor-arg name="mutableAclService" ref="aclService"/> - <constructor-arg name="userDao" ref="userDao"/> <constructor-arg name="pluginPermissionManager" ref="pluginPermissionManager"/> + <constructor-arg name="securityService" ref="securityService"/> </bean> - <bean id="aclUtils" class="org.jtalks.common.security.acl.AclUtil"> + <bean id="aclUtils" class="org.jtalks.jcommune.service.security.acl.AclUtil"> <constructor-arg name="mutableAclService" ref="aclService"/> <property name="objectIdentityGenerator" ref="typeConvertingObjectIdentityGenerator"/> </bean> <bean id="typeConvertingObjectIdentityGenerator" - class="org.jtalks.common.security.acl.TypeConvertingObjectIdentityGenerator"> + class="org.jtalks.jcommune.service.security.acl.TypeConvertingObjectIdentityGenerator"> <property name="additionalConversionRules"> <map> <entry key="org.jtalks.jcommune.model.entity.Branch" value="BRANCH"/> @@ -75,7 +74,7 @@ </bean> <!-- Retrieves, modifies and strores access control lists --> - <bean id="aclService" class="org.jtalks.common.security.acl.JtalksMutableAcService"> + <bean id="aclService" class="org.jtalks.jcommune.service.security.acl.JtalksMutableAcService"> <constructor-arg ref="dataSource"/> <constructor-arg ref="lookupStrategy"/> <constructor-arg ref="aclCache"/> @@ -84,10 +83,10 @@ <property name="sidFactory" ref="jtalksSidFactory"/> </bean> - <bean id="jtalksSidFactory" class="org.jtalks.common.security.acl.sids.JtalksSidFactory"/> + <bean id="jtalksSidFactory" class="org.jtalks.jcommune.service.security.acl.sids.JtalksSidFactory"/> <!-- Responsible for efficient retrieval of ACLs from database --> - <bean id="lookupStrategy" class="org.jtalks.common.security.acl.JtalksLookupStrategy"> + <bean id="lookupStrategy" class="org.jtalks.jcommune.service.security.acl.JtalksLookupStrategy"> <constructor-arg ref="dataSource"/> <constructor-arg ref="aclCache"/> <constructor-arg ref="aclAuthorizationStrategy"/> @@ -99,7 +98,7 @@ </bean> <!-- ACL permission factory --> - <bean name="jtalksPermissionFactory" class="org.jtalks.common.security.acl.JtalksPermissionFactory" + <bean name="jtalksPermissionFactory" class="org.jtalks.jcommune.service.security.JtalksPermissionFactory" init-method="init"/> <bean name="pluginPermissionFactory" class="org.jtalks.jcommune.plugin.api.PluginsPermissionFactory"> @@ -116,19 +115,19 @@ <bean id="aclCache" class="org.springframework.security.acls.domain.EhCacheBasedAclCache"> <constructor-arg> <bean class="org.springframework.cache.ehcache.EhCacheFactoryBean"> - <property name="cacheManager" ref="localCache"/> + <property name="cacheManager" ref="distributedChace"/> <property name="cacheName" value="org.jtalks.EHCOMMON"/> </bean> </constructor-arg> </bean> <!-- Authorization for ACLs administration setup --> - <bean id="aclAuthorizationStrategy" class="org.jtalks.jcommune.service.security.AclAuthorizationStrategyImpl"/> + <bean id="aclAuthorizationStrategy" class="org.jtalks.jcommune.service.security.acl.AclAuthorizationStrategyImpl"/> <bean id="securityContextFacade" class="org.jtalks.common.service.security.SecurityContextHolderFacade"/> - <bean id="aclManager" class="org.jtalks.common.security.acl.AclManager"> + <bean id="aclManager" class="org.jtalks.jcommune.service.security.acl.AclManager"> <constructor-arg name="mutableAclService" ref="aclService"/> <property name="aclUtil" ref="aclUtils"/> </bean> diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/TestUtils.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/TestUtils.java index 14eb016449..68f60778fc 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/TestUtils.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/TestUtils.java @@ -17,7 +17,7 @@ import org.jtalks.common.model.entity.Entity; import org.jtalks.common.model.entity.User; import org.jtalks.common.model.permissions.GeneralPermission; -import org.jtalks.common.security.acl.builders.CompoundAclBuilder; +import org.jtalks.jcommune.service.security.acl.builders.CompoundAclBuilder; import org.mockito.Mockito; import static org.mockito.Mockito.mock; diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/bb2htmlprocessors/BBForeignLinksPostprocessorTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/bb2htmlprocessors/BBForeignLinksPostprocessorTest.java index 7cfc4db37b..48f237fa6f 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/bb2htmlprocessors/BBForeignLinksPostprocessorTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/bb2htmlprocessors/BBForeignLinksPostprocessorTest.java @@ -29,24 +29,17 @@ public class BBForeignLinksPostprocessorTest { private BBForeignLinksPostprocessor service; @Mock private HttpServletRequest request; - private String prefix = "/out?url="; private String relAttr = "rel=\"nofollow\""; private String serverName = "server_name"; @BeforeMethod public void setUp() { service = spy(new BBForeignLinksPostprocessor()); - when(service.getHrefPrefix()).thenReturn(prefix); MockitoAnnotations.initMocks(this); doReturn(request).when(service).getServletRequest(); when(request.getServerName()).thenReturn(serverName); } - @Test(dataProvider = "preProcessingCommonLinks") - public void postprocessorShouldCorrectlyAddPrefix(String incomingText, String outcomingText) { - assertEquals(service.postProcess(incomingText), outcomingText); - } - @Test(dataProvider = "preProcessingImages") public void postprocessorShouldCorrectlyReplaceSpaceInImg(String incomingText, String outcomingText) { assertEquals(service.postProcess(incomingText), outcomingText); @@ -75,13 +68,13 @@ public Object[][] preProcessingImages() { public Object[][] preProcessingCommonLinks() { return new Object[][]{ // {"incoming link (before)", "outcoming link (after)"} {"<a href=\"http://javatalks.ru/common\"></a>", - "<a " + relAttr + " href=\"" + prefix + "http://javatalks.ru/common\"></a>"}, + "<a " + relAttr + " href=\"http://javatalks.ru/common\"></a>"}, {"<a href=\"https://forum.javatalks.ru\"></a>", - "<a " + relAttr + " href=\"" + prefix + "https://forum.javatalks.ru\"></a>"}, + "<a " + relAttr + " href=\"https://forum.javatalks.ru\"></a>"}, {"<a href=\"http://javatalks.ru/common\"></a>", - "<a " + relAttr + " href=\"" + prefix + "http://javatalks.ru/common\"></a>"}, + "<a " + relAttr + " href=\"http://javatalks.ru/common\"></a>"}, {"<a href=\"http://javatalks.ru/common space\"></a>", - "<a " + relAttr + " href=\"" + prefix + "http://javatalks.ru/common%20space\"></a>"} + "<a " + relAttr + " href=\"http://javatalks.ru/common%20space\"></a>"} }; } @@ -90,13 +83,13 @@ public Object[][] preProcessingCommonLinks() { public Object[][] preProcessingSubDomainLinks() { return new Object[][]{ // {"incoming link (before)", "outcoming link (after)"} {"<a href=\"http://blog.javatalks.ru\"></a>", - "<a " + relAttr + " href=\"" + prefix + "http://blog.javatalks.ru\"></a>"}, + "<a " + relAttr + " href=\"http://blog.javatalks.ru\"></a>"}, {"<a href=\"http://www.blog.javatalks.ru\"></a>", - "<a " + relAttr + " href=\"" + prefix + "http://www.blog.javatalks.ru\"></a>"}, + "<a " + relAttr + " href=\"http://www.blog.javatalks.ru\"></a>"}, {"<a href=\"http://com.blog.javatalks.ru\"></a>", - "<a " + relAttr + " href=\"" + prefix + "http://com.blog.javatalks.ru\"></a>"}, + "<a " + relAttr + " href=\"http://com.blog.javatalks.ru\"></a>"}, {"<a href=\"http://com.blog.javatalks.ru/space space\"></a>", - "<a " + relAttr + " href=\"" + prefix + "http://com.blog.javatalks.ru/space%20space\"></a>"}, + "<a " + relAttr + " href=\"http://com.blog.javatalks.ru/space%20space\"></a>"}, }; } diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/bb2htmlprocessors/UrlToLinkConvertPostProcessorTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/bb2htmlprocessors/UrlToLinkConvertPostProcessorTest.java new file mode 100644 index 0000000000..e980f0beae --- /dev/null +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/bb2htmlprocessors/UrlToLinkConvertPostProcessorTest.java @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.service.bb2htmlprocessors; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +public class UrlToLinkConvertPostProcessorTest { + UrlToLinkConvertPostProcessor postProcessor = new UrlToLinkConvertPostProcessor(); + + @Test + void doesNothingIfNoLinksPresent() { + String originalText = "Silent sir say desire fat him letter. Whatever settling goodness too and \n" + + "honoured she building answered her. Strongly thoughts remember mr to do consider debating. \n" + + "Spirits musical behaved on we he farther letters. Repulsive he he as deficient newspaper dashwoods we. \n" + + "Discovered her his pianoforte insipidity entreaties. Began he at terms meant as fancy. Breakfast \n" + + "arranging he if furniture we described on. И чуть-чуть на русском. Astonished thoroughly unpleasant \n" + + "especially you dispatched \n" + + "bed favourable."; + assertEquals(postProcessor.postProcess(originalText), originalText); + } + + @Test + void doesNothingIfNoTextPresent() { + assertEquals(postProcessor.postProcess(""), ""); + } + + @Test(dataProvider = "urlShouldBeHighlighted") + void turnUrlToHrefTag_IfUrlShouldBeHighlighted(String originalText, String expectedOutput) { + assertEquals(postProcessor.postProcess(originalText), expectedOutput); + } + + @Test(dataProvider = "urlShouldNotBeHighlighted") + void dontTurnUrlToHrefTag_IfUrlShouldNotBeHighlighted(String originalText) { + assertEquals(postProcessor.postProcess(originalText), originalText); + } + + @Test(dataProvider = "allJoinedUrlCases") + void turnUrlToHrefTagIfNeeded(String originalText, String expectedOutput) { + assertEquals(postProcessor.postProcess(originalText), expectedOutput); + } + + @DataProvider + public Object[][] urlShouldBeHighlighted() { + return new Object[][] { + {"http://javatalks.ru/common", + "<a href=\"http://javatalks.ru/common\">http://javatalks.ru/common</a>"}, + {"https://javatalks.ru/common", + "<a href=\"https://javatalks.ru/common\">https://javatalks.ru/common</a>"}, + {"www.javatalks.ru/common", + "<a href=\"http://www.javatalks.ru/common\">www.javatalks.ru/common</a>"}, + {"ftp.javatalks.ru/common", + "<a href=\"ftp://ftp.javatalks.ru/common\">ftp.javatalks.ru/common</a>"}, + {"ftp://javatalks.ru/common", + "<a href=\"ftp://javatalks.ru/common\">ftp://javatalks.ru/common</a>"}, + {"file://javatalks.ru/common", + "<a href=\"file://javatalks.ru/common\">file://javatalks.ru/common</a>"}, + {"<div class=divclass>http://javatalks.ru/common</div>", + "<div class=divclass><a href=\"http://javatalks.ru/common\">http://javatalks.ru/common</a></div>"}, + {"<pre class=divclass>http://javatalks.ru/common</pre>", + "<pre class=divclass><a href=\"http://javatalks.ru/common\">http://javatalks.ru/common</a></pre>"}, + {"text text <pre class=divclass>http://javatalks.ru/common</pre> ttt text", + "text text <pre class=divclass><a href=\"http://javatalks.ru/common\">http://javatalks.ru/common</a></pre> ttt text"}, + {"Text текст ftp.javatalks.ru/common text \n text http://javatalks.ru/common text and text и текст \n the end.", + "Text текст <a href=\"ftp://ftp.javatalks.ru/common\">ftp.javatalks.ru/common</a> text \n text " + + "<a href=\"http://javatalks.ru/common\">http://javatalks.ru/common</a> text and text и текст \n the end."}, + {"http://привет.рф/информация", + "<a href=\"http://привет.рф/информация\">http://привет.рф/информация</a>"}, + {"www.привет.рф/информация", + "<a href=\"http://www.привет.рф/информация\">www.привет.рф/информация</a>"}, + {"ftp://привет.рф/информация", + "<a href=\"ftp://привет.рф/информация\">ftp://привет.рф/информация</a>"}, + {"ftp.привет.рф/информация", + "<a href=\"ftp://ftp.привет.рф/информация\">ftp.привет.рф/информация</a>"}, + {"file://привет.рф/информация", + "<a href=\"file://привет.рф/информация\">file://привет.рф/информация</a>"} + }; + } + + @DataProvider + public Object[][] urlShouldNotBeHighlighted() { + return new Object[][] { + {"<a href=http://www.google.com>www.google.com</a>"}, + {"<img src=http://www.ya.ru/img.jpg>img.jpg</img>"}, + {"text <img src=http://www.ya.ru/img.jpg>img.jpg</img> text"} + }; + } + + @DataProvider + public Object[][] allJoinedUrlCases() { + StringBuilder originTextBuilder = new StringBuilder(); + StringBuilder expectedTextBuilder = new StringBuilder(); + for (Object[] objects : urlShouldBeHighlighted()) { + originTextBuilder.append((String) objects[0]); + originTextBuilder.append("\n"); + expectedTextBuilder.append((String) objects[1]); + expectedTextBuilder.append("\n"); + } + for (Object[] objects : urlShouldNotBeHighlighted()) { + originTextBuilder.append((String) objects[0]); + originTextBuilder.append("\n"); + expectedTextBuilder.append((String) objects[0]); + expectedTextBuilder.append("\n"); + } + return new Object[][] { + {originTextBuilder.toString(), expectedTextBuilder.toString()} + }; + } +} diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/dto/EntityToDtoConverterTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/dto/EntityToDtoConverterTest.java similarity index 93% rename from jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/dto/EntityToDtoConverterTest.java rename to jcommune-service/src/test/java/org/jtalks/jcommune/service/dto/EntityToDtoConverterTest.java index d08942df67..8d3ed7c2fb 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/dto/EntityToDtoConverterTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/dto/EntityToDtoConverterTest.java @@ -12,7 +12,7 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -package org.jtalks.jcommune.web.dto; +package org.jtalks.jcommune.service.dto; import org.jtalks.jcommune.model.entity.Topic; import org.jtalks.jcommune.model.entity.TopicTypeName; @@ -59,7 +59,7 @@ public void testConvertToDtoPageForDiscussionWhenTopicNotClosedAndNoTopicPlugins when(pluginLoader.getPlugins(any(PluginFilter.class), any(PluginFilter.class))).thenReturn(Collections.EMPTY_LIST); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -91,7 +91,7 @@ public void testConvertToDtoPageForDiscussionWhenTopicNotClosedButTopicPluginsEn .thenReturn(Arrays.<Plugin>asList(topicPlugin)); when(topicPlugin.getTopicType()).thenReturn("Type 1"); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -125,7 +125,7 @@ public void pluginsShouldNotOverrideDiscussionTopicTypeWhenTopicNotClosed() { .thenReturn(Arrays.<Plugin>asList(topicPlugin)); when(topicPlugin.getTopicType()).thenReturn(TopicTypeName.DISCUSSION.getName()); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -158,7 +158,7 @@ public void testConvertToDtoPageForDiscussionWhenTopicClosedAndNoTopicPluginsEna when(pluginLoader.getPlugins(any(PluginFilter.class), any(PluginFilter.class))).thenReturn(Collections.EMPTY_LIST); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -192,7 +192,7 @@ public void testConvertToDtoPageForDiscussionWhenTopicClosedButTopicPluginsEnabl .thenReturn(Arrays.<Plugin>asList(topicPlugin)); when(topicPlugin.getTopicType()).thenReturn("Type 1"); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -228,7 +228,7 @@ public void pluginsShouldNotOverrideDiscussionTopicTypeWhenTopicClosed() { .thenReturn(Arrays.<Plugin>asList(topicPlugin)); when(topicPlugin.getTopicType()).thenReturn(TopicTypeName.DISCUSSION.getName()); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -261,7 +261,7 @@ public void testConvertToDtoPageForCodeReviewWhenNoTopicPluginsEnabled() { when(pluginLoader.getPlugins(any(PluginFilter.class), any(PluginFilter.class))).thenReturn(Collections.EMPTY_LIST); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -293,7 +293,7 @@ public void testConvertToDtoPageForCodeReviewWhenTopicPluginsEnabled() { .thenReturn(Arrays.<Plugin>asList(topicPlugin)); when(topicPlugin.getTopicType()).thenReturn("Type 1"); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -327,7 +327,7 @@ public void pluginShouldNotOverrideCodeReviewTopicType() { .thenReturn(Arrays.<Plugin>asList(topicPlugin)); when(topicPlugin.getTopicType()).thenReturn(TopicTypeName.CODE_REVIEW.getName()); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -362,7 +362,7 @@ public void testConvertToDtoPageForPlugableTopicWhenAppropriatePluginEnabledAndT .thenReturn(Arrays.<Plugin>asList(topicPlugin)); when(topicPlugin.getTopicType()).thenReturn(topicType); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -403,7 +403,7 @@ public void testConvertToDtoPageForPlugableTopicWhenAppropriatePluginEnabledAndT .thenReturn(Arrays.<Plugin>asList(topicPlugin)); when(topicPlugin.getTopicType()).thenReturn(topicType); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -442,7 +442,7 @@ public void plugableTopicShouldUseDefaultsWhenAppropriateDisabledAndTopicNotClos when(pluginLoader.getPlugins(any(PluginFilter.class), any(PluginFilter.class))).thenReturn(Collections.EMPTY_LIST); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); @@ -475,7 +475,7 @@ public void plugableTopicShouldUseDefaultsWhenAppropriateDisabledAndTopicClosed( when(pluginLoader.getPlugins(any(PluginFilter.class), any(PluginFilter.class))).thenReturn(Collections.EMPTY_LIST); - Page<TopicDto> result = converter.convertToDtoPage(new PageImpl<>(Arrays.asList(topic))); + Page<TopicDto> result = converter.convertTopicPageToTopicDtoPage(new PageImpl<>(Arrays.asList(topic))); assertEquals(result.getNumberOfElements(), 1); TopicDto dto = result.getContent().get(0); diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/BBCodeServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/BBCodeServiceTest.java index 5e2d79ab85..2b8ebf3698 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/BBCodeServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/BBCodeServiceTest.java @@ -15,11 +15,18 @@ package org.jtalks.jcommune.service.nontransactional; import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.service.bb2htmlprocessors.BBCodeListPreprocessor; +import org.kefirsf.bb.TextProcessor; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import static java.util.Collections.list; +import static java.util.Collections.singletonList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; /** * @author Evgeniy Naumenko @@ -41,6 +48,15 @@ public void testQuote() { assertEquals(result, "[quote=\"name\"]source[/quote]"); } + @Test + public void testUserbbProcessorReturnCorrectLinkWhenUserNameContainsUserTag() { + TextProcessor textProcessor = mock(TextProcessor.class); + when(textProcessor.process("[user][user]e-c[/user][/user]")).thenReturn("[user=/jcommune/users/16][user]e-c[/user][/user]"); + service.setPreprocessors(singletonList(textProcessor)); + String result = service.convertBbToHtml("[user][user]e-c[/user][/user]"); + assertEquals(result,"<a href=\"/jcommune/users/16\" class=\"mentioned-user\" >[user]e-c[/user]</a>"); + } + @Test(dataProvider = "validBBCodes") public void testBBCodeConversion(String bbCode, String expectedResult) { assertEquals(service.convertBbToHtml(bbCode), expectedResult); @@ -66,7 +82,11 @@ public Object[][] bbCodesToStrip() { {"[quote=\"admin\"]quote[/quote]", "quote", "strip named quote"}, {"[b][i][u][s][highlight][left][center][right][color=000033][size=12][quote][indent=15]" + "Ваш текст[/indent][/quote][/size][/color][/right][/center][/left][/highlight][/s][/u][/i][/b]", - "Ваш текст", "strip a pack of bb-codes"} + "Ваш текст", "strip a pack of bb-codes"}, + {"[url][/url]", "", "strip empty url"}, + {"[url=1][/url]", "", "strip empty url with non empty parameter" }, + {"[url=][/url]", "", "strip empty url with empty parameter"}, + {"[img][/img]", "", "strip empty img"} }; } @@ -107,7 +127,7 @@ public Object[][] validBBCodes() { "<a title=\"\" href=\"http://narod.ru/avatar.jpg\" class=\"pretty-photo\">" + "<img class=\"thumbnail\" alt=\"\" src=\"http://narod.ru/avatar.jpg\" onError=\"imgError(this)\" /></a>"}, //code - {"[code=sql]println(\"Hi!\");[/code]", "<pre class=\"prettyprint linenums sql\">println("Hi!");</pre>"}, + {"[code]println(\"Hi!\");[/code]", "<pre class=\"prettyprint linenums \">println("Hi!");</pre>"}, //qoutes {"[quote]Some text[/quote]", "<div class=\"quote bb_quote_container\"><span class=\"bb_quote_title\">Quote:" + diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/LocationServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/LocationServiceTest.java index b24c37a221..d6c990465f 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/LocationServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/LocationServiceTest.java @@ -14,97 +14,61 @@ */ package org.jtalks.jcommune.service.nontransactional; -import org.jtalks.common.security.SecurityService; import org.jtalks.jcommune.model.entity.JCUser; -import org.jtalks.jcommune.model.entity.AnonymousUser; import org.jtalks.jcommune.model.entity.Topic; -import org.jtalks.jcommune.service.UserService; +import org.jtalks.jcommune.model.entity.UserInfo; +import org.jtalks.jcommune.service.security.SecurityService; import org.mockito.Mock; -import org.mockito.Mockito; import org.springframework.security.core.session.SessionRegistry; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; /** * @author Andrey Kluev */ public class LocationServiceTest { + @Mock private SessionRegistry sessionRegistry; + @Mock private SecurityService securityService; + private JCUser user; private Topic topic; private LocationService locationService; - @Mock - private UserService userService; - @Mock - private SessionRegistry sessionRegistry; - private JCUser user; - List<Object> list; - Map<JCUser, String> map; - + private UserInfo userInfo; @BeforeMethod protected void setUp() { initMocks(this); - locationService = new LocationService(userService, sessionRegistry); - user = new JCUser("", "", ""); - topic = new Topic(user, ""); - topic.setUuid("uuid"); - list = new ArrayList<Object>(); - map = new ConcurrentHashMap<JCUser, String>(); + locationService = new LocationService(securityService, sessionRegistry); + user = randomUser(); + topic = new Topic(user, "Unit test!"); } @Test - public void testUsersViewing() { - when(userService.getCurrentUser()).thenReturn(user); - list.add(user); - map.put(user, ""); - when(sessionRegistry.getAllPrincipals()).thenReturn(list); - - topic.setUuid(""); - - locationService.getUsersViewing(topic); + public void shouldContainUsersViewingTheTopic() { + userInfo = new UserInfo(user); + when(securityService.getCurrentUserBasicInfo()).thenReturn(userInfo); //returns current user. + when(sessionRegistry.getAllPrincipals()).thenReturn(Collections.<Object>singletonList(userInfo)); // returns all principals from Session registry + List<UserInfo> usersViewing = locationService.getUsersViewing(topic); + assertEquals(usersViewing.size(), 1); + assertTrue(usersViewing.contains(userInfo)); } @Test - public void testUserNotOnline() { - when(userService.getCurrentUser()).thenReturn(user); - JCUser user1 = new JCUser("", "", ""); - list.add(user1); - when(sessionRegistry.getAllPrincipals()).thenReturn(list); - - - locationService.getUsersViewing(topic); + public void shouldNotContainAnonymousUsers() { + when(securityService.getCurrentUserBasicInfo()).thenReturn(null); // returns null coz current user is anonymous. + when(sessionRegistry.getAllPrincipals()).thenReturn(Collections.<Object>singletonList(new UserInfo(user))); // returns all principals from Session registry + List<UserInfo> usersViewing = locationService.getUsersViewing(topic); + assertEquals(usersViewing.size(), 0); } - @Test - public void testCurrentUserIsAnonymous() { - when(sessionRegistry.getAllPrincipals()).thenReturn(list); - when(userService.getCurrentUser()).thenReturn(new AnonymousUser()); - - locationService.getUsersViewing(topic); - } - - @Test - public void testClearUserLocation() { - when(userService.getCurrentUser()).thenReturn(user); - when(userService.getCurrentUser()).thenReturn(user); - - locationService.clearUserLocation(); - } - - @Test - public void testClearUserLocationForAnonymous() { - when(userService.getCurrentUser()).thenReturn(new AnonymousUser()); - - locationService.clearUserLocation(); - @SuppressWarnings("unchecked") - Map<JCUser, String> registerUserMap = mock(Map.class); - verify(registerUserMap, Mockito.never()).remove(Mockito.any()); + private JCUser randomUser() { + return new JCUser("username", "email@jtalk.org", "password"); } } diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/MailServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/MailServiceTest.java index f7227d1a81..66e4987699 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/MailServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/MailServiceTest.java @@ -19,6 +19,8 @@ import org.jtalks.common.model.entity.Property; import org.jtalks.jcommune.model.dao.PropertyDao; import org.jtalks.jcommune.model.entity.*; +import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.jtalks.jcommune.service.exceptions.MailingFailedException; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.mockito.ArgumentCaptor; @@ -64,6 +66,8 @@ public class MailServiceTest { private PropertyDao propertyDao; @Mock private MailSender sender; + @Mock + private EntityToDtoConverter converter; private JCommuneProperty notificationsEnabledProperty = SENDING_NOTIFICATIONS_ENABLED; // private MailService service; @@ -73,6 +77,7 @@ public class MailServiceTest { private Branch branch = new Branch("title Branch", "description"); private ArgumentCaptor<MimeMessage> captor; private ReloadableResourceBundleMessageSource messageSource; + private TopicDto topicDto = new TopicDto(topic); private long topicId = 777; private long branchId = 7; @@ -91,9 +96,11 @@ public void setUp() { messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename("classpath:/org/jtalks/jcommune/service/bundle/TemplatesMessages"); service = new MailService(sender, FROM, velocityEngine, messageSource, notificationsEnabledProperty, - new EscapeTool()); + new EscapeTool(), converter); MimeMessage message = new MimeMessage((Session) null); when(sender.createMimeMessage()).thenReturn(message); + topicDto.setTopicUrl("/topics/" + topicId); + when(converter.convertTopicToDto(any(Topic.class))).thenReturn(topicDto); captor = ArgumentCaptor.forClass(MimeMessage.class); topic.setId(topicId); branch.setId(branchId); @@ -275,39 +282,7 @@ public void testSendReceivedPrivateMessageNotificationFail() { service.sendReceivedPrivateMessageNotification(user, new PrivateMessage(null, null, null, null)); } - @Test - public void testSendTopicMovedMail() throws Exception { - enableEmailNotifications(); - service.sendTopicMovedMail(user, topic); - - this.checkMailCredentials(); - assertTrue(this.getMimeMailBody().contains(USERNAME)); - assertTrue(this.getMimeMailBody().contains("http://coolsite.com:1234/forum/topics/" + topicId)); - assertTrue(this.getMimeMailBody().contains("http://coolsite.com:1234/forum/branches/" + branchId - + "/unsubscribe")); - } - - @Test - public void topicMovedMailShouldNotBeSentIfNotificationsAreDisabled() throws Exception { - disableEmailNotifications(); - service.sendTopicMovedMail(user, topic); - verify(sender, never()).send(any(MimeMessage.class)); - } - - @Test - public void testSendTopicMovedMailFailed() throws Exception { - enableEmailNotifications(); - Exception fail = new MailSendException(""); - doThrow(fail).when(sender).send(Matchers.<SimpleMailMessage>any()); - service.sendTopicMovedMail(user, topic); - - this.checkMailCredentials(); - assertTrue(this.getMimeMailBody().contains("http://coolsite.com:1234/forum/topics/" + topicId)); - assertTrue(this.getMimeMailBody().contains("http://coolsite.com:1234/forum/branches/" + branchId - + "/unsubscribe")); - } - @Test public void sendUserMentionedNotificationShouldSentIt() throws Exception { enableEmailNotifications(); @@ -361,24 +336,6 @@ private void enableEmailNotifications() { when(propertyDao.getByName(PROPERTY_NAME)).thenReturn(enabledProperty); } - @Test - public void testSendRemovingTopicMail() throws Exception { - enableEmailNotifications(); - - service.sendRemovingTopicMail(user, topic); - - this.checkMailCredentials(); - - String subjectTemplate = - messageSource.getMessage("removeTopic.subject", new Object[]{}, user.getLanguage().getLocale()); - - String bodyTemplate = - messageSource.getMessage("removeTopic.content", new Object[]{}, user.getLanguage().getLocale()); - - assertEquals(this.getMimeMailSubject(), subjectTemplate); - assertTrue(this.getMimeMailBody().contains(bodyTemplate)); - } - @Test public void testSendRemovingTopicMailCurrentUserAware() throws Exception{ enableEmailNotifications(); @@ -396,15 +353,6 @@ public void testSendRemovingTopicMailCurrentUserAware() throws Exception{ assertTrue(this.getMimeMailBody().contains(bodyTemplate)); } - @Test - public void removingTopicMailShouldNotBeSentWhenForumNotificationsAreDisabled() throws Exception { - disableEmailNotifications(); - - service.sendRemovingTopicMail(user, topic); - - verify(sender, never()).send(any(MimeMessage.class)); - } - @Test public void removingTopicMailCurrentUserAwareShouldNotBeSentWhenForumNotificationsAreDisabled() throws Exception { @@ -415,24 +363,6 @@ public void removingTopicMailCurrentUserAwareShouldNotBeSentWhenForumNotificatio verify(sender, never()).send(any(MimeMessage.class)); } - @Test - public void testSendRemovingTopicWithCodeReviewMail() throws Exception { - enableEmailNotifications(); - topic.setType(TopicTypeName.CODE_REVIEW.getName()); - service.sendRemovingTopicMail(user, topic); - - this.checkMailCredentials(); - - String subjectTemplate = - messageSource.getMessage("removeCodeReview.subject", new Object[]{}, user.getLanguage().getLocale()); - - String bodyTemplate = - messageSource.getMessage("removeCodeReview.content", new Object[]{}, user.getLanguage().getLocale()); - - assertEquals(this.getMimeMailSubject(), subjectTemplate); - assertTrue(this.getMimeMailBody().contains(bodyTemplate)); - } - @Test public void testSendRemovingTopicWithCodeReviewMailCurrentUserAware() throws Exception { enableEmailNotifications(); diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/MentionedUsersTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/MentionedUsersTest.java index 7119b24610..7198afc5dc 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/MentionedUsersTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/MentionedUsersTest.java @@ -34,8 +34,7 @@ import static java.util.Arrays.asList; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; +import static org.testng.Assert.*; /** @author Anuar_Nurmakanov */ public class MentionedUsersTest { @@ -114,6 +113,30 @@ public void extractMentionedUserShouldReturnAllMentionedUserWithSpacesInBBCodes( assertTrue(extractedUserNames.contains("и в а н о в"), "MentionedUsers should extract usernames with spaces"); } + @Test + public void extractMentionedUserShouldReturnMentionedUserWithSquareBracketsInBBCodes() { + String textWithUsersMentioning = "first [user]userWithoutBrackets[/user]" + + "second [user][userWithBrackets][/user]"; + + MentionedUsers mentionedUsers = MentionedUsers.parse(textWithUsersMentioning); + Set<String> extractedUserNames = mentionedUsers.extractAllMentionedUsers(textWithUsersMentioning); + + assertTrue(extractedUserNames.contains("userWithoutBrackets"), "MentionedUsers should extract username without brackets"); + assertTrue(extractedUserNames.contains("[userWithBrackets]"), "MentionedUsers should extract username with brackets"); + } + + + @Test + public void extractMentionedUserShouldReturnMentionedUserWithSquareBracketsInBBCodesU() { + String textWithUsersMentioning = "second [user][user][userWithBrackets][/user][/user]"; + + MentionedUsers mentionedUsers = MentionedUsers.parse(textWithUsersMentioning); + Set<String> extractedUserNames = mentionedUsers.extractAllMentionedUsers(textWithUsersMentioning); + + assertTrue(extractedUserNames.contains("[user][userWithBrackets][/user]"), "MentionedUsers should extract username with brackets"); + } + + /** * When data is sent to the server for the preview, there is a hack that coverts some special symbols into * unrecognizable character sequence. This replacement was implemented as a temporal solution, but still is there, @@ -146,8 +169,8 @@ public void extractMentionedUserShouldRecognizedEncodedInUriNames() { MentionedUsers mentionedUsers = MentionedUsers.parse(textWithUsersMentioning); Set<String> extractedUserNames = mentionedUsers.extractAllMentionedUsers(textWithUsersMentioning); - assertTrue(extractedUserNames.contains("иванов"), "иванов is mentioned, so he should be extracted."); - assertTrue(extractedUserNames.contains("\\yak"), "\\yak is mentioned, so he should be extracted."); + assertFalse(extractedUserNames.contains("иванов"), "We don't have to decode this values."); + assertFalse(extractedUserNames.contains("\\yak"), "We don't have to decode this value."); } @Test @@ -416,13 +439,13 @@ public void processShouldAttachProfileLinkToExistEncodedCyrillicUsers() throws N MentionedUsers mentionedUsers = MentionedUsers.parse(notProcessedSource); - String expectedAfterProcess = format(MENTIONING_WITH_LINK_TO_PROFILE_TEMPALTE, + String wrongAfterProcess = format(MENTIONING_WITH_LINK_TO_PROFILE_TEMPALTE, cyrillicCharsUserProfile, cyrillicCharsUserName, cyrillicCharsUserWithSpaceProfile, cyrillicCharsUserNameWithSpaces); String actualAfterProcess = mentionedUsers.getTextWithProcessedUserTags(userDao); - assertEquals(actualAfterProcess, expectedAfterProcess); + assertFalse(actualAfterProcess.equals(wrongAfterProcess), "Values which seems to be encoded should not be decoded."); } @Test diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/NotificationServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/NotificationServiceTest.java index ff57042c64..1532268af4 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/NotificationServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/nontransactional/NotificationServiceTest.java @@ -18,6 +18,7 @@ import org.jtalks.jcommune.model.entity.Branch; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.Topic; +import org.jtalks.jcommune.plugin.api.PluginLoader; import org.jtalks.jcommune.service.SubscriptionService; import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.service.exceptions.MailingFailedException; @@ -25,8 +26,7 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -import java.util.ArrayList; -import java.util.Collection; +import java.util.HashSet; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; @@ -42,6 +42,8 @@ public class NotificationServiceTest { private UserService userService; @Mock private SubscriptionService subscriptionService; + @Mock + private PluginLoader pluginLoader; private NotificationService service; private final long TOPIC_ID = 1; @@ -59,7 +61,8 @@ public void setUp() { service = new NotificationService( userService, mailService, - subscriptionService); + subscriptionService, + pluginLoader); topic = new Topic(user1, "title"); topic.setId(TOPIC_ID); branch = new Branch("name", "description"); @@ -142,69 +145,163 @@ public void testBranchChangedNoSubscribers() { @Test public void testTopicMovedWithBranchSubscribers() { - branch.getSubscribers().add(currentUser); + branch.setSubscribers(new HashSet<JCUser>()); branch.getSubscribers().add(user2); branch.getSubscribers().add(user3); + when(subscriptionService.getAllowedSubscribers(branch)).thenReturn(branch.getSubscribers()); + service.sendNotificationAboutTopicMoved(topic); - verify(mailService).sendTopicMovedMail(user2, topic, "current"); - verify(mailService).sendTopicMovedMail(user3, topic, "current"); + verify(mailService).sendTopicMovedMail(user2, topic, currentUser.getUsername(), Branch.class); + verify(mailService).sendTopicMovedMail(user3, topic, currentUser.getUsername(), Branch.class); } @Test - public void testTopicMovedTopicStarterIsNotASubscriber() { + public void testTopicMovedCurrentUserIsBranchSubscriber() { + branch.setSubscribers(new HashSet<JCUser>()); branch.getSubscribers().add(currentUser); branch.getSubscribers().add(user2); - branch.getSubscribers().add(user3); + + when(subscriptionService.getAllowedSubscribers(branch)).thenReturn(branch.getSubscribers()); service.sendNotificationAboutTopicMoved(topic); - verify(mailService, times(2)).sendTopicMovedMail(any(JCUser.class), eq(topic), eq("current")); - verify(mailService).sendTopicMovedMail(user2, topic, "current"); - verify(mailService).sendTopicMovedMail(user3, topic, "current"); - assertEquals(branch.getSubscribers().size(), 3); + verify(mailService, never()).sendTopicMovedMail(currentUser, topic, currentUser.getUsername(), Branch.class); + verify(mailService).sendTopicMovedMail(user2, topic, currentUser.getUsername(), Branch.class); } @Test - public void testTopicMovedWhenUserIsSubscribedForBranchAndTopic() { - branch.getSubscribers().add(user2); - branch.getSubscribers().add(user3); + public void testTopicMovedWithTopicSubscribers() { + topic.setSubscribers(new HashSet<JCUser>()); + topic.getSubscribers().add(user1); + topic.getSubscribers().add(user2); + + when(subscriptionService.getAllowedSubscribers(topic)).thenReturn(topic.getSubscribers()); + + service.sendNotificationAboutTopicMoved(topic); + + verify(mailService).sendTopicMovedMail(user1, topic, currentUser.getUsername(), Topic.class); + verify(mailService).sendTopicMovedMail(user2, topic, currentUser.getUsername(), Topic.class); + } + + @Test + public void testTopicMovedCurrentUserIsTopicSubscriber() { + topic.setSubscribers(new HashSet<JCUser>()); + topic.getSubscribers().add(user1); + topic.getSubscribers().add(currentUser); + + when(subscriptionService.getAllowedSubscribers(topic)).thenReturn(topic.getSubscribers()); service.sendNotificationAboutTopicMoved(topic); - Collection<JCUser> topicSubscribers = new ArrayList(); - topicSubscribers.add(user2); + verify(mailService, never()).sendTopicMovedMail(currentUser, topic, currentUser.getUsername(), Topic.class); + verify(mailService).sendTopicMovedMail(user1, topic, currentUser.getUsername(), Topic.class); + } - when(subscriptionService.getAllowedSubscribers(topic)).thenReturn(topicSubscribers); + @Test + public void testTopicMovedUserSubscribedToBranchAndTopic() { + branch.setSubscribers(new HashSet<JCUser>()); + branch.getSubscribers().add(user1); + topic.getSubscribers().add(user1); + branch.getSubscribers().add(user2); + + when(subscriptionService.getAllowedSubscribers(branch)).thenReturn(branch.getSubscribers()); + when(subscriptionService.getAllowedSubscribers(topic)).thenReturn(topic.getSubscribers()); + + service.sendNotificationAboutTopicMoved(topic); + + verify(mailService).sendTopicMovedMail(user1, topic, currentUser.getUsername(), Topic.class); + verify(mailService, never()).sendTopicMovedMail(user1, topic, currentUser.getUsername(), Branch.class); + verify(mailService, never()).sendTopicMovedMail(user2, topic, currentUser.getUsername(), Topic.class); + verify(mailService).sendTopicMovedMail(user2, topic, currentUser.getUsername(), Branch.class); - verify(mailService, times(1)).sendTopicMovedMail(user3, topic, "current"); } @Test - public void testSendNotificationAboutRemovingTopic() { - Collection<JCUser> subscribers = new ArrayList(); - subscribers.add(user1); - subscribers.add(user2); - service.sendNotificationAboutRemovingTopic(topic, subscribers); - verify(mailService).sendRemovingTopicMail(user2, topic, "current"); + public void testTopicMovedNoSubscribers() { + service.sendNotificationAboutTopicMoved(topic); + + verifyZeroInteractions(mailService); } + @Test - public void testTopicChangedWithFilterByTopicSubscribers() throws MailingFailedException { + public void testTopicRemovedTopicSubscribers() { + topic.setSubscribers(new HashSet<JCUser>()); topic.getSubscribers().add(user1); topic.getSubscribers().add(user2); + + when(subscriptionService.getAllowedSubscribers(topic)).thenReturn(topic.getSubscribers()); + + service.sendNotificationAboutRemovingTopic(topic); + + verify(mailService).sendRemovingTopicMail(user1, topic, currentUser.getUsername()); + verify(mailService).sendRemovingTopicMail(user2, topic, currentUser.getUsername()); + } + + @Test + public void testTopicRemovedCurrentUserIsTopicSubscriber() { + topic.setSubscribers(new HashSet<JCUser>()); topic.getSubscribers().add(currentUser); + topic.getSubscribers().add(user1); + when(subscriptionService.getAllowedSubscribers(topic)).thenReturn(topic.getSubscribers()); - Collection<JCUser> topicSubscribers = new ArrayList(); - topicSubscribers.add(user2); + service.sendNotificationAboutRemovingTopic(topic); - service.subscribedEntityChanged(topic, topicSubscribers); + verify(mailService, never()).sendRemovingTopicMail(currentUser, topic, currentUser.getUsername()); + verify(mailService).sendRemovingTopicMail(user1, topic, currentUser.getUsername()); + } - verify(mailService, times(1)).sendUpdatesOnSubscription(any(JCUser.class), eq(topic)); - verify(mailService).sendUpdatesOnSubscription(user1, topic); - assertEquals(topic.getSubscribers().size(), 2); + @Test + public void testTopicRemovedBranchSubscribers() { + branch.setSubscribers(new HashSet<JCUser>()); + branch.getSubscribers().add(user1); + branch.getSubscribers().add(user2); + + when(subscriptionService.getAllowedSubscribers(branch)).thenReturn(branch.getSubscribers()); + + service.sendNotificationAboutRemovingTopic(topic); + + verify(mailService).sendUpdatesOnSubscription(user1, branch); + verify(mailService).sendUpdatesOnSubscription(user2, branch); + } + + @Test + public void testTopicRemovedCurrentUserIsBranchSubscriber() { + branch.setSubscribers(new HashSet<JCUser>()); + branch.getSubscribers().add(currentUser); + branch.getSubscribers().add(user1); + + when(subscriptionService.getAllowedSubscribers(branch)).thenReturn(branch.getSubscribers()); + + service.sendNotificationAboutRemovingTopic(topic); + + verify(mailService, never()).sendUpdatesOnSubscription(currentUser, branch); + verify(mailService).sendUpdatesOnSubscription(user1, branch); + + } + + @Test + public void testTopicRemovedUserSubscribedToBranchAndTopic() { + branch.setSubscribers(new HashSet<JCUser>()); + branch.getSubscribers().add(user1); + branch.getSubscribers().add(user2); + topic.getSubscribers().add(user2); + topic.getSubscribers().add(user3); + + when(subscriptionService.getAllowedSubscribers(branch)).thenReturn(branch.getSubscribers()); + when(subscriptionService.getAllowedSubscribers(topic)).thenReturn(topic.getSubscribers()); + + service.sendNotificationAboutRemovingTopic(topic); + + verify(mailService).sendUpdatesOnSubscription(user1, branch); + verify(mailService, never()).sendRemovingTopicMail(user1, topic, currentUser.getUsername()); + verify(mailService, never()).sendRemovingTopicMail(user2, topic, currentUser.getUsername()); + verify(mailService).sendUpdatesOnSubscription(user2, branch); + verify(mailService).sendRemovingTopicMail(user3, topic, currentUser.getUsername()); + verify(mailService, never()).sendUpdatesOnSubscription(user3, branch); } @Test diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/AclGroupPermissionEvaluatorTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/AclGroupPermissionEvaluatorTest.java index fbf4b393ad..4701226c01 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/AclGroupPermissionEvaluatorTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/AclGroupPermissionEvaluatorTest.java @@ -14,27 +14,20 @@ */ package org.jtalks.jcommune.service.security; -import static org.mockito.Mockito.when; - -import java.util.ArrayList; -import java.util.List; - import org.jtalks.common.model.dao.GroupDao; import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.entity.User; import org.jtalks.common.model.permissions.BranchPermission; import org.jtalks.common.model.permissions.JtalksPermission; import org.jtalks.common.model.permissions.ProfilePermission; -import org.jtalks.common.security.acl.AclUtil; -import org.jtalks.common.security.acl.ExtendedMutableAcl; -import org.jtalks.common.security.acl.GroupAce; -import org.jtalks.common.security.acl.sids.JtalksSidFactory; -import org.jtalks.common.security.acl.sids.UserGroupSid; -import org.jtalks.common.security.acl.sids.UserSid; -import org.jtalks.jcommune.model.dao.UserDao; import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.UserInfo; import org.jtalks.jcommune.plugin.api.PluginPermissionManager; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.jtalks.jcommune.service.security.acl.*; +import org.jtalks.jcommune.service.security.acl.sids.JtalksSidFactory; +import org.jtalks.jcommune.service.security.acl.sids.UserGroupSid; +import org.jtalks.jcommune.service.security.acl.sids.UserSid; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -49,39 +42,35 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + /** * @author stanislav bashkirtsev */ public class AclGroupPermissionEvaluatorTest { - @Mock - private org.jtalks.common.security.acl.AclManager aclManager; - @Mock - private AclUtil aclUtil; - @Mock - private GroupDao groupDao; - @Mock - private JtalksSidFactory sidFactory; - @Mock - private ExtendedMutableAcl mutableAcl; - @Mock - Authentication authentication; - @Mock - JdbcMutableAclService mutableAclService; - @Mock - MutableAcl acl; - @Mock - UserDao userDao; - @Mock - PluginPermissionManager pluginManager; + @Mock private AclManager aclManager; + @Mock private AclUtil aclUtil; + @Mock private GroupDao groupDao; + @Mock private JtalksSidFactory sidFactory; + @Mock private ExtendedMutableAcl mutableAcl; + @Mock private Authentication authentication; + @Mock private JdbcMutableAclService mutableAclService; + @Mock private MutableAcl acl; + @Mock private PluginPermissionManager pluginManager; + @Mock private SecurityService securityService; + private JCUser user; private AclGroupPermissionEvaluator evaluator; private UserGroupSid groupSid; private UserSid userSid; private ObjectIdentityImpl objectIdentity; - private JCUser user; - private Group group; - private Long targetId = 1L; + private long targetId = 1L; private String targetType = "BRANCH"; private String permission = "BranchPermission.CREATE_POSTS"; private BranchPermission generalPermission = BranchPermission.CREATE_POSTS; @@ -90,24 +79,26 @@ public class AclGroupPermissionEvaluatorTest { @BeforeMethod public void init() throws NotFoundException { MockitoAnnotations.initMocks(this); - evaluator = new AclGroupPermissionEvaluator(aclManager, aclUtil, groupDao, - sidFactory, mutableAclService, userDao, pluginManager); + evaluator = new AclGroupPermissionEvaluator(aclManager, aclUtil, + sidFactory, mutableAclService, pluginManager, securityService); objectIdentity = new ObjectIdentityImpl(targetType, targetId); Mockito.when(aclUtil.createIdentity(targetId, targetType)).thenReturn(objectIdentity); - user = new JCUser("username", "email", "password"); + user = new JCUser("username", "email@jtalks.org", "password"); user.setId(1); - userSid = new UserSid(user); + userSid = new UserSid(user.getId()); groupSid = new UserGroupSid(targetId); - group = Mockito.mock(Group.class); - List<User> users = new ArrayList<>(); - users.add(user); - when(group.getUsers()).thenReturn(users); - when(group.getId()).thenReturn(targetId); + Group group = new Group(); + group.setId(2); + group.setUsers(Collections.<User>singletonList(user)); + user.setGroups(Collections.singletonList(group)); when(sidFactory.createPrincipal(authentication)).thenReturn(userSid); when(sidFactory.create(group)).thenReturn(groupSid); - when(authentication.getPrincipal()).thenReturn(user); + when(authentication.getPrincipal()).thenReturn(new UserInfo(user)); when(mutableAclService.readAclById(Mockito.any(ObjectIdentity.class))).thenReturn(acl); - when(userDao.get(user.getId())).thenReturn(user); + when(securityService.getFullUserInfoFrom(any(Authentication.class))).thenReturn(user); + List<GroupAce> controlEntries = new ArrayList<>(); + Mockito.when(aclManager.getGroupPermissionsFilteredByPermissionOn(Mockito.any(ObjectIdentity.class), Mockito.any(JtalksPermission.class))).thenReturn(controlEntries); + } @Test @@ -120,8 +111,6 @@ public void testHasPermissionForUserSidSuccessTest() throws Exception { Mockito.when(aclUtil.getAclFor(objectIdentity)).thenReturn(mutableAcl); Mockito.when(mutableAcl.getEntries()).thenReturn(aces); - List<GroupAce> controlEntries = new ArrayList<>(); - Mockito.when(aclManager.getGroupPermissionsOn(objectIdentity)).thenReturn(controlEntries); String targetIdString = "1"; Assert.assertTrue(evaluator.hasPermission(authentication, targetIdString, targetType, permission)); Assert.assertTrue(evaluator.hasPermission(authentication, targetId, targetType, permission)); @@ -140,9 +129,10 @@ public void testHasPermissionForPermissionOnGroupSuccessTest() throws Exception @Test public void testHasPermissionForPermissionOnGroupNonExistentUserTest() throws Exception { setEnvForPermissionOnGroupTests(true); - Long nonExistentId = -1L; + long nonExistentId = -1L; user.setId(nonExistentId); - when(userDao.get(nonExistentId)).thenReturn(null); + user.setGroups(Collections.<Group>emptyList()); + when(securityService.getFullUserInfoFrom(any(Authentication.class))).thenReturn(user); Assert.assertFalse(evaluator.hasPermission(authentication, nonExistentId, targetType, permission)); Assert.assertFalse(evaluator.hasPermission(authentication, nonExistentId, targetType, "GeneralPermission.READ")); } @@ -166,11 +156,8 @@ private void setEnvForPermissionOnGroupTests(boolean isGranted) { Mockito.when(aclUtil.getAclFor(groupIdentity)).thenReturn(mutableAcl); List<GroupAce> controlEntries = new ArrayList<>(); - Mockito.when(aclManager.getGroupPermissionsOn(groupIdentity)).thenReturn(controlEntries); - - List<Group> groups = new ArrayList<>(); - groups.add(group); - ((JCUser) user).setGroups(groups); + controlEntries.add(createGroupAce(generalPermission, isGranted)); + Mockito.when(aclManager.getGroupPermissionsFilteredByPermissionOn(Mockito.any(ObjectIdentity.class), Mockito.any(JtalksPermission.class))).thenReturn(controlEntries); } @Test @@ -195,7 +182,7 @@ private void setEnvForGroupSidTests(boolean isGranted) { controlEntries.add(createGroupAce(someOtherPermission, true)); controlEntries.add(createGroupAce(someOtherPermission, false)); controlEntries.add(createGroupAce(generalPermission, isGranted)); - Mockito.when(aclManager.getGroupPermissionsOn(objectIdentity)).thenReturn(controlEntries); + Mockito.when(aclManager.getGroupPermissionsFilteredByPermissionOn(Mockito.any(ObjectIdentity.class), Mockito.any(JtalksPermission.class))).thenReturn(controlEntries); } @Test @@ -206,8 +193,6 @@ public void testHasPermissionForUserSidNotSuccessTest() throws Exception { Mockito.when(mutableAcl.getEntries()).thenReturn(aces); Mockito.when(acl.getEntries()).thenReturn(aces); - List<GroupAce> controlEntries = new ArrayList<>(); - Mockito.when(aclManager.getGroupPermissionsOn(objectIdentity)).thenReturn(controlEntries); Assert.assertFalse(evaluator.hasPermission(authentication, targetId, targetType, permission)); } @@ -218,8 +203,6 @@ public void testHasPermissionForUserSidEmptyTest() throws Exception { Mockito.when(mutableAcl.getEntries()).thenReturn(aces); Mockito.when(acl.getEntries()).thenReturn(aces); - List<GroupAce> controlEntries = new ArrayList<>(); - Mockito.when(aclManager.getGroupPermissionsOn(objectIdentity)).thenReturn(controlEntries); Assert.assertFalse(evaluator.hasPermission(authentication, targetId, targetType, permission)); } @@ -229,9 +212,6 @@ public void testHasPermissionForInvalidPermissionTest() throws Exception { Mockito.when(aclUtil.getAclFor(objectIdentity)).thenReturn(mutableAcl); Mockito.when(mutableAcl.getEntries()).thenReturn(aces); Mockito.when(acl.getEntries()).thenReturn(aces); - - List<GroupAce> controlEntries = new ArrayList<>(); - Mockito.when(aclManager.getGroupPermissionsOn(objectIdentity)).thenReturn(controlEntries); Assert.assertFalse(evaluator.hasPermission(authentication, targetId, targetType, "123")); } @@ -256,6 +236,7 @@ private GroupAce createGroupAce(BranchPermission permission, boolean isGranted) users.add(user); Mockito.when(group.getUsers()).thenReturn(users); Mockito.when(groupAce.getGroup(groupDao)).thenReturn(group); + Mockito.when(groupAce.getGroupId()).thenReturn(2L); Mockito.when(groupAce.isGranting()).thenReturn(isGranted); Mockito.when(groupAce.getPermission()).thenReturn(permission); return groupAce; diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/JCPermissionFactoryTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/JCPermissionFactoryTest.java index 781dcee2d9..c30dc9546a 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/JCPermissionFactoryTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/JCPermissionFactoryTest.java @@ -18,7 +18,6 @@ import org.jtalks.common.model.permissions.BranchPermission; import org.jtalks.common.model.permissions.JtalksPermission; -import org.jtalks.common.security.acl.JtalksPermissionFactory; import org.jtalks.jcommune.plugin.api.PluginsPermissionFactory; import org.springframework.security.acls.model.Permission; import org.testng.annotations.BeforeMethod; diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/PermissionManagerTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/PermissionManagerTest.java index cf4fbe2d65..7e196e1e98 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/PermissionManagerTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/PermissionManagerTest.java @@ -24,12 +24,6 @@ import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.permissions.BranchPermission; import org.jtalks.common.model.permissions.JtalksPermission; -import org.jtalks.common.security.acl.AclManager; -import org.jtalks.common.security.acl.AclUtil; -import org.jtalks.common.security.acl.ExtendedMutableAcl; -import org.jtalks.common.security.acl.GroupAce; -import org.jtalks.common.security.acl.sids.UserGroupSid; -import org.jtalks.common.security.acl.sids.UserSid; import org.jtalks.jcommune.model.dao.GroupDao; import org.jtalks.jcommune.model.dto.GroupsPermissions; import org.jtalks.jcommune.model.dto.PermissionChanges; @@ -38,6 +32,12 @@ import org.jtalks.jcommune.model.entity.ObjectsFactory; import org.jtalks.jcommune.model.entity.PersistedObjectsFactory; import org.jtalks.jcommune.plugin.api.PluginPermissionManager; +import org.jtalks.jcommune.service.security.acl.AclManager; +import org.jtalks.jcommune.service.security.acl.AclUtil; +import org.jtalks.jcommune.service.security.acl.ExtendedMutableAcl; +import org.jtalks.jcommune.service.security.acl.GroupAce; +import org.jtalks.jcommune.service.security.acl.sids.UserGroupSid; +import org.jtalks.jcommune.service.security.acl.sids.UserSid; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/TransactionalPermissionServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/TransactionalPermissionServiceTest.java index bcdb7aaf8b..569f0d30a0 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/TransactionalPermissionServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/security/TransactionalPermissionServiceTest.java @@ -19,10 +19,12 @@ import org.jtalks.common.model.permissions.BranchPermission; import org.jtalks.common.model.permissions.GeneralPermission; import org.jtalks.common.model.permissions.JtalksPermission; -import org.jtalks.common.service.security.SecurityContextHolderFacade; +import org.jtalks.common.service.security.SecurityContextFacade; import org.jtalks.jcommune.model.dto.GroupsPermissions; import org.jtalks.jcommune.model.dto.PermissionChanges; import org.jtalks.jcommune.model.entity.Branch; +import org.jtalks.jcommune.service.security.acl.AclClassName; +import org.jtalks.jcommune.service.security.acl.AclGroupPermissionEvaluator; import org.jtalks.jcommune.service.transactional.TransactionalPermissionService; import org.mockito.Mock; import org.springframework.security.access.AccessDeniedException; @@ -41,7 +43,7 @@ public class TransactionalPermissionServiceTest { @Mock - private SecurityContextHolderFacade contextFacade; + private SecurityContextFacade contextFacade; @Mock private AclGroupPermissionEvaluator aclEvaluator; diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalAuthenticatorTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalAuthenticatorTest.java index 66efb4955a..6e80a5a0a2 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalAuthenticatorTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalAuthenticatorTest.java @@ -19,7 +19,7 @@ import org.jtalks.common.model.dao.GroupDao; import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.entity.User; -import org.jtalks.common.service.security.SecurityContextHolderFacade; +import org.jtalks.common.service.security.SecurityContextFacade; import org.jtalks.jcommune.model.dao.UserDao; import org.jtalks.jcommune.model.dto.RegisterUserDto; import org.jtalks.jcommune.model.dto.UserDto; @@ -31,6 +31,7 @@ import org.jtalks.jcommune.plugin.api.exceptions.UnexpectedErrorException; import org.jtalks.jcommune.service.Authenticator; import org.jtalks.jcommune.service.PluginService; +import org.jtalks.jcommune.service.util.AuthenticationStatus; import org.jtalks.jcommune.service.nontransactional.EncryptionService; import org.jtalks.jcommune.service.nontransactional.ImageService; import org.jtalks.jcommune.service.nontransactional.MailService; @@ -40,12 +41,15 @@ import org.mockito.Mock; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.encoding.Md5PasswordEncoder; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -61,12 +65,12 @@ import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; import static org.testng.Assert.*; +import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals; /** * @author Andrey Pogorelov */ public class TransactionalAuthenticatorTest { - @Mock private PluginLoader pluginLoader; @Mock @@ -82,7 +86,7 @@ public class TransactionalAuthenticatorTest { @Mock private AuthenticationManager authenticationManager; @Mock - private SecurityContextHolderFacade securityFacade; + private SecurityContextFacade securityFacade; @Mock private SecurityContext securityContext; @Mock @@ -157,9 +161,10 @@ public void authenticateExistingUserShouldBeSuccessful() throws Exception { prepareAuth(); preparePlugin(user.getUsername(), passwordHash, authInfo); - boolean result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); + AuthenticationStatus result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); - assertTrue(result, "Authentication existing user with correct credentials should be successful."); + assertEquals(result, AuthenticationStatus.AUTHENTICATED, + "Authentication existing user with correct credentials should be successful."); } @Test @@ -173,9 +178,10 @@ public void authenticateNotExistingUserShouldBeSuccessful() throws Exception { prepareAuth(); preparePlugin(user.getUsername(), passwordHash, authInfo); - boolean result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); + AuthenticationStatus result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); - assertTrue(result, "Authentication not existing user with correct credentials should be successful."); + assertEquals(result, AuthenticationStatus.AUTHENTICATED, + "Authentication not existing user with correct credentials should be successful."); } @Test @@ -198,11 +204,24 @@ public void authenticateUserWithNewCredentialsShouldBeSuccessful() throws Except .thenThrow(new BadCredentialsException(null)).thenReturn(expectedToken); preparePlugin(oldUser.getUsername(), passwordHash, authInfo); - boolean result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); + AuthenticationStatus result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); verify(userDao).saveOrUpdate(oldUser); - assertTrue(result, "Authentication user with new credentials should be successful."); + assertEquals(result, AuthenticationStatus.AUTHENTICATED, + "Authentication user with new credentials should be successful."); + } + + @Test + public void authenticateNotEnabledUserShouldFail() throws Exception { + LoginUserDto loginUserDto = createDefaultLoginUserDto(); + prepareOldUser(loginUserDto.getUserName()); + when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) + .thenThrow(new DisabledException(null)); + AuthenticationStatus result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); + + assertEquals(result, AuthenticationStatus.NOT_ENABLED, + "Authenticate user with bad credentials should fail."); } @Test @@ -223,10 +242,11 @@ public void authenticateUserShouldBeSuccessfulIfPluginAndJCommuneUseTheSameDatab prepareAuth(); preparePlugin(username, passwordHash, authInfo); - boolean result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); + AuthenticationStatus result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); - assertTrue(result, "Authentication not existing user with correct credentials should be successful " + - "if case Plugin and JCommune use the same database."); + assertEquals(result, AuthenticationStatus.AUTHENTICATED, + "Authentication not existing user with correct credentials should be successful " + + "if case Plugin and JCommune use the same database."); } @Test @@ -244,9 +264,10 @@ public void authenticateUserWithNewCredentialsShouldFailIfPluginNotFound() throw when(pluginLoader.getPlugins(any(TypeFilter.class))).thenReturn(Collections.EMPTY_LIST); - boolean result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); + AuthenticationStatus result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); - assertFalse(result, "Authenticate user with new credentials should fail if plugin not found."); + assertEquals(result, AuthenticationStatus.AUTHENTICATION_FAIL, + "Authenticate user with new credentials should fail if plugin not found."); } @Test @@ -263,9 +284,10 @@ public void authenticateUserWithBadCredentialsShouldFail() throws Exception { preparePlugin(oldUser.getUsername(), passwordHash, Collections.EMPTY_MAP); - boolean result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); + AuthenticationStatus result = authenticator.authenticate(loginUserDto, httpRequest, httpResponse); - assertFalse(result, "Authenticate user with bad credentials should fail."); + assertEquals(result, AuthenticationStatus.AUTHENTICATION_FAIL, + "Authenticate user with bad credentials should fail."); } @Test(expectedExceptions = NoConnectionException.class) @@ -403,6 +425,67 @@ public void userShouldBeRegisteredUsingEncryptedPassword() throws Exception{ verify(authenticatorSpy).storeRegisteredUser(refEq(expected)); } + @Test + public void userMustHaveInitialFieldValuesWhenDefaultRegistrationFailWithValidationError() throws Exception { + Validator customValidator = new Validator() { + @Override + public boolean supports(Class<?> clazz) { + return true; + } + + @Override + public void validate(Object target, Errors errors) { + errors.rejectValue("userDto.email", "", "An email format should be like mail@mail.ru"); + } + }; + RegisterUserDto registerUserDto = createRegisterUserDto("username", "password", "email", null); + EncryptionService realEncryptionService = new EncryptionService(new Md5PasswordEncoder()); + ReflectionTestUtils.setField(authenticator, "encryptionService", realEncryptionService); + ReflectionTestUtils.setField(authenticator, "validator", customValidator); + when(pluginService.getRegistrationPlugins()).thenReturn(Collections.EMPTY_MAP); + + authenticator.register(registerUserDto); + RegisterUserDto expectedRegisterUserDto = createRegisterUserDto("username", "password", "email", null); + + assertReflectionEquals(expectedRegisterUserDto, registerUserDto); + } + + @Test + public void userMustHaveInitialFieldValuesWhenPluginRegistrationFailWithValidationError() throws Exception { + RegisterUserDto registerUserDto = createRegisterUserDto("username", "password", "email", null); + EncryptionService realEncryptionService = new EncryptionService(new Md5PasswordEncoder()); + Map<String, String> errors = new HashMap<>(); + errors.put("userDto.email", "An email format should be like mail@mail.ru"); + ReflectionTestUtils.setField(authenticator, "encryptionService", realEncryptionService); + when(registrationPlugin.getState()).thenReturn(Plugin.State.ENABLED); + when(registrationPlugin.validateUser(registerUserDto.getUserDto(), 1L)).thenReturn(errors); + when(pluginService.getRegistrationPlugins()) + .thenReturn(new ImmutableMap.Builder<Long, RegistrationPlugin>().put(1L, registrationPlugin).build()); + + authenticator.register(registerUserDto); + RegisterUserDto expectedRegisterUserDto = createRegisterUserDto("username", "password", "email", null); + + assertReflectionEquals(expectedRegisterUserDto, registerUserDto); + } + + @Test + public void userMustHaveInitialFieldValuesWhenPluginRegistrationFailWithRegistrationError() throws Exception { + RegisterUserDto registerUserDto = createRegisterUserDto("username", "password", "email", null); + EncryptionService realEncryptionService = new EncryptionService(new Md5PasswordEncoder()); + Map<String, String> errors = new HashMap<>(); + errors.put("userDto.email", "An email format should be like mail@mail.ru"); + ReflectionTestUtils.setField(authenticator, "encryptionService", realEncryptionService); + when(registrationPlugin.getState()).thenReturn(Plugin.State.ENABLED); + when(registrationPlugin.registerUser(registerUserDto.getUserDto(), 1L)).thenReturn(errors); + when(pluginService.getRegistrationPlugins()).thenReturn( + new ImmutableMap.Builder<Long, RegistrationPlugin>().put(1L, registrationPlugin).build()); + + authenticator.register(registerUserDto); + RegisterUserDto expectedRegisterUserDto = createRegisterUserDto("username", "password", "email", null); + + assertReflectionEquals(expectedRegisterUserDto, registerUserDto); + } + private RegisterUserDto createRegisterUserDto(String username, String password, String email, String honeypotCaptcha) { RegisterUserDto registerUserDto = new RegisterUserDto(); UserDto userDto = new UserDto(); diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalGroupServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalGroupServiceTest.java new file mode 100644 index 0000000000..43b62ecc4c --- /dev/null +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalGroupServiceTest.java @@ -0,0 +1,80 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.service.transactional; + +import org.jtalks.common.model.entity.Group; +import org.jtalks.common.service.exceptions.NotFoundException; +import org.jtalks.common.validation.ValidationException; +import org.jtalks.jcommune.model.dao.GroupDao; +import org.jtalks.jcommune.model.dto.GroupAdministrationDto; +import org.jtalks.jcommune.service.GroupService; +import org.jtalks.jcommune.service.exceptions.OperationIsNotAllowedException; +import org.jtalks.jcommune.service.security.SecurityService; +import org.mockito.Mock; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.List; + +import static com.google.common.collect.Iterables.getOnlyElement; +import static io.qala.datagen.RandomShortApi.sample; +import static java.util.Collections.singletonList; +import static org.jtalks.jcommune.service.security.AdministrationGroup.PREDEFINED_GROUP_NAMES; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.testng.Assert.assertFalse; + +/** + * @author Pavel Vervenko + */ +public class TransactionalGroupServiceTest { + + @Mock private GroupDao groupDao; + @Mock private SecurityService securityService; + private GroupService groupService; + + @BeforeMethod + public void init() { + initMocks(this); + groupService = new TransactionalGroupService(groupDao, null, null, securityService); + } + + @Test + public void predefinedGroupMarkedAsNotEditable() throws Exception { + when(groupDao.getGroupNamesWithCountOfUsers()).thenReturn(singletonList(new GroupAdministrationDto(sample(PREDEFINED_GROUP_NAMES), 1))); + List<GroupAdministrationDto> groupNamesWithCountOfUsers = groupService.getGroupNamesWithCountOfUsers(); + GroupAdministrationDto groupDto = getOnlyElement(groupNamesWithCountOfUsers); + assertFalse(groupDto.isEditable()); + } + + @Test(expectedExceptions = NotFoundException.class) + public void throwsIfEditingNonExistingGroup() throws NotFoundException { + when(groupDao.get(anyLong())).thenReturn(null); + groupService.saveOrUpdate(new GroupAdministrationDto(100500L, "not exist", "not exists", 0)); + } + + @Test(expectedExceptions = ValidationException.class) + public void throwsIfSavingNewGroupWithNotUniqueName() throws NotFoundException { + when(groupDao.getGroupByName("existing name")).thenReturn(new Group("existing name")); + groupService.saveOrUpdate(new GroupAdministrationDto(null, "existing name", "some description", 100)); + } + + @Test(expectedExceptions = OperationIsNotAllowedException.class) + public void throwsIfEditingPredefinedGroup() throws NotFoundException { + when(groupDao.get(1L)).thenReturn(new Group(sample(PREDEFINED_GROUP_NAMES))); + groupService.saveOrUpdate(new GroupAdministrationDto(1L, "new group name", "some description", 100)); + } +} \ No newline at end of file diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPollServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPollServiceTest.java index 86de8838cf..a351740fb0 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPollServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPollServiceTest.java @@ -19,13 +19,13 @@ import org.jtalks.common.model.dao.GroupDao; import org.jtalks.common.model.entity.User; import org.jtalks.common.model.permissions.JtalksPermission; -import org.jtalks.common.security.SecurityService; -import org.jtalks.common.security.acl.builders.CompoundAclBuilder; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.Poll; import org.jtalks.jcommune.model.entity.PollItem; import org.jtalks.jcommune.service.PollService; import org.jtalks.jcommune.service.UserService; +import org.jtalks.jcommune.service.security.SecurityService; +import org.jtalks.jcommune.service.security.acl.builders.CompoundAclBuilder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPostCommentServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPostCommentServiceTest.java index a935f50605..b7caa5077a 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPostCommentServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPostCommentServiceTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; /** * @author Vyacheslav Mishcheryakov @@ -48,7 +49,7 @@ public class TransactionalPostCommentServiceTest { @Mock NotificationService notificationService; - private TransactionalPostCommentService codeReviewCommentService; + private TransactionalPostCommentService postCommentService; private PostComment comment; private JCUser currentUser; @@ -56,7 +57,7 @@ public class TransactionalPostCommentServiceTest { @BeforeMethod public void initEnvironmental() { initMocks(this); - codeReviewCommentService = new TransactionalPostCommentService( + postCommentService = new TransactionalPostCommentService( dao, permissionService, userService); } @@ -83,28 +84,54 @@ public void prepareTestData() { @Test public void testUpdateCommentSuccess() throws Exception { givenUserHasPermissionToEditOwnPosts(true); - PostComment comment = codeReviewCommentService.updateComment(CR_ID, COMMENT_BODY, BRANCH_ID); + PostComment comment = postCommentService.updateComment(CR_ID, COMMENT_BODY, BRANCH_ID); assertEquals(comment.getBody(), COMMENT_BODY); } @Test(expectedExceptions = NotFoundException.class) public void testUpdateCommentNotFound() throws Exception { - codeReviewCommentService.updateComment(123L, null, BRANCH_ID); + postCommentService.updateComment(123L, null, BRANCH_ID); + } + + @Test + public void testUpdateCommentNotOwnerButHasEditOthersPermission() throws NotFoundException { + givenCurrentUser("not-the-author-of-comment"); + givenUserHasPermissionToEditOwnPosts(false); + givenUserHasPermissionToEditOthersPosts(true); + PostComment comment = postCommentService.updateComment(CR_ID, COMMENT_BODY, BRANCH_ID); + + assertEquals(comment.getBody(), COMMENT_BODY); + } + + @Test + public void testSubscriberNotGetNotificationAboutEditingComment() throws Exception { + postCommentService.updateComment(CR_ID, COMMENT_BODY + "updated", BRANCH_ID); + verifyZeroInteractions(notificationService); + } + + @Test + public void testMarkCommentAsDeleted() { + PostComment postComment = new PostComment(); + + PostComment result = postCommentService.markCommentAsDeleted(null, postComment); + + assertNotNull(result.getDeletionDate()); + verify(dao).saveOrUpdate(postComment); } @Test(expectedExceptions = AccessDeniedException.class) public void testUpdateCommentNoBothPermission() throws NotFoundException { givenUserHasPermissionToEditOwnPosts(false); givenUserHasPermissionToEditOthersPosts(false); - codeReviewCommentService.updateComment(CR_ID, null, BRANCH_ID); + postCommentService.updateComment(CR_ID, null, BRANCH_ID); } @Test(expectedExceptions = AccessDeniedException.class) public void testUpdateCommentNoEditOwnPermission() throws NotFoundException { givenUserHasPermissionToEditOwnPosts(false); givenUserHasPermissionToEditOthersPosts(true); - codeReviewCommentService.updateComment(CR_ID, null, BRANCH_ID); + postCommentService.updateComment(CR_ID, null, BRANCH_ID); } @Test(expectedExceptions = AccessDeniedException.class) @@ -112,24 +139,9 @@ public void testUpdateCommentNotOwnerNoEditOthersPermission() throws NotFoundExc givenCurrentUser("not-the-author-of-comment"); givenUserHasPermissionToEditOthersPosts(false); givenUserHasPermissionToEditOwnPosts(true); - codeReviewCommentService.updateComment(CR_ID, null, BRANCH_ID); - } - - @Test - public void testUpdateCommentNotOwnerButHasEditOthersPermission() throws NotFoundException { - givenCurrentUser("not-the-author-of-comment"); - givenUserHasPermissionToEditOwnPosts(false); - givenUserHasPermissionToEditOthersPosts(true); - PostComment comment = codeReviewCommentService.updateComment(CR_ID, COMMENT_BODY, BRANCH_ID); - - assertEquals(comment.getBody(), COMMENT_BODY); + postCommentService.updateComment(CR_ID, null, BRANCH_ID); } - @Test - public void testSubscriberNotGetNotificationAboutEditingComment() throws Exception { - codeReviewCommentService.updateComment(CR_ID, COMMENT_BODY + "updated", BRANCH_ID); - verifyZeroInteractions(notificationService); - } private void givenUserHasPermissionToEditOwnPosts(boolean isGranted) { doReturn(isGranted).when(permissionService).hasBranchPermission(BRANCH_ID, BranchPermission.EDIT_OWN_POSTS); diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPostServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPostServiceTest.java index 13ada5ee63..46a8cc21f2 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPostServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPostServiceTest.java @@ -15,21 +15,26 @@ package org.jtalks.jcommune.service.transactional; import org.jtalks.common.model.dao.Crud; -import org.jtalks.common.model.permissions.JtalksPermission; -import org.jtalks.common.security.SecurityService; +import org.jtalks.common.model.dao.hibernate.GenericDao; +import org.jtalks.common.model.permissions.BranchPermission; import org.jtalks.jcommune.model.dao.PostDao; import org.jtalks.jcommune.model.dao.TopicDao; import org.jtalks.jcommune.model.dto.PageRequest; import org.jtalks.jcommune.model.entity.*; +import org.jtalks.jcommune.plugin.api.PluginLoader; +import org.jtalks.jcommune.plugin.api.core.Plugin; +import org.jtalks.jcommune.plugin.api.core.TopicPlugin; +import org.jtalks.jcommune.plugin.api.filters.StateFilter; +import org.jtalks.jcommune.plugin.api.filters.TypeFilter; import org.jtalks.jcommune.service.BranchLastPostService; -import org.jtalks.jcommune.service.LastReadPostService; import org.jtalks.jcommune.service.PostService; import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.nontransactional.MentionedUsers; import org.jtalks.jcommune.service.nontransactional.NotificationService; -import org.jtalks.jcommune.service.security.AclClassName; +import org.jtalks.jcommune.service.security.acl.AclClassName; import org.jtalks.jcommune.service.security.PermissionService; +import org.jtalks.jcommune.service.security.SecurityService; import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.Mockito; @@ -45,6 +50,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.HashSet; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; @@ -76,8 +83,6 @@ public class TransactionalPostServiceTest { @Mock private TopicDao topicDao; @Mock - private LastReadPostService lastReadPostService; - @Mock private UserService userService; @Mock private BranchLastPostService branchLastPostService; @@ -87,6 +92,12 @@ public class TransactionalPostServiceTest { private PermissionService permissionService; @Mock private Crud<PostComment> postCommentDao; + @Mock + private PluginLoader pluginLoader; + @Mock + private TopicPlugin topicPlugin; + @Mock + private GenericDao<PostDraft> postDraftDao; private PostService postService; @@ -107,10 +118,11 @@ public void setUp() throws Exception { topicDao, securityService, notificationService, - lastReadPostService, userService, branchLastPostService, - permissionService); + permissionService, + pluginLoader, + postDraftDao); } @Test @@ -200,7 +212,7 @@ public void testDeletePostDeletedPostIsLastModified() throws NotFoundException { assertEquals(topic.getModificationDate(), topic.getFirstPost().getCreationDate()); verify(topicDao).saveOrUpdate(topic); verify(securityService).deleteFromAcl(postForDelete); - verify(notificationService).subscribedEntityChanged(topic); + verify(notificationService).subscribedEntityChanged(postForDelete); } @Test @@ -238,7 +250,7 @@ public void testDeletePostFirstPostIsLastModified() throws NotFoundException { assertEquals(topic.getModificationDate(), topic.getFirstPost().getCreationDate()); verify(topicDao).saveOrUpdate(topic); verify(securityService).deleteFromAcl(postForDelete); - verify(notificationService).subscribedEntityChanged(topic); + verify(notificationService).subscribedEntityChanged(postForDelete); } @@ -285,6 +297,31 @@ public void testDeletePostThatIsNotLastInBranch() throws NotFoundException { verify(branchLastPostService, Mockito.never()).refreshLastPostInBranch(branch); } + @Test + public void whenOwnerRemovesThePost_thenThereIsNoNotificationToTheSubscribers() { + Post post = getPostWithTopicInBranch(); + Topic topic = post.getTopic(); + topic.setSubscribers(Collections.singleton(user)); //add the creator of the post to subscribers + + postService.deletePost(post); + + verify(notificationService).subscribedEntityChanged(post); + } + + @Test + public void whenNotPostCreatorRemovesThePost_thenOnlyPostCreatorGetsNotification() { + Post post = getPostWithTopicInBranch(); + Topic topic = post.getTopic(); + Set<JCUser> subscribers = new HashSet<>(); + subscribers.add(user); //post creator + subscribers.add(currentUser); //user, that removes post + topic.setSubscribers(subscribers); + + postService.deletePost(post); + + verify(notificationService).subscribedEntityChanged(post); + } + @Test public void testPostsOfUser() { Page<Post> expectedPostsPage = getPageWithPost(); @@ -439,6 +476,7 @@ public void testGetLastPostsForBranch() { @Test public void testAddComment() throws Exception { Post post = getPostWithTopicInBranch(); + post.getTopic().setType(TopicTypeName.CODE_REVIEW.getName()); when(postDao.isExist(POST_ID)).thenReturn(true); when(postDao.get(POST_ID)).thenReturn(post); @@ -453,6 +491,7 @@ public void testAddComment() throws Exception { @Test public void testAddCommentWithProperties() throws Exception { Post post = getPostWithTopicInBranch(); + post.getTopic().setType(TopicTypeName.CODE_REVIEW.getName()); when(postDao.isExist(POST_ID)).thenReturn(true); when(postDao.get(POST_ID)).thenReturn(post); Map<String, String> attributes = new HashMap<>(); @@ -472,13 +511,99 @@ public void addCommentShouldThrowExceptionIfPostNotFound() throws Exception { } @Test(expectedExceptions = AccessDeniedException.class) - public void testAddCommentUserHasNoPermission() throws Exception { + public void addingCommentToDiscussionShouldThrowException() throws Exception { + Post post = getPostWithTopicInBranch(); + post.getTopic().setType(TopicTypeName.DISCUSSION.getName()); + when(postDao.isExist(POST_ID)).thenReturn(true); + when(postDao.get(POST_ID)).thenReturn(post); + + postService.addComment(POST_ID, Collections.EMPTY_MAP, "text"); + } + + @Test(expectedExceptions = AccessDeniedException.class) + public void addingCommentsToCodeReviewShouldThrowExceptionIfUserHasNoPermissions() throws Exception { Post post = getPostWithTopicInBranch(); + post.getTopic().setType(TopicTypeName.CODE_REVIEW.getName()); + when(postDao.isExist(POST_ID)).thenReturn(true); + when(postDao.get(POST_ID)).thenReturn(post); + doThrow(new AccessDeniedException("")) - .when(permissionService).checkPermission(anyLong(), any(AclClassName.class), - any(JtalksPermission.class)); + .when(permissionService).checkPermission(post.getTopic().getBranch().getId(), AclClassName.BRANCH, + BranchPermission.LEAVE_COMMENTS_IN_CODE_REVIEW); + + postService.addComment(POST_ID, Collections.EMPTY_MAP, "text"); + } + + @Test + public void addingCommentToPlugablePostShouldBeSucceedIfAppropriatePluginIsEnabledAndUserHasPermissions() + throws Exception { + String topicType = "Plugable"; + Post post = getPostWithTopicInBranch(); + post.getTopic().setType(topicType); + + when(pluginLoader.getPlugins(any(TypeFilter.class), any(StateFilter.class))) + .thenReturn(Arrays.<Plugin>asList(topicPlugin)); + when(topicPlugin.getTopicType()).thenReturn(topicType); + when(postDao.isExist(POST_ID)).thenReturn(true); + when(postDao.get(POST_ID)).thenReturn(post); + + PostComment comment = postService.addComment(POST_ID, Collections.EMPTY_MAP, "text"); + + assertEquals(post.getComments().size(), 1); + assertEquals(comment.getBody(), "text"); + assertEquals(comment.getAuthor(), currentUser); + verify(postDao).saveOrUpdate(post); + + } + + @Test(expectedExceptions = AccessDeniedException.class) + public void addingCommentToPostFromPlugableTopicShouldThrowExceptionIfUserHaveNoPermissions() throws Exception { + String topicType = "Plugable"; + Post post = getPostWithTopicInBranch(); + post.getTopic().setType(topicType); + + when(pluginLoader.getPlugins(any(TypeFilter.class), any(StateFilter.class))) + .thenReturn(Arrays.<Plugin>asList(topicPlugin)); + when(topicPlugin.getTopicType()).thenReturn(topicType); + when(topicPlugin.getCommentPermission()).thenReturn(BranchPermission.CREATE_POSTS); + when(postDao.isExist(POST_ID)).thenReturn(true); + when(postDao.get(POST_ID)).thenReturn(post); + doThrow(new AccessDeniedException("")) + .when(permissionService).checkPermission(post.getTopic().getBranch().getId(), AclClassName.BRANCH, + BranchPermission.CREATE_POSTS); + + postService.addComment(POST_ID, Collections.EMPTY_MAP, "text"); + } + + @Test(expectedExceptions = AccessDeniedException.class) + public void addingCommentsToPostFromPluggableTopicShouldThrowExceptionIfAppropriatePluginNotFound() throws Exception { + String topicType = "Plugable"; + Post post = getPostWithTopicInBranch(); + post.getTopic().setType(topicType); + + when(pluginLoader.getPlugins(any(TypeFilter.class), any(StateFilter.class))).thenReturn(Collections.EMPTY_LIST); + when(postDao.isExist(POST_ID)).thenReturn(true); + when(postDao.get(POST_ID)).thenReturn(post); + + postService.addComment(POST_ID, Collections.EMPTY_MAP, "text"); + } + + @Test(expectedExceptions = AccessDeniedException.class) + public void shouldBeImpossibleToAddCommentsToClosedTopicWithoutCloseTopicPermission() throws Exception { + String topicType = "Plugable"; + Post post = getPostWithTopicInBranch(); + post.getTopic().setType(topicType); + post.getTopic().setClosed(true); + + when(pluginLoader.getPlugins(any(TypeFilter.class), any(StateFilter.class))) + .thenReturn(Arrays.<Plugin>asList(topicPlugin)); + when(topicPlugin.getTopicType()).thenReturn(topicType); + when(topicPlugin.getCommentPermission()).thenReturn(BranchPermission.CREATE_POSTS); when(postDao.isExist(POST_ID)).thenReturn(true); when(postDao.get(POST_ID)).thenReturn(post); + doThrow(new AccessDeniedException("")) + .when(permissionService).checkPermission(post.getTopic().getBranch().getId(), AclClassName.BRANCH, + BranchPermission.CLOSE_TOPICS); postService.addComment(POST_ID, Collections.EMPTY_MAP, "text"); } @@ -487,6 +612,7 @@ public void testAddCommentUserHasNoPermission() throws Exception { public void testAddUserToSubscriptedByComment() throws Exception { JCUser user = new JCUser("username", null, null); Post post = getPostWithTopicInBranch(); + post.getTopic().setType(TopicTypeName.CODE_REVIEW.getName()); user.setAutosubscribe(true); when(userService.getCurrentUser()).thenReturn(user); @@ -503,6 +629,7 @@ public void testAddUserToSubscriptedByComment() throws Exception { public void testNotAddUserToSubscriptedByComment() throws Exception { JCUser user = new JCUser("username", null, null); Post post = getPostWithTopicInBranch(); + post.getTopic().setType(TopicTypeName.CODE_REVIEW.getName()); user.setAutosubscribe(false); when(userService.getCurrentUser()).thenReturn(user); @@ -614,12 +741,141 @@ public void voteForPostInSameDirectionMoreThanOneTimeShouldThrowException() { } + @Test + public void saverOrUpdateDraftShouldCreateNewDraftIfUserStillHasNoDraftsInSpecifiedTopic() { + Topic topic = new Topic(); + JCUser currentUser = new JCUser("username", null, null); + String content = "content"; + + when(userService.getCurrentUser()).thenReturn(currentUser); + + PostDraft draft = postService.saveOrUpdateDraft(topic, content); + + verify(topicDao).saveOrUpdate(topic); + assertEquals(draft.getContent(), content); + assertTrue(topic.getDrafts().contains(draft)); + } + + @Test + public void saverOrUpdateDraftShouldUpdateDraftIfUserAlreadyHasDraftInSpecifiedTopic() { + Topic topic = new Topic(); + JCUser currentUser = new JCUser("username", null, null); + PostDraft draft = new PostDraft("content", currentUser); + topic.addDraft(draft); + String newContent = "Something amazing"; + + when(userService.getCurrentUser()).thenReturn(currentUser); + + PostDraft result = postService.saveOrUpdateDraft(topic, newContent); + + verify(topicDao).saveOrUpdate(topic); + assertEquals(result, draft); + assertEquals(result.getContent(), newContent); + assertTrue(topic.getDrafts().contains(result)); + assertEquals(topic.getDrafts().size(), 1); + } + + @Test + public void saverOrUpdateDraftShouldNotModifyCounterOfUserPosts() { + Topic topic = new Topic(); + JCUser currentUser = new JCUser("username", null, null); + int before = currentUser.getPostCount(); + + when(userService.getCurrentUser()).thenReturn(currentUser); + + postService.saveOrUpdateDraft(topic, "123"); + + assertEquals(before, user.getPostCount()); + } + + @Test + public void saveOrUpdateDraftShouldNotSendNotifications() { + Topic topic = new Topic(); + JCUser currentUser = new JCUser("username", null, null); + + when(userService.getCurrentUser()).thenReturn(currentUser); + + postService.saveOrUpdateDraft(topic, "123"); + + verify(notificationService, never()).subscribedEntityChanged(any(SubscriptionAwareEntity.class)); + } + + @Test + public void testDeleteDraft() throws Exception{ + PostDraft draft = getDraftWithTopicInBranch(); + + when(postDraftDao.isExist(draft.getId())).thenReturn(true); + when(postDraftDao.get(draft.getId())).thenReturn(draft); + + postService.deleteDraft(draft.getId()); + + verify(topicDao).saveOrUpdate(draft.getTopic()); + assertFalse(draft.getTopic().getPosts().contains(draft)); + } + + @Test(expectedExceptions = NotFoundException.class) + public void deleteDraftShouldThrowExceptionIfDraftNotFound() throws Exception { + when(postDraftDao.isExist(anyLong())).thenReturn(false); + + postService.deleteDraft(1l); + } + + @Test(expectedExceptions = AccessDeniedException.class) + public void deleteDraftShouldThrowExceptionIfAnotherUserTryToDeleteDeraft() throws Exception { + PostDraft draft = getDraftWithTopicInBranch(); + draft.setAuthor(new JCUser("name", "mylo@mail.ru", "123")); + + when(postDraftDao.isExist(draft.getId())).thenReturn(true); + when(postDraftDao.get(draft.getId())).thenReturn(draft); + + postService.deleteDraft(draft.getId()); + } + + @Test + public void deleteDraftShouldNotChangeUserPostCounter() throws Exception { + PostDraft draft = getDraftWithTopicInBranch(); + int before = draft.getAuthor().getPostCount(); + + when(postDraftDao.isExist(draft.getId())).thenReturn(true); + when(postDraftDao.get(draft.getId())).thenReturn(draft); + + postService.deleteDraft(draft.getId()); + + assertEquals(draft.getAuthor().getPostCount(), before); + } + + @Test + public void deleteDraftShouldNotSendNotifications() throws Exception { + PostDraft draft = getDraftWithTopicInBranch(); + + when(postDraftDao.isExist(draft.getId())).thenReturn(true); + when(postDraftDao.get(draft.getId())).thenReturn(draft); + + postService.deleteDraft(draft.getId()); + + verify(notificationService, never()).subscribedEntityChanged(any(SubscriptionAwareEntity.class)); + } + + private PostDraft getDraftWithTopicInBranch() { + Branch branch = new Branch(null, null); + branch.setId(1); + Topic topic = new Topic(); + PostDraft draft = new PostDraft("content", currentUser); + draft.setId(1); + topic.addDraft(draft); + return draft; + } + private Post getPostWithTopicInBranch() { Branch branch = new Branch(null, null); + branch.setId(1); Topic topic = new Topic(); - Post firstPost = new Post(null, null); + Post firstPost = new Post(user, null); firstPost.setId(1l); + Post secondPost = new Post(user, null); + secondPost.setId(2l); topic.addPost(firstPost); + topic.addPost(secondPost); topic.setBranch(branch); return firstPost; } diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPrivateMessageServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPrivateMessageServiceTest.java index 9e6b341525..13b041ad90 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPrivateMessageServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalPrivateMessageServiceTest.java @@ -17,8 +17,6 @@ import org.jtalks.common.model.entity.Property; import org.jtalks.common.model.entity.User; import org.jtalks.common.model.permissions.GeneralPermission; -import org.jtalks.common.security.SecurityService; -import org.jtalks.common.security.acl.builders.CompoundAclBuilder; import org.jtalks.jcommune.model.dao.PrivateMessageDao; import org.jtalks.jcommune.model.dao.PropertyDao; import org.jtalks.jcommune.model.dto.PageRequest; @@ -30,6 +28,8 @@ import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.nontransactional.MailService; import org.jtalks.jcommune.service.nontransactional.UserDataCacheService; +import org.jtalks.jcommune.service.security.SecurityService; +import org.jtalks.jcommune.service.security.acl.builders.CompoundAclBuilder; import org.mockito.Matchers; import org.mockito.Mock; import org.springframework.data.domain.Page; @@ -182,9 +182,11 @@ public void testGetDraftsForCurrentUser() { @Test public void testSaveDraft() throws NotFoundException { + JCUser recipient = new JCUser("name", "example@example.com", "pwd"); + when(securityService.<User>createAclBuilder()).thenReturn(aclBuilder); - pmService.saveDraft(PM_ID, USERNAME, "title", "body", JC_USER); + pmService.saveDraft(PM_ID, recipient, "title", "body", JC_USER); verify(pmDao).saveOrUpdate(any(PrivateMessage.class)); verify(aclBuilder).grant(GeneralPermission.WRITE); diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalSimplePageServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalSimplePageServiceTest.java index 626e924f53..67b6fb9131 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalSimplePageServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalSimplePageServiceTest.java @@ -19,8 +19,6 @@ import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.entity.User; import org.jtalks.common.model.permissions.GeneralPermission; -import org.jtalks.common.security.SecurityService; -import org.jtalks.common.security.acl.builders.CompoundAclBuilder; import org.jtalks.jcommune.model.dao.SimplePageDao; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.SimplePage; @@ -28,6 +26,8 @@ import org.jtalks.jcommune.service.dto.SimplePageInfoContainer; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.security.AdministrationGroup; +import org.jtalks.jcommune.service.security.SecurityService; +import org.jtalks.jcommune.service.security.acl.builders.CompoundAclBuilder; import org.mockito.Mock; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -48,18 +48,11 @@ public class TransactionalSimplePageServiceTest { private static final String CONTENT = "content"; private static final String PATH_NAME = "path_name"; - @Mock - private SimplePageDao dao; - - @Mock - private GroupDao groupDao; - - @Mock - private SecurityService securityService; - + @Mock private SimplePageDao dao; + @Mock private GroupDao groupDao; + @Mock private SecurityService securityService; private SimplePageService simplePageService; - private CompoundAclBuilder aclBuilder; @BeforeMethod diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalSpamProtectionServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalSpamProtectionServiceTest.java new file mode 100644 index 0000000000..ce682df22d --- /dev/null +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalSpamProtectionServiceTest.java @@ -0,0 +1,85 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.service.transactional; + +import org.jtalks.common.service.exceptions.NotFoundException; +import org.jtalks.common.validation.ValidationException; +import org.jtalks.jcommune.model.dao.SpamRuleDao; +import org.jtalks.jcommune.model.entity.SpamRule; +import org.jtalks.jcommune.service.SpamProtectionService; +import org.mockito.Mock; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.List; + +import static io.qala.datagen.RandomShortApi.alphanumeric; +import static io.qala.datagen.RandomShortApi.bool; +import static io.qala.datagen.RandomValue.between; +import static io.qala.datagen.StringModifier.Impls.prefix; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +/** + * @author Oleg Tkachenko + */ +public class TransactionalSpamProtectionServiceTest { + private @Mock SpamRuleDao dao; + private SpamProtectionService spamProtectionService; + + @BeforeMethod + public void setUp(){ + initMocks(this); + spamProtectionService = new TransactionalSpamProtectionService(dao); + } + + @Test + public void shouldReturnTrueIfEmailInBlackList(){ + String domain = randomDomain(); + String address = alphanumeric(10) + domain; + List<SpamRule> spamRules = Collections.singletonList(spamRuleToBlock(domain)); + when(dao.getEnabledRules()).thenReturn(spamRules); + boolean inBlackList = spamProtectionService.isEmailInBlackList(address); + Assert.assertTrue(inBlackList); + } + + @Test + public void shouldReturnFalseIfEmailNotInBlackList(){ + String domain = randomDomain(); + String address = alphanumeric(10) + domain; + List<SpamRule> spamRules = Collections.singletonList(spamRuleToBlock(randomDomain())); + when(dao.getEnabledRules()).thenReturn(spamRules); + boolean inBlackList = spamProtectionService.isEmailInBlackList(address); + Assert.assertFalse(inBlackList); + } + + @Test(expectedExceptions = ValidationException.class) + public void shouldThrowValidationExceptionIfRegexIsNotValid() throws NotFoundException { + String brokenRegex = "())"; + SpamRule spamRule = new SpamRule(brokenRegex, alphanumeric(255), bool()); + spamProtectionService.saveOrUpdate(spamRule); + } + + private String randomDomain() { + return between(3, 15).with(prefix("@")).alphanumeric(); + } + + private SpamRule spamRuleToBlock(String domain){ + return new SpamRule(".*" + domain, "testRule", true); + } +} diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalSubscriptionServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalSubscriptionServiceTest.java index cb6af58fd8..51d4ad9065 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalSubscriptionServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalSubscriptionServiceTest.java @@ -26,6 +26,7 @@ import java.util.Collections; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -150,4 +151,22 @@ public void testToggleSubscriptionAlreadySubscribedBranchCase() { assertFalse(branch.getSubscribers().contains(user)); } + + @Test + public void testSubscribeIfUserIsNotSubscribedOnSubscriptionAwareEntity(){ + service.subscribe(topic); + + assertTrue(topic.getSubscribers().contains(user)); + verify(topicDao).saveOrUpdate(topic); + } + + @Test + public void testSubscribeIfUserSubscribedOnSubscriptionAwareEntity(){ + topic.getSubscribers().add(user); + service.subscribe(topic); + + assertTrue(topic.getSubscribers().contains(user)); + verify(topicDao,never()).saveOrUpdate(topic); + } + } diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalTopicDraftServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalTopicDraftServiceTest.java new file mode 100644 index 0000000000..aef66439fa --- /dev/null +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalTopicDraftServiceTest.java @@ -0,0 +1,152 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.service.transactional; + +import org.apache.commons.lang3.RandomStringUtils; +import org.jtalks.common.service.security.SecurityContextFacade; +import org.jtalks.jcommune.model.dao.TopicDraftDao; +import org.jtalks.jcommune.model.entity.Branch; +import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.TopicDraft; +import org.jtalks.jcommune.model.entity.TopicTypeName; +import org.jtalks.jcommune.plugin.api.PluginLoader; +import org.jtalks.jcommune.service.TopicDraftService; +import org.jtalks.jcommune.service.UserService; +import org.mockito.Mock; +import org.springframework.security.access.PermissionEvaluator; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.unitils.reflectionassert.ReflectionAssert.assertReflectionEquals; + +/** + * @author Dmitry S. Dolzhenko + */ +public class TransactionalTopicDraftServiceTest { + + @Mock + private UserService userService; + @Mock + private TopicDraftDao topicDraftDao; + @Mock + private SecurityContextFacade securityContextFacade; + @Mock + private PermissionEvaluator permissionEvaluator; + @Mock + private PluginLoader pluginLoader; + + private TopicDraftService topicDraftService; + + private JCUser currentUser; + + @BeforeMethod + public void setUp() { + initMocks(this); + + currentUser = new JCUser("current", null, null); + when(userService.getCurrentUser()).thenReturn(currentUser); + + topicDraftService = new TransactionalTopicDraftService(userService, + topicDraftDao, securityContextFacade, permissionEvaluator, pluginLoader); + } + + @Test + public void getDraftShouldReturnDraftIfUserHasOne() throws Exception { + TopicDraft expectedDraft = createTopicDraft(); + when(topicDraftDao.getForUser(currentUser)).thenReturn(expectedDraft); + + TopicDraft draft = topicDraftService.getDraft(); + + assertReflectionEquals(draft, expectedDraft); + } + + + @Test + public void getDraftShouldReturnNullIfUserHasNoDraft() throws Exception { + when(topicDraftDao.getForUser(currentUser)).thenReturn(null); + + assertNull(topicDraftService.getDraft()); + } + + @Test + public void saveOrUpdateDraftShouldCreateNewDraftIfUserStillHasNoDraft() { + when(topicDraftDao.getForUser(currentUser)).thenReturn(null); + + TopicDraft topicDraft = topicDraftService.saveOrUpdateDraft(createTopicDraft()); + + verify(topicDraftDao).saveOrUpdate(topicDraft); + } + + @Test + public void saveOrUpdateDraftShouldUpdateDraftIfUserAlreadyHasOne() { + TopicDraft topicDraft = createTopicDraft(); + when(topicDraftDao.getForUser(currentUser)).thenReturn(topicDraft); + + topicDraftService.saveOrUpdateDraft(topicDraft); + + verify(topicDraftDao).saveOrUpdate(topicDraft); + } + + @Test + public void saveOrUpdateDraftShouldNotRewritePollFieldsWithNullValues() { + TopicDraft topicDraftWithPoll = createTopicDraft(); + topicDraftWithPoll.setPollTitle(RandomStringUtils.random(5)); + topicDraftWithPoll.setPollItemsValue(RandomStringUtils.random(5)); + + when(topicDraftDao.getForUser(currentUser)).thenReturn(topicDraftWithPoll); + + TopicDraft topicDraftWithoutPoll = createTopicDraft(); + topicDraftWithoutPoll.setPollTitle(null); + topicDraftWithoutPoll.setPollItemsValue(null); + + topicDraftService.saveOrUpdateDraft(topicDraftWithoutPoll); + + assertNotNull(topicDraftWithPoll.getPollTitle()); + assertNotNull(topicDraftWithPoll.getPollItemsValue()); + } + + @Test + public void deleteDraftShouldDeleteDraftIfUserHasOne() { + TopicDraft topicDraft = createTopicDraft(); + when(topicDraftDao.getForUser(currentUser)).thenReturn(topicDraft); + + topicDraftService.deleteDraft(); + + verify(topicDraftDao).deleteByUser(topicDraft.getTopicStarter()); + } + + private TopicDraft createTopicDraft() { + Branch branch = createBranch(); + + TopicDraft topicDraft = new TopicDraft(currentUser, "title", "content"); + topicDraft.setId(1L); + topicDraft.setBranchId(branch.getId()); + topicDraft.setTopicType(TopicTypeName.DISCUSSION.getName()); + + return topicDraft; + } + + private Branch createBranch() { + Branch branch = new Branch("branch name", "branch description"); + branch.setId(1L); + branch.setUuid("uuid"); + return branch; + } +} diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalTopicFetchServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalTopicFetchServiceTest.java index b0b945aa12..5924cb3d72 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalTopicFetchServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalTopicFetchServiceTest.java @@ -16,12 +16,14 @@ import org.apache.commons.lang.StringUtils; import org.joda.time.DateTime; +import org.jtalks.common.model.entity.Component; import org.jtalks.jcommune.model.dao.TopicDao; import org.jtalks.jcommune.model.dao.search.TopicSearchDao; import org.jtalks.jcommune.model.dto.PageRequest; import org.jtalks.jcommune.model.entity.Branch; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.Topic; +import org.jtalks.jcommune.service.ComponentService; import org.jtalks.jcommune.service.TopicFetchService; import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; @@ -59,11 +61,13 @@ public class TransactionalTopicFetchServiceTest { private TopicFetchService topicFetchService; private JCUser user; + @Mock + private ComponentService componentService; @BeforeMethod public void init(){ initMocks(this); - topicFetchService = new TransactionalTopicFetchService(topicDao, userService, searchDao); + topicFetchService = new TransactionalTopicFetchService(topicDao, componentService, userService, searchDao); user = new JCUser("username", "email@mail.com", "password"); when(userService.getCurrentUser()).thenReturn(user); } @@ -169,8 +173,16 @@ public Object[][] parameterSearchPostsWithEmptySearchPhrase() { }; } + private Component setupComponentMock() { + Component component = new Component(); + component.setId(1L); + when(componentService.getComponentOfForum()).thenReturn(component); + return component; + } + @Test public void testRebuildIndex() { + setupComponentMock(); topicFetchService.rebuildSearchIndex(); Mockito.verify(searchDao).rebuildIndex(); diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalTopicModificationServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalTopicModificationServiceTest.java index 78866e37ad..434e81bef9 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalTopicModificationServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalTopicModificationServiceTest.java @@ -17,8 +17,6 @@ import org.joda.time.DateTime; import org.jtalks.common.model.entity.User; import org.jtalks.common.model.permissions.GeneralPermission; -import org.jtalks.common.security.SecurityService; -import org.jtalks.common.security.acl.builders.CompoundAclBuilder; import org.jtalks.common.service.security.SecurityContextFacade; import org.jtalks.jcommune.model.dao.BranchDao; import org.jtalks.jcommune.model.dao.PostDao; @@ -29,6 +27,8 @@ import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.nontransactional.MentionedUsers; import org.jtalks.jcommune.service.nontransactional.NotificationService; +import org.jtalks.jcommune.service.security.SecurityService; +import org.jtalks.jcommune.service.security.acl.builders.CompoundAclBuilder; import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.Mockito; @@ -65,6 +65,8 @@ public class TransactionalTopicModificationServiceTest { private final String TOPIC_TITLE = "topic title"; private JCUser user; private final String ANSWER_BODY = "Test Answer Body"; + private final String SUBSCRIBED = "User have to be subscribed on the topic"; + private final String UNSUBSCRIBED = "User does not have to be subscribed on the topic"; private TopicModificationService topicService; @@ -85,6 +87,8 @@ public class TransactionalTopicModificationServiceTest { @Mock private TopicFetchService topicFetchService; @Mock + private TopicDraftService topicDraftService; + @Mock private SecurityContextFacade securityContextFacade; @Mock private PermissionEvaluator permissionEvaluator; @@ -121,6 +125,7 @@ public void setUp() throws Exception { lastReadPostService, postDao, topicFetchService, + topicDraftService, pluginLoader); user = new JCUser("username", "email@mail.com", "password"); @@ -128,58 +133,118 @@ public void setUp() throws Exception { } @Test - public void testReplyToTopic() throws NotFoundException { - Topic answeredTopic = new Topic(user, "title"); - answeredTopic.setType(TopicTypeName.DISCUSSION.getName()); - answeredTopic.setBranch(new Branch("name", "description")); - when(userService.getCurrentUser()).thenReturn(user); - when(topicFetchService.getTopicSilently(TOPIC_ID)).thenReturn(answeredTopic); - when(securityService.<User>createAclBuilder()).thenReturn(aclBuilder); + public void testCreateTopic() throws NotFoundException { + Branch branch = createBranch(); + createTopicStubs(); + Topic dto = createTopic(); + Topic createdTopic = topicService.createTopic(dto, ANSWER_BODY); + Post createdPost = createdTopic.getFirstPost(); - Post createdPost = topicService.replyToTopic(TOPIC_ID, ANSWER_BODY, BRANCH_ID); + createTopicAssertions(branch, createdTopic, createdPost); + createTopicVerifications(createdTopic); + } - assertEquals(createdPost.getPostContent(), ANSWER_BODY); - assertEquals(createdPost.getUserCreated(), user); - assertEquals(user.getPostCount(), 1); + @Test + public void testGetSubscriptionByCreateTopicIfAutosubscribeEnabled() throws NotFoundException { + Topic createdTopic = ObjectsFactory.topics(user, 1).get(0); + user.setAutosubscribe(true); + createTopicStubs(); - verify(aclBuilder).grant(GeneralPermission.WRITE); - verify(aclBuilder).to(user); - verify(aclBuilder).on(createdPost); - verify(notificationService).subscribedEntityChanged(answeredTopic); + Topic createdTopicLast = topicService.createTopic(createdTopic, ANSWER_BODY); + verify(subscriptionService).subscribe(createdTopicLast); + } + + @Test + public void testDoesNotGetSubscriptionByCreateTopicIfAutosubscribeDisabled() throws NotFoundException { + Topic createdTopic = ObjectsFactory.topics(user, 1).get(0); + user.setAutosubscribe(false); + createTopicStubs(); + + Topic createdTopicLast = topicService.createTopic(createdTopic, ANSWER_BODY); + verify(subscriptionService,never()).subscribe(createdTopicLast); } @Test - public void testAutoSubscriptionOnTopicReplyIfAutosubscribeEnabled() throws NotFoundException { + public void testGetSubscriptionOnTopicReplyIfAutosubscribeEnabled() throws NotFoundException { Topic answeredTopic = ObjectsFactory.topics(user, 1).get(0); user.setAutosubscribe(true); - when(userService.getCurrentUser()).thenReturn(user); - when(topicFetchService.getTopicSilently(TOPIC_ID)).thenReturn(answeredTopic); - when(securityService.<User>createAclBuilder()).thenReturn(aclBuilder); + replyTopicStubs(answeredTopic); topicService.replyToTopic(TOPIC_ID, ANSWER_BODY, BRANCH_ID); - - assertTrue(answeredTopic.userSubscribed(user)); + verify(subscriptionService).subscribe(answeredTopic); } @Test - public void testAutoSubscriptionOnTopicReplyIfAutosubscribeDisabled() throws NotFoundException { - user.setAutosubscribe(false); + public void testDoesNotGetSubscriptionOnTopicReplyIfAutosubscribeDisabled() throws NotFoundException { Topic answeredTopic = ObjectsFactory.topics(user, 1).get(0); - when(userService.getCurrentUser()).thenReturn(user); - when(topicFetchService.getTopicSilently(TOPIC_ID)).thenReturn(answeredTopic); - when(securityService.<User>createAclBuilder()).thenReturn(aclBuilder); + user.setAutosubscribe(false); + replyTopicStubs(answeredTopic); topicService.replyToTopic(TOPIC_ID, ANSWER_BODY, BRANCH_ID); + verify(subscriptionService,never()).subscribe(answeredTopic); + } + + @Test + void testDoesNotGetSubscriptionOnUpdateTopicIfAutosubscribeEnabledAndUserIsNotSubscribedOnTopic() throws NotFoundException { + Topic updatedTopic = ObjectsFactory.topics(user, 1).get(0); + user.setAutosubscribe(true); + + topicService.updateTopic(updatedTopic, null); + assertFalse(updatedTopic.userSubscribed(user), UNSUBSCRIBED); + } + + @Test + void testGetSubscriptionOnUpdateTopicIfAutosubscribeEnabledAndUserIsSubscribedOnTopic() throws NotFoundException { + Topic updatedTopic = ObjectsFactory.topics(user, 1).get(0); + user.setAutosubscribe(true); + updatedTopic.getSubscribers().add(user); + + topicService.updateTopic(updatedTopic, null); + assertTrue(updatedTopic.userSubscribed(user), SUBSCRIBED); + } + + @Test + void testDoesNotGetSubscriptionOnUpdateTopicIfAutosubscribeDisabledAndUserIsNotSubscribedOnTopic() throws NotFoundException { + Topic updatedTopic = ObjectsFactory.topics(user, 1).get(0); + user.setAutosubscribe(false); + + topicService.updateTopic(updatedTopic, null); + assertFalse(updatedTopic.userSubscribed(user), UNSUBSCRIBED); + } + + @Test + void testGetSubscriptionOnUpdateTopicIfAutosubscribeDisabledAndUserIsSubscribedOnTopic() throws NotFoundException { + Topic updatedTopic = ObjectsFactory.topics(user, 1).get(0); + user.setAutosubscribe(false); + updatedTopic.getSubscribers().add(user); + + topicService.updateTopic(updatedTopic, null); + assertTrue(updatedTopic.userSubscribed(user), SUBSCRIBED); + } + + @Test + public void testReplyToTopic() throws NotFoundException { + Topic answeredTopic = new Topic(user, "title"); + answeredTopic.setType(TopicTypeName.DISCUSSION.getName()); + answeredTopic.setBranch(new Branch("name", "description")); + replyTopicStubs(answeredTopic); + + Post createdPost = topicService.replyToTopic(TOPIC_ID, ANSWER_BODY, BRANCH_ID); + + assertEquals(createdPost.getPostContent(), ANSWER_BODY); + assertEquals(createdPost.getUserCreated(), user); + assertEquals(user.getPostCount(), 1); - assertFalse(answeredTopic.userSubscribed(user)); + verify(aclBuilder).grant(GeneralPermission.WRITE); + verify(aclBuilder).to(user); + verify(aclBuilder).on(createdPost); + verify(notificationService).subscribedEntityChanged(answeredTopic); } @Test public void replyTopicShouldNotifyMentionedInReplyUsers() throws NotFoundException { Topic answeredTopic = ObjectsFactory.topics(user, 1).get(0); - when(userService.getCurrentUser()).thenReturn(user); - when(topicFetchService.getTopicSilently(TOPIC_ID)).thenReturn(answeredTopic); - when(securityService.<User>createAclBuilder()).thenReturn(aclBuilder); + replyTopicStubs(answeredTopic); String answerWithUserMentioning = "[user]Shogun[/user] was mentioned"; Post answerPost = topicService.replyToTopic(TOPIC_ID, answerWithUserMentioning, BRANCH_ID); @@ -227,41 +292,10 @@ public void testReplyToNonexistentTopic() throws Exception { topicService.replyToTopic(TOPIC_ID, ANSWER_BODY, BRANCH_ID); } - @Test - public void testRunSubscriptionByCreateTopicWhenNotificationTrue() throws NotFoundException { - Branch branch = createBranch(); - user.setAutosubscribe(true); - when(userService.getCurrentUser()).thenReturn(user); - createTopicStubs(branch); - Topic dto = createTopic(); - Topic createdTopic = topicService.createTopic(dto, ANSWER_BODY); - Post createdPost = createdTopic.getFirstPost(); - - createTopicAssertions(branch, createdTopic, createdPost); - createTopicVerifications(createdTopic); - verify(subscriptionService).toggleTopicSubscription(createdTopic); - } - - @Test - public void testNotRunSubscriptionByCreateTopicWhenNotificationFalse() throws NotFoundException { - Branch branch = createBranch(); - user.setAutosubscribe(false); - when(userService.getCurrentUser()).thenReturn(user); - createTopicStubs(branch); - Topic dto = createTopic(); - Topic createdTopic = topicService.createTopic(dto, ANSWER_BODY); - Post createdPost = createdTopic.getFirstPost(); - - createTopicAssertions(branch, createdTopic, createdPost); - createTopicVerifications(createdTopic); - verify(subscriptionService, never()).toggleTopicSubscription(createdTopic); - } - @Test public void createTopicShouldNotifyMentionedUsers() throws NotFoundException { - Branch branch = createBranch(); user.setAutosubscribe(false); - createTopicStubs(branch); + createTopicStubs(); String answerBodyWithUserMentioning = "[user]Shogun[/user] you are mentioned"; Topic topicWithUserNotification = createTopic(); @@ -270,13 +304,24 @@ public void createTopicShouldNotifyMentionedUsers() throws NotFoundException { verify(userService).notifyAndMarkNewlyMentionedUsers(createdTopic.getFirstPost()); } + @Test + public void createTopicShouldDeleteDraft() throws NotFoundException { + user.setAutosubscribe(false); + createTopicStubs(); + String bodyText = "topic content"; + Topic topic = createTopic(); + + topicService.createTopic(topic, bodyText); + + verify(topicDraftService).deleteDraft(); + } + @Test public void testCreateCodeReviewWithWrappedBbCode() throws NotFoundException { JCUser user = new JCUser("", "", ""); user.setAutosubscribe(false); when(userService.getCurrentUser()).thenReturn(user); - Branch branch = createBranch(); - createTopicStubs(branch); + createTopicStubs(); Topic dto = createTopic(); dto.setType(TopicTypeName.CODE_REVIEW.getName()); Topic createdTopic = topicService.createTopic(dto, ANSWER_BODY); @@ -285,41 +330,17 @@ public void testCreateCodeReviewWithWrappedBbCode() throws NotFoundException { } @Test - private void updateLastPostInBranchByCreateTopic() throws NotFoundException { + public void updateLastPostInBranchByCreateTopic() throws NotFoundException { Branch branch = createBranch(); - createTopicStubs(branch); + createTopicStubs(); Topic tmp = createTopic(); tmp.setBranch(branch); Topic topic = topicService.createTopic(tmp, "content"); assertEquals(branch.getLastPost(), topic.getFirstPost()); } - - private void createTopicStubs(Branch branch) throws NotFoundException { - when(userService.getCurrentUser()).thenReturn(user); - when(branchDao.get(BRANCH_ID)).thenReturn(branch); - when(securityService.<User>createAclBuilder()).thenReturn(aclBuilder); - } - - private void createTopicAssertions(Branch branch, Topic createdTopic, Post createdPost) { - assertEquals(createdTopic.getTitle(), TOPIC_TITLE); - assertEquals(createdTopic.getTopicStarter(), user); - assertEquals(createdTopic.getBranch(), branch); - assertEquals(createdPost.getUserCreated(), user); - assertEquals(createdPost.getPostContent(), ANSWER_BODY); - assertEquals(user.getPostCount(), 1); - } - - private void createTopicVerifications(Topic topic) - throws NotFoundException { - verify(aclBuilder, times(2)).grant(GeneralPermission.WRITE); - verify(notificationService).sendNotificationAboutTopicCreated(topic); - verify(lastReadPostService).markTopicAsRead(topic); - } - @Test public void testDeleteTopic() throws NotFoundException { - Collection<JCUser> subscribers = new ArrayList<>(); Topic topic = new Topic(user, "title"); topic.setId(TOPIC_ID); Post firstPost = new Post(user, ANSWER_BODY); @@ -336,9 +357,7 @@ public void testDeleteTopic() throws NotFoundException { assertEquals(user.getPostCount(), 0); verify(branchDao).saveOrUpdate(branch); verify(securityService).deleteFromAcl(Topic.class, TOPIC_ID); - verify(notificationService).subscribedEntityChanged(branch, new ArrayList()); - verify(notificationService).sendNotificationAboutRemovingTopic(topic, subscribers); - verify(subscriptionService).getAllowedSubscribers(topic); + verify(notificationService).sendNotificationAboutRemovingTopic(topic); } @@ -417,20 +436,6 @@ public void testDeleteTopicSilentNonExistent() throws NotFoundException { topicService.deleteTopicSilent(TOPIC_ID); } - @Test - void testUpdateTopicWithSubscribe() throws NotFoundException { - user.setAutosubscribe(true); - when(userService.getCurrentUser()).thenReturn(user); - - Topic topic = createTopic(); - topic.addPost(createPost()); - when(userService.getCurrentUser()).thenReturn(user); - - topicService.updateTopic(topic, null); - - verify(subscriptionService).toggleTopicSubscription(topic); - } - @Test void testUpdateTopicWithRepeatedSubscribe() throws NotFoundException { user.setAutosubscribe(true); @@ -446,22 +451,6 @@ void testUpdateTopicWithRepeatedSubscribe() throws NotFoundException { verify(notificationService, times(0)).subscribedEntityChanged(topic); } - @Test - void testUpdateTopicWithUnsubscribe() throws NotFoundException { - user.setAutosubscribe(false); - when(userService.getCurrentUser()).thenReturn(user); - - Topic topic = createTopic(); - Post post = createPost(); - topic.addPost(post); - subscribeUserOnTopic(user, topic); - when(userService.getCurrentUser()).thenReturn(user); - - topicService.updateTopic(topic, null); - - verify(subscriptionService).toggleTopicSubscription(topic); - } - @Test void testUpdateTopicWithRepeatedUnsubscribe() throws NotFoundException { user.setAutosubscribe(true); @@ -695,6 +684,27 @@ public void testOpenTopic() { verify(topicDao).saveOrUpdate(topic); } + private void createTopicAssertions(Branch branch, Topic createdTopic, Post createdPost) { + assertEquals(createdTopic.getTitle(), TOPIC_TITLE); + assertEquals(createdTopic.getTopicStarter(), user); + assertEquals(createdTopic.getBranch(), branch); + assertEquals(createdPost.getUserCreated(), user); + assertEquals(createdPost.getPostContent(), ANSWER_BODY); + assertEquals(user.getPostCount(), 1); + } + + private void createTopicVerifications(Topic topic) + throws NotFoundException { + verify(aclBuilder, times(2)).grant(GeneralPermission.WRITE); + verify(notificationService).sendNotificationAboutTopicCreated(topic); + verify(lastReadPostService).markTopicAsRead(topic); + } + + private void createTopicStubs() throws NotFoundException { + when(userService.getCurrentUser()).thenReturn(user); + when(securityService.<User>createAclBuilder()).thenReturn(aclBuilder); + } + private Branch createBranch() { Branch branch = new Branch("branch name", "branch description"); branch.setId(BRANCH_ID); @@ -731,5 +741,9 @@ private void subscribeUserOnTopic(JCUser user, Topic topic) { subscribers.add(user); topic.setSubscribers(subscribers); } - + private void replyTopicStubs(Topic answeredTopic) throws NotFoundException { + when(userService.getCurrentUser()).thenReturn(user); + when(topicFetchService.getTopicSilently(TOPIC_ID)).thenReturn(answeredTopic); + when(securityService.<User>createAclBuilder()).thenReturn(aclBuilder); + } } diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalUserContactsServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalUserContactsServiceTest.java index 9a41b55f14..cda15e4d43 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalUserContactsServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalUserContactsServiceTest.java @@ -103,6 +103,19 @@ public void saveEditedContactsShouldUpdateEditedContacts() throws NotFoundExcept assertEquals(resultContacts.get(0).getType().getId(), contactTypeId2, "Type of contact 1 was not changed"); } + @Test + public void saveEditedContactsShouldDeleteContactWithEmptyValue() throws NotFoundException { + + List<UserContactContainer> contacts = new ArrayList<>(addContactsToUser(user, 1, 1)); + contacts.get(0).setValue(null); + + prepareContactsMocks(); + when(userContactsDao.isExist(anyLong())).thenReturn(true); + + JCUser resultUser = userContactsService.saveEditedUserContacts(user.getId(), contacts); + assertEquals(resultUser.getContacts().size(), 0,"Contact with empty value was not removed"); + } + @Test public void saveEditedContactsShouldAddNewContacts() throws NotFoundException { long contactTypeId1 = 1; diff --git a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalUserServiceTest.java b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalUserServiceTest.java index 77fab9bed3..b63918ac2d 100644 --- a/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalUserServiceTest.java +++ b/jcommune-service/src/test/java/org/jtalks/jcommune/service/transactional/TransactionalUserServiceTest.java @@ -15,17 +15,18 @@ package org.jtalks.jcommune.service.transactional; import com.google.common.collect.Lists; -import org.apache.commons.lang.StringUtils; import org.joda.time.DateTime; import org.jtalks.common.model.dao.GroupDao; import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.entity.User; -import org.jtalks.common.security.SecurityService; -import org.jtalks.common.security.acl.builders.CompoundAclBuilder; +import org.jtalks.common.service.security.SecurityContextFacade; +import org.jtalks.common.service.security.SecurityContextHolderFacade; import org.jtalks.jcommune.model.dao.PostDao; import org.jtalks.jcommune.model.dao.UserDao; +import org.jtalks.jcommune.model.dto.LoginUserDto; import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.plugin.api.exceptions.NoConnectionException; +import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.exceptions.UnexpectedErrorException; import org.jtalks.jcommune.service.Authenticator; import org.jtalks.jcommune.service.UserService; @@ -33,18 +34,20 @@ import org.jtalks.jcommune.service.dto.UserNotificationsContainer; import org.jtalks.jcommune.service.dto.UserSecurityContainer; import org.jtalks.jcommune.service.exceptions.MailingFailedException; -import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; -import org.jtalks.jcommune.service.exceptions.UserTriesActivatingAccountAgainException; import org.jtalks.jcommune.service.nontransactional.Base64Wrapper; import org.jtalks.jcommune.service.nontransactional.EncryptionService; import org.jtalks.jcommune.service.nontransactional.MailService; import org.jtalks.jcommune.service.nontransactional.MentionedUsers; -import org.jtalks.jcommune.service.security.AdministrationGroup; +import org.jtalks.jcommune.service.security.SecurityService; +import org.jtalks.jcommune.service.security.acl.AclManager; +import org.jtalks.jcommune.service.util.AuthenticationStatus; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.Matchers; import org.mockito.Mock; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletWebRequest; import org.testng.annotations.BeforeMethod; @@ -59,13 +62,14 @@ import static java.lang.String.format; import static java.util.Arrays.asList; -import org.jtalks.jcommune.model.dto.LoginUserDto; -import static org.jtalks.jcommune.service.TestUtils.mockAclBuilder; -import org.mockito.ArgumentMatcher; -import static org.mockito.Matchers.any; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.collection.IsCollectionContaining.hasItems; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.matches; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.argThat; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; import static org.testng.Assert.*; @@ -96,15 +100,13 @@ public class TransactionalUserServiceTest { private static final String MENTIONING_MESSAGE_WHEN_USER_NOT_FOUND = "This post contains not notified %s mentioning " + "and notified %s mentioning"; - private UserService userService; + private SecurityContextFacade securityContextFacade; @Mock private UserDao userDao; @Mock private GroupDao groupDao; @Mock - private SecurityService securityService; - @Mock private MailService mailService; @Mock private Base64Wrapper base64Wrapper; @@ -114,15 +116,16 @@ public class TransactionalUserServiceTest { private PostDao postDao; @Mock private Authenticator authenticator; - + @Mock + private AclManager aclManager; @BeforeMethod public void setUp() throws Exception { initMocks(this); when(encryptionService.encryptPassword(PASSWORD)) .thenReturn(PASSWORD_MD5_HASH); - CompoundAclBuilder<User> aclBuilder = mockAclBuilder(); - when(securityService.<User>createAclBuilder()).thenReturn(aclBuilder); + securityContextFacade = new SecurityContextHolderFacade(); + SecurityService securityService = new SecurityService(userDao, aclManager, securityContextFacade); userService = new TransactionalUserService( userDao, groupDao, @@ -154,7 +157,6 @@ public void getByUsernameSholdThrowErrorWhenUserWasNofFound() throws NotFoundExc @Test public void editUserProfileShouldUpdateHimAndSaveInRepository() throws NotFoundException { JCUser user = user(USERNAME); - when(securityService.getCurrentUserUsername()).thenReturn(StringUtils.EMPTY); when(userDao.getByUsername(anyString())).thenReturn(user); when(userDao.getByEmail(EMAIL)).thenReturn(null); when(encryptionService.encryptPassword(NEW_PASSWORD)).thenReturn(NEW_PASSWORD_MD5_HASH); @@ -188,7 +190,6 @@ public void editUserProfileShouldNotUpdateHimAndSaveInRepositoryIfHeIsNotFound() @Test public void editUserProfileShouldNotUpdateOtherSettings() throws NotFoundException { JCUser user = user(USERNAME); - when(securityService.getCurrentUserUsername()).thenReturn(StringUtils.EMPTY); when(userDao.getByUsername(anyString())).thenReturn(user); when(userDao.getByEmail(EMAIL)).thenReturn(null); when(encryptionService.encryptPassword(NEW_PASSWORD)) @@ -232,14 +233,12 @@ public void editUserProfileSecurityShouldUpdatePassword() throws NotFoundExcepti @Test public void editUserProfileShouldNotChangePasswordToNull() throws NotFoundException { JCUser user = user(USERNAME); - when(securityService.getCurrentUserUsername()).thenReturn(StringUtils.EMPTY); when(userDao.getByUsername(anyString())).thenReturn(user); when(encryptionService.encryptPassword(null)).thenReturn(null); when(userDao.isExist(USER_ID)).thenReturn(Boolean.TRUE); when(userDao.get(USER_ID)).thenReturn(user); - String newPassword = null; JCUser editedUser = userService.saveEditedUserSecurity(USER_ID, - new UserSecurityContainer(PASSWORD, newPassword)); + new UserSecurityContainer(PASSWORD, null)); assertEquals(editedUser.getPassword(), user.getPassword()); } @@ -317,7 +316,6 @@ private void assertUserProfileAndSecurityAreSame(JCUser user, JCUser editedUser) @Test public void testEditUserProfileSameEmail() throws Exception { JCUser user = user(USERNAME); - when(securityService.getCurrentUserUsername()).thenReturn(""); when(userDao.getByUsername(anyString())).thenReturn(user); when(userDao.getByEmail(EMAIL)).thenReturn(null); when(userDao.isExist(USER_ID)).thenReturn(Boolean.TRUE); @@ -400,49 +398,6 @@ public void testRestorePasswordFail() throws NotFoundException, MailingFailedExc } } - @Test - public void activateAccountShouldEnableUser() throws Exception { - JCUser user = new JCUser(USERNAME, EMAIL, PASSWORD); - when(userDao.getByUuid(user.getUuid())).thenReturn(user); - when(groupDao.getGroupByName(AdministrationGroup.USER.getName())).thenReturn(new Group()); - - userService.activateAccount(user.getUuid()); - assertTrue(user.isEnabled()); - } - - @Test - public void activateAccountShouldAddUserToRegisteredUsersGroup() throws Exception { - JCUser user = new JCUser(USERNAME, EMAIL, PASSWORD); - when(userDao.getByUuid(user.getUuid())).thenReturn(user); - Group registeredUsersGroup = new Group(); - when(groupDao.getGroupByName(AdministrationGroup.USER.getName())).thenReturn(registeredUsersGroup); - - userService.activateAccount(user.getUuid()); - assertTrue(user.getGroups().contains(registeredUsersGroup)); - } - - @Test(expectedExceptions = NotFoundException.class) - public void testActivateNotFoundAccountTest() throws NotFoundException, UserTriesActivatingAccountAgainException { - when(userDao.getByUsername(USERNAME)).thenReturn(null); - - userService.activateAccount(USERNAME); - } - - @Test(expectedExceptions = UserTriesActivatingAccountAgainException.class) - public void testActivateAccountAlreadyEnabled() throws NotFoundException, UserTriesActivatingAccountAgainException { - JCUser user = new JCUser(USERNAME, EMAIL, PASSWORD); - user.setEnabled(true); - when(userDao.getByUuid(user.getUuid())).thenReturn(user); - Group group = new Group(); - when(groupDao.getGroupByName(AdministrationGroup.USER.getName())).thenReturn(group); - - userService.activateAccount(user.getUuid()); - - assertTrue(user.isEnabled()); - verify(groupDao, never()).saveOrUpdate(any(Group.class)); - assertFalse(group.getUsers().contains(user)); - } - @Test public void testNonActivatedAccountExpiration() throws NotFoundException { JCUser user1 = new JCUser(USERNAME, EMAIL, PASSWORD); @@ -462,24 +417,20 @@ public void testNonActivatedAccountExpiration() throws NotFoundException { } @Test - public void testGetCurrentUser() { + public void shouldReturnUserInfoIfAuthenticated() { JCUser expected = user(USERNAME); - - when(securityService.getCurrentUserUsername()).thenReturn(USERNAME); - when(userDao.getByUsername(USERNAME)).thenReturn(expected); - + securityContextFacade.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(new UserInfo(expected), null)); + when(userDao.loadById(anyLong())).thenReturn(expected); JCUser actual = userService.getCurrentUser(); - assertEquals(actual, expected); } @Test - public void testGetCurrentUserForAnonymous() { - when(securityService.getCurrentUserUsername()).thenReturn(null); - + public void shouldReturnAnonymousUserIfNotAuthenticated() { + securityContextFacade.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(null, null)); JCUser user = userService.getCurrentUser(); assertNotNull(user); - assertTrue(user instanceof AnonymousUser); + assertTrue(user.isAnonymous()); } @Test @@ -489,11 +440,12 @@ public void testLoginUserWithCorrectCredentialsShouldBeSuccessful() HttpServletResponse httpResponse = new MockHttpServletResponse(); LoginUserDto loginUserDto = new LoginUserDto("username", "password", true, "192.168.1.1"); when(authenticator.authenticate(loginUserDto, httpRequest, httpResponse)) - .thenReturn(true); + .thenReturn(AuthenticationStatus.AUTHENTICATED); - boolean result = userService.loginUser(loginUserDto, httpRequest, httpResponse); + AuthenticationStatus result = userService.loginUser(loginUserDto, httpRequest, httpResponse); - assertTrue(result, "Login user with correct credentials should be successful."); + assertEquals(result, AuthenticationStatus.AUTHENTICATED, + "Login user with correct credentials should be successful."); } @Test @@ -503,11 +455,24 @@ public void testLoginUserWithBadCredentialsShouldFail() HttpServletResponse httpResponse = new MockHttpServletResponse(); LoginUserDto loginUserDto = new LoginUserDto("", "password", true, "192.168.1.1"); when(authenticator.authenticate(loginUserDto, httpRequest, httpResponse)) - .thenReturn(false); + .thenReturn(AuthenticationStatus.AUTHENTICATION_FAIL); + + AuthenticationStatus result = userService.loginUser(loginUserDto, httpRequest, httpResponse); + + assertEquals(result, AuthenticationStatus.AUTHENTICATION_FAIL, "Login user with bad credentials should fail."); + } + + @Test + public void testLoginNotActivatedUserShouldFail() throws Exception { + HttpServletRequest httpRequest = new MockHttpServletRequest(); + HttpServletResponse httpResponse = new MockHttpServletResponse(); + LoginUserDto loginUserDto = new LoginUserDto("username", "password", true, "192.168.1.1"); + when(authenticator.authenticate(loginUserDto, httpRequest, httpResponse)) + .thenReturn(AuthenticationStatus.NOT_ENABLED); - boolean result = userService.loginUser(loginUserDto, httpRequest, httpResponse); + AuthenticationStatus result = userService.loginUser(loginUserDto, httpRequest, httpResponse); - assertFalse(result, "Login user with bad credentials should fail."); + assertEquals(result, AuthenticationStatus.NOT_ENABLED, "Login not activated user should fail."); } @Test @@ -562,12 +527,64 @@ public void testChangeLanguage() { verify(userDao).saveOrUpdate(argThat(new ArgumentMatcher<JCUser>() { @Override public boolean matches(Object argument) { - JCUser argUser = (JCUser)argument; + JCUser argUser = (JCUser) argument; return argument == user && Language.RUSSIAN.equals(argUser.getLanguage()); } })); } - + + @Test + public void testFindByUsernameOrEmail() { + String searchKey = "key"; + List<JCUser> users = Lists.newArrayList(user("user1"), user("user2"), user("user3")); + + when(userDao.findByUsernameOrEmail(searchKey, TransactionalUserService.MAX_SEARCH_USER_COUNT)).thenReturn(users); + + List<JCUser> result = userService.findByUsernameOrEmail(1L, searchKey); + + assertEquals(result, users); + } + + @Test + public void testGetUserGroupIDs() throws NotFoundException { + Long[] expectedGroupIDs = {4l, 5l, 6l}; + + JCUser jcUser = createUserWithGroups(expectedGroupIDs); + when(userDao.get(anyLong())).thenReturn(jcUser); + + assertThat(userService.getUserGroupIDs(0l, 1l), hasItems(expectedGroupIDs)); + } + + @Test + public void testAddUserToGroup() throws NotFoundException { + JCUser jcUser = createUserWithGroups(4l, 5l, 6l); + Group group = new Group(); + + when(userDao.get(anyLong())).thenReturn(jcUser); + when(groupDao.get(anyLong())).thenReturn(group); + + userService.addUserToGroup(0l, 1l, 2l); + + assertThat(group.getUsers().contains(jcUser), is(true)); + assertThat(jcUser.getGroups().contains(group), is(true)); + } + + @Test + public void testDeleteUserFromGroup() throws NotFoundException { + long groupForDeleteID = 5l; + + JCUser jcUser = createUserWithGroups(4l, groupForDeleteID, 6l); + Group group = new Group(); + group.setId(groupForDeleteID); + + when(userDao.get(anyLong())).thenReturn(jcUser); + when(groupDao.get(anyLong())).thenReturn(group); + + userService.deleteUserFromGroup(0l, 1l, groupForDeleteID); + + assertThat(jcUser.getGroups().contains(group), is(false)); + } + public static <T> Set<T> asSet(T... values) { return new HashSet<>(asList(values)); } @@ -597,4 +614,14 @@ private Post post(JCUser toBeNotified, String postContent) { post.setTopic(new Topic()); return post; } + + private JCUser createUserWithGroups(Long... expectedGroupIDs) { + JCUser jcUser = new JCUser("user", "email@email.com", "pwd"); + for (int i = 0; i < expectedGroupIDs.length; i++) { + Group group = new Group("group" + i); + group.setId(expectedGroupIDs[i]); + jcUser.getGroups().add(group); + } + return jcUser; + } } diff --git a/jcommune-service/src/test/resources/ValidationMessages_en.properties b/jcommune-service/src/test/resources/ValidationMessages_en.properties index 2dcf4f98db..cfbb30cea2 100644 --- a/jcommune-service/src/test/resources/ValidationMessages_en.properties +++ b/jcommune-service/src/test/resources/ValidationMessages_en.properties @@ -1,12 +1,10 @@ -title.length=should be {min} - {max} characters -body.length=should be {min} - {max} characters -not_empty=can't be empty +not_empty=Can't be empty validation.not_null=Must not be empty password_not_matches=Password and confirmation password do not match user.email.illegal_length=Email field should not contain more than {max} symbols validation.email.not_empty=Email field should not be empty -user.username.already_exists=User with the username already exists. -user.email.already_exists=User with the email already exists. +user.username.already_exists=User with this username already exists +user.email.already_exists=User with this email already exists validation.incorrectCurrentPassword=Password does not match to the current password validation.wrong_recipient=User not found validation.draft.need.at.least.one.field=You need to fill at least one of these fields @@ -57,4 +55,5 @@ user.first_name.illegal_length=should be {min} - {max} characters #administration validation.param.length=Please use value with length between {min} and {max} -access.denied=Access denied! +access.denied=Access denied\! +topicDraft.fields.not_null=At least one field must be not null diff --git a/jcommune-service/src/test/resources/ValidationMessages_es.properties b/jcommune-service/src/test/resources/ValidationMessages_es.properties index cbd6c0f610..e16c53b680 100644 --- a/jcommune-service/src/test/resources/ValidationMessages_es.properties +++ b/jcommune-service/src/test/resources/ValidationMessages_es.properties @@ -1,5 +1,3 @@ -title.length=debe contener {min} - {max} caracteres -body.length=debe contener {min} - {max} caracteres not_empty=no puede estar vac\u00edo validation.not_null=No puede estar vac\u00edo password_not_matches=La contrase\u00f1a y la verificaci\u00f3n de la contrase\u00f1a no coinciden @@ -57,4 +55,5 @@ user.first_name.illegal_length=debe tener entre {min} - {max} caracteres #administration validation.param.length=Por favor indica un valor con longitud entre {min} y {max} -access.denied=\u00a1Acceso denegado! +access.denied=\u00a1Acceso denegado\! +topicDraft.fields.not_null=Al menos un campo debe ser no nula diff --git a/jcommune-view/jcommune-web-controller/pom.xml b/jcommune-view/jcommune-web-controller/pom.xml index a26a083fc9..c2c210c74a 100644 --- a/jcommune-view/jcommune-web-controller/pom.xml +++ b/jcommune-view/jcommune-web-controller/pom.xml @@ -4,7 +4,7 @@ <parent> <artifactId>jcommune-view</artifactId> <groupId>org.jtalks.jcommune</groupId> - <version>2.14-SNAPSHOT</version> + <version>3.13-SNAPSHOT</version> </parent> <artifactId>jcommune-web-controller</artifactId> <name>${project.artifactId}</name> @@ -52,6 +52,10 @@ <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> </dependency> + <dependency> + <groupId>org.mockejb</groupId> + <artifactId>mockejb</artifactId> + </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> @@ -68,6 +72,10 @@ <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + </dependency> <dependency> <groupId>rome</groupId> <artifactId>rome</artifactId> @@ -88,6 +96,14 @@ <groupId>ru.sape</groupId> <artifactId>javasape</artifactId> </dependency> + <dependency> + <groupId>org.springframework.retry</groupId> + <artifactId>spring-retry</artifactId> + </dependency> + <dependency> + <groupId>io.qala.datagen</groupId> + <artifactId>qala-datagen</artifactId> + </dependency> </dependencies> <properties> diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AdministrationController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AdministrationController.java index d9b95b01b8..78934cdaff 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AdministrationController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AdministrationController.java @@ -16,68 +16,78 @@ import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.permissions.JtalksPermission; -import org.jtalks.jcommune.model.dto.GroupsPermissions; -import org.jtalks.jcommune.model.dto.PermissionChanges; +import org.jtalks.common.validation.ValidationError; +import org.jtalks.common.validation.ValidationException; +import org.jtalks.jcommune.model.dto.*; import org.jtalks.jcommune.model.entity.Branch; import org.jtalks.jcommune.model.entity.ComponentInformation; -import org.jtalks.jcommune.service.BranchService; -import org.jtalks.jcommune.service.ComponentService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.service.*; import org.jtalks.jcommune.service.security.PermissionManager; -import org.jtalks.jcommune.web.dto.BranchDto; -import org.jtalks.jcommune.web.dto.BranchPermissionDto; -import org.jtalks.jcommune.web.dto.GroupDto; -import org.jtalks.jcommune.web.dto.PermissionGroupsDto; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.web.dto.*; +import org.jtalks.jcommune.web.listeners.SessionSetupListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; +import org.springframework.data.domain.Page; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; +import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Set; /** * @author Andrei Alikov * Controller for processing forum administration related requests * such as setting up Forum title, description, logo and fav icon + * Some methods in controller are directly used from test classes, + * and some of them are not @SuppressWarnings("unused") is used to + * disable IDE warnings about methods without direct access. */ +@SuppressWarnings("unused") @Controller public class AdministrationController { /** * Session's marker attribute name for Administration mode */ - public static final String ADMIN_ATTRIBUTE_NAME = "adminMode"; + static final String ADMIN_ATTRIBUTE_NAME = "adminMode"; private static final String ACCESS_DENIED_MESSAGE = "access.denied"; + private static final int GROUP_USER_PAGE_SIZE = 20; private final ComponentService componentService; + private final GroupService groupService; + private final UserService userService; private final MessageSource messageSource; private final BranchService branchService; private final PermissionManager permissionManager; + private final SpamProtectionService spamProtectionService; - /** - * Creates instance of the service - * - * @param componentService service to work with the forum component - * @param messageSource to resolve locale-dependent messages - * @param permissionManager - */ @Autowired public AdministrationController(ComponentService componentService, MessageSource messageSource, BranchService branchService, - PermissionManager permissionManager) { + PermissionManager permissionManager, + GroupService groupService, + SpamProtectionService spamProtectionService, + UserService userService) { this.messageSource = messageSource; this.componentService = componentService; this.branchService = branchService; this.permissionManager = permissionManager; + this.groupService = groupService; + this.spamProtectionService = spamProtectionService; + this.userService = userService; } /** @@ -90,8 +100,7 @@ public AdministrationController(ComponentService componentService, @RequestMapping(value = "/admin/enter", method = RequestMethod.GET) public String enterAdministrationMode(HttpServletRequest request) { if (componentService.getComponentOfForum() != null) { - long componentId = componentService.getComponentOfForum().getId(); - componentService.checkPermissionsForComponent(componentId); + checkForAdminPermissions(); } request.getSession().setAttribute(ADMIN_ATTRIBUTE_NAME, true); @@ -129,6 +138,9 @@ public JsonResponse setForumInformation(@Valid @RequestBody ComponentInformation try { componentService.setComponentInformation(componentInformation); + // SessionSetupListener read session timeout property once on application startup and + // keeps it in memory, so when property is updated we need to read it again. + SessionSetupListener.resetSessionTimeoutProperty(); } catch (AccessDeniedException e) { String errorMessage = messageSource.getMessage(ACCESS_DENIED_MESSAGE, null, locale); return new JsonResponse(JsonResponseStatus.FAIL, errorMessage); @@ -216,10 +228,10 @@ public JsonResponse getGroupsForBranchPermission(@RequestBody BranchPermissionDt } catch (NotFoundException e) { return new JsonResponse(JsonResponseStatus.FAIL, null); } - List<GroupDto> alreadySelected = GroupDto.convertGroupList(selectedGroups, true); + List<GroupDto> alreadySelected = GroupDto.convertToGroupDtoList(selectedGroups, GroupDto.BY_NAME_COMPARATOR); List<Group> availableGroups = permissionManager.getAllGroupsWithoutExcluded(selectedGroups, branchPermission); - List<GroupDto> available = GroupDto.convertGroupList(availableGroups, true); + List<GroupDto> available = GroupDto.convertToGroupDtoList(availableGroups, GroupDto.BY_NAME_COMPARATOR); permission.setSelectedGroups(alreadySelected); permission.setAvailableGroups(available); @@ -249,6 +261,86 @@ public JsonResponse editBranchPermissions(@RequestBody BranchPermissionDto permi } return new JsonResponse(JsonResponseStatus.SUCCESS); } + /** + * Display to user list of groups with count of users. + */ + @RequestMapping(value = "/group/list", method = RequestMethod.GET) + public ModelAndView showGroupsWithUsers() { + checkForAdminPermissions(); + List<GroupAdministrationDto> groupAdministrationDtos = groupService.getGroupNamesWithCountOfUsers(); + return new ModelAndView("groupAdministration").addObject("groups", groupAdministrationDtos); + } + + /** + * Register {@link org.jtalks.common.model.entity.Group} from populated in form {@link GroupAdministrationDto}. + * + * @param groupDto {@link GroupAdministrationDto} populated in form + * @return JsonResponse with JsonResponseStatus. SUCCESS if registration successful or FAIL if failed + */ + @RequestMapping(value = "/group", method = RequestMethod.POST) + @ResponseBody + public JsonResponse createNewGroup(@Valid @RequestBody GroupAdministrationDto groupDto, BindingResult result, Locale locale) throws org.jtalks.common.service.exceptions.NotFoundException { + return saveOrUpdateGroup(groupDto, result, locale); + } + + @RequestMapping(value = "/group/{groupId}", method = RequestMethod.PUT) + @ResponseBody + public JsonResponse editGroup(@Valid @RequestBody GroupAdministrationDto groupDto, + BindingResult result, + @PathVariable("groupId") Long groupId, + Locale locale) throws org.jtalks.common.service.exceptions.NotFoundException { + return saveOrUpdateGroup(groupDto, result, locale); + } + + @RequestMapping(value = "/group/{groupId}", method = RequestMethod.GET) + public ModelAndView getGroupUsers(@PathVariable("groupId") long groupId, + @RequestParam(value = "page", defaultValue = "1", required = false) int page) + throws org.jtalks.common.service.exceptions.NotFoundException { + checkForAdminPermissions(); + GroupDto groupDto = new GroupDto(groupService.get(groupId)); + Page<UserDto> pagedGroupUsers = getPagedGroupUsers(groupId, page); + return new ModelAndView("groupUserList") + .addObject("group", groupDto) + .addObject("groupUsersPage", pagedGroupUsers); + } + + @RequestMapping(value = "/ajax/group/{groupId}", method = RequestMethod.GET) + @ResponseBody + public JsonResponse ajaxGetGroupUsers(@PathVariable("groupId") long groupId, + @RequestParam(value = "page", defaultValue = "1", required = false) int page) + throws org.jtalks.common.service.exceptions.NotFoundException { + Page<UserDto> pagedGroupUsers = getPagedGroupUsers(groupId, page); + return new JsonResponse(JsonResponseStatus.SUCCESS, pagedGroupUsers); + } + + @RequestMapping(value = "/group/{groupId}", method = RequestMethod.DELETE) + @ResponseBody + public JsonResponse deleteGroup(@PathVariable("groupId") Long groupId) throws org.jtalks.common.service.exceptions.NotFoundException { + checkForAdminPermissions(); + Group group = groupService.get(groupId); + groupService.deleteGroup(group); + return new JsonResponse(JsonResponseStatus.SUCCESS); + } + + @RequestMapping(value = "/spamprotection", method = RequestMethod.GET) + public ModelAndView getSpamProtectionPage(){ + checkForAdminPermissions(); + List<SpamRuleDto> ruleDtos = SpamRuleDto.fromEntities(spamProtectionService.getAllRules()); + return new ModelAndView("spamProtection").addObject("rules", ruleDtos); + } + + @RequestMapping(value = "/user", params = {"notInGroupId", "pattern"}, method = RequestMethod.GET) + @ResponseBody + public JsonResponse findUserNotInGroup(@RequestParam("notInGroupId") long notInGroupId, + @Valid @ModelAttribute SearchQueryDto searchQueryDto, + BindingResult result) { + checkForAdminPermissions(); + if (result.hasFieldErrors() || result.hasGlobalErrors()) { + return new JsonResponse(JsonResponseStatus.FAIL, result.getAllErrors()); + } + List<UserDto> users = userService.findByUsernameOrEmailNotInGroup(searchQueryDto.getPattern(), notInGroupId, 10); + return new JsonResponse(JsonResponseStatus.SUCCESS, users); + } /** * Returns redirect string to previous page @@ -259,5 +351,44 @@ private String getRedirectToPrevPage(HttpServletRequest request) { return "redirect:" + request.getHeader("Referer"); } + /** + * Check if currently logged user has permissions for administrative + * functions for forum + */ + private void checkForAdminPermissions() { + long forumId = componentService.getComponentOfForum().getId(); + componentService.checkPermissionsForComponent(forumId); + } + private JsonResponse saveOrUpdateGroup(@Valid @RequestBody GroupAdministrationDto groupDto, BindingResult result, Locale locale) throws org.jtalks.common.service.exceptions.NotFoundException { + checkForAdminPermissions(); + if (result.hasFieldErrors() || result.hasGlobalErrors()) { + return new JsonResponse(JsonResponseStatus.FAIL, result.getAllErrors()); + } + try { + groupService.saveOrUpdate(groupDto); + } catch (ValidationException e) { + return new JsonResponse(JsonResponseStatus.FAIL, convertErrors(e.getErrors(), result.getObjectName(), locale)); + } + return new JsonResponse(JsonResponseStatus.SUCCESS); + } + + private List<ObjectError> convertErrors(Set<ValidationError> errors, String objectName, Locale locale) { + List<ObjectError> objectErrors = new ArrayList<>(); + for (ValidationError error : errors) { + objectErrors.add(new FieldError(objectName, + error.getFieldName(), + messageSource.getMessage(error.getErrorMessageCode(), null, locale))); + + } + return objectErrors; + } + + private Page<UserDto> getPagedGroupUsers(long groupId, int page) + throws org.jtalks.common.service.exceptions.NotFoundException { + checkForAdminPermissions(); + GroupDto groupDto = new GroupDto(groupService.get(groupId)); + PageRequest pageRequest = new PageRequest(page, GROUP_USER_PAGE_SIZE); + return groupService.getPagedGroupUsers(groupId, pageRequest); + } } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AdministrationImagesController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AdministrationImagesController.java index 577b8963ef..26556fa89b 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AdministrationImagesController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AdministrationImagesController.java @@ -186,7 +186,7 @@ private void processImageRequest(HttpServletRequest request, HttpServletResponse forumModificationDate = startTime; } - Date ifModifiedDate = getIfModifiedSineDate(request.getHeader(IF_MODIFIED_SINCE_HEADER)); + Date ifModifiedDate = getIfModifiedSinceDate(request.getHeader(IF_MODIFIED_SINCE_HEADER)); if (!forumModificationDate.after(ifModifiedDate)) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AvatarController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AvatarController.java index 0f4bd17f03..c63c640c88 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AvatarController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/AvatarController.java @@ -15,10 +15,11 @@ package org.jtalks.jcommune.web.controller; +import org.joda.time.DateTime; import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.service.exceptions.ImageProcessException; -import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.web.util.ImageControllerUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -34,6 +35,9 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; /** * Controller for processing avatar related request. @@ -42,10 +46,8 @@ * @author Anuar Nurmakanov * @author Andrei Alikov */ - @Controller public class AvatarController extends ImageUploadController { - private UserService userService; private ImageControllerUtils avatarControllerUtils; @@ -114,23 +116,25 @@ public Map<String, String> uploadAvatar(@RequestBody byte[] bytes, public void renderAvatar( HttpServletRequest request, HttpServletResponse response, - @PathVariable Long id) - throws NotFoundException, IOException { + @PathVariable Long id) throws NotFoundException, IOException { JCUser user = userService.get(id); - Date ifModifiedDate = getIfModifiedSineDate(request.getHeader(IF_MODIFIED_SINCE_HEADER)); - if (!user.getAvatarLastModificationTime().isAfter(ifModifiedDate.getTime())) { - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - } else { + Date ifModifiedDate = super.getIfModifiedSinceDate(request.getHeader(IF_MODIFIED_SINCE_HEADER)); + // using 0 unix time if avatar has never changed (the date is null). It's easier to work with something + // non-null than to check for null all the time. + DateTime avatarLastModificationTime = defaultIfNull(user.getAvatarLastModificationTime(), new DateTime(0)); + // if-modified-since header doesn't include milliseconds, so if it floors the value (millis = 0), then + // actual modification date will always be after the if-modified-since and we'll always be returning avatar + avatarLastModificationTime = avatarLastModificationTime.withMillisOfSecond(0); + if (avatarLastModificationTime.isAfter(ifModifiedDate.getTime())) { byte[] avatar = user.getAvatar(); response.setContentType("image/jpeg"); response.setContentLength(avatar.length); response.getOutputStream().write(avatar); + } else { + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } - - Date avatarLastModificationDate = new Date( - user.getAvatarLastModificationTime().getMillis()); - setupAvatarHeaders(response, avatarLastModificationDate); + setupAvatarHeaders(response, new Date(avatarLastModificationTime.getMillis())); } /** @@ -144,7 +148,7 @@ public void renderAvatar( @RequestMapping(value = "/defaultAvatar", method = RequestMethod.GET) @ResponseBody public String getDefaultAvatar() throws ImageProcessException, IOException { - Map<String, String> responseContent = new HashMap<String, String>(); + Map<String, String> responseContent = new HashMap<>(); avatarControllerUtils.prepareNormalResponse(avatarControllerUtils.getDefaultImage(), responseContent); return avatarControllerUtils.getResponceJSONString(responseContent); } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/BranchController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/BranchController.java index 9483ec3ba9..ed5c1dfa65 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/BranchController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/BranchController.java @@ -32,7 +32,7 @@ import org.jtalks.jcommune.web.dto.BranchDto; import org.jtalks.jcommune.plugin.api.web.dto.Breadcrumb; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; -import org.jtalks.jcommune.web.dto.EntityToDtoConverter; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.security.access.PermissionEvaluator; @@ -147,7 +147,7 @@ public ModelAndView showPage(@PathVariable("branchId") long branchId, return new ModelAndView("topic/topicList") .addObject("viewList", locationService.getUsersViewing(branch)) .addObject("branch", branch) - .addObject("topicsPage", converter.convertToDtoPage(topicsPage)) + .addObject("topicsPage", converter.convertTopicPageToTopicDtoPage(topicsPage)) .addObject("breadcrumbList", breadcrumbs) .addObject("topicTypes", getTopicTypes(branchId)) .addObject("subscribed", branch.getSubscribers().contains(currentUser)); @@ -206,7 +206,7 @@ public ModelAndView recentBranchPostsPage(@PathVariable("branchId") long branchI return new ModelAndView("posts/recent") .addObject("feedTitle", branch.getName()) .addObject("feedDescription", branch.getDescription()) - .addObject("urlSuffix", branch.prepareUrlSuffix()) + .addObject("urlSuffix", branch.getUrlSuffix()) .addObject("posts", posts); } @@ -224,7 +224,7 @@ public ModelAndView recentTopicsPage( lastReadPostService.fillLastReadPostForTopics(topicsPage.getContent()); return new ModelAndView("topic/recent") - .addObject("topicsPage", converter.convertToDtoPage(topicsPage)) + .addObject("topicsPage", converter.convertTopicPageToTopicDtoPage(topicsPage)) .addObject("topics", topicsPage.getContent()); // for rssViewer } @@ -240,7 +240,7 @@ public ModelAndView unansweredTopicsPage(@RequestParam(value = PAGE, defaultValu Page<Topic> topicsPage = topicFetchService.getUnansweredTopics(page); lastReadPostService.fillLastReadPostForTopics(topicsPage.getContent()); return new ModelAndView("topic/unansweredTopics") - .addObject("topicsPage", converter.convertToDtoPage(topicsPage)); + .addObject("topicsPage", converter.convertTopicPageToTopicDtoPage(topicsPage)); } /** diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/CodeReviewCommentController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/CodeReviewCommentController.java index 4be19eac60..3eb309b1bc 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/CodeReviewCommentController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/CodeReviewCommentController.java @@ -19,13 +19,14 @@ import org.jtalks.jcommune.service.PostCommentService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.PostService; +import org.jtalks.jcommune.service.nontransactional.BBCodeService; import org.jtalks.jcommune.web.dto.CodeReviewCommentDto; import org.jtalks.jcommune.web.dto.CodeReviewDto; -import org.jtalks.jcommune.web.dto.json.FailJsonResponse; -import org.jtalks.jcommune.web.dto.json.FailValidationJsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseReason; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.FailJsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.FailValidationJsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseReason; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.security.access.AccessDeniedException; @@ -50,16 +51,15 @@ public class CodeReviewCommentController { private PostCommentService postCommentService; private PostService postService; + private BBCodeService bbCodeService; - /** - * @param postCommentService to operate with {@link org.jtalks.jcommune.model.entity.PostComment} entities - * @param postService to operate with {@link org.jtalks.jcommune.model.entity.Post} entities - */ @Autowired public CodeReviewCommentController(PostCommentService postCommentService, - PostService postService) { + PostService postService, + BBCodeService bbCodeService) { this.postCommentService = postCommentService; this.postService = postService; + this.bbCodeService = bbCodeService; } /** @@ -83,14 +83,31 @@ public void initBinder(WebDataBinder binder) { * result field * @throws NotFoundException if code review was not found */ - @RequestMapping(value = "/reviews/{postId}/json", method = RequestMethod.GET) + @RequestMapping(value = "/reviews/{postId}", method = RequestMethod.GET) @ResponseBody public JsonResponse getCodeReview(@PathVariable("postId") Long postId) throws NotFoundException { - Post post = postService.get(postId); - return new JsonResponse(JsonResponseStatus.SUCCESS, new CodeReviewDto(post)); + CodeReviewDto postDto = new CodeReviewDto(post); + for (CodeReviewCommentDto postComment : postDto.getComments()) { + postComment.setBody(bbCodeService.convertBbToHtml(postComment.getBody())); + } + return new JsonResponse(JsonResponseStatus.SUCCESS, postDto); } + @RequestMapping(value = "/review/comment/edit/{commentId}", method = RequestMethod.GET) + @ResponseBody + public JsonResponse getCodeReviewCommentForEdit(@PathVariable("commentId") Long commentId) throws NotFoundException { + PostComment postComment = postCommentService.get(commentId); + return new JsonResponse(JsonResponseStatus.SUCCESS, new CodeReviewCommentDto(postComment)); + } + + @RequestMapping(value = "/review/comment/render/{commentId}", method = RequestMethod.GET) + @ResponseBody + public JsonResponse getCodeReviewCommentForRender(@PathVariable("commentId") Long commentId) throws NotFoundException { + PostComment postComment = postCommentService.get(commentId); + postComment.setBody(bbCodeService.convertBbToHtml(postComment.getBody())); + return new JsonResponse(JsonResponseStatus.SUCCESS, new CodeReviewCommentDto(postComment)); + } /** * Adds CR comment to review @@ -112,6 +129,7 @@ public JsonResponse addComment( } PostComment addedComment = postService.addComment(postId, commentDto.getCommentAttributes(), commentDto.getBody()); + addedComment.setBody(bbCodeService.convertBbToHtml(addedComment.getBody())); CodeReviewCommentDto addedCommentDto = new CodeReviewCommentDto(addedComment); return new JsonResponse(JsonResponseStatus.SUCCESS, addedCommentDto); } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/CodeReviewController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/CodeReviewController.java index 2adfcbed3b..0a7cd06769 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/CodeReviewController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/CodeReviewController.java @@ -14,9 +14,8 @@ */ package org.jtalks.jcommune.web.controller; -import org.jtalks.jcommune.model.entity.Branch; -import org.jtalks.jcommune.model.entity.Topic; -import org.jtalks.jcommune.model.entity.TopicTypeName; +import org.apache.commons.lang3.ObjectUtils; +import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.service.*; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; @@ -24,7 +23,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; @@ -46,13 +47,15 @@ public class CodeReviewController { public static final String BREADCRUMB_LIST = "breadcrumbList"; private static final String SUBMIT_URL = "submitUrl"; private static final String TOPIC_DTO = "topicDto"; + private static final String TOPIC_DRAFT = "topicDraft"; private static final String REDIRECT_URL = "redirect:/topics/"; private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); private BranchService branchService; private BreadcrumbBuilder breadcrumbBuilder; private TopicModificationService topicModificationService; - private UserService userService; + private TopicDraftService topicDraftService; + private RetryTemplate retryTemplate; /** * @param branchService the object which provides actions on @@ -66,13 +69,13 @@ public class CodeReviewController { public CodeReviewController(BranchService branchService, BreadcrumbBuilder breadcrumbBuilder, TopicModificationService topicModificationService, - LastReadPostService lastReadPostService, - UserService userService, - PostService postService) { + TopicDraftService topicDraftService, + RetryTemplate retryTemplate) { this.branchService = branchService; this.breadcrumbBuilder = breadcrumbBuilder; this.topicModificationService = topicModificationService; - this.userService = userService; + this.topicDraftService = topicDraftService; + this.retryTemplate = retryTemplate; } /** @@ -84,12 +87,19 @@ public CodeReviewController(BranchService branchService, */ @RequestMapping(value = "/reviews/new", method = RequestMethod.GET) public ModelAndView showNewCodeReviewPage(@RequestParam(BRANCH_ID) Long branchId) throws NotFoundException { + + TopicDraft draft = ObjectUtils.defaultIfNull( + topicDraftService.getDraft(), new TopicDraft()); + + TopicDto dto = new TopicDto(draft); + Branch branch = branchService.get(branchId); - Topic topic = new Topic(); - topic.setBranch(branch); - TopicDto dto = new TopicDto(topic); + dto.getTopic().setBranch(branch); + dto.getTopic().setType(TopicTypeName.CODE_REVIEW.getName()); + return new ModelAndView(CODE_REVIEW_VIEW) .addObject(TOPIC_DTO, dto) + .addObject(TOPIC_DRAFT, draft) .addObject(BRANCH_ID, branchId) .addObject(SUBMIT_URL, "/reviews/new?branchId=" + branchId) .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getNewTopicBreadcrumb(branch)); @@ -105,39 +115,30 @@ public ModelAndView showNewCodeReviewPage(@RequestParam(BRANCH_ID) Long branchId * @throws NotFoundException when branch not found */ @RequestMapping(value = "/reviews/new", method = RequestMethod.POST) - public ModelAndView createCodeReview(@Valid @ModelAttribute TopicDto topicDto, + public ModelAndView createCodeReview(@Valid @ModelAttribute final TopicDto topicDto, BindingResult result, @RequestParam(BRANCH_ID) Long branchId) throws NotFoundException { Branch branch = branchService.get(branchId); + final Topic topic = topicDto.getTopic(); + topic.setBranch(branch); + topic.setType(TopicTypeName.CODE_REVIEW.getName()); + if (result.hasErrors()) { return new ModelAndView(CODE_REVIEW_VIEW) .addObject(TOPIC_DTO, topicDto) .addObject(BRANCH_ID, branchId) .addObject(SUBMIT_URL, "/reviews/new?branchId=" + branchId) - .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(branch)); + .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getNewTopicBreadcrumb(branch)); } - Topic topic = topicDto.getTopic(); - topic.setBranch(branch); - topic.setType(TopicTypeName.CODE_REVIEW.getName()); - Topic createdTopic = topicModificationService.createTopic(topic, topicDto.getBodyText()); - - return new ModelAndView(REDIRECT_URL + createdTopic.getId()); - } - private Topic createCodeReviewWithLockHandling(Topic topic, TopicDto topicDto) throws NotFoundException { - for (int i = 0; i < UserController.LOGIN_TRIES_AFTER_LOCK; i++) { - try { + Topic createdTopic = retryTemplate.execute(new RetryCallback<Topic, NotFoundException>() { + @Override + public Topic doWithRetry(RetryContext context) throws NotFoundException { return topicModificationService.createTopic(topic, topicDto.getBodyText()); - } catch (HibernateOptimisticLockingFailureException e) { } - } - try { - return topicModificationService.createTopic(topic, topicDto.getBodyText()); - } catch (HibernateOptimisticLockingFailureException e) { - LOGGER.error("User has been optimistically locked and can't be reread {} times. Username: {}", - UserController.LOGIN_TRIES_AFTER_LOCK, userService.getCurrentUser().getUsername()); - throw e; - } + }); + + return new ModelAndView(REDIRECT_URL + createdTopic.getId()); } } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ErrorsHandlerController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ErrorsHandlerController.java index c42560b46a..3c5745a4f4 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ErrorsHandlerController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ErrorsHandlerController.java @@ -14,8 +14,6 @@ */ package org.jtalks.jcommune.web.controller; -import org.jtalks.jcommune.service.UserService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -25,51 +23,40 @@ * This controller is needed to handle errors and set correct status * * @author Andrey Ivanov + * @author Aleksei Usharovskii */ @Controller @RequestMapping("/errors/") public class ErrorsHandlerController { - @Autowired - private UserService userService; - - @RequestMapping(value = "500") - @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) - public String handleInternalServerError() { - return "/errors/500"; - } - - @RequestMapping(value = "404") - @ResponseStatus(value = HttpStatus.NOT_FOUND) - public String handleNotFoundError() { - return "/errors/404"; - } - - @RequestMapping(value = "redirect/404") - public String handleNotFoundRedirect() { - return "redirect:/errors/404"; - } - @RequestMapping(value = "400") @ResponseStatus(value = HttpStatus.BAD_REQUEST) public String handleBadRequestError() { return "/errors/400"; } - @RequestMapping(value = "redirect/403") - public String handleForbiddenRedirect() { - return "redirect:/errors/403"; - } - @RequestMapping(value = "403") @ResponseStatus(value = HttpStatus.FORBIDDEN) public String handleForbiddenError() { - return "/errors/accessDenied"; + return "/errors/403"; + } + + @RequestMapping(value = "404") + @ResponseStatus(value = HttpStatus.NOT_FOUND) + public String handleNotFoundError() { + return "/errors/404"; + } + + @RequestMapping(value = "405") + @ResponseStatus(value = HttpStatus.METHOD_NOT_ALLOWED) + public String handleMethodNotSupportedError() { + return "/errors/405"; } - @RequestMapping(value = "redirect/501") - public String handleNotImplementedRedirect() { - return "redirect:/errors/501"; + @RequestMapping(value = "500") + @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) + public String handleInternalServerError() { + return "/errors/500"; } @RequestMapping(value = "501") diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ExternalLinkController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ExternalLinkController.java index 9a12fe53ce..9d62a325f5 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ExternalLinkController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ExternalLinkController.java @@ -18,8 +18,8 @@ import org.jtalks.jcommune.model.entity.ExternalLink; import org.jtalks.jcommune.service.ComponentService; import org.jtalks.jcommune.service.ExternalLinkService; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ImageUploadController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ImageUploadController.java index 297cbf3d9f..31d536503f 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ImageUploadController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ImageUploadController.java @@ -20,8 +20,8 @@ import org.jtalks.jcommune.service.exceptions.ImageFormatException; import org.jtalks.jcommune.service.exceptions.ImageProcessException; import org.jtalks.jcommune.service.exceptions.ImageSizeException; -import org.jtalks.jcommune.web.dto.json.FailJsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseReason; +import org.jtalks.jcommune.plugin.api.web.dto.json.FailJsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseReason; import org.jtalks.jcommune.web.util.ImageControllerUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,12 +42,14 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.concurrent.TimeUnit; /** * Base class for handling uploaded images and generating preview for them */ public class ImageUploadController { - + /** We cannot set it to forever since avatars sometimes change, but this is a pretty rare event. */ + static long AVATAR_CACHE_AGE_SEC = TimeUnit.DAYS.toSeconds(30); private MessageSource messageSource; static final String WRONG_FORMAT_RESOURCE_MESSAGE = "image.wrong.format"; @@ -122,16 +124,11 @@ public MessageSource getMessageSource() { * @param response - HTTP response object where set headers * @param avatarLastModificationTime - last modification time of avatar */ - protected void setupAvatarHeaders(HttpServletResponse response, - Date avatarLastModificationTime) { + protected void setupAvatarHeaders(HttpServletResponse response, Date avatarLastModificationTime) { response.setHeader("Pragma", "public"); + response.setDateHeader("Expires", System.currentTimeMillis() + AVATAR_CACHE_AGE_SEC * 1000); response.setHeader("Cache-Control", "public"); - response.addHeader("Cache-Control", "must-revalidate"); - response.addHeader("Cache-Control", "max-age=0"); - String formattedDateExpires = DateFormatUtils.format( - new Date(System.currentTimeMillis()), - HTTP_HEADER_DATETIME_PATTERN, Locale.US); - response.setHeader("Expires", formattedDateExpires); + response.addHeader("Cache-Control", "max-age=" + AVATAR_CACHE_AGE_SEC); String formattedDateLastModified = DateFormatUtils.format( avatarLastModificationTime, @@ -148,7 +145,7 @@ protected void setupAvatarHeaders(HttpServletResponse response, * @return If-Modified-Since header or Jan 1, 1970 if it is not set or * can't be parsed */ - public Date getIfModifiedSineDate(String ifModifiedSinceHeader) { + public Date getIfModifiedSinceDate(String ifModifiedSinceHeader) { Date ifModifiedSinceDate = new Date(0); if (ifModifiedSinceHeader != null) { try { @@ -177,7 +174,7 @@ protected ResponseEntity<String> createPreviewOfImage(MultipartFile file, ImageC throws IOException, ImageProcessException { HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setContentType(MediaType.TEXT_HTML); - Map<String, String> responseContent = new HashMap<String, String>(); + Map<String, String> responseContent = new HashMap<>(); return imageControllerUtils.prepareResponse(file, responseHeaders, responseContent); } @@ -193,7 +190,7 @@ protected ResponseEntity<String> createPreviewOfImage(MultipartFile file, ImageC protected Map<String, String> createPreviewOfImage(byte[] imageBytes, HttpServletResponse response, ImageControllerUtils imageControllerUtils) throws ImageProcessException { - Map<String, String> responseContent = new HashMap<String, String>(); + Map<String, String> responseContent = new HashMap<>(); imageControllerUtils.prepareResponse(imageBytes, response, responseContent); return responseContent; } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/JetmHttpConsoleServlet.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/JetmHttpConsoleServlet.java index 3b4c78bd62..e59ca28ca3 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/JetmHttpConsoleServlet.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/JetmHttpConsoleServlet.java @@ -28,6 +28,8 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import static org.apache.commons.lang3.StringUtils.defaultIfBlank; + /** * <p>Servlet that provides access to JETM HTTP-console like the * {@link etm.contrib.integration.spring.web.SpringHttpConsoleServlet}, but additionally check JNDI for the performance @@ -40,6 +42,7 @@ public class JetmHttpConsoleServlet extends SpringHttpConsoleServlet { private static final String ACTIVE_PROFILE_PROPERTY = "spring.profiles.active"; + private static final String ACTIVE_PROFILE_ENV_VAR = "SPRING_PROFILES_ACTIVE"; private static final String PERFORMANCE_PROFILE = "performance"; private ComponentService componentService; @@ -89,6 +92,8 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se private boolean profileIsActive() { String profile = new JndiAwarePropertyPlaceholderConfigurer().resolveJndiProperty(ACTIVE_PROFILE_PROPERTY); + profile = defaultIfBlank(profile, System.getProperty(ACTIVE_PROFILE_PROPERTY)); + profile = defaultIfBlank(profile, System.getenv(ACTIVE_PROFILE_ENV_VAR)); return PERFORMANCE_PROFILE.equals(profile); } } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PluginController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PluginController.java index ce174b8407..c064813a89 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PluginController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PluginController.java @@ -27,8 +27,8 @@ import org.jtalks.jcommune.service.ComponentService; import org.jtalks.jcommune.service.PluginService; import org.jtalks.jcommune.service.UserService; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PostController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PostController.java index d2e8aaba20..969c2c9ee3 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PostController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PostController.java @@ -15,33 +15,36 @@ package org.jtalks.jcommune.web.controller; import org.apache.commons.lang.StringUtils; -import org.jtalks.jcommune.model.entity.JCUser; -import org.jtalks.jcommune.model.entity.Post; -import org.jtalks.jcommune.model.entity.PostVote; -import org.jtalks.jcommune.model.entity.Topic; +import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.service.*; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.jtalks.jcommune.service.nontransactional.BBCodeService; import org.jtalks.jcommune.service.nontransactional.LocationService; import org.jtalks.jcommune.plugin.api.web.dto.PostDto; +import org.jtalks.jcommune.plugin.api.web.dto.PostDraftDto; import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.data.domain.Page; -import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException; -import org.springframework.security.core.session.SessionRegistry; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.web.util.WebUtils; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import javax.validation.Valid; /** @@ -63,7 +66,7 @@ public class PostController { public static final String POST_DTO = "postDto"; public static final String TOPIC_TITLE = "topicTitle"; public static final String BREADCRUMB_LIST = "breadcrumbList"; - private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); + private static final Logger LOGGER = LoggerFactory.getLogger(PostController.class); private PostService postService; private LastReadPostService lastReadPostService; @@ -73,11 +76,8 @@ public class PostController { private BBCodeService bbCodeService; private UserService userService; private LocationService locationService; - private SessionRegistry sessionRegistry; - - - @Autowired(required = true) - private ViewResolver viewResolver; + private EntityToDtoConverter converter; + private RetryTemplate retryTemplate; /** * This method turns the trim binder on. Trim binder @@ -100,12 +100,16 @@ public void initBinder(WebDataBinder binder) { * @param bbCodeService to create valid quotes * @param lastReadPostService not to track user posts as updates for himself * @param userService to get the current user information + * @param converter instance of {@link EntityToDtoConverter} needed to + * obtain link to the topic + * @param retryTemplate retry mechanism */ @Autowired public PostController(PostService postService, BreadcrumbBuilder breadcrumbBuilder, TopicFetchService topicFetchService, TopicModificationService topicModificationService, BBCodeService bbCodeService, LastReadPostService lastReadPostService, - UserService userService, LocationService locationService, SessionRegistry sessionRegistry) { + UserService userService, LocationService locationService, EntityToDtoConverter converter, + RetryTemplate retryTemplate) { this.postService = postService; this.breadcrumbBuilder = breadcrumbBuilder; this.topicFetchService = topicFetchService; @@ -114,43 +118,31 @@ public PostController(PostService postService, BreadcrumbBuilder breadcrumbBuild this.lastReadPostService = lastReadPostService; this.userService = userService; this.locationService = locationService; - this.sessionRegistry = sessionRegistry; + this.converter = converter; + this.retryTemplate = retryTemplate; } /** * Delete post by given id - * + * * @param postId post * @return redirect to post next to deleted one. Redirects to previous post in case if it's last post in topic. * @throws NotFoundException when post was not found */ @RequestMapping(method = RequestMethod.DELETE, value = "/posts/{postId}") - public ModelAndView delete(@PathVariable(POST_ID) Long postId) + public ModelAndView delete(@PathVariable(POST_ID) final Long postId) throws NotFoundException { - Post post = this.postService.get(postId); + final Post post = this.postService.get(postId); Post nextPost = post.getTopic().getNeighborPost(post); - deletePostWithLockHandling(postId); - return new ModelAndView("redirect:/posts/" + nextPost.getId()); - } - - private Topic deletePostWithLockHandling(Long postId) throws NotFoundException { - for (int i = 0; i < UserController.LOGIN_TRIES_AFTER_LOCK; i++) { - try { + retryTemplate.execute(new RetryCallback<Object, NotFoundException>() { + @Override + public Object doWithRetry(RetryContext context) throws NotFoundException { Post post = postService.get(postId); - postService.deletePost(post); - return post.getTopic(); - } catch (HibernateOptimisticLockingFailureException e) { + postService.deletePost(post); + return null; } - } - try { - Post post = postService.get(postId); - postService.deletePost(post); - return post.getTopic(); - } catch (HibernateOptimisticLockingFailureException e) { - LOGGER.error("User has been optimistically locked and can't be reread {} times. Username: {}", - UserController.LOGIN_TRIES_AFTER_LOCK, userService.getCurrentUser().getUsername()); - throw e; - } + }); + return new ModelAndView("redirect:/posts/" + nextPost.getId()); } /** @@ -222,51 +214,66 @@ public JsonResponse getQuote(@PathVariable(POST_ID) Long postId, * @return redirect to the topic or back to answer pae if validation failed * @throws NotFoundException when topic or branch not found */ - @RequestMapping(method = RequestMethod.POST, value = "/topics/{topicId}") + @RequestMapping(method = RequestMethod.POST, value = "/topics/{topicId}") // public ModelAndView create(@RequestParam(value = "page", defaultValue = "1", required = false) String page, @PathVariable(TOPIC_ID) Long topicId, - @Valid @ModelAttribute PostDto postDto, - BindingResult result) throws NotFoundException { + @Valid @ModelAttribute final PostDto postDto, + BindingResult result, RedirectAttributes attr) throws NotFoundException { postDto.setTopicId(topicId); if (result.hasErrors()) { - JCUser currentUser = userService.getCurrentUser(); - Topic topic = topicFetchService.get(topicId); - postDto.setTopicId(topicId); - Page<Post> postsPage = postService.getPosts(topic, page); - - return new ModelAndView("topic/postList") - .addObject("viewList", locationService.getUsersViewing(topic)) - .addObject("usersOnline", sessionRegistry.getAllPrincipals()) - .addObject("postsPage", postsPage) - .addObject("topic", topic) - .addObject(POST_DTO, postDto) - .addObject("subscribed", topic.getSubscribers().contains(currentUser)) - .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); + attr.addFlashAttribute("postDto", postDto); + return new ModelAndView("redirect:/topics/error/" + topicId + "?page=" + page); } - - Post newbie = replyToTopicWithLockHandling(postDto, topicId); + final Topic topic = topicFetchService.get(topicId); + final long branchId = topic.getBranch().getId(); + Post newbie = retryTemplate.execute(new RetryCallback<Post, NotFoundException>() { + @Override + public Post doWithRetry(RetryContext context) throws NotFoundException { + return topicModificationService.replyToTopic( + postDto.getTopicId(), postDto.getBodyText(), branchId); + } + }); lastReadPostService.markTopicAsRead(newbie.getTopic()); return new ModelAndView(this.redirectToPageWithPost(newbie.getId())); } - private Post replyToTopicWithLockHandling(PostDto postDto, Long topicId) throws NotFoundException { + /** + * Gets validation errors from 'create' methods to redirect them to the view. We need it + * to implement POST/redirect/GET pattern, which leads to preventing of repeating POST request + * on browser refresh. + * + * @param page page of the current post + * @param topicId ID of a topic + * @param postDto Dto with failed validation + * @param result validation result + * + * @return {@code ModelAndView} object which shows form with an error message + * @throws NotFoundException when topic, branch or post not found + */ + @RequestMapping(method = RequestMethod.GET, value = "/topics/error/{topicId}") + public ModelAndView errorRedirect(@RequestParam(value = "page", required = false) String page, + @PathVariable(TOPIC_ID) Long topicId, @ModelAttribute @Valid PostDto postDto, + BindingResult result) throws NotFoundException { + JCUser currentUser = userService.getCurrentUser(); Topic topic = topicFetchService.get(topicId); - long branchId = topic.getBranch().getId(); - for (int i = 0; i < UserController.LOGIN_TRIES_AFTER_LOCK; i++) { - try { - return topicModificationService.replyToTopic( - postDto.getTopicId(), postDto.getBodyText(), branchId); - } catch (HibernateOptimisticLockingFailureException e) { - } - } - try { - return topicModificationService.replyToTopic( - postDto.getTopicId(), postDto.getBodyText(), branchId); - } catch (HibernateOptimisticLockingFailureException e) { - LOGGER.error("User has been optimistically locked and can't be reread {} times. Username: {}", - UserController.LOGIN_TRIES_AFTER_LOCK, userService.getCurrentUser().getUsername()); - throw e; + + PostDraft draft = topic.getDraftForUser(currentUser); + if (draft != null) { + // If we create new dto object instead of using already existing + // we lose error messages linked with it + postDto.fillFrom(draft); } + + postDto.setTopicId(topicId); + Page<Post> postsPage = postService.getPosts(topic, page); + + return new ModelAndView("topic/postList") + .addObject("viewList", locationService.getUsersViewing(topic)) + .addObject("postsPage", postsPage) + .addObject("topic", topic) + .addObject(POST_DTO, postDto) + .addObject("subscribed", topic.getSubscribers().contains(currentUser)) + .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); } /** @@ -274,6 +281,9 @@ private Post replyToTopicWithLockHandling(PostDto postDto, Long topicId) throws * Method clients should not wary about paging at all, post id * is enough to be transferred to the proper page. * + * If post belongs to plugable topic and appropriated plugin is enabled redirects + * to plugable topic view. + * * @param postId unique post identifier * @return redirect view to the certain topic page * @throws NotFoundException is the is no post for the identifier given @@ -282,13 +292,8 @@ private Post replyToTopicWithLockHandling(PostDto postDto, Long topicId) throws public String redirectToPageWithPost(@PathVariable Long postId) throws NotFoundException { Post post = postService.get(postId); int page = postService.calculatePageForPost(post); - return new StringBuilder("redirect:/topics/") - .append(post.getTopic().getId()) - .append("?page=") - .append(page) - .append("#") - .append(postId) - .toString(); + String topicUrl = converter.convertTopicToDto(post.getTopic()).getTopicUrl(); + return "redirect:" + topicUrl + "?page=" + page + "#" + postId; } /** @@ -319,21 +324,76 @@ public ModelAndView preview(@Valid @ModelAttribute TopicDto topicDto, BindingRes return getPreviewModelAndView(result).addObject("content", topicDto.getBodyText()); } + /** + * Votes up for post with specified id + * + * @param postId id of a post to vote up + * @param request HttpServletRequest + * + * @return response in JSON format + * + * @throws NotFoundException if post with specified id not found + */ @RequestMapping(method = RequestMethod.GET, value = "/posts/{postId}/voteup") @ResponseBody - public JsonResponse voteUp(@PathVariable Long postId) throws NotFoundException{ - Post post = postService.get(postId); + public JsonResponse voteUp(@PathVariable Long postId, HttpServletRequest request) throws NotFoundException { PostVote vote = new PostVote(true); - postService.vote(post, vote); + voteWithSessionLocking(postId, vote, request); return new JsonResponse(JsonResponseStatus.SUCCESS); } + /** + * Votes down for post with specified id + * + * @param postId id of a post to vote down + * @param request HttpServletRequest + * + * @return response in JSON format + * + * @throws NotFoundException if post with specified id not found + */ @RequestMapping(method = RequestMethod.GET, value = "/posts/{postId}/votedown") @ResponseBody - public JsonResponse voteDown(@PathVariable Long postId) throws NotFoundException{ - Post post = postService.get(postId); + public JsonResponse voteDown(@PathVariable Long postId, HttpServletRequest request) throws NotFoundException { PostVote vote = new PostVote(false); - postService.vote(post, vote); + voteWithSessionLocking(postId, vote, request); + return new JsonResponse(JsonResponseStatus.SUCCESS); + } + + /** + * Saves new draft or update if it already exist + * + * @param postDraftDto post draft dto populated in form + * @param result validation result + * + * @return response in JSON format + * + * @throws NotFoundException if topic to store draft not exist + */ + @RequestMapping(value = "/posts/savedraft", method = RequestMethod.POST) + @ResponseBody + public JsonResponse saveDraft(@Valid @RequestBody PostDraftDto postDraftDto, BindingResult result) throws NotFoundException { + if (result.hasErrors()) { + return new JsonResponse(JsonResponseStatus.FAIL); + } + Topic topic = topicFetchService.getTopicSilently(postDraftDto.getTopicId()); + PostDraft saved = postService.saveOrUpdateDraft(topic, postDraftDto.getBodyText()); + return new JsonResponse(JsonResponseStatus.SUCCESS, saved.getId()); + } + + /** + * Deletes draft + * + * @param draftId id of draft to delete + * + * @return response in JSON format + * + * @throws NotFoundException if post with specified id not exist + */ + @RequestMapping(value = "drafts/{draftId}/delete", method = RequestMethod.GET) + @ResponseBody + public JsonResponse deleteDraft(@PathVariable Long draftId) throws NotFoundException { + postService.deleteDraft(draftId); return new JsonResponse(JsonResponseStatus.SUCCESS); } @@ -343,9 +403,46 @@ public JsonResponse voteDown(@PathVariable Long postId) throws NotFoundException * @return prepared ModelAndView for preview */ private ModelAndView getPreviewModelAndView(BindingResult result) { - String signature = userService.getCurrentUser().getSignature(); - return new ModelAndView("ajax/postPreview").addObject("signature", signature) + return new ModelAndView("ajax/postPreview") .addObject("isInvalid", result.hasFieldErrors("bodyText")) .addObject("errors", result.getFieldErrors("bodyText")); } + + /** + * Performs vote with session locking to prevent handling of concurrent requests from same user + * + * @param postId id of a post to vote + * @param vote {@link PostVote} object + * @param request HttpServletRequest + * + * @throws NotFoundException if post with specified id not found + */ + private void voteWithSessionLocking(Long postId, PostVote vote, HttpServletRequest request) throws NotFoundException { + /** + * We should not create session here to prevent possibility of creating multiplier sessions for same user in + * concurrent requests + */ + HttpSession session = request.getSession(false); + if (session != null) { + Object mutex = WebUtils.getSessionMutex(session); + /** + * Next operations performed in synchronized block to prevent handling of concurrent requests from same + * user. We use session mutex as the lock object. In many cases, the HttpSession reference itself is a safe + * mutex as well, since it will always be the same object reference for the same active logical session. + * However, this is not guaranteed across different servlet containers; the only 100% safe way is a session + * mutex. + */ + synchronized (mutex) { + Post post = postService.get(postId); + postService.vote(post, vote); + } + } else { + /** + * If <code>HttpSession</code> is <code>null</code> we have no mutex object, so we perform operations + * without synchronization + */ + Post post = postService.get(postId); + postService.vote(post, vote); + } + } } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PrivateMessageController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PrivateMessageController.java index 00c77f301b..83be21b91b 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PrivateMessageController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/PrivateMessageController.java @@ -21,6 +21,7 @@ import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.nontransactional.BBCodeService; +import org.jtalks.jcommune.web.dto.PrivateMessageDraftDto; import org.jtalks.jcommune.web.dto.PrivateMessageDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.propertyeditors.StringTrimmerEditor; @@ -32,6 +33,7 @@ import org.springframework.web.servlet.ModelAndView; import javax.validation.Valid; +import java.util.Arrays; import java.util.List; /** @@ -47,7 +49,6 @@ public class PrivateMessageController { public static final String PM_IDENTIFIERS = "pmIdentifiers"; - public static final String SENDER_ID = "senderId"; private PrivateMessageService pmService; private BBCodeService bbCodeService; private UserService userService; @@ -89,7 +90,7 @@ public PrivateMessageController(PrivateMessageService pmService, BBCodeService b * @param page the private message page number. * @return {@code ModelAndView} with added {@link Page} instance with of private messages. */ - @RequestMapping(value = "/inbox", method = RequestMethod.GET) + @RequestMapping(value = {"/inbox","/pm"}, method = RequestMethod.GET) public ModelAndView inboxPage(@RequestParam(value = "page", defaultValue = "1", required = false) String page) { Page<PrivateMessage> inboxPage = pmService.getInboxForCurrentUser(page); @@ -241,21 +242,35 @@ public ModelAndView editDraftPage(@PathVariable(PM_ID) Long id) throws NotFoundE * @return redirect to "drafts" folder if saved successfully or show form with error message */ @RequestMapping(value = "/pm/save", method = {RequestMethod.POST, RequestMethod.GET}) - public String saveDraft(@Valid @ModelAttribute PrivateMessageDto pmDto, BindingResult result) { + public ModelAndView saveDraft(@Valid @ModelAttribute("privateMessageDto") PrivateMessageDraftDto pmDto, BindingResult result) { String targetView = "redirect:/drafts"; - if (result.hasErrors()) { - return PM_FORM; - } - + long pmDtoId = pmDto.getId(); JCUser userFrom = userService.getCurrentUser(); - try { - pmService.saveDraft(pmDto.getId(), pmDto.getRecipient(), pmDto.getTitle(), pmDto.getBody(), userFrom); - } catch (NotFoundException e) { - result.rejectValue("recipient", "validation.wrong_recipient"); - targetView = PM_FORM; + JCUser userTo = null; + if (pmDto.getRecipient() != null) { + try { + userTo = userService.getByUsername(pmDto.getRecipient()); + } catch (NotFoundException e) { + //Catch block is empty because we don't need any logic if recipient not found. We should leave it null + } } - - return targetView; + if (userTo == null && result.hasGlobalErrors()) { + // The case when field "To:" filled incorrectly and fields "Title:" and "Body" are both empty . + if (pmDtoId != 0) { //means that we try to edit existing draft + try { + pmService.delete(Arrays.asList(pmDtoId)); + } catch (NotFoundException e) { + // Catch block is empty because we don't need any additional logic in case if user removed + // draft in separate browser tab. We should just redirect him to list of drafts + } + } + return new ModelAndView(targetView); + } + if (result.hasFieldErrors()){ + return new ModelAndView(PM_FORM).addObject(DTO,pmDto); + } + pmService.saveDraft(pmDtoId, userTo, pmDto.getTitle(), pmDto.getBody(), userFrom); + return new ModelAndView(targetView); } /** diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ReadPostsController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ReadPostsController.java index ee14a1a840..90fc899ee1 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ReadPostsController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/ReadPostsController.java @@ -15,14 +15,21 @@ package org.jtalks.jcommune.web.controller; import org.jtalks.jcommune.model.entity.Branch; +import org.jtalks.jcommune.model.entity.Topic; import org.jtalks.jcommune.service.BranchService; import org.jtalks.jcommune.service.LastReadPostService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.jtalks.jcommune.service.TopicFetchService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; /** * Handles all "mark all read" requests that aren't related to the list @@ -35,8 +42,10 @@ */ @Controller public class ReadPostsController { - private BranchService branchService; - private LastReadPostService lastReadPostService; + private final BranchService branchService; + private final TopicFetchService topicService; + private final LastReadPostService lastReadPostService; + private final RetryTemplate retryTemplate; /** * Constructs an instance with required fields. @@ -45,9 +54,12 @@ public class ReadPostsController { * @param lastReadPostService to mark all forum as read for current user */ @Autowired - public ReadPostsController(BranchService branchService, LastReadPostService lastReadPostService) { + public ReadPostsController(BranchService branchService, LastReadPostService lastReadPostService, + RetryTemplate retryTemplate, TopicFetchService topicService) { this.branchService = branchService; this.lastReadPostService = lastReadPostService; + this.retryTemplate = retryTemplate; + this.topicService = topicService; } /** @@ -57,7 +69,13 @@ public ReadPostsController(BranchService branchService, LastReadPostService last */ @RequestMapping(value = "/recent/forum/markread", method = RequestMethod.GET) public String markAllForumAsReadFromRecentActivity() { - lastReadPostService.markAllForumAsReadForCurrentUser(); + retryTemplate.execute(new RetryCallback<Void, RuntimeException>() { + @Override + public Void doWithRetry(RetryContext context) throws HibernateOptimisticLockingFailureException { + lastReadPostService.markAllForumAsReadForCurrentUser(); + return null; + } + }); return "redirect:/topics/recent"; } @@ -86,4 +104,20 @@ public String markAllTopicsAsRead(@PathVariable long id) throws NotFoundExceptio lastReadPostService.markAllTopicsAsRead(branch); return "redirect:/branches/" + id; } + + /** + * Marks the specified page of the topic as read. + * + * @param topicId topic id to mark + * @param pageNum page number to mark + * @throws NotFoundException if the topic not found + */ + @ResponseBody + @RequestMapping("/topics/{topicId}/page/{pageNum}/markread") + public String markTopicPageAsReadById(@PathVariable long topicId, + @PathVariable int pageNum) throws NotFoundException { + Topic topic = topicService.get(topicId); + lastReadPostService.markTopicPageAsRead(topic, pageNum); + return ""; + } } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/SecurityController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/SecurityController.java index fc08013b21..f4a2fc9c6c 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/SecurityController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/SecurityController.java @@ -15,8 +15,8 @@ package org.jtalks.jcommune.web.controller; import org.jtalks.jcommune.service.security.PermissionService; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/TopicController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/TopicController.java index 0111856b34..d5dac6a7a5 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/TopicController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/TopicController.java @@ -14,26 +14,27 @@ */ package org.jtalks.jcommune.web.controller; +import org.apache.commons.lang3.ObjectUtils; import org.joda.time.DateTime; import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; -import org.jtalks.jcommune.service.nontransactional.LocationService; -import org.jtalks.jcommune.web.dto.EntityToDtoConverter; import org.jtalks.jcommune.plugin.api.web.dto.PostDto; import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; import org.jtalks.jcommune.service.*; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.jtalks.jcommune.service.nontransactional.LocationService; -import org.jtalks.jcommune.web.dto.EntityToDtoConverter; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; import org.jtalks.jcommune.web.validation.editors.DateTimeEditor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.data.domain.Page; -import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.support.RetryTemplate; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.session.SessionRegistry; import org.springframework.stereotype.Controller; @@ -44,7 +45,6 @@ import org.springframework.web.servlet.ModelAndView; import javax.validation.Valid; -import java.util.concurrent.TimeUnit; /** * Serves topic management web requests @@ -55,6 +55,7 @@ * @author Max Malakhov * @author Evgeniy Naumenko * @author Eugeny Batov + * @author Dmitry S. Dolzhenko * @see Topic */ @Controller @@ -70,17 +71,19 @@ public class TopicController { public static final String POST_DTO = "postDto"; private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); public static final String POLL = "poll"; + private static final String TOPIC_DRAFT = "topicDraft"; private TopicModificationService topicModificationService; private TopicFetchService topicFetchService; + private TopicDraftService topicDraftService; private PostService postService; private BranchService branchService; - private LastReadPostService lastReadPostService; private UserService userService; private BreadcrumbBuilder breadcrumbBuilder; private LocationService locationService; private SessionRegistry sessionRegistry; private EntityToDtoConverter converter; + private RetryTemplate retryTemplate; /** * This method turns the trim binder on. Trim binder @@ -103,7 +106,6 @@ public void initBinder(WebDataBinder binder) { * {@link org.jtalks.jcommune.model.entity.Post} entity * @param branchService the object which provides actions on * {@link org.jtalks.jcommune.model.entity.Branch} entity - * @param lastReadPostService to perform post-related actions * @param userService to determine the current user logged in * @param breadcrumbBuilder to create Breadcrumbs for pages * @param locationService to track user location on forum (what page he is viewing now) @@ -114,23 +116,25 @@ public void initBinder(WebDataBinder binder) { public TopicController(TopicModificationService topicModificationService, PostService postService, BranchService branchService, - LastReadPostService lastReadPostService, UserService userService, BreadcrumbBuilder breadcrumbBuilder, LocationService locationService, SessionRegistry sessionRegistry, TopicFetchService topicFetchService, - EntityToDtoConverter converter) { + TopicDraftService topicDraftService, + EntityToDtoConverter converter, + RetryTemplate retryTemplate) { this.topicModificationService = topicModificationService; this.postService = postService; this.branchService = branchService; - this.lastReadPostService = lastReadPostService; this.userService = userService; this.breadcrumbBuilder = breadcrumbBuilder; this.locationService = locationService; this.sessionRegistry = sessionRegistry; this.topicFetchService = topicFetchService; + this.topicDraftService = topicDraftService; this.converter = converter; + this.retryTemplate = retryTemplate; } /** @@ -142,13 +146,18 @@ public TopicController(TopicModificationService topicModificationService, */ @RequestMapping(value = "/topics/new", method = RequestMethod.GET) public ModelAndView showNewTopicPage(@RequestParam(BRANCH_ID) Long branchId) throws NotFoundException { + + TopicDraft topicDraft = ObjectUtils.defaultIfNull( + topicDraftService.getDraft(), new TopicDraft()); + TopicDto dto = new TopicDto(topicDraft); + Branch branch = branchService.get(branchId); - Topic topic = new Topic(); - topic.setBranch(branch); - topic.setPoll(new Poll()); - TopicDto dto = new TopicDto(topic); + dto.getTopic().setBranch(branch); + dto.getTopic().setType(TopicTypeName.DISCUSSION.getName()); + return new ModelAndView(TOPIC_VIEW) .addObject(TOPIC_DTO, dto) + .addObject(TOPIC_DRAFT, topicDraft) .addObject(BRANCH_ID, branchId) .addObject(SUBMIT_URL, "/topics/new?branchId=" + branchId) .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getNewTopicBreadcrumb(branch)); @@ -164,42 +173,67 @@ public ModelAndView showNewTopicPage(@RequestParam(BRANCH_ID) Long branchId) thr * @throws NotFoundException when branch not found */ @RequestMapping(value = "/topics/new", method = RequestMethod.POST) - public ModelAndView createTopic(@Valid @ModelAttribute TopicDto topicDto, + public ModelAndView createTopic(@Valid @ModelAttribute final TopicDto topicDto, BindingResult result, @RequestParam(BRANCH_ID) Long branchId) throws NotFoundException { + Branch branch = branchService.get(branchId); + topicDto.getTopic().setType(TopicTypeName.DISCUSSION.getName()); + if (result.hasErrors()) { + TopicDraft topicDraft = ObjectUtils.defaultIfNull( + topicDraftService.getDraft(), new TopicDraft()); + return new ModelAndView(TOPIC_VIEW) .addObject(BRANCH_ID, branchId) .addObject(TOPIC_DTO, topicDto) + .addObject(TOPIC_DRAFT, topicDraft) .addObject(SUBMIT_URL, "/topics/new?branchId=" + branchId) - .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(branch)); + .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getNewTopicBreadcrumb(branch)); } - Topic topic = topicDto.getTopic(); + + final Topic topic = topicDto.getTopic(); topic.setBranch(branch); - topic.setType(TopicTypeName.DISCUSSION.getName()); - Topic createdTopic = createTopicWithLockHandling(topic, topicDto); + Topic createdTopic = retryTemplate.execute(new RetryCallback<Topic, NotFoundException>() { + @Override + public Topic doWithRetry(RetryContext context) throws NotFoundException { + return topicModificationService.createTopic(topic, topicDto.getBodyText()); + } + }); + return new ModelAndView(REDIRECT_URL + createdTopic.getId()); } - private Topic createTopicWithLockHandling(Topic topic, TopicDto topicDto) throws NotFoundException { - for (int i = 0; i < UserController.LOGIN_TRIES_AFTER_LOCK; i++) { - try { - return topicModificationService.createTopic(topic, topicDto.getBodyText()); - } catch (HibernateOptimisticLockingFailureException lockingFailureException) { - try { - TimeUnit.MILLISECONDS.sleep(UserController.SLEEP_MILLISECONDS_AFTER_LOCK); - } catch (InterruptedException e) { - } - } - } - try { - return topicModificationService.createTopic(topic, topicDto.getBodyText()); - } catch (HibernateOptimisticLockingFailureException e) { - LOGGER.error("User has been optimistically locked and can't be reread {} times. Username: {}", - UserController.LOGIN_TRIES_AFTER_LOCK, userService.getCurrentUser().getUsername()); - throw e; + /** + * Saves new draft or update if it already exists + * + * @param topicDraft draft topic + * @param result validation result + * @return response in JSON format + */ + @RequestMapping(value = "/topics/draft", method = RequestMethod.POST) + @ResponseBody + public JsonResponse saveDraft(@Valid @RequestBody TopicDraft topicDraft, + BindingResult result) throws NotFoundException { + if (result.hasErrors()) { + return new JsonResponse(JsonResponseStatus.FAIL); } + + topicDraft = topicDraftService.saveOrUpdateDraft(topicDraft); + + return new JsonResponse(JsonResponseStatus.SUCCESS, topicDraft.getId()); + } + + /** + * Deletes a draft topic of the current user if it exists + * + * @return response in JSON format + */ + @RequestMapping(value = "/topics/draft", method = RequestMethod.DELETE) + @ResponseBody + public JsonResponse deleteDraft() { + topicDraftService.deleteDraft(); + return new JsonResponse(JsonResponseStatus.SUCCESS); } /** @@ -237,16 +271,20 @@ public ModelAndView showTopicPage(WebRequest request, @PathVariable(TOPIC_ID) Lo if (request.checkNotModified(topic.getLastModificationPostDate().getMillis())) { return null; } - - lastReadPostService.markTopicPageAsRead(topic, postsPage.getNumber()); + PostDto postDto = new PostDto(); + PostDraft draft = topic.getDraftForUser(currentUser); + if (draft != null) { + postDto = PostDto.getDtoFor(draft); + } return new ModelAndView("topic/postList") .addObject("viewList", locationService.getUsersViewing(topic)) .addObject("usersOnline", sessionRegistry.getAllPrincipals()) .addObject("postsPage", postsPage) .addObject("topic", topic) - .addObject(POST_DTO, new PostDto()) + .addObject(POST_DTO, postDto) .addObject("subscribed", topic.getSubscribers().contains(currentUser)) - .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)); + .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb(topic)) + .addObject("markAsReadLink", topic.getMarkAsReadUrl(currentUser, page).orNull()); } /** diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/TopicSearchController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/TopicSearchController.java index 2e5ea1f258..121eee2b7b 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/TopicSearchController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/TopicSearchController.java @@ -17,7 +17,7 @@ import org.jtalks.jcommune.model.entity.Topic; import org.jtalks.jcommune.service.LastReadPostService; import org.jtalks.jcommune.service.TopicFetchService; -import org.jtalks.jcommune.web.dto.EntityToDtoConverter; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.http.HttpStatus; @@ -93,7 +93,7 @@ public ModelAndView initSearch(@RequestParam(value = "text", defaultValue = "", HashMap<String, Object> urlParams = new HashMap<>(); urlParams.put("text", searchText); return new ModelAndView(SEARCH_RESULT_VIEW_NAME). - addObject(SEARCH_RESULT_ATTRIBUTE_NAME, converter.convertToDtoPage(searchResultPage)). + addObject(SEARCH_RESULT_ATTRIBUTE_NAME, converter.convertTopicPageToTopicDtoPage(searchResultPage)). addObject(SEARCH_TEXT_ATTRIBUTE_NAME, searchText). addObject("urlParams", urlParams); } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/UserController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/UserController.java index a6fd22e282..b73ae269e1 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/UserController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/UserController.java @@ -15,24 +15,28 @@ package org.jtalks.jcommune.web.controller; import com.google.common.collect.ImmutableMap; +import org.apache.commons.lang.StringUtils; +import org.jtalks.common.model.entity.Group; +import org.jtalks.jcommune.model.dto.LoginUserDto; import org.jtalks.jcommune.model.dto.RegisterUserDto; +import org.jtalks.jcommune.model.dto.UserDto; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.Language; import org.jtalks.jcommune.plugin.api.core.ExtendedPlugin; import org.jtalks.jcommune.plugin.api.core.Plugin; import org.jtalks.jcommune.plugin.api.core.RegistrationPlugin; import org.jtalks.jcommune.plugin.api.exceptions.NoConnectionException; +import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.exceptions.UnexpectedErrorException; -import org.jtalks.jcommune.service.Authenticator; -import org.jtalks.jcommune.service.PluginService; -import org.jtalks.jcommune.service.UserService; +import org.jtalks.jcommune.plugin.api.filters.TypeFilter; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.service.*; import org.jtalks.jcommune.service.exceptions.MailingFailedException; -import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.exceptions.UserTriesActivatingAccountAgainException; -import org.jtalks.jcommune.plugin.api.filters.TypeFilter; +import org.jtalks.jcommune.service.nontransactional.MailService; +import org.jtalks.jcommune.service.util.AuthenticationStatus; import org.jtalks.jcommune.web.dto.RestorePasswordDto; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; import org.jtalks.jcommune.web.interceptors.RefererKeepInterceptor; import org.jtalks.jcommune.web.util.MutableHttpRequest; import org.jtalks.jcommune.web.validation.editors.DefaultStringEditor; @@ -40,9 +44,11 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.propertyeditors.StringTrimmerEditor; -import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException; -import org.springframework.security.web.WebAttributes; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.support.RetryTemplate; import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; +import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; @@ -58,10 +64,9 @@ import javax.servlet.http.HttpSession; import javax.validation.Valid; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; -import org.jtalks.jcommune.model.dto.LoginUserDto; - /** @@ -85,38 +90,57 @@ public class UserController { public static final String REG_SERVICE_CONNECTION_ERROR_URL = "redirect:/user/new?reg_error=1"; public static final String REG_SERVICE_UNEXPECTED_ERROR_URL = "redirect:/user/new?reg_error=2"; public static final String REG_SERVICE_HONEYPOT_FILLED_ERROR_URL = "redirect:/user/new?reg_error=3"; + public static final String REG_SERVICE_SPAM_PROTECTION_ERROR_URL = "redirect:/user/new?reg_error=4"; + public static final String USER_SEARCH = "userSearch"; public static final String NULL_REPRESENTATION = "null"; public static final String MAIN_PAGE_REFERER = "/"; public static final String CUSTOM_ERROR = "customError"; public static final String CONNECTION_ERROR = "connectionError"; public static final String UNEXPECTED_ERROR = "unexpectedError"; public static final String HONEYPOT_CAPTCHA_ERROR = "honeypotCaptchaNotNull"; + public static final String SPAM_PROTECTION_ERROR = "spamProtectionError"; public static final String LOGIN_DTO = "loginUserDto"; - public static final int LOGIN_TRIES_AFTER_LOCK = 3; - public static final int SLEEP_MILLISECONDS_AFTER_LOCK = 500; + public static final String USERS_ATTR_NAME ="users"; + public static final String GROUPS_ATTR_NAME ="groups"; protected static final String ATTR_USERNAME = "username"; - protected static final String ATTR_LOGIN_ERROR = "login_error"; - public static final String HONEYPOT_FIELD = "honeypotCaptcha"; private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); private static final String REMEMBER_ME_ON = "on"; private final UserService userService; private final Authenticator authenticator; private final PluginService pluginService; private final UserService plainPasswordUserService; + private final MailService mailService; + private final RetryTemplate retryTemplate; + private final ComponentService componentService; + private final GroupService groupService; + private final SpamProtectionService spamProtectionService; + private final RequestCache requestCache; /** * @param userService to delegate business logic invocation * @param authenticator default authenticator * @param pluginService for communication with available registration or authentication plugins * @param plainPasswordUserService strategy for authenticating by password without hashing + * @param mailService to send account confirmation + * @param componentService to check component permissions + * @param spamProtectionService to check is email in blacklist */ @Autowired public UserController(UserService userService, Authenticator authenticator, PluginService pluginService, - UserService plainPasswordUserService) { + UserService plainPasswordUserService, MailService mailService, + RetryTemplate retryTemplate, ComponentService componentService, + GroupService groupService, SpamProtectionService spamProtectionService, + RequestCache requestCache) { this.userService = userService; this.authenticator = authenticator; this.pluginService = pluginService; this.plainPasswordUserService = plainPasswordUserService; + this.mailService = mailService; + this.retryTemplate = retryTemplate; + this.componentService = componentService; + this.groupService = groupService; + this.spamProtectionService = spamProtectionService; + this.requestCache = requestCache; } /** @@ -175,6 +199,8 @@ public ModelAndView restorePassword(@Valid @ModelAttribute("dto") RestorePasswor /** * Render registration page with bind objects to form. + * Also checks if user is already logged in. + * If so he is redirected to main page. * * @param request Servlet request. * @param locale To set currently selected language as user's default @@ -183,10 +209,15 @@ public ModelAndView restorePassword(@Valid @ModelAttribute("dto") RestorePasswor */ @RequestMapping(value = "/user/new", method = RequestMethod.GET) public ModelAndView registrationPage(HttpServletRequest request, Locale locale) { - Map<String, String> registrationPlugins = getRegistrationPluginsHtml(request, locale); - return new ModelAndView(REGISTRATION) - .addObject("newUser", new RegisterUserDto()) - .addObject("registrationPlugins", registrationPlugins); + JCUser currentUser = userService.getCurrentUser(); + if (currentUser.isAnonymous()) { + Map<String, String> registrationPlugins = getRegistrationPluginsHtml(request, locale); + return new ModelAndView(REGISTRATION) + .addObject("newUser", new RegisterUserDto()) + .addObject("registrationPlugins", registrationPlugins); + } else { + return new ModelAndView("redirect:" + MAIN_PAGE_REFERER); + } } /** @@ -206,10 +237,15 @@ public ModelAndView registerUser(@ModelAttribute("newUser") RegisterUserDto regi if (isHoneypotCaptchaFilled(registerUserDto, getClientIpAddress(request))) { return new ModelAndView(REG_SERVICE_HONEYPOT_FILLED_ERROR_URL); } + UserDto userDto = registerUserDto.getUserDto(); + if (spamProtectionService.isEmailInBlackList(userDto.getEmail())) { + logBotInfo(userDto, request); + return new ModelAndView(REG_SERVICE_SPAM_PROTECTION_ERROR_URL); + } Map<String, String> registrationPlugins = getRegistrationPluginsHtml(request, locale); BindingResult errors; try { - registerUserDto.getUserDto().setLanguage(Language.byLocale(locale)); + userDto.setLanguage(Language.byLocale(locale)); errors = authenticator.register(registerUserDto); } catch (NoConnectionException e) { return new ModelAndView(REG_SERVICE_CONNECTION_ERROR_URL); @@ -240,11 +276,16 @@ public JsonResponse registerUserAjax(@ModelAttribute("newUser") RegisterUserDto HttpServletRequest request, Locale locale) { if (isHoneypotCaptchaFilled(registerUserDto, getClientIpAddress(request))) { - return getCustomErrorJsonResponse(HONEYPOT_CAPTCHA_ERROR); + return getCustomErrorJsonResponse(HONEYPOT_CAPTCHA_ERROR); + } + UserDto userDto = registerUserDto.getUserDto(); + if (spamProtectionService.isEmailInBlackList(userDto.getEmail())) { + logBotInfo(userDto, request); + return getCustomErrorJsonResponse(SPAM_PROTECTION_ERROR); } BindingResult errors; try { - registerUserDto.getUserDto().setLanguage(Language.byLocale(locale)); + userDto.setLanguage(Language.byLocale(locale)); errors = authenticator.register(registerUserDto); } catch (NoConnectionException e) { return getCustomErrorJsonResponse(CONNECTION_ERROR); @@ -257,6 +298,12 @@ public JsonResponse registerUserAjax(@ModelAttribute("newUser") RegisterUserDto return new JsonResponse(JsonResponseStatus.SUCCESS); } + private void logBotInfo(UserDto userDto, HttpServletRequest request) { + LOGGER.warn("Spam protection alert! Bot tries to register. Username - [{}], email - [{}], ip - [{}]", + new String[]{userDto.getUsername(), + userDto.getEmail(), getClientIpAddress(request)}); + } + /** * Detects the presence honeypot captcha filing error. * If honeypot captcha filled it means that bot try to register. . @@ -264,7 +311,7 @@ public JsonResponse registerUserAjax(@ModelAttribute("newUser") RegisterUserDto */ private boolean isHoneypotCaptchaFilled(RegisterUserDto registerUserDto, String ip) { if (registerUserDto.getHoneypotCaptcha() != null) { - LOGGER.debug("Bot tried to register. Username - {}, email - {}, ip - {}", + LOGGER.warn("Bot tried to register. Username - {}, email - {}, ip - {}", new String[]{registerUserDto.getUserDto().getUsername(), registerUserDto.getUserDto().getEmail(),ip}); return true; @@ -328,14 +375,14 @@ public void pluginAction(@PathVariable String pluginId, @PathVariable String act */ @RequestMapping(value = "user/activate/{uuid}") public String activateAccount(@PathVariable String uuid, HttpServletRequest request, HttpServletResponse response) - throws UnexpectedErrorException, NoConnectionException { + throws Exception { try { - userService.activateAccount(uuid); JCUser user = userService.getByUuid(uuid); + authenticator.activateAccount(user.getUuid()); MutableHttpRequest wrappedRequest = new MutableHttpRequest(request); wrappedRequest.addParameter(AbstractRememberMeServices.DEFAULT_PARAMETER, "true"); LoginUserDto loginUserDto = new LoginUserDto(user.getUsername(), user.getPassword(), true, getClientIpAddress(request)); - loginWithLockHandling(loginUserDto, wrappedRequest, response, plainPasswordUserService); + retryTemplate.execute(new LoginRetryCallback(loginUserDto, request, response, plainPasswordUserService)); return "redirect:/"; } catch (NotFoundException e) { return "errors/activationExpired"; @@ -346,16 +393,15 @@ public String activateAccount(@PathVariable String uuid, HttpServletRequest requ /** * Shows login page. Also checks if user is already logged in. - * If so he is redirected to main page. + * If so he is redirected to referer page. * * @param request Current servlet request * @return login view name or redirect to main page */ @RequestMapping(value = "/login", method = RequestMethod.GET) - public ModelAndView loginPage(HttpServletRequest request) { + public ModelAndView loginPage(HttpServletRequest request, HttpServletResponse response) { JCUser currentUser = userService.getCurrentUser(); - - String referer = getReferer(request); + String referer = getReferer(request, response); if (currentUser.isAnonymous()) { ModelAndView mav = new ModelAndView(LOGIN); mav.addObject(REFERER_ATTR, referer); @@ -372,11 +418,11 @@ public ModelAndView loginPage(HttpServletRequest request) { * most cases when user browses our forum we put the referer on our own - the page user previously was at. This is * done so that we can sign in and sign out user and redirect him back to original page. */ - private String getReferer(HttpServletRequest request) { + private String getReferer(HttpServletRequest request, HttpServletResponse response) { String referer = request.getHeader("referer"); + SavedRequest savedRequest = requestCache.getRequest(request, response); HttpSession session = request.getSession(false); if (session != null) { - SavedRequest savedRequest = (SavedRequest) session.getAttribute(WebAttributes.SAVED_REQUEST); if (savedRequest != null) { referer = savedRequest.getRedirectUrl(); } else { @@ -405,22 +451,30 @@ public JsonResponse loginAjax(@RequestParam("userName") String username, @RequestParam("password") String password, @RequestParam(value = "_spring_security_remember_me", defaultValue = "off") String rememberMe, - HttpServletRequest request, HttpServletResponse response) { + HttpServletRequest request, HttpServletResponse response) throws Exception { LoginUserDto loginUserDto = new LoginUserDto(username, password, rememberMe.equals(REMEMBER_ME_ON), getClientIpAddress(request)); - boolean isAuthenticated; + AuthenticationStatus authenticationStatus; try { - isAuthenticated = loginWithLockHandling(loginUserDto, request, response, - userService); + authenticationStatus = retryTemplate.execute(new LoginRetryCallback(loginUserDto, request, response, + userService)); } catch (NoConnectionException e) { return getCustomErrorJsonResponse("connectionError"); } catch (UnexpectedErrorException e) { return getCustomErrorJsonResponse("unexpectedError"); } - if (isAuthenticated) { + if (authenticationStatus.equals(AuthenticationStatus.AUTHENTICATED)) { LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request); localeResolver.setLocale(request, response, userService.getCurrentUser().getLanguage().getLocale()); return new JsonResponse(JsonResponseStatus.SUCCESS); + } else if(authenticationStatus.equals(AuthenticationStatus.NOT_ENABLED)){ + JCUser user; + try { + user = userService.getByUsername(username); + } catch (NotFoundException e) { + return getCustomErrorJsonResponse("unexpectedError"); + } + return new JsonResponse(JsonResponseStatus.FAIL, user.getId()); } else { return new JsonResponse(JsonResponseStatus.FAIL); } @@ -439,7 +493,7 @@ public ModelAndView login(@ModelAttribute(LOGIN_DTO) LoginUserDto loginUserDto, @RequestParam(REFERER_ATTR) String referer, @RequestParam(value = "_spring_security_remember_me", defaultValue = "off") String rememberMe, - HttpServletRequest request, HttpServletResponse response) { + HttpServletRequest request, HttpServletResponse response) throws Exception { boolean isAuthenticated; loginUserDto.setRememberMe(rememberMe.equals(REMEMBER_ME_ON)); loginUserDto.setClientIp(getClientIpAddress(request)); @@ -447,8 +501,8 @@ public ModelAndView login(@ModelAttribute(LOGIN_DTO) LoginUserDto loginUserDto, referer = MAIN_PAGE_REFERER; } try { - isAuthenticated = loginWithLockHandling(loginUserDto, request, response, - userService); + isAuthenticated = retryTemplate.execute(new LoginRetryCallback(loginUserDto, request, response, + userService)).equals(AuthenticationStatus.AUTHENTICATED); } catch (NoConnectionException e) { return new ModelAndView(AUTH_SERVICE_FAIL_URL); } catch (UnexpectedErrorException e) { @@ -466,25 +520,6 @@ public ModelAndView login(@ModelAttribute(LOGIN_DTO) LoginUserDto loginUserDto, } } - private boolean loginWithLockHandling(LoginUserDto loginUserDto, HttpServletRequest request, - HttpServletResponse response, UserService userService) - throws UnexpectedErrorException, NoConnectionException { - for (int i = 0; i < LOGIN_TRIES_AFTER_LOCK; i++) { - try { - return userService.loginUser(loginUserDto, request, response); - } catch (HibernateOptimisticLockingFailureException e) { - //we don't handle the exception for several times, just re-reading the content and trying again - //after the max times exceeds, only then we give up. - } - } - try { - return userService.loginUser(loginUserDto, request, response); - } catch (HibernateOptimisticLockingFailureException e) { - LOGGER.error("User have been locked {} times. Username: {}", LOGIN_TRIES_AFTER_LOCK, loginUserDto.getUserName()); - throw e; - } - } - /** * Get usernames by pattern * @@ -505,5 +540,92 @@ private String getClientIpAddress(HttpServletRequest request) { return ipAddress; } + @RequestMapping(value = "/confirm", method=RequestMethod.GET) + @ResponseBody + public JsonResponse sendEmailConfirmation(@RequestParam("id") long id){ + try { + JCUser recipient = userService.get(id); + mailService.sendAccountActivationMail(recipient); + } catch (Exception e) { + return new JsonResponse(JsonResponseStatus.FAIL); + } + return new JsonResponse(JsonResponseStatus.SUCCESS); + } + + @ResponseBody + @RequestMapping(value = "/user/{userID}/groups", method=RequestMethod.GET) + public JsonResponse userGroups(@PathVariable("userID") long userID){ + try { + long forumId = componentService.getComponentOfForum().getId(); + List<Long> groupsIDs = userService.getUserGroupIDs(forumId, userID); + + return new JsonResponse(JsonResponseStatus.SUCCESS, groupsIDs); + } catch (Exception e) { + return new JsonResponse(JsonResponseStatus.FAIL); + } + } + + @ResponseBody + @RequestMapping(value = "/user/{userID}/groups/{groupID}", method=RequestMethod.POST) + public JsonResponse addUserToGroup(@PathVariable long userID, @PathVariable long groupID){ + try { + long forumId = componentService.getComponentOfForum().getId(); + userService.addUserToGroup(forumId, userID, groupID); + return new JsonResponse(JsonResponseStatus.SUCCESS); + } catch (Exception e) { + return new JsonResponse(JsonResponseStatus.FAIL); + } + } + + @ResponseBody + @RequestMapping(value = "/user/{userID}/groups/{groupID}", method=RequestMethod.DELETE) + public JsonResponse deleteUserFromGroup(@PathVariable long userID, @PathVariable long groupID){ + try { + long forumId = componentService.getComponentOfForum().getId(); + userService.deleteUserFromGroup(forumId, userID, groupID); + + return new JsonResponse(JsonResponseStatus.SUCCESS); + } catch (Exception e) { + return new JsonResponse(JsonResponseStatus.FAIL); + } + } + + @RequestMapping(value = "/users/list", method = RequestMethod.GET) + public ModelAndView searchUsers(@RequestParam(required = false) String searchKey) { + ModelAndView mav = new ModelAndView(USER_SEARCH); + long forumId = componentService.getComponentOfForum().getId(); + if (StringUtils.isBlank(searchKey)) { + componentService.checkPermissionsForComponent(forumId); + } else { + List<JCUser> users = userService.findByUsernameOrEmail(forumId, searchKey.trim()); + mav.addObject(USERS_ATTR_NAME, users); + + List<Group> groups = groupService.getAll(); + mav.addObject(GROUPS_ATTR_NAME, groups); + } + return mav; + } + + private class LoginRetryCallback implements RetryCallback<AuthenticationStatus, Exception> { + + private LoginUserDto loginUserDto; + private HttpServletRequest request; + private HttpServletResponse response; + private UserService userService; + + private LoginRetryCallback(LoginUserDto loginUserDto, HttpServletRequest request, + HttpServletResponse response, UserService userService) { + this.loginUserDto = loginUserDto; + this.request = request; + this.response = response; + this.userService = userService; + } + + @Override + public AuthenticationStatus doWithRetry(RetryContext context) throws UnexpectedErrorException, NoConnectionException { + return userService.loginUser(loginUserDto, request, response); + + } + } } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/UserProfileController.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/UserProfileController.java index f45f41953d..f04a0e8d29 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/UserProfileController.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/UserProfileController.java @@ -21,6 +21,7 @@ import org.jtalks.jcommune.service.UserContactsService; import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.jtalks.jcommune.service.nontransactional.ImageConverter; import org.jtalks.jcommune.service.nontransactional.ImageService; import org.jtalks.jcommune.web.dto.*; @@ -34,7 +35,9 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.data.domain.Page; -import org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; @@ -86,6 +89,8 @@ public class UserProfileController { private ImageConverter imageConverter; private PostService postService; private UserContactsService contactsService; + private EntityToDtoConverter converter; + private RetryTemplate retryTemplate; /** * This method turns the trim binder on. Trim binder @@ -123,13 +128,17 @@ public UserProfileController(UserService userService, ImageConverter imageConverter, PostService postService, UserContactsService contactsService, - @Qualifier("avatarService") ImageService imageService) { + @Qualifier("avatarService") ImageService imageService, + EntityToDtoConverter converter, + RetryTemplate retryTemplate) { this.userService = userService; this.breadcrumbBuilder = breadcrumbBuilder; this.imageConverter = imageConverter; this.postService = postService; this.contactsService = contactsService; this.imageService = imageService; + this.converter = converter; + this.retryTemplate = retryTemplate; } /** @@ -258,7 +267,7 @@ public ModelAndView saveEditedProfile(@Valid @ModelAttribute(EDITED_USER) EditUs } long editedUserId = editedProfileDto.getUserProfileDto().getUserId(); checkPermissionsToEditProfile(editedUserId); - JCUser user = saveEditedProfileWithLockHandling(editedUserId, editedProfileDto, PROFILE); + JCUser user = retryTemplate.execute(new SaveProfileRetryCallback(editedUserId, editedProfileDto, PROFILE)); //redirect to the view profile page return new ModelAndView("redirect:/users/" + user.getId() + "/" + PROFILE); } @@ -283,7 +292,7 @@ public ModelAndView saveEditedNotifications(@Valid @ModelAttribute(EDITED_USER) } long editedUserId = editedProfileDto.getUserNotificationsDto().getUserId(); checkPermissionForEditNotificationsOrSecurity(editedUserId); - JCUser user = saveEditedProfileWithLockHandling(editedUserId, editedProfileDto, NOTIFICATIONS); + JCUser user = retryTemplate.execute(new SaveProfileRetryCallback(editedUserId, editedProfileDto, NOTIFICATIONS)); //redirect to the view profile page return new ModelAndView("redirect:/users/" + user.getId() + "/" + NOTIFICATIONS); } @@ -309,7 +318,7 @@ public ModelAndView saveEditedSecurity(@Valid @ModelAttribute(EDITED_USER) EditU } long editedUserId = editedProfileDto.getUserSecurityDto().getUserId(); checkPermissionForEditNotificationsOrSecurity(editedUserId); - JCUser user = saveEditedProfileWithLockHandling(editedUserId, editedProfileDto, SECURITY); + JCUser user = retryTemplate.execute(new SaveProfileRetryCallback(editedUserId, editedProfileDto, SECURITY)); if (editedProfileDto.getUserSecurityDto().getNewUserPassword() != null) { redirectAttributes.addFlashAttribute(IS_PASSWORD_CHANGED_ATTRIB, true); } @@ -338,7 +347,7 @@ public ModelAndView saveEditedContacts(@Valid @ModelAttribute(EDITED_USER) EditU } long editedUserId = editedProfileDto.getUserId(); checkPermissionsToEditProfile(editedUserId); - JCUser user = saveEditedProfileWithLockHandling(editedUserId, editedProfileDto, CONTACTS); + JCUser user = retryTemplate.execute(new SaveProfileRetryCallback(editedUserId, editedProfileDto, CONTACTS)); //redirect to the view profile page return new ModelAndView("redirect:/users/" + user.getId() + "/" + CONTACTS); } @@ -391,40 +400,29 @@ public ModelAndView showUserPostList(@PathVariable Long id, Page<Post> postsPage = postService.getPostsOfUser(user, page); return new ModelAndView("userPostList") .addObject("user", user) - .addObject("postsPage", postsPage) + .addObject("postsPage", converter.convertPostPageToPostDtoPage(postsPage)) .addObject(BREADCRUMB_LIST, breadcrumbBuilder.getForumBreadcrumb()); } @RequestMapping(value = "**/language", method = RequestMethod.GET) public String saveUserLanguage(@RequestParam(value = "lang", defaultValue = "en") String lang, HttpServletResponse response, HttpServletRequest request) throws ServletException { - JCUser jcuser = userService.getCurrentUser(); - Language languageFromRequest = Language.byLocale(new Locale(lang)); + final JCUser jcuser = userService.getCurrentUser(); + final Language languageFromRequest = Language.byLocale(new Locale(lang)); if (!jcuser.isAnonymous()) { - changeLanguageWithLockHandling(jcuser, languageFromRequest); + retryTemplate.execute(new RetryCallback<Void, RuntimeException>() { + @Override + public Void doWithRetry(RetryContext context) throws RuntimeException { + userService.changeLanguage(jcuser, languageFromRequest); + return null; + } + }); } LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request); localeResolver.setLocale(request, response, languageFromRequest.getLocale()); return "redirect:" + request.getHeader("Referer"); } - private void changeLanguageWithLockHandling(JCUser user, Language language) { - for (int i = 0; i < UserController.LOGIN_TRIES_AFTER_LOCK; i++) { - try { - userService.changeLanguage(user, language); - return; - } catch (HibernateOptimisticLockingFailureException ignored) { - } - } - try { - userService.changeLanguage(user, language); - } catch (HibernateOptimisticLockingFailureException e) { - LOGGER.error("User has been optimistically locked and can't be reread {} times. Username: {}", - UserController.LOGIN_TRIES_AFTER_LOCK, user.getUsername()); - throw e; - } - } - /** * Save user profile settings depending on settings type. * @@ -449,21 +447,21 @@ private JCUser saveUserData(long userId, EditUserProfileDto userProfileDto, Stri } } - private JCUser saveEditedProfileWithLockHandling(long editedUserId, EditUserProfileDto editedProfileDto, - String settingsType) - throws NotFoundException { - for (int i = 0; i < UserController.LOGIN_TRIES_AFTER_LOCK; i++) { - try { - return saveUserData(editedUserId, editedProfileDto, settingsType); - } catch (HibernateOptimisticLockingFailureException ignored) { - } + private class SaveProfileRetryCallback implements RetryCallback<JCUser, NotFoundException> { + + private long editedUserId; + private EditUserProfileDto editedProfileDto; + private String settingsType; + + public SaveProfileRetryCallback(long editedUserId, EditUserProfileDto editedProfileDto, String settingsType) { + this.editedUserId = editedUserId; + this.editedProfileDto = editedProfileDto; + this.settingsType = settingsType; } - try { + + @Override + public JCUser doWithRetry(RetryContext context) throws NotFoundException { return saveUserData(editedUserId, editedProfileDto, settingsType); - } catch (HibernateOptimisticLockingFailureException e) { - LOGGER.error("User has been optimistically locked and can't be reread {} times. Username: {}", - UserController.LOGIN_TRIES_AFTER_LOCK, editedProfileDto.getUsername()); - throw e; } } } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/api/SpamRulesApi.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/api/SpamRulesApi.java new file mode 100644 index 0000000000..2bc51b0c24 --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/controller/api/SpamRulesApi.java @@ -0,0 +1,133 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.web.controller.api; + +import org.jtalks.common.validation.ValidationError; +import org.jtalks.common.validation.ValidationException; +import org.jtalks.jcommune.model.dto.SpamRuleDto; +import org.jtalks.jcommune.model.entity.SpamRule; +import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.service.ComponentService; +import org.jtalks.jcommune.service.SpamProtectionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.validation.Validator; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Oleg Tkachenko + */ +@Controller +@RequestMapping(value = "/api/spam-rules") +public class SpamRulesApi { + + private final ComponentService componentService; + private final SpamProtectionService spamProtectionService; + private final Validator validator; + + @Autowired + public SpamRulesApi(ComponentService componentService, SpamProtectionService spamProtectionService, Validator validator) { + this.componentService = componentService; + this.spamProtectionService = spamProtectionService; + this.validator = validator; + } + + @ResponseBody @RequestMapping(method = RequestMethod.GET) + public JsonResponse getAll(){ + checkForAdminPermissions(); + List<SpamRuleDto> ruleDtos = SpamRuleDto.fromEntities(spamProtectionService.getAllRules()); + return new JsonResponse(JsonResponseStatus.SUCCESS, ruleDtos); + } + + @ResponseBody @RequestMapping(method = RequestMethod.POST) + public JsonResponse add(@Valid @RequestBody SpamRuleDto ruleDto, BindingResult result) throws org.jtalks.common.service.exceptions.NotFoundException, InterruptedException { + checkForAdminPermissions(); + trimAndValidate(ruleDto, result); + if (result.hasFieldErrors() || result.hasGlobalErrors()) { + return new JsonResponse(JsonResponseStatus.FAIL, result.getAllErrors()); + } + SpamRule spamRule = ruleDto.toEntity(); + return saveOrUpdateSpamRule(result, spamRule); + } + + @ResponseBody @RequestMapping(value = "/{ruleId}", method = RequestMethod.GET) + public JsonResponse get(@PathVariable("ruleId") long ruleId){ + checkForAdminPermissions(); + SpamRuleDto spamRule = null; + try { + spamRule = SpamRuleDto.fromEntity(spamProtectionService.get(ruleId)); + } catch (NotFoundException e) { + return new JsonResponse(JsonResponseStatus.FAIL, e.getMessage()); + } + return new JsonResponse(JsonResponseStatus.SUCCESS, spamRule); + } + + @ResponseBody @RequestMapping(value = "/{ruleId}", method = RequestMethod.PUT) + public JsonResponse edit(@Valid @RequestBody SpamRuleDto ruleDto, BindingResult result, @PathVariable("ruleId") long ruleId) { + checkForAdminPermissions(); + trimAndValidate(ruleDto, result); + if (result.hasFieldErrors() || result.hasGlobalErrors()) { + return new JsonResponse(JsonResponseStatus.FAIL, result.getAllErrors()); + } + SpamRule spamRule = ruleDto.toEntity(); + spamRule.setId(ruleId); + return saveOrUpdateSpamRule(result, spamRule); + } + + @ResponseBody @RequestMapping(value = "/{ruleId}", method = RequestMethod.DELETE) + public JsonResponse delete(@PathVariable("ruleId") long ruleId) { + checkForAdminPermissions(); + spamProtectionService.deleteRule(ruleId); + return new JsonResponse(JsonResponseStatus.SUCCESS); + } + private JsonResponse saveOrUpdateSpamRule(BindingResult result, SpamRule spamRule) { + try { + spamProtectionService.saveOrUpdate(spamRule); + } catch (org.jtalks.common.service.exceptions.NotFoundException e) { + return new JsonResponse(JsonResponseStatus.FAIL, e.getMessage()); + } catch (ValidationException ex) { + ArrayList<ObjectError> errors = new ArrayList<>(); + for (ValidationError validationError : ex.getErrors()) { + errors.add(new FieldError(result.getObjectName(), validationError.getFieldName(), validationError.getErrorMessageCode())); + } + return new JsonResponse(JsonResponseStatus.FAIL, errors); + } + return new JsonResponse(JsonResponseStatus.SUCCESS, SpamRuleDto.fromEntity(spamRule)); + } + + private void trimAndValidate(SpamRuleDto ruleDto, BindingResult result) { + if (result.hasFieldErrors() || result.hasGlobalErrors()) return; + ruleDto.setRegex(ruleDto.getRegex().trim()).setDescription(ruleDto.getDescription().trim()); + validator.validate(ruleDto, result); + } + /** + * Check if currently logged user has permissions for administrative + * functions for forum + */ + private void checkForAdminPermissions() { + long forumId = componentService.getComponentOfForum().getId(); + componentService.checkPermissionsForComponent(forumId); + } +} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/CodeReviewCommentDto.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/CodeReviewCommentDto.java index d634b560f9..09f42ef14c 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/CodeReviewCommentDto.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/CodeReviewCommentDto.java @@ -14,10 +14,9 @@ */ package org.jtalks.jcommune.web.dto; -import javax.validation.constraints.Size; - -import org.hibernate.validator.constraints.NotBlank; +import org.joda.time.DateTime; import org.jtalks.jcommune.model.entity.PostComment; +import org.jtalks.jcommune.model.validation.annotations.NotBlankSized; import java.util.HashMap; import java.util.Map; @@ -32,16 +31,14 @@ public class CodeReviewCommentDto { public static final String LINE_NUMBER_PROPERTY_NAME = "line_number"; private long id; - private int lineNumber; - - @NotBlank - @Size(min = PostComment.BODY_MIN_LENGTH, max = PostComment.BODY_MAX_LENGTH) + @NotBlankSized(min = PostComment.BODY_MIN_LENGTH, max = PostComment.BODY_MAX_LENGTH) private String body; - private long authorId; - private String authorUsername; + private long editorId; + private String editorUsername; + private DateTime modificationDate; public CodeReviewCommentDto() { } @@ -59,6 +56,11 @@ public CodeReviewCommentDto(PostComment comment) { this.body = comment.getBody(); this.authorId = comment.getAuthor().getId(); this.authorUsername = comment.getAuthor().getUsername(); + if (comment.getUserChanged() != null) { + this.editorId = comment.getUserChanged().getId(); + this.editorUsername = comment.getUserChanged().getUsername(); + } + this.modificationDate = comment.getModificationDate(); } /** @@ -131,6 +133,30 @@ public void setAuthorUsername(String authorUsername) { this.authorUsername = authorUsername; } + public long getEditorId() { + return editorId; + } + + public void setEditorId(long editorId) { + this.editorId = editorId; + } + + public String getEditorUsername() { + return editorUsername; + } + + public void setEditorUsername(String editorUsername) { + this.editorUsername = editorUsername; + } + + public DateTime getModificationDate() { + return modificationDate; + } + + public void setModificationDate(DateTime modificationDate) { + this.modificationDate = modificationDate; + } + /** * Gets list of attributes for comment. In this case contains only line_number property * @@ -141,5 +167,4 @@ public Map<String, String> getCommentAttributes() { properties.put(LINE_NUMBER_PROPERTY_NAME, String.valueOf(lineNumber)); return properties; } - } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/EditUserProfileDto.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/EditUserProfileDto.java index 7286796381..b3d0f81587 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/EditUserProfileDto.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/EditUserProfileDto.java @@ -39,6 +39,7 @@ public class EditUserProfileDto { private long userId; private String username; private String avatar; + private Long userProfileVersion; @Valid private UserProfileDto userProfileDto; @@ -58,8 +59,7 @@ public class EditUserProfileDto { * @param user edited user */ public EditUserProfileDto(UserProfileDto userProfileDto, JCUser user) { - this.userId = user.getId(); - this.username = user.getUsername(); + this(user); this.userProfileDto = userProfileDto; } @@ -69,8 +69,7 @@ public EditUserProfileDto(UserProfileDto userProfileDto, JCUser user) { * @param user edited user */ public EditUserProfileDto(UserSecurityDto userSecurityDto, JCUser user) { - this.userId = user.getId(); - this.username = user.getUsername(); + this(user); this.userSecurityDto = userSecurityDto; } @@ -80,8 +79,7 @@ public EditUserProfileDto(UserSecurityDto userSecurityDto, JCUser user) { * @param user edited user */ public EditUserProfileDto(UserNotificationsDto userNotificationsDto, JCUser user) { - this.userId = user.getId(); - this.username = user.getUsername(); + this(user); this.userNotificationsDto = userNotificationsDto; } @@ -91,8 +89,7 @@ public EditUserProfileDto(UserNotificationsDto userNotificationsDto, JCUser user * @param user edited user */ public EditUserProfileDto(UserContactsDto userContactsDto, JCUser user) { - this.userId = user.getId(); - this.username = user.getUsername(); + this(user); this.userContactsDto = userContactsDto; } @@ -120,6 +117,7 @@ public EditUserProfileDto() { public EditUserProfileDto(JCUser user) { this.userId = user.getId(); this.username = user.getUsername(); + this.userProfileVersion = user.getVersion(); } /** @@ -177,69 +175,47 @@ public List<UserContactContainer> getUserContacts() { } /** - * Get the primary id of the user. + * Returns all the languages available for the user + * to choose from. * - * @return the id + * @return array of languages for user to choose */ + public Language[] getLanguagesAvailable() { + return Language.values(); + } + public long getUserId() { return userId; } - /** - * Set the primary id of the user. - * - * @param userId the id - */ public void setUserId(long userId) { this.userId = userId; } - - /** - * Returns all the languages available for the user - * to choose from. - * - * @return array of languages for user to choose - */ - public Language[] getLanguagesAvailable() { - return Language.values(); - } - - /** - * Get user's name(login). - * - * @return user's name(login) - */ public String getUsername() { return username; } - /** - * Set user's name(login). - * - * @param username user's name - */ public void setUsername(String username) { this.username = username; } - - /** - * @return - user avatar - */ public String getAvatar() { return avatar; } - /** - * Set user avatar. - * - * @param avatar - user avatar - */ public void setAvatar(String avatar) { this.avatar = avatar; } + public void setUserProfileVersion(Long version) { + this.userProfileVersion = version; + } + + public Long getUserProfileVersion() { + return userProfileVersion; + } + /** * @return dto with user profile fields */ diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/GroupDto.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/GroupDto.java index 878839c77e..af7a44ce8a 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/GroupDto.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/GroupDto.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; +import org.jtalks.jcommune.model.dto.UserDto; /** * @author Andrei Alikov @@ -29,87 +30,60 @@ public class GroupDto { private long id; private String name; + private List<UserDto> users; - /** - * Instantiates new GroupDto object based on the Group object - * @param group source data for the DTO object - */ public GroupDto(Group group) { this.id = group.getId(); this.name = group.getName(); } - /** - * Instantiates new GroupDto object - * @param id id of the group - * @param name name of the group - */ - public GroupDto(long id, String name) { + public GroupDto(long id, String name, List<UserDto> users) { this.id = id; this.name = name; + this.users = users; } - /** - * Default constructor. Needed for initialization from request body. - */ public GroupDto() { } - /** - * Gets id of the group - * @return id of the group - */ public long getId() { return id; } - /** - * Sets id of the group - * @param id id of the group - */ public void setId(long id) { this.id = id; } - /** - * Gets name of the group - * @return name of the group - */ public String getName() { return name; } - /** - * Sets name of the group - * @param name name of the group - */ public void setName(String name) { this.name = name; } - /** - * Converts list of the Group objects to the list of the GroupDto objects - * @param groups source information about the Groups - * @param sortByName if true than result list will be sorted by the group names - * @return result list with GroupDto based on the source list of the Group objects - */ - public static List<GroupDto> convertGroupList(List<Group> groups, boolean sortByName) { - List<GroupDto> groupDtoList = new ArrayList<GroupDto>(); + public List<UserDto> getUsers() { + return users; + } + + public void setUsers(List<UserDto> users) { + this.users = users; + } + + public static List<GroupDto> convertToGroupDtoList(List<Group> groups, Comparator<GroupDto> comparator) { + List<GroupDto> groupDtoList = new ArrayList<>(); + if (groups == null) { + return groupDtoList; + } for (Group group: groups) { groupDtoList.add(new GroupDto(group)); } - - if (sortByName) { - Collections.sort(groupDtoList, BY_NAME_COMPARATOR); + if (comparator != null) { + Collections.sort(groupDtoList, comparator); } - return groupDtoList; } - - /** - * Comparator comparing two objects by their names - */ public static Comparator<GroupDto> BY_NAME_COMPARATOR = new Comparator<GroupDto>() { @Override public int compare(GroupDto o1, GroupDto o2) { diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/PrivateMessageDraftDto.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/PrivateMessageDraftDto.java new file mode 100644 index 0000000000..2d51bc0a9f --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/PrivateMessageDraftDto.java @@ -0,0 +1,72 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.web.dto; + +import org.jtalks.jcommune.model.entity.PrivateMessage; +import org.jtalks.jcommune.web.validation.annotations.AtLeastOneNotEmpty; + +import javax.validation.constraints.Size; + +/** + * This class has same fields as {@link org.jtalks.jcommune.web.dto.PrivateMessageDto} but different validation + * rules. Needed to implement different validation rules while saving drafts + * + * @author Mikhail Stryzhonok + */ +@AtLeastOneNotEmpty(fieldNames = {"body", "title"}) +public class PrivateMessageDraftDto { + + @Size(max = PrivateMessage.MAX_TITLE_LENGTH) + private String title; + + @Size(max = PrivateMessage.MAX_MESSAGE_LENGTH) + private String body; + + private String recipient; + + private long id; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getRecipient() { + return recipient; + } + + public void setRecipient(String recipient) { + this.recipient = recipient; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } +} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/PrivateMessageDto.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/PrivateMessageDto.java index 34c70ec399..96ece567bf 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/PrivateMessageDto.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/PrivateMessageDto.java @@ -17,6 +17,7 @@ import org.hibernate.validator.constraints.NotBlank; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.PrivateMessage; +import org.jtalks.jcommune.model.validation.annotations.NotBlankSized; import org.jtalks.jcommune.plugin.api.web.validation.annotations.BbCodeAwareSize; import org.jtalks.jcommune.plugin.api.web.validation.annotations.BbCodeNesting; import org.jtalks.jcommune.web.validation.annotations.Exists; @@ -36,13 +37,10 @@ public class PrivateMessageDto { - @NotBlank - @Size(min = PrivateMessage.MIN_TITLE_LENGTH, max = PrivateMessage.MAX_TITLE_LENGTH, message = "{title.length}") + @NotBlankSized(min = PrivateMessage.MIN_TITLE_LENGTH, max = PrivateMessage.MAX_TITLE_LENGTH) private String title; - @NotBlank - @BbCodeAwareSize(min = PrivateMessage.MIN_MESSAGE_LENGTH, - max = PrivateMessage.MAX_MESSAGE_LENGTH, message = "{body.length}") + @BbCodeAwareSize(min = PrivateMessage.MIN_MESSAGE_LENGTH, max = PrivateMessage.MAX_MESSAGE_LENGTH) @BbCodeNesting private String body; diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/SearchQueryDto.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/SearchQueryDto.java new file mode 100644 index 0000000000..cc07c28ae7 --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/SearchQueryDto.java @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.web.dto; + +import org.jtalks.jcommune.model.validation.annotations.NotBlankSized; + +/** + * DTO for receiving and validating search pattern from client side. + * + * @author Evgeniy Cheban + */ +public class SearchQueryDto { + + /** + * Search pattern from client side. Should be between 2 and 50 characters. + */ + @NotBlankSized(min = 2, max = 50, message = "{search-query.pattern.size.message}") + private String pattern; + + /** + * @return search pattern. + */ + public String getPattern() { + return pattern; + } + + /** + * Sets search pattern. + * + * @param pattern - search pattern. + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } +} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/UserContactDto.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/UserContactDto.java index 0cda0eae46..1435b1c716 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/UserContactDto.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/UserContactDto.java @@ -18,7 +18,6 @@ import org.apache.commons.lang.StringUtils; import org.jtalks.jcommune.model.entity.UserContact; import org.jtalks.jcommune.model.entity.UserContactType; -import org.jtalks.jcommune.web.validation.annotations.ValidUserContact; import javax.validation.constraints.Size; @@ -28,7 +27,6 @@ * @author Michael Gamov */ -@ValidUserContact(field="value", storedTypeId="type.id", message = "{validation.usercontact.notmatch}") public class UserContactDto implements Comparable<UserContactDto> { private Long id; @@ -36,8 +34,6 @@ public class UserContactDto implements Comparable<UserContactDto> { @Size(max = UserContact.CONTACT_MAX_LENGTH, message = "{user.contact.illegal_length}") private String value; - private String displayValue; - private UserContactType type; /** @@ -103,7 +99,7 @@ public void setType(UserContactType type) { * @return actual ready-to-display contact */ public String getDisplayValue() { - String replacement = StringUtils.defaultIfBlank(value, ""); + String replacement = StringUtils.defaultIfEmpty(value, ""); return type.getDisplayValue(replacement); } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/UserSecurityDto.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/UserSecurityDto.java index 0d2e43a391..1db23f017d 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/UserSecurityDto.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/dto/UserSecurityDto.java @@ -36,7 +36,8 @@ public class UserSecurityDto { private long userId; private String currentUserPassword; - @Size(min = User.PASSWORD_MIN_LENGTH, max = User.PASSWORD_MAX_LENGTH) + @Size(min = User.PASSWORD_MIN_LENGTH, max = User.PASSWORD_MAX_LENGTH, + message = "{length.constraint}") private String newUserPassword; private String newUserPasswordConfirm; diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/exception/PrettyLogExceptionResolver.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/exception/PrettyLogExceptionResolver.java index d45c667904..a640c4d347 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/exception/PrettyLogExceptionResolver.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/exception/PrettyLogExceptionResolver.java @@ -17,21 +17,20 @@ import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.springframework.beans.TypeMismatchException; import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.Assert; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.security.Principal; import java.util.Enumeration; import java.util.Properties; /** * Catches all the exceptions thrown in controllers, logs them and directs to the error pages. The standard - * {@link org.springframework.web.servlet.handler.SimpleMappingExceptionResolver} wasn't sufficient because it was logging all exceptions as warnings while + * {@link SimpleMappingExceptionResolver} wasn't sufficient because it was logging all exceptions as warnings while * some of them are expected and should be logged as INFO (such as 404 topic not found). * * @author Vitaliy Kravchenko @@ -41,7 +40,6 @@ public class PrettyLogExceptionResolver extends SimpleMappingExceptionResolver { private static final String ACCESS_DENIED_MESSAGE = "Access was denied for user [%s] trying to %s %s"; /** Constant for anonymous user */ private static final String NOT_AUTHORIZED_USERNAME = "anonymousUser"; - /** * {@inheritDoc} */ @@ -59,19 +57,21 @@ protected void logException(Exception ex, HttpServletRequest request) { logger.info(accessDeniedMessage); } else { super.logException(ex, request); + logger.info(getLogMessage(request, ex)); } } /** - * Get info about occured exception: request method, url, cookies and data. + * Get info about occurred exception: request method, url, cookies and data. * @param request request - * @param ex exception + * @param ex exception * @return log message */ private String getLogMessage(HttpServletRequest request, Exception ex) { String data = ""; String exceptionMessage = ex.getMessage(); try { + Assert.notNull(request.getInputStream()); BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream())); String line; StringBuilder stringBuilder = new StringBuilder(); @@ -79,7 +79,7 @@ private String getLogMessage(HttpServletRequest request, Exception ex) { stringBuilder.append(line).append("\n"); } data = stringBuilder.append(exceptionMessage).toString(); - } catch (IOException e) { + } catch (IOException | IllegalArgumentException e) { logger.warn("Could not parse data from request"); } String queryString = request.getQueryString(); @@ -98,6 +98,7 @@ public ModelAndView resolveException(HttpServletRequest request, HttpServletResp Object handler, Exception ex) { if (shouldApplyTo(request, handler)) { logException(ex, request); + logger.debug(request,ex); prepareResponse(ex, response); return doResolveException(request, response, handler, ex); } @@ -134,4 +135,4 @@ protected String findMatchingViewName(Properties exceptionMappings, Exception ex } return viewName; } -} +} \ No newline at end of file diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/filters/LoggingConfigurationFilter.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/filters/LoggingConfigurationFilter.java index 0a1228a393..18b1e2d688 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/filters/LoggingConfigurationFilter.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/filters/LoggingConfigurationFilter.java @@ -15,7 +15,7 @@ package org.jtalks.jcommune.web.filters; import org.apache.commons.lang.StringUtils; -import org.jtalks.common.security.SecurityService; +import org.jtalks.jcommune.service.security.SecurityService; import org.jtalks.jcommune.web.logging.LoggerMdc; import javax.servlet.*; diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/interceptors/PropertiesInterceptor.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/interceptors/PropertiesInterceptor.java index b7a8f90dc7..bc2c2e20c7 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/interceptors/PropertiesInterceptor.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/interceptors/PropertiesInterceptor.java @@ -40,6 +40,9 @@ public class PropertiesInterceptor extends HandlerInterceptorAdapter { private static final String PARAM_ADMIN_INFO_CHANGE_DATE = "infoChangeDate"; private static final String PARAM_COPYRIGHT_TEMPLATE = "copyrightTemplate"; private static final String PARAM_USER_DEFINED_COPYRIGHT = "userDefinedCopyright"; + private static final String PARAM_SESSION_TIMEOUT = "sessionTimeout"; + private static final String PARAM_AVATAR_MAX_SIZE = "avatarMaxSize"; + private static final String PARAM_EMAIL_NOTIFICATION = "emailNotification"; private static final String CURRENT_YEAR_PLACEHOLDER = "{current_year}"; @@ -50,6 +53,10 @@ public class PropertiesInterceptor extends HandlerInterceptorAdapter { private JCommuneProperty adminInfoChangeDateProperty; private JCommuneProperty allPagesTitlePrefixProperty; private JCommuneProperty copyrightProperty; + private JCommuneProperty urlAddressProperty; + private JCommuneProperty sessionTimeoutProperty; + private JCommuneProperty avatarMaxSizeProperty; + private JCommuneProperty emailNotificationProperty; private final String CURRENT_YEAR = String.valueOf(new LocalDateTime().getYear()); @@ -67,7 +74,11 @@ public PropertiesInterceptor(JCommuneProperty componentNameProperty, JCommuneProperty logoTooltipProperty, JCommuneProperty adminInfoChangeDateProperty, JCommuneProperty allPagesTitlePrefixProperty, - JCommuneProperty copyrightProperty) { + JCommuneProperty copyrightProperty, + JCommuneProperty sessionTimeoutProperty, + JCommuneProperty avatarMaxSizeProperty, + JCommuneProperty emailNotificationProperty + ) { this.componentDescriptionProperty = componentDescriptionProperty; this.componentNameProperty = componentNameProperty; this.sapeShowDummyLinksProperty = sapeShowDummyLinksProperty; @@ -75,6 +86,9 @@ public PropertiesInterceptor(JCommuneProperty componentNameProperty, this.adminInfoChangeDateProperty = adminInfoChangeDateProperty; this.allPagesTitlePrefixProperty = allPagesTitlePrefixProperty; this.copyrightProperty = copyrightProperty; + this.sessionTimeoutProperty = sessionTimeoutProperty; + this.avatarMaxSizeProperty = avatarMaxSizeProperty; + this.emailNotificationProperty = emailNotificationProperty; } /** @@ -89,9 +103,9 @@ public PropertiesInterceptor(JCommuneProperty componentNameProperty, @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { - //do not apply to the redirected requests: it's unnecessary and may cause error pages to work incorrectly - if (modelAndView != null - && (modelAndView.getViewName() == null || !modelAndView.getViewName().contains("redirect:"))) { + //do not apply to the redirected requests and error pages + if (modelAndView != null && + (modelAndView.getViewName() == null || checkViewNameForErrorAndRedirection(modelAndView))) { modelAndView.addObject(PARAM_CMP_NAME, componentNameProperty.getValueOfComponent()); modelAndView.addObject(PARAM_CMP_DESCRIPTION, componentDescriptionProperty.getValueOfComponent()); modelAndView.addObject(PARAM_SHOW_DUMMY_LINKS, sapeShowDummyLinksProperty.booleanValue()); @@ -100,9 +114,17 @@ public void postHandle(HttpServletRequest request, HttpServletResponse response, modelAndView.addObject(PARAM_ADMIN_INFO_CHANGE_DATE, adminInfoChangeDateProperty.getValue()); modelAndView.addObject(PARAM_COPYRIGHT_TEMPLATE, copyrightProperty.getValue()); modelAndView.addObject(PARAM_USER_DEFINED_COPYRIGHT, getCopyrightWithYear()); + modelAndView.addObject(PARAM_SESSION_TIMEOUT, sessionTimeoutProperty.getValue()); + modelAndView.addObject(PARAM_AVATAR_MAX_SIZE, avatarMaxSizeProperty.getValue()); + modelAndView.addObject(PARAM_EMAIL_NOTIFICATION, emailNotificationProperty.booleanValue()); } } + private boolean checkViewNameForErrorAndRedirection(ModelAndView modelAndView) { + return !modelAndView.getViewName().contains("redirect:") && + !modelAndView.getViewName().contains("errors/"); + } + private String getCopyrightWithYear() { return copyrightProperty.getValue().replace(CURRENT_YEAR_PLACEHOLDER, CURRENT_YEAR); } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/listeners/HttpRequestListener.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/listeners/HttpRequestListener.java new file mode 100644 index 0000000000..2e3f43d80c --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/listeners/HttpRequestListener.java @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.web.listeners; + +import org.jtalks.jcommune.service.security.SecurityService; + +import javax.servlet.ServletRequestEvent; +import javax.servlet.ServletRequestListener; + +import static org.jtalks.jcommune.web.util.AppContextUtils.getBeanFormApplicationContext; + +/** + * Since SecurityService contain thread-local storage for JCUser objects and we are using + * Tomcat's thread pool some threads may contain thread-local variables and Tomcat can't + * clean them after application stop. So we should clean thread-local storage after each Http request. + * + * @author Oleg Tkachenko + */ +public class HttpRequestListener implements ServletRequestListener { + @Override + public void requestDestroyed(ServletRequestEvent sre) { + SecurityService securityService = getBeanFormApplicationContext(sre.getServletContext(), SecurityService.class); + securityService.cleanThreadLocalStorage(); + } + + @Override + public void requestInitialized(ServletRequestEvent sre) { + // DO NOTHING + } +} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/listeners/LoggerInitializationListener.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/listeners/LoggerInitializationListener.java new file mode 100644 index 0000000000..abae1c16b8 --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/listeners/LoggerInitializationListener.java @@ -0,0 +1,290 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.web.listeners; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Properties; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; +import org.apache.log4j.PropertyConfigurator; +import org.apache.log4j.xml.DOMConfigurator; + +import com.google.common.annotations.VisibleForTesting; +import org.jtalks.jcommune.model.utils.JndiAwarePropertyPlaceholderConfigurer; + +/** + * Application startup listener which initialize logger properties that are used during standard logger initialization + * (reading {@code log4j.xml}). <p/> + * <p>This listener should be registered and started before any other servlet/listener (which use logging). So it + * starts before usual logger initialization.</p> + * <p>Logger search for parameters in: + * <ol> + * <li>datasource.properties file</li> + * </ol></p> + * <p>We can't use Spring IoC, because order of bean initialization is not managed and someclasses can start using + * logger which configured differently.</p> + * + * @author Evgeny Kapinos + * @author Andrey Strelnikov + * @see <a href="http://logging.apache.org/log4j/1.2/manual.html#defaultInit" + * >Default Log4j initialization procedure</a> + * @see <a href="http://logging.apache.org/log4j/1.2/manual.html#Example_Configurations" + * >Example log4j configurations</a> + * @see <a href="http://wiki.apache.org/logging-log4j/SystemPropertiesInConfiguration" + * >Log4j - how to set parameters using System Properties</a> + */ +public class LoggerInitializationListener implements ServletContextListener { + + /** This property name is used to search in environment variables. */ + @VisibleForTesting + protected static final String LOG4J_CONFIGURATION_FILE = "JCOMMUNE_LOG4J_CONFIGURATION_FILE"; + + /** Properties file where log4j configuration file info we should check */ + private static final String PROPERTIES_FILE = "/org/jtalks/jcommune/model/datasource.properties"; + + /** Embedded log4j configuration file path in {@code war} */ + private static final String LOG4J_EMBEDDED_CONFIGURATION_FILE = "/log4j.xml"; + + /** System property witch allows to skip standard and auto Log4j initialization */ + private static final String LOG4J_INIT_OVERRIDE_PROPERTY = "log4j.defaultInitOverride"; + + /** Prefix witch used when this class put messages into servlet container log stream */ + private static final String CONTAINER_LOG_PREFIX = "[JCOMMUNE][log4j init] "; + + /** current servlet context for logging */ + private ServletContext servletContext; + + /** + * Initializing logger by configuration file + * <p>{@inheritDoc}</p> + */ + @Override + public void contextInitialized(ServletContextEvent event) { + + // Save container context for logging before Log4j will be configured + servletContext = event.getServletContext(); + + // Search external Log4j configuration file + FileInfo fileInfo = getConfigurationFileNameFromJNDI(); + if (fileInfo == null) { + fileInfo = getConfigurationFileNameFromDatasourcePropertiesFile(); + } + if (fileInfo == null) { + fileInfo = getConfigurationFileNameFromSystemProperties(); + } + + // Skip standard Log4j auto configuration on first call + String previousLog4jInitOverrideValue = System.setProperty(LOG4J_INIT_OVERRIDE_PROPERTY, "true"); + + // Manual Log4j configuration + if (!loadLog4jConfigurationFromExternalFile(fileInfo)){ + loadEmbeddedLog4jConfiguration(); + } + + // Return previous auto configuration property. It shared between all applications in Tomcat + if (previousLog4jInitOverrideValue == null){ + System.clearProperty(LOG4J_INIT_OVERRIDE_PROPERTY); + } else { + System.setProperty(LOG4J_INIT_OVERRIDE_PROPERTY, previousLog4jInitOverrideValue); + } + + } + + /** {@inheritDoc} */ + @Override + public void contextDestroyed(ServletContextEvent event) { + // Nothing to do + } + + /** + * Shows information about log4j configuration progress in standard servlet container log + * @param message for logging + */ + private void logToConsole(String message) { + servletContext.log(CONTAINER_LOG_PREFIX+message); + } + + /** + * Shows information about exceptions during log4j configuration progress in standard servlet container log + * @param message for logging + * @param e exception + */ + private void servletContainerlog(String message, Throwable e) { + servletContext.log(CONTAINER_LOG_PREFIX + message, e); + } + + + /** + * Check {@value #LOG4J_CONFIGURATION_FILE} property from JNDI + * @return {@link FileInfo} or {@code null} + */ + private FileInfo getConfigurationFileNameFromJNDI() { + String logFileName = new JndiAwarePropertyPlaceholderConfigurer().resolveJndiProperty(LOG4J_CONFIGURATION_FILE); + if (logFileName == null) { + return null; + } + return new FileInfo("JNDI", logFileName); + } + + + /** + * Check {@value #LOG4J_CONFIGURATION_FILE} property from {@value #PROPERTIES_FILE} file + * @return {@link FileInfo} or {@code null} + */ + private FileInfo getConfigurationFileNameFromDatasourcePropertiesFile() { + Properties prop = new Properties(); + InputStream propertiesFileStream = null; + String logFileName = null; + try { + propertiesFileStream = getPropertiesFileStream(); + prop.load(propertiesFileStream); + logFileName = prop.getProperty(LOG4J_CONFIGURATION_FILE); + } catch (IOException e) { + servletContainerlog("Error during reading \"" + PROPERTIES_FILE + "\" stream: ", e); + } finally { + if (propertiesFileStream != null) { + try { + propertiesFileStream.close(); + } catch (IOException e) { + servletContainerlog("Error during closing \"" + PROPERTIES_FILE + "\" stream: ", e); + } + } + } + if (logFileName == null) { + return null; + } + return new FileInfo("\"" + PROPERTIES_FILE + "\" file", logFileName); + } + + /** + * Checks {@value #LOG4J_CONFIGURATION_FILE} property from system properties + * @return {@link FileInfo} or {@code null} + */ + private FileInfo getConfigurationFileNameFromSystemProperties() { + String logFileName = System.getProperty(LOG4J_CONFIGURATION_FILE); + if (logFileName == null) { + return null; + } + return new FileInfo("system properties", logFileName); + } + + /** + * Opens file with properties and return stream + * @return {@link InputStream} + */ + @VisibleForTesting + protected InputStream getPropertiesFileStream() { + return getClass().getResourceAsStream(PROPERTIES_FILE); + } + + /** + * Configures log4j from external file + * @param fileInfo file description and path + */ + private boolean loadLog4jConfigurationFromExternalFile(FileInfo fileInfo){ + + if (fileInfo == null){ + return false; + } + logToConsole("Log4j configuration file has been taken from " + fileInfo.getSourceDescriptor() + " and set to \"" + + fileInfo.getLogFileName() + "\""); + + String log4jConfigurationFile = fileInfo.getLogFileName(); + File file = new File(log4jConfigurationFile.trim()); + URL url; + try { + url = file.toURI().toURL(); + } catch (MalformedURLException e) { + return false; + } + return configureLog4j(url); + } + + /** + * Configures log4j with embedded configuration file + */ + private void loadEmbeddedLog4jConfiguration(){ + logToConsole("Log4j embedded configuration loded"); + URL url = getClass().getResource(LOG4J_EMBEDDED_CONFIGURATION_FILE); + configureLog4j(url); // always returns true + } + + /** + * Configures log4j from {@link URL} and checks regular Log4j configure class by extension (like default log4j + * loader) + * @param url to configuration file + * @return {@code true} if one or more appenders was added, {@code false} otherwise + */ + @VisibleForTesting + protected boolean configureLog4j(URL url){ + + if (url.getFile().toLowerCase().endsWith(".xml")){ + DOMConfigurator.configure(url); + } else { + PropertyConfigurator.configure(url); + } + + // Previous calls doesn't throw any exceptions and doesn't return fail state. + // So we check appenders, as most representative error indicator + Logger rootLogger = LogManager.getRootLogger(); + if (!rootLogger.getAllAppenders().hasMoreElements()){ + logToConsole("Log4j error during load configuraton file or no appenders presented"); + LogManager.resetConfiguration(); + return false; + } + return true; + } + + /** + * Auxiliary class which contains all necessary information about file and its source + */ + private static class FileInfo { + private final String sourceDescriptor; + private final String logFileName; + + /** + * Creates new instance file information container + * @param sourceDescriptor user friendly descriptor about this file source + * @param logFileName path to file + */ + public FileInfo(String sourceDescriptor, String logFileName) { + this.sourceDescriptor = sourceDescriptor; + this.logFileName = logFileName; + } + + /** + * @return descriptor of file + */ + public String getSourceDescriptor() { + return sourceDescriptor; + } + + /** + * @return path to file + */ + public String getLogFileName() { + return logFileName; + } + } +} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/listeners/SessionSetupListener.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/listeners/SessionSetupListener.java index 081b15daea..f4c738b4b4 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/listeners/SessionSetupListener.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/listeners/SessionSetupListener.java @@ -15,49 +15,107 @@ package org.jtalks.jcommune.web.listeners; import org.jtalks.jcommune.model.entity.JCommuneProperty; -import org.springframework.context.ApplicationContext; -import org.springframework.web.context.support.WebApplicationContextUtils; +import org.jtalks.jcommune.service.nontransactional.LocationService; +import org.jtalks.jcommune.web.controller.AdministrationController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import javax.servlet.ServletContext; +import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; import java.util.concurrent.TimeUnit; +import static org.jtalks.jcommune.web.util.AppContextUtils.getBeanFormApplicationContext; + /** * Performs initial session setup. * Any general session settings are to be set here. * * @author Evgeniy Naumenko + * @author Oleg Tkachenko */ public class SessionSetupListener implements HttpSessionListener { + private static volatile JCommuneProperty sessionTimeoutProperty = null; + private static LocationService locationService = null; + private static int TIME_OUT_SECONDS; + private Logger logger = LoggerFactory.getLogger(SessionSetupListener.class); /** * Sets session timeout for any crated session based on a database-located property. - * As for now it affects registered user only. * * @param se session event to get new session from */ @Override public void sessionCreated(HttpSessionEvent se) { - ServletContext servletContext = se.getSession().getServletContext(); - ApplicationContext ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext); - JCommuneProperty property = (JCommuneProperty) ctx.getBean("sessionTimeoutProperty"); - int timeoutInSeconds = (int) TimeUnit.SECONDS.convert(property.intValue(), TimeUnit.MINUTES); - if (timeoutInSeconds > 0) { - se.getSession().setMaxInactiveInterval(timeoutInSeconds); - } else { - /* Zero property value should mean infinitive session. - To disable session expiration we should pass negative value here. - Setting zero as is results in weird Tomcat behavior.*/ - se.getSession().setMaxInactiveInterval(-1); + try { + if (sessionTimeoutProperty == null) { + synchronized (this) { + if (sessionTimeoutProperty == null){ + ServletContext context = se.getSession().getServletContext(); + sessionTimeoutProperty = getBeanFormApplicationContext(context, JCommuneProperty.class, "sessionTimeoutProperty"); + locationService = getBeanFormApplicationContext(context, LocationService.class); + int seconds = extractValueFromProperty(sessionTimeoutProperty); + /* Zero property value should mean infinitive session. + To disable session expiration we should pass negative value here. + Setting zero as is results in weird Tomcat behavior.*/ + TIME_OUT_SECONDS = seconds > 0 ? seconds : -1; + } + } + } + se.getSession().setMaxInactiveInterval(TIME_OUT_SECONDS); + } catch (Exception ex) { + logger.warn("Bean instantiation error: " + ex); + throw ex; } } /** - * {@inheritDoc} + * If session contain user when destroyed, then we need to clean location info of this user. + * + * @param se session event to get session and check for user in security context. */ @Override public void sessionDestroyed(HttpSessionEvent se) { - //noop + Object principal = getPrincipalFromSession(se.getSession()); + if (principal != null) { + try { + locationService.clearUserLocation(principal); + } catch (Exception ex) { + logger.warn("Error clearing user location when session being destroyed: " + ex.getMessage()); + } + } + } + + /** + * Returns principal from SecurityContext + * + * @param currentSession current session + * @return {@link Object} authenticated principal. + */ + private Object getPrincipalFromSession(HttpSession currentSession) { + Object securityContext = currentSession.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + if (securityContext == null) return null; + Authentication authentication = ((SecurityContext) securityContext).getAuthentication(); + return authentication != null ? authentication.getPrincipal() : null; + } + + /** + * @param sessionTimeoutProperty property to read + * @return time in seconds + */ + private int extractValueFromProperty(JCommuneProperty sessionTimeoutProperty) { + int value = sessionTimeoutProperty != null ? sessionTimeoutProperty.intValue() : 0; + return (int) TimeUnit.SECONDS.convert(value, TimeUnit.MINUTES); + } + + /** + * Needed in tests and {@link AdministrationController} when sessionTimeoutProperty is updated. + */ + public static void resetSessionTimeoutProperty(){ + sessionTimeoutProperty = null; } } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/util/AppContextUtils.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/util/AppContextUtils.java new file mode 100644 index 0000000000..b83fa73298 --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/util/AppContextUtils.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.jtalks.jcommune.web.util; + +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +import javax.servlet.ServletContext; + +/** + * Provides possibility to get bean from ApplicationContext + * + * @author Oleg Tkachenko + */ +public class AppContextUtils { + /** + * Returns bean from application context. + * + * @param servletContext to find the web application context for + * @param tClass interface or superclass of the actual class. + * @param beanName the name of the bean to retrieve + * @return an instance of the bean + * @throws IllegalStateException if the root WebApplicationContext could not be found + * @throws ClassCastException if the bean found in context is not assignable to the type of tClass + */ + public static <T> T getBeanFormApplicationContext(ServletContext servletContext, Class<T> tClass, String beanName) { + WebApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext); + if (beanName == null) return context.getBean(tClass); + return tClass.cast(context.getBean(beanName)); + } + + public static <T> T getBeanFormApplicationContext(ServletContext servletContext, Class<T> tClass) { + return getBeanFormApplicationContext(servletContext , tClass, null); + } +} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/util/ImageControllerUtils.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/util/ImageControllerUtils.java index 30c5fb4402..498e19f4c4 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/util/ImageControllerUtils.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/util/ImageControllerUtils.java @@ -17,7 +17,7 @@ import org.jtalks.jcommune.service.exceptions.ImageProcessException; import org.jtalks.jcommune.service.nontransactional.ImageService; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -40,16 +40,14 @@ public class ImageControllerUtils { public static final String SRC_PREFIX = "srcPrefix"; public static final String SRC_IMAGE = "srcImage"; - private ImageService imageService; private JSONUtils jsonUtils; /** * @param imageService object for working with uploaded images - * @param jsonUtils object for preparing JSON repsonse + * @param jsonUtils object for preparing JSON response */ - public ImageControllerUtils(ImageService imageService, - JSONUtils jsonUtils) { + public ImageControllerUtils(ImageService imageService, JSONUtils jsonUtils) { this.imageService = imageService; this.jsonUtils = jsonUtils; } @@ -61,9 +59,8 @@ public ImageControllerUtils(ImageService imageService, * @param responseHeaders response HTTP headers * @param responseContent response content * @return ResponseEntity with image processing results - * @throws java.io.IOException defined in the JsonFactory implementation, caller must implement exception processing - * @throws org.jtalks.jcommune.service.exceptions.ImageProcessException - * if error occurred while image processing + * @throws IOException defined in the JsonFactory implementation, caller must implement exception processing + * @throws ImageProcessException if error occurred while image processing */ public ResponseEntity<String> prepareResponse( MultipartFile file, @@ -74,7 +71,7 @@ public ResponseEntity<String> prepareResponse( imageService.validateImageSize(bytes); prepareNormalResponse(bytes, responseContent); String body = getResponceJSONString(responseContent); - return new ResponseEntity<String>(body, responseHeaders, HttpStatus.OK); + return new ResponseEntity<>(body, responseHeaders, HttpStatus.OK); } public String getResponceJSONString(Map<String, String> responseContent) throws IOException { @@ -105,8 +102,7 @@ public void prepareResponse(byte[] bytes, * @param responseContent response payload * @throws ImageProcessException due to common image processing error */ - public void prepareNormalResponse(byte[] bytes, - Map<String, String> responseContent) throws ImageProcessException { + public void prepareNormalResponse(byte[] bytes, Map<String, String> responseContent) throws ImageProcessException { String srcImage = imageService.preProcessAndEncodeInString64(bytes); responseContent.put(STATUS, String.valueOf(JsonResponseStatus.SUCCESS)); responseContent.put(SRC_PREFIX, imageService.getHtmlSrcImagePrefix()); diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/util/RssUtils.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/util/RssUtils.java new file mode 100644 index 0000000000..46acccf501 --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/util/RssUtils.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.web.util; + +import org.apache.commons.lang.StringUtils; + +/** + * @author denis.berezhnoy + */ +public class RssUtils { + /** + * Method for skip invalid characters in content for RSS feed. + * <p/> + * Description valid chars in XML specification + * http://www.w3.org/TR/REC-xml/#charsets + * + * @param in - post content. + */ + public static String skipInValidXMLChars(String in) { + if (StringUtils.isBlank(in)) return StringUtils.EMPTY; + String pattern = "[^" + + "\u0009\r\n" + + "\u0020-\uD7FF" + + "\uE000-\uFFFD" + + "\ud800\udc00-\udbff\udfff" + + "]"; + return in.replaceAll(pattern, StringUtils.EMPTY); + } +} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/annotations/AtLeastOneNotEmpty.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/annotations/AtLeastOneNotEmpty.java new file mode 100644 index 0000000000..980b6eecaf --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/annotations/AtLeastOneNotEmpty.java @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.web.validation.annotations; + +import org.jtalks.jcommune.web.validation.validators.AtLeastOneNotEmptyValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Validates that at lest of specified string fields in class is not empty + * + * @author Mikhail Stryzhonok + */ +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = AtLeastOneNotEmptyValidator.class) +@Documented +public @interface AtLeastOneNotEmpty { + + /** + * Array with names of fields to check + */ + String[] fieldNames(); + + /** + * Message for display when validation fails. + */ + String message() default ""; + + /** + * Groups element that specifies the processing groups with which the + * constraint declaration is associated. + */ + Class<?>[] groups() default {}; + + /** + * Payload element that specifies the payload with which the the + * constraint declaration is associated. + */ + Class<? extends Payload>[] payload() default {}; +} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/annotations/Exists.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/annotations/Exists.java index bee09f3de9..f8e78154b2 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/annotations/Exists.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/annotations/Exists.java @@ -39,6 +39,11 @@ @Constraint(validatedBy = ExistenceValidator.class) public @interface Exists { + /** + * If set to <b>true</b> checking field can be null + */ + boolean isNullableAllowed() default false; + /** * Resource bundle code for error message */ diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/annotations/ValidUserContact.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/annotations/ValidUserContact.java deleted file mode 100644 index ba4b133bfc..0000000000 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/annotations/ValidUserContact.java +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (C) 2011 JTalks.org Team - * This library 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 2.1 of the License, or (at your option) any later version. - * This library 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 this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package org.jtalks.jcommune.web.validation.annotations; - - -import org.jtalks.jcommune.web.validation.validators.ValidUserContactValidator; - -import javax.validation.Constraint; -import javax.validation.Payload; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.ANNOTATION_TYPE; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * Constraint for checking that user contact is valid (matches regular - * expression for its type). - * This constraint for use with JSR-303 validator. - * <p/> - * You must annotate your class with {@link ValidUserContact} annotation - * You must fill in the parameters <code>field</code> and <code>storedTypeId</code> - * field names to test. Fields must have getters. <code>field</code> fields - * must be of type {@link java.lang.String}, <code>storedTypeId</code> must - * be numeric - * <p/> - * Example: - * Validate that <code>field1</code> is valid contact - * {@code - * @ValidUserContact(field = "field1", fieldWithPattern = "typeId") - * class Test { - * private String field1; - * private Integer typeId; - * public String getField1() { - * return field1; - * } - * public Integer getTypeId() { - * return typeId; - * } - * } - * - * @author Vyacheslav Mishcheryakov - * @see ValidUserContactValidator - */ -@Target({TYPE, ANNOTATION_TYPE}) -@Retention(RUNTIME) -@Constraint(validatedBy = ValidUserContactValidator.class) -@Documented -public @interface ValidUserContact { - - /** - * Message for display when validation fails. - */ - String message() default "{org.jtalks.jcommune.web.validation.annotations.MatchesRegExp.message}"; - - /** - * Groups element that specifies the processing groups with which the - * constraint declaration is associated. - */ - Class<?>[] groups() default {}; - - /** - * Payload element that specifies the payload with which the the - * constraint declaration is associated. - */ - Class<? extends Payload>[] payload() default {}; - - /** - * Name of field to validate. - */ - String field(); - - /** - * Path to property containing id of {@link org.jtalks.jcommune.model.entity.UserContactType} to get pattern - * from it. - */ - String storedTypeId(); -} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/AtLeastOneNotEmptyValidator.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/AtLeastOneNotEmptyValidator.java new file mode 100644 index 0000000000..eb77232018 --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/AtLeastOneNotEmptyValidator.java @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.web.validation.validators; + +import net.sf.ehcache.hibernate.management.impl.BeanUtils; +import org.jtalks.jcommune.web.validation.annotations.AtLeastOneNotEmpty; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +/** + * @author Mikhail Stryzhonok + */ +public class AtLeastOneNotEmptyValidator implements ConstraintValidator<AtLeastOneNotEmpty, Object> { + + private String[] fieldNames; + + @Override + public void initialize(AtLeastOneNotEmpty constraintAnnotation) { + fieldNames = constraintAnnotation.fieldNames(); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + for (String name : fieldNames) { + Object property = BeanUtils.getBeanProperty(value, name); + if (property instanceof String && ((String) property).trim().length() > 0) { + return true; + } + } + return false; + } +} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/ExistenceValidator.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/ExistenceValidator.java index 3444c93a1f..8f2c98f44c 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/ExistenceValidator.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/ExistenceValidator.java @@ -33,6 +33,7 @@ public class ExistenceValidator implements ConstraintValidator<Exists, String> { private Class<? extends Entity> entity; private String field; private boolean ignoreCase; + private boolean nullableAllowed; private ValidatorDao<String> dao; @@ -52,6 +53,7 @@ public void initialize(Exists annotation) { this.entity = annotation.entity(); this.field = annotation.field(); this.ignoreCase = annotation.ignoreCase(); + this.nullableAllowed = annotation.isNullableAllowed(); } /** @@ -59,6 +61,10 @@ public void initialize(Exists annotation) { */ @Override public boolean isValid(String value, ConstraintValidatorContext context) { - return (value != null) && dao.isExists(entity, field, value, ignoreCase); + if (nullableAllowed) { + return (value == null) || dao.isExists(entity, field, value, ignoreCase); + } else { + return (value != null) && dao.isExists(entity, field, value, ignoreCase); + } } } diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/ValidUserContactValidator.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/ValidUserContactValidator.java deleted file mode 100644 index b7a02b6bd8..0000000000 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/ValidUserContactValidator.java +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright (C) 2011 JTalks.org Team - * This library 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 2.1 of the License, or (at your option) any later version. - * This library 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 this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package org.jtalks.jcommune.web.validation.validators; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; - -import org.apache.commons.beanutils.BeanUtils; -import org.jtalks.jcommune.model.entity.UserContactType; -import org.jtalks.jcommune.service.UserContactsService; -import org.jtalks.jcommune.web.validation.annotations.ValidUserContact; -import org.springframework.beans.factory.annotation.Autowired; - -/** - * Validator for {@link ValidUserContact}. Checks that string property - * matches validation pattern for contact type whose ID is stored in <code> - * storedTypeId</code> property. Actually checks that value is valid contact type. - * - * @author Vyacheslav Mishcheryakov - * @see ValidUserContact - */ -public class ValidUserContactValidator implements ConstraintValidator<ValidUserContact, Object>{ - - private String propertyToValidate; - private String pathToTypeId; - private String fieldValue; - private String pattern; - - - private UserContactsService contactsService; - - - /** - * @param contactsService the contactsService to set - */ - @Autowired - public void setContactsService(UserContactsService contactsService) { - this.contactsService = contactsService; - } - - /** - * Initialize validator fields from annotation instance. - * - * @param constraintAnnotation {@link ValidUserContact} annotation from class - * @see ValidUserContact - */ - @Override - public void initialize(ValidUserContact constraintAnnotation) { - this.propertyToValidate = constraintAnnotation.field(); - this.pathToTypeId = constraintAnnotation.storedTypeId(); - - } - - /** - * Validate object with {@link ValidUserContact} annotation. - * - * @param value object with {@link ValidUserContact} annotation - * @param context validation context - * @return {@code true} if validation successful or false if fails - */ - @Override - public boolean isValid(Object value, ConstraintValidatorContext context) { - fetchDataForValidation(value); - boolean result; - if (pattern == null) { - result = true; - } else if (fieldValue == null) { - result = false; - } else { - result = fieldValue.matches(pattern); - } - return result; - } - - /** - * Retrieving necessary fields from object. - * Throws {@code IllegalStateException} if field not found. - * - * @param value object from which we take values ​​of fields - */ - private void fetchDataForValidation(Object value) { - try { - fieldValue = BeanUtils.getProperty(value, propertyToValidate); - long typeId = Long.parseLong(BeanUtils.getProperty(value, pathToTypeId)); - UserContactType type = contactsService.get(typeId); - pattern = type.getValidationPattern(); - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - -} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/ValidatorStub.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/ValidatorStub.java new file mode 100644 index 0000000000..9bd96b7a6e --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/validation/validators/ValidatorStub.java @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.web.validation.validators; + +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +/** + * Simple implementation of {@link org.springframework.validation.Validator} interface. + * Should be used in tests with Spring MockMVC framework + * + * @author Mikhail Stryzhonok + */ +public class ValidatorStub implements Validator { + + private String[] errorFields; + private boolean hasGlobalErrors = false; + + public ValidatorStub(String ... errorFields) { + this.errorFields = errorFields; + } + + public void setGlobalError() { + hasGlobalErrors = true; + } + + @Override + public boolean supports(Class<?> clazz) { + return true; + } + + @Override + public void validate(Object target, Errors errors) { + if (errorFields != null) { + for (String field : errorFields) { + errors.rejectValue(field, "Test Error"); + } + } + if (hasGlobalErrors) { + errors.reject("Test error"); + } + } +} diff --git a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/view/RssViewer.java b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/view/RssViewer.java index 0243e869fa..ef89427f73 100644 --- a/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/view/RssViewer.java +++ b/jcommune-view/jcommune-web-controller/src/main/java/org/jtalks/jcommune/web/view/RssViewer.java @@ -21,6 +21,7 @@ import com.sun.syndication.feed.rss.Item; import org.jtalks.common.model.entity.Component; import org.jtalks.jcommune.model.entity.Topic; +import org.jtalks.jcommune.web.util.RssUtils; import org.springframework.web.servlet.view.feed.AbstractRssFeedView; import javax.servlet.http.HttpServletRequest; @@ -43,14 +44,14 @@ public class RssViewer extends AbstractRssFeedView { /** * Set meta data for all RSS feed * - * @param model news model + * @param newsComponents components of the RSS feed * @param feed news feed * @param request http request */ @Override - protected void buildFeedMetadata(Map<String, Object> model, Channel feed, + protected void buildFeedMetadata(Map<String, Object> newsComponents, Channel feed, HttpServletRequest request) { - Component component = (Component)model.get("forumComponent"); + Component component = (Component)newsComponents.get("forumComponent"); String feedTitle = DEFAULT_FEED_TITLE; String feedDescription = DEFAULT_FEED_DESCRIPTION; @@ -63,30 +64,30 @@ protected void buildFeedMetadata(Map<String, Object> model, Channel feed, feed.setDescription(feedDescription); feed.setLink(buildURL(request)); - super.buildFeedMetadata(model, feed, request); + super.buildFeedMetadata(newsComponents, feed, request); } /** * Set list data item news in RSS feed * - * @param model news model + * @param newsComponents components of the RSS feed * @param request http request * @param response http response * @return list items * @throws IOException i/o exception */ @Override - protected List<Item> buildFeedItems(Map<String, Object> model, + protected List<Item> buildFeedItems(Map<String, Object> newsComponents, HttpServletRequest request, HttpServletResponse response) throws IOException { String url = buildURL(request); - List<Topic> listContent = (List<Topic>) model.get("topics"); + List<Topic> listContent = (List<Topic>) newsComponents.get("topics"); if (listContent == null) { response.sendRedirect(request.getContextPath() + "/errors/404"); return null; } - List<Item> items = new ArrayList<Item>(listContent.size()); + List<Item> items = new ArrayList<>(listContent.size()); for (Topic topic : listContent) { items.add(createFeedItem(topic, url)); @@ -108,7 +109,9 @@ private Item createFeedItem(Topic topic, String url) { Item item = new Item(); Description description = new Description(); description.setType("text"); - description.setValue(topic.getLastPost().getPostContent()); + String postContent = topic.getLastPost().getPostContent(); + postContent = RssUtils.skipInValidXMLChars(postContent); + description.setValue(postContent); Content content = new Content(); item.setContent(content); diff --git a/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_en.properties b/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_en.properties index 2695131623..6cf598a0d3 100644 --- a/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_en.properties +++ b/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_en.properties @@ -1,12 +1,12 @@ -title.length=should be {min} - {max} characters -body.length=should be {min} - {max} characters -not_empty=can't be empty +javax.validation.constraints.Size.message=Size must be between {min} and {max} +####################################################### +not_empty=Can't be empty validation.not_null=Must not be empty password_not_matches=Password and confirmation password do not match user.email.illegal_length=Email field should not contain more than {max} symbols validation.email.not_empty=Email field should not be empty -user.username.already_exists=User with the username already exists. -user.email.already_exists=User with the email already exists. +user.username.already_exists=User with this username already exists +user.email.already_exists=User with this email already exists validation.incorrectCurrentPassword=Password does not match to the current password validation.wrong_recipient=User not found validation.draft.need.at.least.one.field=You need to fill at least one of these fields @@ -63,3 +63,12 @@ branch.name.length_constraint_violation = Branch name length should be {max} cha branch.description.length_constraint_violation = Branch description length should be {max} characters max length.constraint=Length must be between {min} and {max} symbols +javax.validation.constraints.IntegerRange.message=Value must be between {min} and {max} bytes +group.name.illegal_length=Group name length must be between {min} and {max} characters +group.description.illegal_length=Group description length must be between {min} and {max} characters +group.already_exists=Group with this name already exists +spam.regex.illegal_length=Rule regex length must be between {min} and {max} characters +spam.description.illegal_length=Rule description length must be between {min} and {max} characters + +search-query.pattern.size.message=Search length must be between {min} and {max} characters +add-users-in-group.button.tooltip.message=Add users to group diff --git a/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_es.properties b/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_es.properties index 77bee6e46d..de262715c9 100644 --- a/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_es.properties +++ b/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_es.properties @@ -1,6 +1,6 @@ -title.length=debe tener {min} - {max} caracteres -body.length=debe tener {min} - {max} caracteres -not_empty=no puede estar vac\u00EDo +javax.validation.constraints.Size.message=El tama\u00F1o tiene que estar entre {min} y {max} +####################################################### +not_empty=No puede estar vac\u00EDo validation.not_null=No puede estar vac\u00EDo password_not_matches=La contrase\u00F1a y la verificaci\u00F3n de la contrase\u00F1a no coinciden. user.email.illegal_length=El campo de e-mail no puede contener m\u00E1s de {max} s\u00EDmbolos @@ -62,4 +62,10 @@ branch.name.emptiness_constraint_violation = El nombre de la categor\u00EDa no p branch.name.length_constraint_violation = El nombre de la categor\u00EDa debe contener como m\u00E1ximo {max} caracteres branch.description.length_constraint_violation = La descripci\u00F3n de la categor\u00EDa debe tener como m\u00E1ximo {max} caracteres length.constraint=La longitud debe ser de {min} y {max} caracteres +javax.validation.constraints.IntegerRange.message=Value must be between {min} and {max} bytes +group.name.illegal_length=Group name length must be between {min} and {max} characters +group.description.illegal_length=Group description length must be between {min} and {max} characters +group.already_exists=Group with this name already exists +spam.regex.illegal_length=Rule regex length must be between {min} and {max} characters +spam.description.illegal_length=Rule description length must be between {min} and {max} characters diff --git a/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_ru.properties b/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_ru.properties index 9ec542c9db..bd61634da9 100644 --- a/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_ru.properties +++ b/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_ru.properties @@ -19,9 +19,7 @@ org.hibernate.validator.constraints.Range.message=\u0414\u043E\u043B\u0436\u043D org.hibernate.validator.constraints.URL.message=\u0414\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u044B\u0439 URL ####################################################### validation.bbcode.not_nesting=\u0421\u043B\u0438\u0448\u043A\u043E\u043C \u0433\u043B\u0443\u0431\u043E\u043A\u043E\u0435 \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435 bb-\u043A\u043E\u0434\u043E\u0432 -title.length=\u0414\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C {min} - {max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 -body.length=\u0414\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C {min} - {max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 -not_empty=\u0414\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E +not_empty=\u041D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C validation.not_null=\u041D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C password_not_matches=\u041F\u0430\u0440\u043E\u043B\u044C \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u0430\u0440\u043E\u043B\u044F \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u044E\u0442 user.email.illegal_length=\u041F\u043E\u043B\u0435 Email \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u0442\u044C \u0431\u043E\u043B\u0435\u0435 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 @@ -84,4 +82,13 @@ branch.name.emptiness_constraint_violation = \u041D\u0430\u0437\u0432\u0430\u043 branch.name.length_constraint_violation = \u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0440\u0430\u0437\u0434\u0435\u043B\u0430 \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0434\u043B\u0438\u043D\u043D\u0435\u0435 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 branch.description.length_constraint_violation = \u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u0440\u0430\u0437\u0434\u0435\u043B\u0430 \u043D\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0434\u043B\u0438\u043D\u043D\u0435\u0435 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 length.constraint=\u0414\u043B\u0438\u043D\u0430 \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {min} \u0438 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0430\u043C\u0438 +javax.validation.constraints.IntegerRange.message=\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043E\u0442 {min} \u0434\u043E {max} \u0431\u0430\u0439\u0442 +group.name.illegal_length=\u0414\u043B\u0438\u043D\u0430 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u044F \u0433\u0440\u0443\u043F\u043F\u044B \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {min} \u0438 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0430\u043C\u0438 +group.description.illegal_length=\u0414\u043B\u0438\u043D\u0430 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u044F \u0433\u0440\u0443\u043F\u043F\u044B \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {min} \u0438 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0430\u043C\u0438 +group.already_exists=\u0413\u0440\u0443\u043F\u043F\u0430 \u0441 \u0442\u0430\u043A\u0438\u043C \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435\u043C \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 +spam.regex.illegal_length=\u0414\u043B\u0438\u043D\u0430 \u0440\u0435\u0433\u0443\u043B\u044F\u0440\u043D\u043E\u0433\u043E \u0432\u044B\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {min} \u0438 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0430\u043C\u0438 +spam.description.illegal_length=\u0414\u043B\u0438\u043D\u0430 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u044F \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {min} \u0438 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0430\u043C\u0438 + +search-query.pattern.size.message=\u0414\u043B\u0438\u043D\u0430 \u043F\u043E\u0438\u0441\u043A\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u043C\u0435\u0436\u0434\u0443 {min} \u0438 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0430\u043C\u0438 +add-users-in-group.button.tooltip.message=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439 \u0432 \u0433\u0440\u0443\u043F\u043F\u0443 diff --git a/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_uk.properties b/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_uk.properties index f97d00176a..ee43f803a2 100644 --- a/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_uk.properties +++ b/jcommune-view/jcommune-web-controller/src/main/resources/ValidationMessages_uk.properties @@ -18,10 +18,8 @@ org.hibernate.validator.constraints.NotEmpty.message=\u041D\u0435 \u043C\u043E\u org.hibernate.validator.constraints.Range.message=\u041F\u043E\u0432\u0438\u043D\u043D\u043E \u0431\u0443\u0442\u0438 \u043C\u0456\u0436 {min} \u0442\u0430 {max} org.hibernate.validator.constraints.URL.message=\u0410\u0434\u0440\u0435\u0441\u0430 \u043F\u043E\u0432\u0438\u043D\u043D\u0430 \u0431\u0443\u0442\u0438 \u043A\u043E\u0440\u0435\u043A\u0442\u043D\u043E\u044E validation.bbcode.not_nesting=\u0417\u0430\u043D\u0430\u0434\u0442\u043E \u0433\u043B\u0438\u0431\u043E\u043A\u043E BB-\u043A\u043E\u0434 \u0432\u043A\u043B\u0430\u0434\u0435\u043D\u043D\u044F -####################################################### -title.length=\u041F\u043E\u0432\u0438\u043D\u043D\u043E \u0431\u0443\u0442\u0438 {min} - {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 -body.length=\u041F\u043E\u0432\u0438\u043D\u043D\u043E \u0431\u0443\u0442\u0438 {min} - {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 -not_empty=\u041F\u043E\u0432\u0438\u043D\u043D\u043E \u0431\u0443\u0442\u0438 \u0437\u0430\u043F\u043E\u0432\u043D\u0435\u043D\u043E +######################################################## +not_empty=\u041D\u0435 \u043C\u043E\u0436\u0435 \u0431\u0443\u0442\u0438 \u043F\u043E\u0440\u043E\u0436\u043D\u0456\u043C validation.not_null=\u041D\u0435 \u043C\u043E\u0436\u0435 \u0431\u0443\u0442\u0438 \u043F\u0443\u0441\u0442\u0438\u043C password_not_matches=\u041F\u0430\u0440\u043E\u043B\u044C \u0442\u0430 \u043F\u0456\u0434\u0442\u0432\u0435\u0440\u0436\u0435\u043D\u043D\u044F \u043F\u0430\u0440\u043E\u043B\u044F \u043D\u0435 \u0441\u043F\u0456\u0432\u043F\u0430\u0434\u0430\u044E\u0442\u044C user.email.illegal_length=\u041F\u043E\u043B\u0435 Email \u043D\u0435 \u043C\u043E\u0436\u0435 \u043C\u0456\u0441\u0442\u0438\u0442\u0438 \u0431\u0456\u043B\u044C\u0448\u0435 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 @@ -80,4 +78,13 @@ branch.name.emptiness_constraint_violation = \u041D\u0430\u0437\u0432\u0430 \u04 branch.name.length_constraint_violation = \u041D\u0430\u0437\u0432\u0430 \u0440\u043E\u0437\u0434\u0456\u043B\u0443 \u043D\u0435 \u043F\u043E\u0432\u0438\u043D\u043D\u0430 \u0431\u0443\u0442\u0438 \u0431\u0456\u043B\u044C\u0448\u043E\u044E \u0437\u0430 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 branch.description.length_constraint_violation = \u041E\u043F\u0438\u0441 \u0440\u043E\u0437\u0434\u0456\u043B\u0443 \u043D\u0435 \u043F\u043E\u0432\u0438\u043D\u0435\u043D \u0431\u0443\u0442\u0438 \u0431\u0456\u043B\u044C\u0448\u0438\u0439 \u0437\u0430 {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 length.constraint=\u0414\u043E\u0432\u0436\u0438\u043D\u0430 \u043F\u043E\u0432\u0438\u043D\u043D\u0430 \u0431\u0443\u0442\u0438 \u0432\u0456\u0434 {min} \u0434\u043E {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 +javax.validation.constraints.IntegerRange.message=\u0417\u043D\u0430\u0447\u0435\u043D\u043D\u044F \u043F\u043E\u0432\u0438\u043D\u043D\u043E \u0431\u0443\u0442\u0438 \u0432\u0456\u0434 {min} \u0434\u043E {max} \u0431\u0430\u0439\u0442\u0456\u0432 +group.name.illegal_length=\u0414\u043E\u0432\u0436\u0438\u043D\u0430 \u043D\u0430\u0437\u0432\u0438 \u0433\u0440\u0443\u043F\u0438 \u043F\u043E\u0432\u0438\u043D\u043D\u0430 \u0431\u0443\u0442\u0438 \u0432\u0456\u0434 {min} \u0434\u043E {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 +group.description.illegal_length=\u0414\u043E\u0432\u0436\u0438\u043D\u0430 \u043E\u043F\u0438\u0441\u0443 \u0433\u0440\u0443\u043F\u0438 \u043F\u043E\u0432\u0438\u043D\u043D\u0430 \u0431\u0443\u0442\u0438 \u0432\u0456\u0434 {min} \u0434\u043E {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 +group.already_exists=\u0413\u0440\u0443\u043F\u0430 \u0437 \u0442\u0430\u043A\u043E\u044E \u043D\u0430\u0437\u0432\u043E\u044E \u0432\u0436\u0435 \u0456\u0441\u043D\u0443\u0454 +spam.regex.illegal_length=\u0414\u043E\u0432\u0436\u0438\u043D\u0430 \u0440\u0435\u0433\u0443\u043B\u044F\u0440\u043D\u043E\u0433\u043E \u0432\u0438\u0440\u0430\u0437\u0443 \u043F\u043E\u0432\u0438\u043D\u043D\u0430 \u0431\u0443\u0442\u0438 \u0432\u0456\u0434 {min} \u0434\u043E {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 +spam.description.illegal_length=\u0414\u043E\u0432\u0436\u0438\u043D\u0430 \u043E\u043F\u0438\u0441\u0443 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043F\u043E\u0432\u0438\u043D\u043D\u0430 \u0431\u0443\u0442\u0438 \u0432\u0456\u0434 {min} \u0434\u043E {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 + +search-query.pattern.size.message=\u0414\u043E\u0432\u0436\u0438\u043D\u0430 \u043F\u043E\u0448\u0443\u043A\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u043F\u0438\u0442\u0443 \u043F\u043E\u0432\u0438\u043D\u043D\u0430 \u0431\u0443\u0442\u0438 \u0432\u0456\u0434 {min} \u0434\u043E {max} \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 +add-users-in-group.button.tooltip.message=\u0414\u043E\u0434\u0430\u0442\u0438 \u043A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456\u0432 \u0434\u043E \u0433\u0440\u0443\u043F\u0438 diff --git a/jcommune-view/jcommune-web-controller/src/main/resources/org/jtalks/jcommune/web/applicationContext-controller.xml b/jcommune-view/jcommune-web-controller/src/main/resources/org/jtalks/jcommune/web/applicationContext-controller.xml index 044c71d3a7..081181a269 100644 --- a/jcommune-view/jcommune-web-controller/src/main/resources/org/jtalks/jcommune/web/applicationContext-controller.xml +++ b/jcommune-view/jcommune-web-controller/src/main/resources/org/jtalks/jcommune/web/applicationContext-controller.xml @@ -84,8 +84,19 @@ </property> </bean> - <bean id="entityToDtoConverter" class="org.jtalks.jcommune.web.dto.EntityToDtoConverter"> - <constructor-arg ref="pluginLoader"/> + <bean id="retryPolicy" class="org.springframework.retry.policy.SimpleRetryPolicy"> + <constructor-arg value="3"/> + <constructor-arg > + <map> + <entry key="org.springframework.orm.hibernate3.HibernateOptimisticLockingFailureException" + value="true"/> + </map> + </constructor-arg> + </bean> + + <bean id="retryTemplate" class="org.springframework.retry.support.RetryTemplate"> + <property name="retryPolicy" ref="retryPolicy"/> </bean> + </beans> diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AdministrationControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AdministrationControllerTest.java index eaeec2e034..90d9c9f62c 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AdministrationControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AdministrationControllerTest.java @@ -17,20 +17,18 @@ import org.jtalks.common.model.entity.Component; import org.jtalks.common.model.entity.Group; import org.jtalks.common.model.permissions.BranchPermission; -import org.jtalks.jcommune.model.dto.GroupsPermissions; -import org.jtalks.jcommune.model.dto.PermissionChanges; +import org.jtalks.jcommune.model.dto.*; import org.jtalks.jcommune.model.entity.Branch; import org.jtalks.jcommune.model.entity.ComponentInformation; -import org.jtalks.jcommune.service.BranchService; -import org.jtalks.jcommune.service.ComponentService; +import org.jtalks.jcommune.service.*; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.nontransactional.ImageService; import org.jtalks.jcommune.service.security.PermissionManager; import org.jtalks.jcommune.web.dto.BranchDto; import org.jtalks.jcommune.web.dto.BranchPermissionDto; import org.jtalks.jcommune.web.dto.PermissionGroupsDto; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.jtalks.jcommune.web.util.ImageControllerUtils; import org.mockito.Mock; import org.springframework.context.MessageSource; @@ -45,18 +43,15 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; +import java.util.*; -import static org.jgroups.util.Util.assertTrue; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNull; +import static org.testng.AssertJUnit.assertTrue; /** * @author Andrei Alikov @@ -86,6 +81,13 @@ public class AdministrationControllerTest { @Mock PermissionManager permissionManager; + @Mock + GroupService groupService; + @Mock + UserService userService; + + @Mock + private SpamProtectionService spamProtectionService; private MockMvc mockMvc; @@ -96,7 +98,7 @@ public class AdministrationControllerTest { public void init() { initMocks(this); - administrationController = new AdministrationController(componentService, messageSource, branchService, permissionManager); + administrationController = new AdministrationController(componentService, messageSource, branchService, permissionManager, groupService, spamProtectionService, userService); } @Test @@ -299,4 +301,17 @@ private BranchPermissionDto createBranchPermissionDto(BranchPermission targetPer dto.setPermissionMask(targetPermission.getMask()); return dto; } -} + + @Test + public void groupAdministrationPageShouldContainListOfGroups() throws Exception { + setupComponentMock(); + List<GroupAdministrationDto> expected = new ArrayList<>(); + expected.add(new GroupAdministrationDto("group",0)); + expected.add(new GroupAdministrationDto("group1",0)); + expected.add(new GroupAdministrationDto("group2",0)); + mockMvc = MockMvcBuilders.standaloneSetup(administrationController).build(); + when(groupService.getGroupNamesWithCountOfUsers()).thenReturn(expected); + this.mockMvc.perform(get("/group/list").accept(MediaType.TEXT_HTML)) + .andExpect(model().attribute("groups", expected)); + } +} \ No newline at end of file diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AdministrationImagesControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AdministrationImagesControllerTest.java index 026953aa3d..0f6cbc5b90 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AdministrationImagesControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AdministrationImagesControllerTest.java @@ -417,8 +417,6 @@ private void checkResponse(MockHttpServletResponse response) { assertEquals(response.getHeader("Pragma"), "public"); List<String> cacheControlHeaders = response.getHeaders("Cache-Control"); Assert.assertTrue(cacheControlHeaders.contains("public")); - Assert.assertTrue(cacheControlHeaders.contains("must-revalidate")); - Assert.assertTrue(cacheControlHeaders.contains("max-age=0")); assertNotNull(response.getHeader("Expires")); assertNotNull(response.getHeader("Last-Modified")); } diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AvatarControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AvatarControllerTest.java index a16f777f55..5fb5056752 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AvatarControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/AvatarControllerTest.java @@ -16,15 +16,14 @@ import org.joda.time.DateTime; import org.jtalks.jcommune.model.entity.JCUser; +import org.jtalks.jcommune.model.entity.ObjectsFactory; import org.jtalks.jcommune.service.UserService; import org.jtalks.jcommune.service.exceptions.ImageProcessException; -import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.web.util.ImageControllerUtils; import org.mockito.Matchers; import org.mockito.Mock; import org.springframework.context.MessageSource; import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockMultipartFile; @@ -33,14 +32,15 @@ import org.testng.annotations.Test; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Date; -import java.util.HashMap; -import java.util.List; - -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static org.jtalks.jcommune.web.controller.ImageUploadController.HTTP_HEADER_DATETIME_PATTERN; +import static org.jtalks.jcommune.web.controller.ImageUploadController.IF_MODIFIED_SINCE_HEADER; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; import static org.testng.Assert.*; @@ -67,7 +67,7 @@ public class AvatarControllerTest { // private AvatarController avatarController; - private byte[] validAvatar = new byte[] {-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, + private byte[] validAvatar = new byte[]{-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 4, 0, 0, 0, 4, 1, 0, 0, 0, 0, -127, -118, -93, -45, 0, 0, 0, 9, 112, 72, 89, 115, 0, 0, 1, -118, 0, 0, 1, -118, 1, 51, -105, 48, 88, 0, 0, 0, 32, 99, 72, 82, 77, 0, 0, 122, 37, 0, 0, -128, -125, 0, 0, -7, -1, 0, 0, -128, -23, 0, 0, 117, 48, 0, 0, -22, 96, 0, 0, 58, -104, 0, 0, @@ -84,33 +84,29 @@ public void setUp() throws Exception { @Test @SuppressWarnings("unchecked") - public void uploadAvatarForOperaAndIEShouldReturnPreviewInResponce() - throws IOException, ImageProcessException { + public void uploadAvatarForOperaAndIe_mustReturnPreviewInResponse() throws Exception { MultipartFile file = new MockMultipartFile("qqfile", validAvatar); - - ResponseEntity<String> actualResponseEntity = avatarController.uploadAvatar(file); - + avatarController.uploadAvatar(file); verify(imageControllerUtils).prepareResponse(eq(file), any(HttpHeaders.class), any(HashMap.class)); } @Test - public void uploadAvatarForChromeAndFFShouldReturnPreviewInResponce() throws ImageProcessException { + @SuppressWarnings("unchecked") + public void uploadAvatarForChromeAndFf_mustReturnPreviewInResponse() throws ImageProcessException { MockHttpServletResponse response = new MockHttpServletResponse(); - avatarController.uploadAvatar(validAvatar, response); - - verify(imageControllerUtils).prepareResponse(eq(validAvatar), eq(response), any(HashMap.class)); + verify(imageControllerUtils).prepareResponse(eq(validAvatar), eq(response), any(Map.class)); } - + @Test - public void renderAvatarShouldReturnModifiedAvatarInResponse() throws IOException, NotFoundException { + public void renderAvatarShouldReturnModifiedAvatarInResponse() throws Exception { MockHttpServletResponse response = new MockHttpServletResponse(); JCUser user = getUser(); user.setAvatar(validAvatar); user.setAvatarLastModificationTime(new DateTime(1000)); when(userService.get(anyLong())).thenReturn(user); MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader(avatarController.IF_MODIFIED_SINCE_HEADER, new Date(0)); + request.addHeader(IF_MODIFIED_SINCE_HEADER, new Date(0)); avatarController.renderAvatar(request, response, 0L); @@ -120,36 +116,29 @@ public void renderAvatarShouldReturnModifiedAvatarInResponse() throws IOExceptio assertEquals(response.getHeader("Pragma"), "public"); List<String> cacheControlHeaders = response.getHeaders("Cache-Control"); assertTrue(cacheControlHeaders.contains("public")); - assertTrue(cacheControlHeaders.contains("must-revalidate")); - assertTrue(cacheControlHeaders.contains("max-age=0")); - assertNotNull(response.getHeader("Expires"));//System.currentTimeMillis() is used assertNotNull(response.getHeader("Last-Modified"));// depends on current timezone } - + @Test - public void renderAvatarShouldNotReturnNotModifiedAvatarInResponse() throws IOException, NotFoundException { + public void renderAvatarShouldNotReturnNotModifiedAvatarInResponse() throws Exception { JCUser user = getUser(); user.setAvatar(validAvatar); user.setAvatarLastModificationTime(new DateTime(0)); when(userService.get(anyLong())).thenReturn(user); MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader(avatarController.IF_MODIFIED_SINCE_HEADER, new Date(1000)); + request.addHeader(IF_MODIFIED_SINCE_HEADER, new Date(1000)); avatarController.renderAvatar(request, response, 0L); - assertEquals(response.getStatus(), HttpServletResponse.SC_NOT_MODIFIED); assertNotSame(response.getContentAsByteArray(), validAvatar); assertEquals(response.getHeader("Pragma"), "public"); List<String> cacheControlHeaders = response.getHeaders("Cache-Control"); assertTrue(cacheControlHeaders.contains("public")); - assertTrue(cacheControlHeaders.contains("must-revalidate")); - assertTrue(cacheControlHeaders.contains("max-age=0")); - assertNotNull(response.getHeader("Expires"));//System.currentTimeMillis() is used assertNotNull(response.getHeader("Last-Modified"));// depends on current timezone } - + private JCUser getUser() { JCUser newUser = new JCUser(USER_NAME, EMAIL, PASSWORD); newUser.setFirstName(FIRST_NAME); @@ -158,7 +147,8 @@ private JCUser getUser() { } @SuppressWarnings("unchecked") - public void getDefaultAvatarShouldReturnDefaultAvatarInBase64String() throws IOException, ImageProcessException { + @Test + public void getDefaultAvatarShouldReturnDefaultAvatarInBase64String() throws Exception { String expectedJSON = "{\"team\": \"larks\"}"; when(imageControllerUtils.getDefaultImage()).thenReturn(validAvatar); when(imageControllerUtils.convertImageToIcoInString64(validAvatar)).thenReturn(IMAGE_BYTE_ARRAY_IN_BASE_64_STRING); @@ -168,4 +158,60 @@ public void getDefaultAvatarShouldReturnDefaultAvatarInBase64String() throws IOE assertEquals(actualJSON, expectedJSON); } + + @Test + public void renderAvatar_mustReturnNotModifiedStatus_ifAvatarWasNeverModified() throws Exception { + JCUser user = ObjectsFactory.getRandomUser(); + user.setAvatarLastModificationTime(null); + doReturn(user).when(userService).get(1L); + + MockHttpServletResponse response = new MockHttpServletResponse(); + avatarController.renderAvatar(new MockHttpServletRequest(), response, 1L); + + assertEquals(response.getStatus(), HttpServletResponse.SC_NOT_MODIFIED); + } + + @Test + public void renderAvatar_mustReturnNotModifiedStatus_ifAvatarWasNotChangedSinceLastTime() throws Exception { + JCUser user = ObjectsFactory.getRandomUser(); + doReturn(user).when(userService).get(1L); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader( + IF_MODIFIED_SINCE_HEADER, + user.getAvatarLastModificationTime().toString(HTTP_HEADER_DATETIME_PATTERN, Locale.US)); + avatarController.renderAvatar(request, response, 1L); + + assertEquals(response.getStatus(), HttpServletResponse.SC_NOT_MODIFIED); + } + + @Test + public void renderAvatar_mustReturnModifiedStatus_ifAvatarChangedSinceLastTime() throws Exception { + JCUser user = ObjectsFactory.getRandomUser(); + doReturn(user).when(userService).get(1L); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockHttpServletRequest request = new MockHttpServletRequest(); + String modifiedSinceIsBeforeAvatarModification = user.getAvatarLastModificationTime().minusSeconds(1) + .toString(HTTP_HEADER_DATETIME_PATTERN, Locale.US); + request.addHeader(IF_MODIFIED_SINCE_HEADER, modifiedSinceIsBeforeAvatarModification); + avatarController.renderAvatar(request, response, 1L); + + assertEquals(response.getStatus(), HttpServletResponse.SC_OK); + } + + @Test + public void renderAvatar_mustCacheFor30Days() throws Exception { + JCUser user = ObjectsFactory.getRandomUser(); + doReturn(user).when(userService).get(1L); + + MockHttpServletResponse response = new MockHttpServletResponse(); + avatarController.renderAvatar(new MockHttpServletRequest(), response, 1L); + + long actualMaxAge = Long.parseLong(response.getHeaders("Cache-Control").get(1).replace("max-age=", "")); + long actualCacheExpiration = Long.parseLong(response.getHeader("Expires")); + assertEquals(actualMaxAge, 30L * 24 * 60 * 60); + assertTrue(actualCacheExpiration - (System.currentTimeMillis() + TimeUnit.DAYS.toSeconds(30) * 1000) < 1000); + } } diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/BranchControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/BranchControllerTest.java index fc5d0eff35..22b1030bea 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/BranchControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/BranchControllerTest.java @@ -26,7 +26,7 @@ import org.jtalks.jcommune.web.dto.BranchDto; import org.jtalks.jcommune.plugin.api.web.dto.Breadcrumb; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; -import org.jtalks.jcommune.web.dto.EntityToDtoConverter; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.jtalks.jcommune.web.util.ForumStatisticsProvider; import org.mockito.Mock; import org.springframework.data.domain.Page; @@ -116,7 +116,7 @@ public void showPage() throws NotFoundException { when(breadcrumbBuilder.getForumBreadcrumb(branchService.get(branchId))) .thenReturn(new ArrayList<Breadcrumb>()); when(forumStatisticsProvider.getOnlineRegisteredUsers()).thenReturn(new ArrayList<>()); - when(converter.convertToDtoPage(topicsPage)).thenReturn(dtoPage); + when(converter.convertTopicPageToTopicDtoPage(topicsPage)).thenReturn(dtoPage); SecurityContext securityContext = mock(SecurityContext.class); when(securityContextFacade.getContext()).thenReturn(securityContext); Authentication authentication = mock(Authentication.class); @@ -148,7 +148,7 @@ public void recentTopicsPage() throws NotFoundException { Page<TopicDto> dtoPage = new PageImpl<>(new ArrayList<TopicDto>()); //set expectations when(topicFetchService.getRecentTopics(page)).thenReturn(topicsPage); - when(converter.convertToDtoPage(topicsPage)).thenReturn(dtoPage); + when(converter.convertTopicPageToTopicDtoPage(topicsPage)).thenReturn(dtoPage); //invoke the object under test ModelAndView mav = controller.recentTopicsPage(page); @@ -169,7 +169,7 @@ public void unansweredTopicsPage() { Page<TopicDto> dtoPage = new PageImpl<>(new ArrayList<TopicDto>()); //set expectations when(topicFetchService.getUnansweredTopics(page)).thenReturn(topicsPage); - when(converter.convertToDtoPage(topicsPage)).thenReturn(dtoPage); + when(converter.convertTopicPageToTopicDtoPage(topicsPage)).thenReturn(dtoPage); //invoke the object under test ModelAndView mav = controller.unansweredTopicsPage(page); diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/CodeReviewCommentControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/CodeReviewCommentControllerTest.java index 5abeb04137..83ffe23e96 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/CodeReviewCommentControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/CodeReviewCommentControllerTest.java @@ -15,13 +15,14 @@ package org.jtalks.jcommune.web.controller; import org.jtalks.jcommune.model.entity.*; +import org.jtalks.jcommune.plugin.api.web.dto.json.*; import org.jtalks.jcommune.service.PostCommentService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.PostService; +import org.jtalks.jcommune.service.nontransactional.BBCodeService; import org.jtalks.jcommune.service.nontransactional.NotificationService; import org.jtalks.jcommune.web.dto.CodeReviewCommentDto; import org.jtalks.jcommune.web.dto.CodeReviewDto; -import org.jtalks.jcommune.web.dto.json.*; import org.mockito.Mock; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.security.access.AccessDeniedException; @@ -60,6 +61,8 @@ public class CodeReviewCommentControllerTest { private NotificationService notificationService; @Mock private PostService postService; + @Mock + private BBCodeService bbCodeService; private CodeReviewCommentController controller; @@ -68,7 +71,8 @@ public void initEnvironment() { initMocks(this); controller = new CodeReviewCommentController( postCommentService, - postService); + postService, + bbCodeService); } @BeforeMethod @@ -111,7 +115,7 @@ public void testAddCommentSuccess() throws AccessDeniedException, NotFoundExcept when(bindingResult.hasErrors()).thenReturn(false); when(postService.addComment(anyLong(), anyMap(), anyString())) .thenReturn(createComment()); - + when(bbCodeService.convertBbToHtml(COMMENT_BODY)).thenReturn(COMMENT_BODY); JsonResponse response = controller.addComment( new CodeReviewCommentDto(), bindingResult, 1L); diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/CodeReviewControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/CodeReviewControllerTest.java index 779d8fb5bb..9a625520f5 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/CodeReviewControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/CodeReviewControllerTest.java @@ -14,6 +14,7 @@ */ package org.jtalks.jcommune.web.controller; +import org.jtalks.common.model.entity.Section; import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.service.*; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; @@ -21,12 +22,15 @@ import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; import org.mockito.Mock; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.support.RetryTemplate; import org.springframework.validation.BindingResult; import org.springframework.web.servlet.ModelAndView; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.ArrayList; +import java.util.List; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; @@ -50,30 +54,34 @@ public class CodeReviewControllerTest { @Mock private TopicModificationService topicModificationService; @Mock - private LastReadPostService lastReadPostService; + private TopicDraftService topicDraftService; @Mock - private UserService userService; + private LastReadPostService lastReadPostService; @Mock private PostService postService; + private RetryTemplate retryTemplate; + private CodeReviewController controller; @BeforeMethod public void initEnvironment() { initMocks(this); + retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); controller = new CodeReviewController( branchService, breadcrumbBuilder, topicModificationService, - lastReadPostService, - userService, - postService); + topicDraftService, + retryTemplate); } @BeforeMethod public void prepareTestData() { - branch = new Branch("", "description"); + branch = new Branch("namebranch", "description"); + branch.setSection(new Section("namesection")); branch.setId(BRANCH_ID); user = new JCUser("username", "email@mail.com", "password"); } @@ -97,6 +105,24 @@ public void createPage() throws NotFoundException { assertModelAttributeAvailable(mav, "breadcrumbList"); } + @Test + public void showNewCodeReviewPageShouldShowDraft() throws NotFoundException { + Topic expectedTopic = createTopic(); + TopicDraft expectedDraft = new TopicDraft(user, expectedTopic.getTitle(), expectedTopic.getBodyText()); + when(topicDraftService.getDraft()).thenReturn(expectedDraft); + + when(branchService.get(BRANCH_ID)).thenReturn(branch); + when(breadcrumbBuilder.getNewTopicBreadcrumb(branch)).thenReturn(new ArrayList<Breadcrumb>()); + + ModelAndView mav = controller.showNewCodeReviewPage(BRANCH_ID); + + TopicDto topicDto = assertAndReturnModelAttributeOfType(mav, "topicDto", TopicDto.class); + + Topic topic = topicDto.getTopic(); + assertEquals(topic.getTitle(), expectedDraft.getTitle()); + assertEquals(topicDto.getBodyText(), expectedDraft.getContent()); + } + @Test public void createValidationPass() throws Exception { user.setAutosubscribe(true); @@ -127,12 +153,19 @@ public void createValidationFail() throws Exception { //set expectations when(result.hasErrors()).thenReturn(true); when(branchService.get(BRANCH_ID)).thenReturn(branch); - when(breadcrumbBuilder.getForumBreadcrumb(branch)).thenReturn(new ArrayList<Breadcrumb>()); + List<Breadcrumb> breadcrumbs = new BreadcrumbBuilder().getNewTopicBreadcrumb(branch); + when(breadcrumbBuilder.getNewTopicBreadcrumb(branch)).thenReturn(breadcrumbs); //invoke the object under test ModelAndView mav = controller.createCodeReview(getDto(), result, BRANCH_ID); //check result + List<Breadcrumb> breadcrumbsFromModel = (List) mav.getModel().get("breadcrumbList"); + for (int i = 0; i < breadcrumbs.size(); i++) { + assertEquals(breadcrumbs.get(i).getValue(), breadcrumbsFromModel.get(i).getValue()); + } + assertEquals(breadcrumbsFromModel.get(1).getValue(), branch.getSection().getName()); + assertEquals(breadcrumbsFromModel.get(2).getValue(), branch.getName()); assertViewName(mav, "codeReviewForm"); long branchId = assertAndReturnModelAttributeOfType(mav, "branchId", Long.class); assertEquals(branchId, BRANCH_ID); diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ExternalLinkControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ExternalLinkControllerTest.java index 74157dcbb4..1ad4cf7d02 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ExternalLinkControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ExternalLinkControllerTest.java @@ -18,8 +18,8 @@ import org.jtalks.jcommune.model.entity.ExternalLink; import org.jtalks.jcommune.service.ComponentService; import org.jtalks.jcommune.service.ExternalLinkService; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.mockito.Mock; import org.springframework.validation.BindingResult; import org.testng.annotations.BeforeMethod; diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ImageUploadControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ImageUploadControllerTest.java index 3a065d9e54..4eeba4210c 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ImageUploadControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ImageUploadControllerTest.java @@ -19,9 +19,9 @@ import org.apache.commons.lang.time.DateFormatUtils; import org.jtalks.jcommune.service.exceptions.ImageFormatException; import org.jtalks.jcommune.service.exceptions.ImageSizeException; -import org.jtalks.jcommune.web.dto.json.FailJsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseReason; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.FailJsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseReason; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.mockito.Matchers; import org.mockito.Mock; import org.springframework.context.MessageSource; @@ -113,14 +113,14 @@ public void testGetIfModifiedSinceDate() { ImageUploadController.HTTP_HEADER_DATETIME_PATTERN, Locale.US); - Date result = imageUploadController.getIfModifiedSineDate(dateAsString); + Date result = imageUploadController.getIfModifiedSinceDate(dateAsString); assertEquals(result.getTime(), date.getTime()); } @Test public void testGetIfModifiedSinceDateNullHeader() { - Date result = imageUploadController.getIfModifiedSineDate(null); + Date result = imageUploadController.getIfModifiedSinceDate(null); assertEquals(result, new Date(0)); } diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PluginControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PluginControllerTest.java index b68d247534..c2665d9207 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PluginControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PluginControllerTest.java @@ -23,8 +23,8 @@ import org.jtalks.jcommune.service.ComponentService; import org.jtalks.jcommune.service.PluginService; import org.jtalks.jcommune.service.UserService; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.jtalks.jcommune.plugin.api.dto.PluginActivatingDto; import org.jtalks.jcommune.plugin.api.filters.NameFilter; import org.jtalks.jcommune.plugin.api.PluginLoader; diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PostControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PostControllerTest.java index 6e0939b06b..76eb65f466 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PostControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PostControllerTest.java @@ -17,25 +17,32 @@ import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.service.*; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.jtalks.jcommune.service.nontransactional.BBCodeService; import org.jtalks.jcommune.service.nontransactional.LocationService; import org.jtalks.jcommune.plugin.api.web.dto.Breadcrumb; import org.jtalks.jcommune.plugin.api.web.dto.PostDto; -import org.jtalks.jcommune.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.PostDraftDto; +import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.mockito.ArgumentMatcher; import org.mockito.Matchers; import org.mockito.Mock; import org.springframework.beans.propertyeditors.StringTrimmerEditor; -import org.springframework.security.core.session.SessionRegistry; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.support.RetryTemplate; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import java.util.ArrayList; import static java.util.Arrays.asList; @@ -72,7 +79,15 @@ public class PostControllerTest { @Mock private LocationService locationService; @Mock - private SessionRegistry sessionRegistry; + private EntityToDtoConverter converter; + @Mock + private HttpServletRequest request; + @Mock + private HttpSession session; + @Mock + private BindingResult result; + + private RetryTemplate retryTemplate; public static final long POST_ID = 1; public static final long TOPIC_ID = 1L; @@ -99,13 +114,17 @@ public void init() throws NotFoundException { post.setTopic(topic); topic.getPosts().addAll(asList(post)); + retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); + when(postService.get(POST_ID)).thenReturn(post); when(topicFetchService.get(TOPIC_ID)).thenReturn(topic); when(breadcrumbBuilder.getForumBreadcrumb(topic)).thenReturn(new ArrayList<Breadcrumb>()); controller = new PostController( postService, breadcrumbBuilder, topicFetchService, topicModificationService, - bbCodeService, lastReadPostService, userService, locationService, sessionRegistry); + bbCodeService, lastReadPostService, userService, locationService, converter, + retryTemplate); } @Test @@ -188,17 +207,21 @@ public void testPartialQuotedAjax() throws NotFoundException { @Test public void testSubmitAnswerValidationPass() throws NotFoundException { BeanPropertyBindingResult resultWithoutErrors = mock(BeanPropertyBindingResult.class); + TopicDto dto = new TopicDto(post.getTopic()); + dto.setTopicUrl("/topics/" + TOPIC_ID); + + when(converter.convertTopicToDto(any(Topic.class))).thenReturn(dto); when(resultWithoutErrors.hasErrors()).thenReturn(false); when(topicModificationService.replyToTopic(anyLong(), Matchers.<String>any(), eq(BRANCH_ID))).thenReturn(post); when(postService.calculatePageForPost(post)).thenReturn(1); //invoke the object under test - ModelAndView mav = controller.create(null, TOPIC_ID, getDto(), resultWithoutErrors); + ModelAndView mav = controller.create(null, TOPIC_ID, getDto(), resultWithoutErrors, null); //check expectations verify(topicModificationService).replyToTopic(TOPIC_ID, POST_CONTENT, BRANCH_ID); //check result - assertViewName(mav, "redirect:/topics/" + TOPIC_ID + "?page=1#" + POST_ID); + assertViewName(mav, "redirect:" + dto.getTopicUrl() + "?page=1#" + POST_ID); } @@ -207,22 +230,26 @@ public void testSubmitAnswerValidationFail() throws NotFoundException { BeanPropertyBindingResult resultWithErrors = mock(BeanPropertyBindingResult.class); when(resultWithErrors.hasErrors()).thenReturn(true); //invoke the object under test - ModelAndView mav = controller.create(null, TOPIC_ID, getDto(), resultWithErrors); + ModelAndView mav = controller.create(null, TOPIC_ID, getDto(), resultWithErrors, new RedirectAttributesModelMap()); //check expectations verify(topicModificationService, never()).replyToTopic(anyLong(), anyString(), eq(BRANCH_ID)); //check result - assertViewName(mav, "topic/postList"); + assertEquals(mav.getViewName(), "redirect:/topics/error/" + TOPIC_ID + "?page=null"); } @Test public void testRedirectToPageWithPost() throws NotFoundException { + TopicDto dto = new TopicDto(post.getTopic()); + dto.setTopicUrl("/topics/" + TOPIC_ID); + + when(converter.convertTopicToDto(any(Topic.class))).thenReturn(dto); when(postService.calculatePageForPost(post)).thenReturn(5); String result = controller.redirectToPageWithPost(POST_ID); - assertEquals(result, "redirect:/topics/" + TOPIC_ID + "?page=5#" + POST_ID); + assertEquals(result, "redirect:" + dto.getTopicUrl() + "?page=5#" + POST_ID); } @Test(expectedExceptions = NotFoundException.class) @@ -235,12 +262,9 @@ public void testRedirectToPageWithPostNotFound() throws NotFoundException { @Test public void testPostPreview() throws Exception { BeanPropertyBindingResult BindingResult = mock(BeanPropertyBindingResult.class); - when(userService.getCurrentUser()).thenReturn(user); ModelAndView mav = controller.preview(getDto(), BindingResult); - verify(userService, times(1)).getCurrentUser(); assertViewName(mav, "ajax/postPreview"); assertModelAttributeAvailable(mav, "errors"); - assertModelAttributeAvailable(mav, "signature"); assertModelAttributeAvailable(mav, "content"); assertModelAttributeAvailable(mav, "isInvalid"); } @@ -251,8 +275,9 @@ public void testVoteUpSuccess() throws Exception { Post post = new Post(user, "text"); when(postService.get(anyLong())).thenReturn(post); + when(request.getSession()).thenReturn(session); - JsonResponse response = controller.voteUp(1L); + JsonResponse response = controller.voteUp(1L, request); assertEquals(response.getStatus(), JsonResponseStatus.SUCCESS); verify(postService).vote(eq(post), argThat(new PostVoteMacher(true))); @@ -264,8 +289,9 @@ public void testVoteDownSuccess() throws Exception{ Post post = new Post(user, "text"); when(postService.get(anyLong())).thenReturn(post); + when(request.getSession()).thenReturn(session); - JsonResponse response = controller.voteDown(1L); + JsonResponse response = controller.voteDown(1L, request); assertEquals(response.getStatus(), JsonResponseStatus.SUCCESS); verify(postService).vote(eq(post), argThat(new PostVoteMacher(false))); @@ -274,13 +300,59 @@ public void testVoteDownSuccess() throws Exception{ @Test(expectedExceptions = NotFoundException.class) public void voteUpShouldThrowExceptionIfPostNotFound() throws Exception { when(postService.get(anyLong())).thenThrow(new NotFoundException()); - controller.voteUp(1L); + when(request.getSession()).thenReturn(session); + + controller.voteUp(1L, request); } @Test(expectedExceptions = NotFoundException.class) public void voteDownShouldThrowExceptionIfPostNotFound() throws Exception { when(postService.get(anyLong())).thenThrow(new NotFoundException()); - controller.voteDown(1L); + when(request.getSession()).thenReturn(session); + + controller.voteDown(1L, request); + } + + @Test + public void testSaveDraft() throws Exception { + PostDraftDto dto = getPostDraftDto(); + Topic topic = new Topic(); + PostDraft saved = new PostDraft("content", new JCUser("name", null, null)); + saved.setId(1); + + when(topicFetchService.getTopicSilently(dto.getTopicId())).thenReturn(topic); + when(postService.saveOrUpdateDraft(topic, dto.getBodyText())).thenReturn(saved); + + JsonResponse response = controller.saveDraft(dto, result); + + assertEquals(response.getStatus(), JsonResponseStatus.SUCCESS); + assertEquals(1, (long)response.getResult()); + } + + @Test + public void saveDraftShouldReturnFailResponseIfValidationErrorsOccurred() throws Exception { + when(result.hasErrors()).thenReturn(true); + + JsonResponse response = controller.saveDraft(getPostDraftDto(), result); + + assertEquals(response.getStatus(), JsonResponseStatus.FAIL); + } + + @Test(expectedExceptions = NotFoundException.class) + public void saveDraftShouldThrowExceptionIfTopicNotFound() throws Exception { + PostDraftDto dto = getPostDraftDto(); + + when(topicFetchService.getTopicSilently(dto.getTopicId())).thenThrow(new NotFoundException()); + + controller.saveDraft(dto, result); + } + + @Test + public void testDeleteDraft() throws Exception { + + controller.deleteDraft(1l); + + verify(postService).deleteDraft(1l); } private class PostVoteMacher extends ArgumentMatcher<PostVote> { @@ -292,11 +364,8 @@ public PostVoteMacher(boolean isVotedUp) { @Override public boolean matches(Object o) { - if (o instanceof PostVote) { - //should be both true or both false - return !(isVotedUp ^ ((PostVote)o).isVotedUp()); - } - return false; + //should be both true or both false + return o instanceof PostVote && !(isVotedUp ^ ((PostVote) o).isVotedUp()); } } /* @Test @@ -337,4 +406,11 @@ private PostDto getDto() { dto.setTopicId(TOPIC_ID); return dto; } + + private PostDraftDto getPostDraftDto() { + PostDraftDto dto = new PostDraftDto(); + dto.setBodyText(POST_CONTENT); + dto.setTopicId(TOPIC_ID); + return dto; + } } diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PrivateMessageControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PrivateMessageControllerTest.java index 1007d0241e..3e2815e6b6 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PrivateMessageControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/PrivateMessageControllerTest.java @@ -14,12 +14,13 @@ */ package org.jtalks.jcommune.web.controller; +import org.jtalks.common.model.entity.User; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.PrivateMessage; import org.jtalks.jcommune.model.entity.PrivateMessageStatus; +import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.PrivateMessageService; import org.jtalks.jcommune.service.UserService; -import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.nontransactional.BBCodeService; import org.jtalks.jcommune.web.dto.PrivateMessageDto; import org.mockito.Mock; @@ -27,22 +28,32 @@ import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.servlet.ModelAndView; +import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import static io.qala.datagen.RandomShortApi.alphanumeric; +import static io.qala.datagen.RandomValue.between; +import static io.qala.datagen.StringModifier.Impls.prefix; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; import static org.springframework.test.web.ModelAndViewAssert.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.testng.Assert.assertEquals; /** @@ -50,10 +61,16 @@ * @author Max Malakhov * @author Alexandre Teterin * @author Evheniy Naumenko + * @author Oleg Tkachenko */ public class PrivateMessageControllerTest { - public static final long PM_ID = 2L; + private static final long PM_ID = 2L; + private static final String PAGE_NUMBER = "1"; + private static final JCUser JC_USER = getRandomJCUser(); + private static final String USERNAME = JC_USER.getUsername(); + private static final String TITLE = alphanumeric(PrivateMessage.MIN_TITLE_LENGTH, PrivateMessage.MAX_TITLE_LENGTH); + private static final String BODY = alphanumeric(PrivateMessage.MIN_MESSAGE_LENGTH, PrivateMessage.MAX_MESSAGE_LENGTH); private PrivateMessageController controller; @Mock @@ -63,14 +80,20 @@ public class PrivateMessageControllerTest { @Mock private UserService userService; - private static final String USERNAME = "username"; - private static final JCUser JC_USER = new JCUser(USERNAME, "123@123.ru", "123"); + private MockMvc mockMvc; + private Page<PrivateMessage> expectedPage; + + @BeforeClass + public void setUp(){ + expectedPage = new PageImpl<>(Collections.singletonList(getPrivateMessage())); + } @BeforeMethod public void init() { JC_USER.setId(1); MockitoAnnotations.initMocks(this); controller = new PrivateMessageController(pmService, bbCodeService, userService); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); } @Test @@ -82,19 +105,14 @@ public void testInitBinder() { @Test public void inboxPage() { - String page = "1"; - List<PrivateMessage> messages = Arrays.asList(new PrivateMessage(JC_USER, JC_USER, - "Message title", "Private message body")); - Page<PrivateMessage> expectedPage = new PageImpl<>(messages); - - when(pmService.getInboxForCurrentUser(page)).thenReturn(expectedPage); + when(pmService.getInboxForCurrentUser(PAGE_NUMBER)).thenReturn(expectedPage); when(userService.getCurrentUser()).thenReturn(JC_USER); //invoke the object under test - ModelAndView mav = controller.inboxPage(page); + ModelAndView mav = controller.inboxPage(PAGE_NUMBER); //check expectations - verify(pmService).getInboxForCurrentUser(page); + verify(pmService).getInboxForCurrentUser(PAGE_NUMBER); //check result assertViewName(mav, "pm/inbox"); @@ -103,19 +121,14 @@ public void inboxPage() { @Test public void outboxPage() { - String page = "1"; - List<PrivateMessage> messages = Arrays.asList(new PrivateMessage(JC_USER, JC_USER, - "Message title", "Private message body")); - Page<PrivateMessage> expectedPage = new PageImpl<>(messages); - - when(pmService.getOutboxForCurrentUser(page)).thenReturn(expectedPage); + when(pmService.getOutboxForCurrentUser(PAGE_NUMBER)).thenReturn(expectedPage); when(userService.getCurrentUser()).thenReturn(JC_USER); //invoke the object under test - ModelAndView mav = controller.outboxPage(page); + ModelAndView mav = controller.outboxPage(PAGE_NUMBER); //check expectations - verify(pmService).getOutboxForCurrentUser(page); + verify(pmService).getOutboxForCurrentUser(PAGE_NUMBER); //check result assertViewName(mav, "pm/outbox"); assertModelAttributeAvailable(mav, "outboxPage"); @@ -123,18 +136,13 @@ public void outboxPage() { @Test public void draftsPage() { - String page = "1"; - List<PrivateMessage> messages = Arrays.asList(new PrivateMessage(JC_USER, JC_USER, - "Message title", "Private message body")); - Page<PrivateMessage> expectedPage = new PageImpl<>(messages); - - when(pmService.getDraftsForCurrentUser(page)).thenReturn(expectedPage); + when(pmService.getDraftsForCurrentUser(PAGE_NUMBER)).thenReturn(expectedPage); //invoke the object under test - ModelAndView mav = controller.draftsPage(page); + ModelAndView mav = controller.draftsPage(PAGE_NUMBER); //check expectations - verify(pmService).getDraftsForCurrentUser(page); + verify(pmService).getDraftsForCurrentUser(PAGE_NUMBER); //check result assertViewName(mav, "pm/drafts"); assertModelAttributeAvailable(mav, "draftsPage"); @@ -179,7 +187,6 @@ public void sendMessage() throws NotFoundException { when(userService.getByUsername(USERNAME)).thenReturn(JC_USER); when(userService.getCurrentUser()).thenReturn(JC_USER); PrivateMessageDto dto = getPrivateMessageDto(); - dto.setRecipient(USERNAME); BindingResult bindingResult = new BeanPropertyBindingResult(dto, "privateMessageDto"); ModelAndView mav = controller.sendMessage(dto, bindingResult); @@ -193,7 +200,6 @@ public void sendDraftMessage() throws NotFoundException { when(userService.getByUsername(USERNAME)).thenReturn(JC_USER); when(userService.getCurrentUser()).thenReturn(JC_USER); PrivateMessageDto dto = getPrivateMessageDto(); - dto.setRecipient(USERNAME); dto.setId(4); BindingResult bindingResult = new BeanPropertyBindingResult(dto, "privateMessageDto"); @@ -318,31 +324,50 @@ public void editDraftPageNotDraft() throws NotFoundException { } @Test - public void saveDraft() throws NotFoundException { + public void saveDraftPost() throws Exception { when(userService.getByUsername(USERNAME)).thenReturn(JC_USER); when(userService.getCurrentUser()).thenReturn(JC_USER); - PrivateMessageDto dto = getPrivateMessageDto(); - dto.setRecipient(USERNAME); - BindingResult bindingResult = new BeanPropertyBindingResult(dto, "privateMessageDto"); - - String view = controller.saveDraft(dto, bindingResult); - assertEquals(view, "redirect:/drafts"); - verify(pmService).saveDraft(dto.getId(), USERNAME, dto.getTitle(), dto.getBody(), JC_USER); + saveDraft(TITLE, BODY, USERNAME) + .andExpect(model().hasNoErrors()) + .andExpect(status().isMovedTemporarily()) + .andExpect(redirectedUrl("/drafts")); + verify(pmService).saveDraft(PM_ID, JC_USER, TITLE, BODY, JC_USER); } @Test - public void saveDraftWithWrongUser() throws NotFoundException { - PrivateMessageDto dto = getPrivateMessageDto(); - doThrow(new NotFoundException()).when(pmService) - .saveDraft(anyLong(), anyString(), anyString(), anyString(), any(JCUser.class)); - BindingResult bindingResult = new BeanPropertyBindingResult(dto, "privateMessageDto"); + public void testSaveDraftGet() throws Exception { + when(userService.getByUsername(USERNAME)).thenReturn(JC_USER); + when(userService.getCurrentUser()).thenReturn(JC_USER); - String view = controller.saveDraft(dto, bindingResult); + mockMvc.perform(get("/pm/save") + .param("title", TITLE) + .param("body", BODY) + .param("recipient", USERNAME)) + .andExpect(model().hasNoErrors()) + .andExpect(status().isMovedTemporarily()) + .andExpect(redirectedUrl("/drafts")); + verify(pmService).saveDraft(0, JC_USER, TITLE, BODY, JC_USER); + } - assertEquals(view, "pm/pmForm"); - assertEquals(bindingResult.getErrorCount(), 1); - verify(pmService).saveDraft(anyLong(), anyString(), anyString(), anyString(), any(JCUser.class)); + @Test + public void saveDraftShouldBackToPmFormIfTitleIsInvalid() throws Exception { + String invalidTitle = alphanumeric(PrivateMessage.MAX_TITLE_LENGTH + 1); + saveDraft(invalidTitle, BODY, USERNAME) + .andExpect(model() + .attributeHasFieldErrors("privateMessageDto", "title")) + .andExpect(status().isOk()) + .andExpect(forwardedUrl("pm/pmForm")); + } + + @Test + public void saveDraftShouldDeleteDraftIfTitleAndBodyAreEmptyAndRecipientIsNotExists() throws Exception { + saveDraft("", "", null) + .andExpect(model() + .attributeHasErrors("privateMessageDto")) + .andExpect(view().name("redirect:/drafts")) + .andExpect(status().isMovedTemporarily()); + verify(pmService).delete(anyList()); } @Test @@ -358,14 +383,32 @@ public void testDeletePm() throws NotFoundException { private PrivateMessageDto getPrivateMessageDto() { PrivateMessageDto dto = new PrivateMessageDto(); - dto.setBody("body"); - dto.setTitle("title"); + dto.setBody(BODY); + dto.setTitle(TITLE); dto.setRecipient(USERNAME); return dto; } private PrivateMessage getPrivateMessage() { - return new PrivateMessage(new JCUser("username", "email", "password"), - new JCUser("username2", "email2", "password2"), "title", "body"); + return new PrivateMessage(JC_USER, + getRandomJCUser(), TITLE, BODY); + } + + private static JCUser getRandomJCUser() { + String username = alphanumeric(User.USERNAME_MIN_LENGTH, User.USERNAME_MAX_LENGTH); + String email = alphanumeric(1, 30) + + between(3, 15).with(prefix("@")).alphanumeric() + + between(3, 5).with(prefix(".")).english(); + String password = alphanumeric(User.PASSWORD_MIN_LENGTH, User.PASSWORD_MAX_LENGTH); + + return new JCUser(username, email, password); + } + + private ResultActions saveDraft(String title, String body, String recipient) throws Exception { + return this.mockMvc.perform(post("/pm/save") + .param("id", String.valueOf(PM_ID)) + .param("title", title) + .param("body", body) + .param("recipient", recipient)); } } diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ReadPostsControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ReadPostsControllerTest.java index a09a004461..c35544d9b6 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ReadPostsControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/ReadPostsControllerTest.java @@ -18,14 +18,19 @@ import org.jtalks.jcommune.model.entity.Branch; +import org.jtalks.jcommune.model.entity.Topic; import org.jtalks.jcommune.service.BranchService; import org.jtalks.jcommune.service.LastReadPostService; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.jtalks.jcommune.service.TopicFetchService; import org.mockito.Mock; import static org.mockito.MockitoAnnotations.initMocks; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; + +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.support.RetryTemplate; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -39,12 +44,18 @@ public class ReadPostsControllerTest { private BranchService branchService; @Mock private LastReadPostService lastReadPostService; + @Mock + private TopicFetchService topicFetchService; + private ReadPostsController controller; + RetryTemplate retryTemplate; @BeforeMethod public void init() { initMocks(this); - controller = new ReadPostsController(branchService, lastReadPostService); + retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); + controller = new ReadPostsController(branchService, lastReadPostService, retryTemplate, topicFetchService); } @Test @@ -74,4 +85,12 @@ public void markAllTopicsAsReadShouldMarkThemAndUpdatePage() throws NotFoundExce assertEquals(result, "redirect:/branches/" + String.valueOf(markedBranchId)); verify(lastReadPostService).markAllTopicsAsRead(willBeMarkedBranch); } + + @Test + public void markTopicAsReadByIdShouldMarkItAsRead() throws NotFoundException { + Topic topicToRead = new Topic(); + when(topicFetchService.get(1L)).thenReturn(topicToRead); + controller.markTopicPageAsReadById(1L, 10); + verify(lastReadPostService).markTopicPageAsRead(topicToRead, 10); + } } diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/SecurityControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/SecurityControllerTest.java index 7bffd1d8f5..2c7d301a92 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/SecurityControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/SecurityControllerTest.java @@ -15,8 +15,8 @@ package org.jtalks.jcommune.web.controller; import org.jtalks.jcommune.service.security.PermissionService; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.mockito.Mock; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/TopicControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/TopicControllerTest.java index 36c34e48c4..3d93426174 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/TopicControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/TopicControllerTest.java @@ -14,6 +14,7 @@ */ package org.jtalks.jcommune.web.controller; +import org.jtalks.common.model.entity.Section; import org.jtalks.jcommune.model.dto.PageRequest; import org.jtalks.jcommune.model.entity.*; import org.jtalks.jcommune.service.*; @@ -22,14 +23,16 @@ import org.jtalks.jcommune.plugin.api.web.dto.Breadcrumb; import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; import org.jtalks.jcommune.plugin.api.web.util.BreadcrumbBuilder; -import org.jtalks.jcommune.web.dto.EntityToDtoConverter; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; import org.mockito.Matchers; import org.mockito.Mock; import org.springframework.beans.propertyeditors.StringTrimmerEditor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.support.RetryTemplate; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.session.SessionRegistry; import org.springframework.validation.BeanPropertyBindingResult; @@ -69,6 +72,8 @@ public class TopicControllerTest { @Mock private TopicFetchService topicFetchService; @Mock + private TopicDraftService topicDraftService; + @Mock private PostService postService; @Mock private BranchService branchService; @@ -81,31 +86,37 @@ public class TopicControllerTest { @Mock private SessionRegistry registry; @Mock - private LastReadPostService lastReadPostService; - @Mock private EntityToDtoConverter converter; private TopicController controller; + @Mock + private BindingResult result; + + private RetryTemplate retryTemplate; @BeforeMethod public void initEnvironment() { initMocks(this); + retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); controller = new TopicController( topicModificationService, postService, branchService, - lastReadPostService, userService, breadcrumbBuilder, locationService, registry, topicFetchService, - converter); + topicDraftService, + converter, + retryTemplate); } @BeforeMethod public void prepareTestData() { branch = new Branch("", "description"); + branch.setSection(new Section("sectionname")); branch.setId(BRANCH_ID); user = new JCUser("username", "email@mail.com", "password"); } @@ -141,7 +152,6 @@ public void showTopicPageShouldShowListOfPostsWithUpdatedInfoAboutLastReadPosts( ModelAndView mav = controller.showTopicPage(request, TOPIC_ID, page); verify(topicFetchService).checkViewTopicPermission(topic.getBranch().getId()); - verify(lastReadPostService).markTopicPageAsRead(topic, Integer.valueOf(page)); // assertViewName(mav, "topic/postList"); assertAndReturnModelAttributeOfType(mav, "postsPage", Page.class); @@ -200,18 +210,52 @@ public void createTopicShouldNotPassAndMustShowTopicErrorIfItIsInvalid() throws BindingResult result = mock(BindingResult.class); when(result.hasErrors()).thenReturn(true); when(branchService.get(BRANCH_ID)).thenReturn(branch); - when(breadcrumbBuilder.getForumBreadcrumb(branch)).thenReturn(new ArrayList<Breadcrumb>()); + List<Breadcrumb> breadcrumbs = new BreadcrumbBuilder().getNewTopicBreadcrumb(branch); + when(breadcrumbBuilder.getNewTopicBreadcrumb(branch)).thenReturn(breadcrumbs); ModelAndView mav = controller.createTopic(getDto(), result, BRANCH_ID); verify(branchService).get(BRANCH_ID); - verify(breadcrumbBuilder).getForumBreadcrumb(branch); - // + verify(breadcrumbBuilder).getNewTopicBreadcrumb(branch); + + List<Breadcrumb> breadcrumbsFromModel = (List) mav.getModel().get("breadcrumbList"); + for (int i = 0; i < breadcrumbs.size(); i++) { + assertEquals(breadcrumbs.get(i).getValue(), breadcrumbsFromModel.get(i).getValue()); + } + assertEquals(breadcrumbsFromModel.get(1).getValue(), branch.getSection().getName()); + assertEquals(breadcrumbsFromModel.get(2).getValue(), branch.getName()); assertViewName(mav, "topic/topicForm"); long branchId = assertAndReturnModelAttributeOfType(mav, "branchId", Long.class); assertEquals(branchId, BRANCH_ID); } + @Test + public void testSaveDraft() throws Exception { + TopicDraft savedDraft = createTopicDraft(); + when(topicDraftService.saveOrUpdateDraft(savedDraft)).thenReturn(savedDraft); + + JsonResponse response = controller.saveDraft(savedDraft, result); + + assertEquals(response.getStatus(), JsonResponseStatus.SUCCESS); + assertEquals((long) response.getResult(), savedDraft.getId()); + } + + @Test + public void saveDraftShouldReturnFailResponseIfValidationErrorsOccurred() throws Exception { + when(result.hasErrors()).thenReturn(true); + + JsonResponse response = controller.saveDraft(createTopicDraft(), result); + + assertEquals(response.getStatus(), JsonResponseStatus.FAIL); + } + + @Test + public void testDeleteDraft() { + controller.deleteDraft(); + + verify(topicDraftService).deleteDraft(); + } + @Test public void showNewTopicPageShouldReturnTemplateForNewTopic() throws NotFoundException { when(branchService.get(BRANCH_ID)).thenReturn(branch); @@ -237,6 +281,22 @@ public void showNewTopicPageShouldReturnTemplateForNewTopic() throws NotFoundExc assertModelAttributeAvailable(mav, "breadcrumbList"); } + @Test + public void showNewTopicPageShouldShowDraft() throws NotFoundException { + Topic expectedTopic = createTopic(); + TopicDraft expectedDraft = new TopicDraft(user, expectedTopic.getTitle(), expectedTopic.getBodyText()); + + when(topicDraftService.getDraft()).thenReturn(expectedDraft); + + ModelAndView mav = controller.showNewTopicPage(BRANCH_ID); + + TopicDto topicDto = assertAndReturnModelAttributeOfType(mav, "topicDto", TopicDto.class); + + Topic topic = topicDto.getTopic(); + assertEquals(topic.getTitle(), expectedDraft.getTitle()); + assertEquals(topicDto.getBodyText(), expectedDraft.getContent()); + } + @Test public void editTopicPageShouldOpenEditFormAndNotEnableNotificationsIfUserNotSubcribed() throws NotFoundException { Topic topic = this.createTopic(); @@ -379,6 +439,17 @@ public void reopenTopic() throws NotFoundException { verify(topicModificationService).openTopic(topic); } + @Test + public void pageContainLinkToMarkAsRead() throws NotFoundException { + String page = "1"; + Topic topic = createTopic(); + prepareViewTopicMocks(topic, page); + + ModelAndView mav = controller.showTopicPage(mock(WebRequest.class), TOPIC_ID, page); + + assertModelAttributeAvailable(mav, "markAsReadLink"); + } + private Branch createBranch() { Branch branch = new Branch("branch name", "branch description"); branch.setId(BRANCH_ID); @@ -395,6 +466,14 @@ private Topic createTopic() { return topic; } + private TopicDraft createTopicDraft() { + TopicDraft topicDraft = new TopicDraft(user, "Topic theme", TOPIC_CONTENT); + topicDraft.setBranchId(BRANCH_ID); + topicDraft.setTopicType(TopicTypeName.DISCUSSION.getName()); + + return topicDraft; + } + private TopicDto getDto() { TopicDto dto = new TopicDto(); Topic topic = createTopic(); diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/TopicSearchControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/TopicSearchControllerTest.java index a8f79eb631..6706f0afcb 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/TopicSearchControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/TopicSearchControllerTest.java @@ -18,7 +18,7 @@ import org.jtalks.jcommune.plugin.api.web.dto.TopicDto; import org.jtalks.jcommune.service.LastReadPostService; import org.jtalks.jcommune.service.TopicFetchService; -import org.jtalks.jcommune.web.dto.EntityToDtoConverter; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.data.domain.Page; @@ -45,8 +45,8 @@ public class TopicSearchControllerTest { private TopicFetchService topicFetchService; @Mock private LastReadPostService lastReadPostService; - @Mock - private EntityToDtoConverter converter; + @Mock + private EntityToDtoConverter converter; private TopicSearchController topicSearchController; @@ -70,7 +70,7 @@ public void testInitSearch() { when(topicFetchService.searchByTitleAndContent(DEFAULT_SEARCH_TEXT, START_PAGE)) .thenReturn(searchResultPage); - when(converter.convertToDtoPage(searchResultPage)).thenReturn(searchDtoResultPage); + when(converter.convertTopicPageToTopicDtoPage(searchResultPage)).thenReturn(searchDtoResultPage); ModelAndView modelAndView = topicSearchController.initSearch(DEFAULT_SEARCH_TEXT, "1"); Map<String, Object> model = modelAndView.getModel(); @@ -89,7 +89,7 @@ public void testContinueSearch() { when(topicFetchService.searchByTitleAndContent(DEFAULT_SEARCH_TEXT, page)) .thenReturn(searchResultPage); - when(converter.convertToDtoPage(searchResultPage)).thenReturn(searchDtoResultPage); + when(converter.convertTopicPageToTopicDtoPage(searchResultPage)).thenReturn(searchDtoResultPage); ModelAndView modelAndView = topicSearchController.initSearch(DEFAULT_SEARCH_TEXT, page); Map<String, Object> model = modelAndView.getModel(); diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/UserControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/UserControllerTest.java index 6de5733cc2..1d85c692a3 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/UserControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/UserControllerTest.java @@ -16,7 +16,9 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; -import org.jtalks.common.service.security.SecurityContextHolderFacade; +import org.jtalks.common.model.entity.Component; +import org.jtalks.common.service.security.SecurityContextFacade; +import org.jtalks.jcommune.model.dto.LoginUserDto; import org.jtalks.jcommune.model.dto.RegisterUserDto; import org.jtalks.jcommune.model.dto.UserDto; import org.jtalks.jcommune.model.entity.AnonymousUser; @@ -24,28 +26,30 @@ import org.jtalks.jcommune.plugin.api.core.ExtendedPlugin; import org.jtalks.jcommune.plugin.api.core.RegistrationPlugin; import org.jtalks.jcommune.plugin.api.exceptions.NoConnectionException; +import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.plugin.api.exceptions.UnexpectedErrorException; -import org.jtalks.jcommune.service.Authenticator; -import org.jtalks.jcommune.service.PluginService; -import org.jtalks.jcommune.service.UserService; +import org.jtalks.jcommune.plugin.api.filters.TypeFilter; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponse; +import org.jtalks.jcommune.plugin.api.web.dto.json.JsonResponseStatus; +import org.jtalks.jcommune.service.*; import org.jtalks.jcommune.service.exceptions.MailingFailedException; -import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; import org.jtalks.jcommune.service.exceptions.UserTriesActivatingAccountAgainException; -import org.jtalks.jcommune.plugin.api.filters.TypeFilter; +import org.jtalks.jcommune.service.nontransactional.MailService; +import org.jtalks.jcommune.service.util.AuthenticationStatus; import org.jtalks.jcommune.web.dto.RestorePasswordDto; -import org.jtalks.jcommune.web.dto.json.JsonResponse; -import org.jtalks.jcommune.web.dto.json.JsonResponseStatus; import org.jtalks.jcommune.web.util.MutableHttpRequest; import org.jtalks.jcommune.web.validation.editors.DefaultStringEditor; import org.springframework.beans.propertyeditors.StringTrimmerEditor; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.support.RetryTemplate; import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.web.savedrequest.HttpSessionRequestCache; +import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.support.RequestContextUtils; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -55,14 +59,12 @@ import java.io.IOException; import java.util.List; import java.util.Locale; -import org.jtalks.jcommune.model.dto.LoginUserDto; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; import static org.springframework.test.web.ModelAndViewAssert.*; import static org.testng.Assert.*; -import static org.testng.Assert.assertEquals; /** * @author Evgeniy Naumenko @@ -73,27 +75,38 @@ public class UserControllerTest { private final String EMAIL = "mail@mail.com"; private UserController userController; private UserService userService; + private MailService mailService; private PluginService pluginService; private Authenticator authenticator; - private LocaleResolver localeResolver; - private RequestContextUtils requestContextUtils; private HttpServletRequest request; private HttpServletResponse response; + private RetryTemplate retryTemplate; + private ComponentService componentService; + private GroupService groupService; + private SpamProtectionService spamProtectionService; + private RequestCache requestCache; @BeforeMethod public void setUp() throws IOException { userService = mock(UserService.class); + mailService = mock(MailService.class); pluginService = mock(PluginService.class); authenticator = mock(Authenticator.class); - requestContextUtils = mock(RequestContextUtils.class); - localeResolver = mock(LocaleResolver.class, "en"); request = mock(HttpServletRequest.class); response = mock(HttpServletResponse.class); - SecurityContextHolderFacade securityFacade = mock(SecurityContextHolderFacade.class); + componentService = mock(ComponentService.class); + groupService = mock(GroupService.class); + retryTemplate = new RetryTemplate(); + spamProtectionService = mock(SpamProtectionService.class); + requestCache = new HttpSessionRequestCache(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); + SecurityContextFacade securityFacade = mock(SecurityContextFacade.class); SecurityContext securityContext = mock(SecurityContext.class); when(securityFacade.getContext()).thenReturn(securityContext); when(request.getHeader("X-FORWARDED-FOR")).thenReturn("192.168.1.1"); - userController = new UserController(userService, authenticator, pluginService, userService); + when(spamProtectionService.isEmailInBlackList(anyString())).thenReturn(false); + userController = new UserController(userService, authenticator, pluginService, userService, + mailService, retryTemplate, componentService, groupService, spamProtectionService, requestCache); } @Test @@ -115,7 +128,8 @@ public void testInitBinderDefaultStringEditor() { } @Test - public void testRegistrationPage() throws Exception { + public void userMustBeAbleToOpenRegistrationPage_ifHeIsNotLoggedIn() throws Exception { + when(userService.getCurrentUser()).thenReturn(new AnonymousUser()); ModelAndView mav = userController.registrationPage(null, null); assertViewName(mav, "registration"); @@ -123,6 +137,14 @@ public void testRegistrationPage() throws Exception { assertNullFields(dto); } + @Test + public void whenOpeningRegistrationPage_userMustBeRedirectedToMainPage_ifHeIsLoggedIn() throws Exception { + when(userService.getCurrentUser()).thenReturn(new JCUser("username", null, null)); + ModelAndView mav = userController.registrationPage(null, null); + + assertViewName(mav, "redirect:/"); + } + @Test public void testRegistrationFormWithoutAnyPluginShouldBeSuccessful() { JsonResponse response = userController.registrationForm(request, Locale.ENGLISH); @@ -311,33 +333,28 @@ public void testRestorePasswordFail() throws Exception { @Test public void testActivateAccount() throws Exception { - JCUser user = new JCUser("username", "password", null); user.setPassword("password"); when(userService.getByUuid(USER_NAME)).thenReturn(user); String viewName = userController.activateAccount(USER_NAME, request, response); - verify(userService, times(1)).activateAccount(USER_NAME); + verify(authenticator, times(1)).activateAccount(user.getUuid()); verify(userService, times(1)).loginUser(any(LoginUserDto.class), any(MutableHttpRequest.class), eq(response)); assertEquals("redirect:/", viewName); } @Test public void testActivateAccountFail() throws Exception { - doThrow(new NotFoundException()).when(userService).activateAccount(anyString()); - + doThrow(new NotFoundException()).when(userService).getByUuid(anyString()); String viewName = userController.activateAccount(USER_NAME, request, response); - assertEquals("errors/activationExpired", viewName); } @Test public void testActivateAccountAgain() throws Exception { - JCUser user = new JCUser("username", "password", null); user.setEnabled(true); when(userService.getByUuid(USER_NAME)).thenReturn(user); - doThrow(new UserTriesActivatingAccountAgainException()).when(userService).activateAccount(anyString()); - + doThrow(new UserTriesActivatingAccountAgainException()).when(authenticator).activateAccount(anyString()); String viewName = userController.activateAccount(USER_NAME, request, response); assertEquals("redirect:/", viewName); } @@ -347,7 +364,7 @@ public void testLoginUserLogged(String referer) { when(userService.getCurrentUser()).thenReturn(new JCUser("username", null, null)); when(request.getHeader("referer")).thenReturn(referer); - ModelAndView mav = userController.loginPage(request); + ModelAndView mav = userController.loginPage(request, response); assertEquals(mav.getViewName(), "redirect:" + referer); verify(userService).getCurrentUser(); @@ -357,7 +374,7 @@ public void testLoginUserLogged(String referer) { public void testLoginUserNotLogged() { when(userService.getCurrentUser()).thenReturn(new AnonymousUser()); - ModelAndView mav = userController.loginPage(request); + ModelAndView mav = userController.loginPage(request, response); assertEquals(mav.getViewName(), UserController.LOGIN); verify(userService).getCurrentUser(); @@ -375,7 +392,7 @@ public Object[][] referers() { @Test(enabled = false) public void testAjaxLoginSuccess() throws Exception { when(userService.loginUser(any(LoginUserDto.class), any(HttpServletRequest.class), - any(HttpServletResponse.class))).thenReturn(true); + any(HttpServletResponse.class))).thenReturn(AuthenticationStatus.AUTHENTICATED); JsonResponse response = userController.loginAjax(null, null, "on", null, null); assertEquals(response.getStatus(), JsonResponseStatus.SUCCESS); @@ -387,7 +404,7 @@ public void testAjaxLoginSuccess() throws Exception { @Test public void testAjaxLoginFailure() throws Exception { when(userService.loginUser(any(LoginUserDto.class),any(HttpServletRequest.class), - any(HttpServletResponse.class))).thenReturn(false); + any(HttpServletResponse.class))).thenReturn(AuthenticationStatus.AUTHENTICATION_FAIL); JsonResponse response = userController.loginAjax(null, null, "on", request, null); assertEquals(response.getStatus(), JsonResponseStatus.FAIL); verify(userService).loginUser(any(LoginUserDto.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); @@ -417,7 +434,7 @@ public void testLoginAjaxUserShouldFailIfUnexpectedErrorOccurred() throws Except public void testLoginWithCorrectParametersShouldBeSuccessful() throws Exception { LoginUserDto loginUserDto = new LoginUserDto("userName", "password", true, "192.168.1.1"); when(userService.loginUser(loginUserDto, any(HttpServletRequest.class), any(HttpServletResponse.class))) - .thenReturn(true); + .thenReturn(AuthenticationStatus.AUTHENTICATED); ModelAndView view = userController.login(loginUserDto, "on", null, request, null); @@ -428,12 +445,11 @@ public void testLoginWithCorrectParametersShouldBeSuccessful() throws Exception @Test public void testLoginWithIncorrectParametersShouldFail() throws Exception { when(userService.loginUser(any(LoginUserDto.class), any(HttpServletRequest.class), any(HttpServletResponse.class))) - .thenReturn(false); + .thenReturn(AuthenticationStatus.AUTHENTICATION_FAIL); LoginUserDto loginUserDto = new LoginUserDto(); - ModelAndView view = userController.login(loginUserDto, null, "on", request, null); + userController.login(loginUserDto, null, "on", request, null); -// assertEquals(view.getViewName(), "login"); verify(userService).loginUser(any(LoginUserDto.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); } @@ -456,7 +472,7 @@ public void testLoginUserShouldFailIfUnexpectedErrorOccurred() throws Exception .thenThrow(new UnexpectedErrorException()); LoginUserDto loginUserDto = new LoginUserDto(); - ModelAndView view = userController.login(loginUserDto, null, "on", request, null); + ModelAndView view = userController.login(loginUserDto, null, "on", request, null); assertEquals(view.getViewName(), UserController.AUTH_SERVICE_FAIL_URL); verify(userService).loginUser(eq(loginUserDto), @@ -473,6 +489,105 @@ public void testGetUsernameListSuccess() { assertEquals(response.getStatus(), JsonResponseStatus.SUCCESS); } + @Test + public void testSearchUsersNullSearchKey() { + Component component = createDefaultComponent(); + + when(componentService.getComponentOfForum()).thenReturn(component); + + ModelAndView mav = userController.searchUsers(null); + + verify(componentService).checkPermissionsForComponent(component.getId()); + assertEquals(mav.getViewName(), UserController.USER_SEARCH); + assertFalse(mav.getModel().containsKey(UserController.USERS_ATTR_NAME)); + } + + private Component createDefaultComponent() { + Component component = new Component(); + component.setId(1); + return component; + } + + @Test + public void testSearchUsersEmptySearchKey() { + Component component = createDefaultComponent(); + + when(componentService.getComponentOfForum()).thenReturn(component); + + ModelAndView mav = userController.searchUsers(""); + + verify(componentService).checkPermissionsForComponent(component.getId()); + assertEquals(mav.getViewName(), UserController.USER_SEARCH); + assertFalse(mav.getModel().containsKey(UserController.USERS_ATTR_NAME)); + } + + @Test + public void testSearchUsers() { + Component component = createDefaultComponent(); + String searchKey = "key"; + List<JCUser> users = Lists.asList(new JCUser("user", "email@email.com", "pwd"), new JCUser[0]); + + when(componentService.getComponentOfForum()).thenReturn(component); + when(userService.findByUsernameOrEmail(component.getId(), searchKey)).thenReturn(users); + + ModelAndView mav = userController.searchUsers(searchKey); + + assertEquals(mav.getViewName(), UserController.USER_SEARCH); + assertEquals(mav.getModel().get(UserController.USERS_ATTR_NAME), users); + } + + @Test + public void searchUsersShouldTrimSearchKey() { + Component component = createDefaultComponent(); + String searchKey = " key "; + + when(componentService.getComponentOfForum()).thenReturn(component); + + userController.searchUsers(searchKey); + + verify(userService).findByUsernameOrEmail(component.getId(), searchKey.trim()); + } + + @Test + public void getUserGroups() throws NotFoundException { + long userID = 1l; + Component component = createDefaultComponent(); + + when(componentService.getComponentOfForum()).thenReturn(component); + + userController.userGroups(userID); + + verify(userService).getUserGroupIDs(component.getId(), userID); + } + + @Test + public void addUserToGroup() throws Exception { + long userID = 1l; + long groupID = 2l; + + Component component = createDefaultComponent(); + + when(componentService.getComponentOfForum()).thenReturn(component); + + userController.addUserToGroup(userID, groupID); + + verify(userService).addUserToGroup(component.getId(), userID, groupID); + } + + @Test + public void deleteUserFromGroup() throws Exception { + long userID = 1l; + long groupID = 2l; + + Component component = createDefaultComponent(); + + when(componentService.getComponentOfForum()).thenReturn(component); + + userController.deleteUserFromGroup(userID, groupID); + + verify(userService).deleteUserFromGroup(component.getId(), userID, groupID); + } + private void assertNullFields(RegisterUserDto dto) { assertNull(dto.getUserDto()); assertNull(dto.getPasswordConfirm()); diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/UserProfileControllerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/UserProfileControllerTest.java index 8456eea142..e71b0d7699 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/UserProfileControllerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/controller/UserProfileControllerTest.java @@ -20,6 +20,7 @@ import org.jtalks.jcommune.service.PostService; import org.jtalks.jcommune.service.UserContactsService; import org.jtalks.jcommune.service.UserService; +import org.jtalks.jcommune.service.dto.EntityToDtoConverter; import org.jtalks.jcommune.service.dto.UserInfoContainer; import org.jtalks.jcommune.service.dto.UserNotificationsContainer; import org.jtalks.jcommune.service.dto.UserSecurityContainer; @@ -35,6 +36,8 @@ import org.springframework.data.domain.PageImpl; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.retry.policy.NeverRetryPolicy; +import org.springframework.retry.support.RetryTemplate; import org.springframework.security.access.AccessDeniedException; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; @@ -97,6 +100,10 @@ public class UserProfileControllerTest { private ImageService imageService; @Mock private RedirectAttributes redirectAttributes; + @Mock + private EntityToDtoConverter converter; + + private RetryTemplate retryTemplate; @BeforeClass public void mockAvatar() { @@ -106,13 +113,17 @@ public void mockAvatar() { @BeforeMethod public void setUp() { initMocks(this); + retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); profileController = new UserProfileController( userService, breadcrumbBuilder, imageConverter, postService, userContactsService, - imageService); + imageService, + converter, + retryTemplate); } @Test @@ -534,6 +545,7 @@ public void showUserPostListShouldShowThemToUser() throws NotFoundException { ModelAndView mav = profileController.showUserPostList(user.getId(), "1"); verify(userService).get(user.getId()); + verify(converter).convertPostPageToPostDtoPage(any(Page.class)); assertViewName(mav, "userPostList"); assertModelAttributeAvailable(mav, "user"); assertModelAttributeAvailable(mav, "breadcrumbList"); diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/exception/PrettyLogExceptionResolverTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/exception/PrettyLogExceptionResolverTest.java index 4d189f1e33..f900c33100 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/exception/PrettyLogExceptionResolverTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/exception/PrettyLogExceptionResolverTest.java @@ -17,7 +17,10 @@ import org.apache.commons.logging.Log; import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; +import org.mockito.InOrder; +import org.springframework.beans.TypeMismatchException; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.util.ReflectionUtils; @@ -43,12 +46,27 @@ public void setUp() throws Exception { @Test public void testLogExceptionWithIncomingNotFoundException() throws Exception { Log mockLog = replaceLoggerWithMock(prettyLogExceptionResolver); - NotFoundException notFoundException = new NotFoundException("Entity not found"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + NotFoundException notFoundException = new NotFoundException("Entity not found"); + String logMessage = String.format("[%s][%s][%s][%s]", request.getMethod(), request.getRequestURL().toString(), + request.getHeader("Cookie"), "Entity not found"); request.setContent("".getBytes()); prettyLogExceptionResolver.logException(notFoundException, request); - verify(mockLog).info("Entity not found"); + verify(mockLog).info(logMessage); + } + + @Test + public void testLogExceptionWithIncomingTypeMismatchException() throws Exception { + Log mockLog = replaceLoggerWithMock(prettyLogExceptionResolver); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + TypeMismatchException typeMismatchException = new TypeMismatchException("Not a number", Number.class); + String logMessage = String.format("[%s][%s][%s][%s]", request.getMethod(), request.getRequestURL().toString(), + request.getHeader("Cookie"), typeMismatchException.getMessage()); + request.setContent("".getBytes()); + prettyLogExceptionResolver.logException(typeMismatchException, request); + + verify(mockLog).info(logMessage); } @Test @@ -79,9 +97,25 @@ public void testLogExceptionWithoutNotFoundException() throws Exception { verify(mockLog, times(1)).info(anyString()); } + @Test + public void testResolveExceptionFirstlyLogExceptionThenDebug() throws Exception { + Log mockLog = replaceLoggerWithMock(prettyLogExceptionResolver); + Exception exception = new Exception(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + MockHttpServletResponse response = new MockHttpServletResponse(); + Object handler = new Object(); + request.setContent("".getBytes()); + prettyLogExceptionResolver.resolveException(request, response, handler, exception); + + InOrder inOrder = inOrder(mockLog); + inOrder.verify(mockLog, times(1)).info(anyString()); + inOrder.verify(mockLog, times(1)).debug(request,exception); + } + private Log replaceLoggerWithMock(PrettyLogExceptionResolver resolver) throws Exception { Log mockLog = mock(Log.class); Field loggerField = ReflectionUtils.findField(PrettyLogExceptionResolver.class, "logger"); + assert loggerField != null; loggerField.setAccessible(true); loggerField.set(resolver, mockLog); return mockLog; diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/interceptors/LocalInterceptorTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/interceptors/LocalInterceptorTest.java index 947bbf3fb0..1e37656f27 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/interceptors/LocalInterceptorTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/interceptors/LocalInterceptorTest.java @@ -23,9 +23,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import static org.jgroups.util.Util.assertTrue; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; +import static org.testng.Assert.assertTrue; /** * diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/interceptors/PropertiesInterceptorTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/interceptors/PropertiesInterceptorTest.java index 46106455ee..1e034f37f6 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/interceptors/PropertiesInterceptorTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/interceptors/PropertiesInterceptorTest.java @@ -14,21 +14,21 @@ */ package org.jtalks.jcommune.web.interceptors; -import org.hamcrest.MatcherAssert; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsEqual.equalTo; import org.joda.time.LocalDateTime; import org.jtalks.jcommune.model.dao.ComponentDao; import org.jtalks.jcommune.model.entity.JCommuneProperty; import org.mockito.Mock; import org.springframework.web.servlet.ModelAndView; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; import static org.mockito.MockitoAnnotations.initMocks; import static org.springframework.test.web.ModelAndViewAssert.assertAndReturnModelAttributeOfType; -import static org.testng.Assert.assertNull; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; /** * @author Vyacheslav Mishcheryakov @@ -41,13 +41,19 @@ public class PropertiesInterceptorTest { private static final String PARAM_SAPE_SHOW_DUMMY_LINKS = "sapeShowDummyLinks"; private static final String PARAM_LOGO_TOOLTIP = "logoTooltip"; private static final String PARAM_LAST_INFO_CHAGE = "infoChangeDate"; + private static final String PARAM_SESSION_TIMEOUT = "sessionTimeout"; + private static final String PARAM_AVATAR_MAX_SIZE = "avatarMaxSize"; + private static final String PARAM_EMAIL_NOTIFICATION = "emailNotification"; private static final String CMP_NAME = PARAM_CMP_NAME; private static final String CMP_DESCRIPTION = PARAM_CMP_DESCRIPTION; private static final boolean SAPE_SHOW_DUMMY_LINKS = false; private static final String LOGO_TOOLTIP = PARAM_LOGO_TOOLTIP; private static final String LAST_CHANGE_DATE = "2013.01.10 00:00:00"; - + private static final String SESSION_TIMEOUT = "24"; + private static final String AVATAR_MAX_SIZE = "1234"; + private static final boolean EMAIL_NOTIFICATION = true; + private JCommuneProperty cmpName = JCommuneProperty.CMP_NAME; private JCommuneProperty cmpDescription = JCommuneProperty.CMP_DESCRIPTION; private JCommuneProperty sapeShowDummyLinks = JCommuneProperty.CMP_SAPE_SHOW_DUMMY_LINKS; @@ -55,6 +61,9 @@ public class PropertiesInterceptorTest { private JCommuneProperty lastChangeDate = JCommuneProperty.ADMIN_INFO_LAST_UPDATE_TIME; private JCommuneProperty titlePrefix = JCommuneProperty.ALL_PAGES_TITLE_PREFIX; private JCommuneProperty copyright = JCommuneProperty.COPYRIGHT; + private JCommuneProperty sessionTimeout = JCommuneProperty.SESSION_TIMEOUT; + private JCommuneProperty avatarMaxSize = JCommuneProperty.AVATAR_MAX_SIZE; + private JCommuneProperty sendingNotificationsEnabled = JCommuneProperty.SENDING_NOTIFICATIONS_ENABLED; @Mock private ComponentDao componentDao; @@ -65,8 +74,10 @@ public class PropertiesInterceptorTest { public void init() { initMocks(this); - propertiesInterceptor = new PropertiesInterceptor(cmpName, - cmpDescription, sapeShowDummyLinks, logoToolTip, lastChangeDate, titlePrefix, copyright); + propertiesInterceptor = new PropertiesInterceptor( + cmpName, cmpDescription, sapeShowDummyLinks, logoToolTip, lastChangeDate, titlePrefix, copyright, + sessionTimeout, avatarMaxSize, sendingNotificationsEnabled + ); cmpName.setName("cmp.name"); cmpDescription.setName("cmp.description"); @@ -74,6 +85,9 @@ public void init() { logoToolTip.setName("sape.show.dummy.links"); lastChangeDate.setName("last.change.date"); titlePrefix.setName("prefix_title"); + sessionTimeout.setName("jcommune.session_timeout"); + avatarMaxSize.setName("jcommune.avatar_max_size"); + sendingNotificationsEnabled.setName("jcommune.sending_notifications_enabled"); cmpName.setDefaultValue(CMP_NAME); cmpDescription.setDefaultValue(CMP_DESCRIPTION); @@ -82,6 +96,9 @@ public void init() { lastChangeDate.setDefaultValue(LAST_CHANGE_DATE); titlePrefix.setDefaultValue("prefix of the title"); copyright.setDefaultValue("My Copyright {current_year}"); + sessionTimeout.setDefaultValue(SESSION_TIMEOUT); + avatarMaxSize.setDefaultValue(AVATAR_MAX_SIZE); + sendingNotificationsEnabled.setDefaultValue(String.valueOf(EMAIL_NOTIFICATION)); cmpName.setComponentDao(componentDao); cmpDescription.setComponentDao(componentDao); @@ -89,6 +106,9 @@ public void init() { logoToolTip.setComponentDao(componentDao); lastChangeDate.setComponentDao(componentDao); titlePrefix.setComponentDao(componentDao); + sessionTimeout.setComponentDao(componentDao); + avatarMaxSize.setComponentDao(componentDao); + sendingNotificationsEnabled.setComponentDao(componentDao); } @@ -102,6 +122,9 @@ public void testPostHandleNormal() { boolean showDummyLinks = assertAndReturnModelAttributeOfType(mav, PARAM_SAPE_SHOW_DUMMY_LINKS, Boolean.class); String logoTooltip = assertAndReturnModelAttributeOfType(mav, PARAM_LOGO_TOOLTIP, String.class); String lastChangeDate = assertAndReturnModelAttributeOfType(mav, PARAM_LAST_INFO_CHAGE, String.class); + String sessionTimeout = assertAndReturnModelAttributeOfType(mav, PARAM_SESSION_TIMEOUT, String.class); + String avatarMaxSize = assertAndReturnModelAttributeOfType(mav, PARAM_AVATAR_MAX_SIZE, String.class); + boolean emailNotification = assertAndReturnModelAttributeOfType(mav, PARAM_EMAIL_NOTIFICATION, Boolean.class); String titlePrefixProperty = assertAndReturnModelAttributeOfType(mav, "cmpTitlePrefix", String.class); String copyrightValue = assertAndReturnModelAttributeOfType(mav, "userDefinedCopyright", String.class); @@ -110,6 +133,9 @@ public void testPostHandleNormal() { assertEquals(showDummyLinks, SAPE_SHOW_DUMMY_LINKS); assertEquals(logoTooltip, LOGO_TOOLTIP); assertEquals(lastChangeDate, LAST_CHANGE_DATE); + assertEquals(sessionTimeout, SESSION_TIMEOUT); + assertEquals(avatarMaxSize, AVATAR_MAX_SIZE); + assertEquals(emailNotification, EMAIL_NOTIFICATION); assertEquals(titlePrefixProperty, "prefix of the title"); assertThat(copyrightValue, equalTo("My Copyright " + getCurrentYear())); } @@ -119,9 +145,9 @@ public void testPostHandleMavIsNull() { propertiesInterceptor.postHandle(null, null, null, null); } - @Test - public void testPostHandleRedirectRequest() { - ModelAndView mav = new ModelAndView("redirect:/somewhere"); + @Test(dataProvider = "redirectAndErrorHandleViewNames") + public void testPostHandleRedirectAndErrorRequest(String viewName) { + ModelAndView mav = new ModelAndView(viewName); propertiesInterceptor.postHandle(null, null, null, mav); assertNull(mav.getModel().get(PARAM_CMP_NAME)); @@ -129,14 +155,22 @@ public void testPostHandleRedirectRequest() { assertNull(mav.getModel().get(PARAM_SAPE_SHOW_DUMMY_LINKS)); assertNull(mav.getModel().get(PARAM_LOGO_TOOLTIP)); assertNull(mav.getModel().get(PARAM_LAST_INFO_CHAGE)); + assertNull(mav.getModel().get(PARAM_SESSION_TIMEOUT)); + assertNull(mav.getModel().get(PARAM_AVATAR_MAX_SIZE)); + assertNull(mav.getModel().get(PARAM_EMAIL_NOTIFICATION)); assertNull(mav.getModel().get("cmpTitlePrefix")); assertNull(mav.getModel().get("userDefinedCopyright")); } + @DataProvider + public Object[][] redirectAndErrorHandleViewNames() { + return new Object[][] { + {"redirect:/somewhere"}, + {"/errors/errorcode"} + }; + } + private int getCurrentYear() { return new LocalDateTime().getYear(); } - - - } diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/listeners/LoggerInitializationListenerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/listeners/LoggerInitializationListenerTest.java new file mode 100644 index 0000000000..42d31ec5d5 --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/listeners/LoggerInitializationListenerTest.java @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.web.listeners; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.InputStream; +import java.net.URL; + +import org.mockejb.jndi.MockContextFactory; +import static org.mockito.AdditionalMatchers.not; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertEquals; + +/** + * @author Evgeny Kapinos + */ +public class LoggerInitializationListenerTest { + + private Context tomcatContext; + private InputStream emptyDatasourcePropertesFile; + private LoggerInitializationListener sut; + private ServletContextEvent servletContextEvent; + private String testFile; + private URL testFileURI; + + @BeforeClass + public void setUpCommonTestData() throws Exception { + MockContextFactory.setAsInitial(); + tomcatContext = new MockContextFactory().getInitialContext(null); + new InitialContext().bind("java:/comp/env", tomcatContext); + + emptyDatasourcePropertesFile = new ByteArrayInputStream(new byte[0]); + + ServletContext servletContext = mock(ServletContext.class); + doNothing().when(servletContext).log(anyString()); + doNothing().when(servletContext).log(anyString(), any(Throwable.class)); + servletContextEvent = mock(ServletContextEvent.class); + doReturn(servletContext).when(servletContextEvent).getServletContext(); + + testFile = "/somepath/log4j.xml"; + testFileURI = new File(testFile).toURI().toURL(); + } + + @BeforeMethod + public void setUpCurrentTest() throws Exception { + sut = spy(new LoggerInitializationListener()); + doReturn(true).when(sut).configureLog4j(any(URL.class)); + emptyDatasourcePropertesFile.reset(); + doReturn(emptyDatasourcePropertesFile).when(sut).getPropertiesFileStream(); + tomcatContext.unbind(LoggerInitializationListener.LOG4J_CONFIGURATION_FILE); + System.clearProperty(LoggerInitializationListener.LOG4J_CONFIGURATION_FILE); // filled after all tests + } + + @Test + public void shouldLookForConfigurationInJndi() throws Exception { + tomcatContext.bind(LoggerInitializationListener.LOG4J_CONFIGURATION_FILE, testFile); + sut.contextInitialized(servletContextEvent); + verify(sut).configureLog4j(eq(testFileURI)); + verify(sut, never()).configureLog4j(not(eq(testFileURI))); + } + + @Test + public void shouldLookForConfigurationInDataSourceClass() throws Exception { + byte[] is = (LoggerInitializationListener.LOG4J_CONFIGURATION_FILE+"="+testFile).getBytes("UTF-8"); + InputStream datasourcePropertesFile = new ByteArrayInputStream(is); + doReturn(datasourcePropertesFile).when(sut).getPropertiesFileStream(); + sut.contextInitialized(servletContextEvent); + verify(sut).configureLog4j(eq(testFileURI)); + verify(sut, never()).configureLog4j(not(eq(testFileURI))); + } + + @Test + public void shouldLookForConfigurationInSystemProperties() throws Exception { + System.setProperty(LoggerInitializationListener.LOG4J_CONFIGURATION_FILE, testFile); + sut.contextInitialized(servletContextEvent); + verify(sut).configureLog4j(eq(testFileURI)); + verify(sut, never()).configureLog4j(not(eq(testFileURI))); + } + + @Test + public void shouldLoadDefaultConfiguration() throws Exception { + sut.contextInitialized(servletContextEvent); + verify(sut, never()).configureLog4j(eq(testFileURI)); + verify(sut).configureLog4j(not(eq(testFileURI))); + } + + @Test + public void shouldReturnLog4jOverrideProperty() throws Exception { + System.setProperty("log4j.defaultInitOverride", "salt"); + sut.contextInitialized(servletContextEvent); + assertEquals("salt", System.getProperty("log4j.defaultInitOverride")); + } +} \ No newline at end of file diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/listeners/SessionSetupListenerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/listeners/SessionSetupListenerTest.java index 51a77b3a35..c362daec1d 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/listeners/SessionSetupListenerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/listeners/SessionSetupListenerTest.java @@ -18,7 +18,6 @@ import org.jtalks.jcommune.model.dao.PropertyDao; import org.jtalks.jcommune.model.entity.JCommuneProperty; import org.mockito.Mock; -import org.mockito.Mockito; import org.springframework.mock.web.MockHttpSession; import org.springframework.web.context.WebApplicationContext; import org.testng.annotations.BeforeMethod; @@ -62,7 +61,7 @@ public void setUp() throws Exception { sessionTimeoutProperty.setPropertyDao(propertyDao); sessionTimeoutProperty.setName(PROPERTY_NAME); - when(context.getBean(Mockito.anyString())).thenReturn(sessionTimeoutProperty); + when(context.getBean("sessionTimeoutProperty")).thenReturn(sessionTimeoutProperty); session = new MockHttpSession(); session.getServletContext().setAttribute( @@ -70,6 +69,7 @@ public void setUp() throws Exception { context); event = new HttpSessionEvent(session); + SessionSetupListener.resetSessionTimeoutProperty(); } @Test diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/logging/LoggingConfigurationFilterTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/logging/LoggingConfigurationFilterTest.java index d327fa91c0..f979f9a4ec 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/logging/LoggingConfigurationFilterTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/logging/LoggingConfigurationFilterTest.java @@ -14,7 +14,7 @@ */ package org.jtalks.jcommune.web.logging; -import org.jtalks.common.security.SecurityService; +import org.jtalks.jcommune.service.security.SecurityService; import org.jtalks.jcommune.web.filters.LoggingConfigurationFilter; import org.mockito.Mock; import org.springframework.mock.web.MockFilterChain; diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/rememberme/RememberMeCookieDecoderImplTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/rememberme/RememberMeCookieDecoderImplTest.java index 86ccb25359..19d369259f 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/rememberme/RememberMeCookieDecoderImplTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/rememberme/RememberMeCookieDecoderImplTest.java @@ -14,15 +14,15 @@ */ package org.jtalks.jcommune.web.rememberme; -import static org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNull; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.crypto.codec.Base64; +import org.testng.annotations.Test; import javax.servlet.http.Cookie; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.security.core.codec.Base64; -import org.testng.annotations.Test; +import static org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; /** diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/ExistsValidatorTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/ExistsValidatorTest.java index f771de1bdc..c7509f648d 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/ExistsValidatorTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/ExistsValidatorTest.java @@ -16,6 +16,7 @@ import org.jtalks.common.model.entity.Entity; import org.jtalks.jcommune.model.dao.ValidatorDao; +import org.jtalks.jcommune.web.validation.annotations.Exists; import org.jtalks.jcommune.web.validation.validators.ExistenceValidator; import org.mockito.Matchers; import org.mockito.Mock; @@ -38,6 +39,8 @@ public class ExistsValidatorTest { private ExistenceValidator validator; @Mock private ValidatorDao<String> dao; + @Mock + private Exists existsAnnotation; @BeforeMethod public void init() { @@ -63,4 +66,12 @@ public void testValueDoesntExist() { public void testNullValue() { assertFalse(validator.isValid(null, null)); } + + @Test + public void testNullValueIfWhenNullableAllowed() { + when(existsAnnotation.isNullableAllowed()).thenReturn(true); + validator.initialize(existsAnnotation); + + assertTrue(validator.isValid(null, null)); + } } diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/ValidUserContactValidatorTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/ValidUserContactValidatorTest.java deleted file mode 100644 index 44e07449d2..0000000000 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/ValidUserContactValidatorTest.java +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Copyright (C) 2011 JTalks.org Team - * This library 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 2.1 of the License, or (at your option) any later version. - * This library 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 this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package org.jtalks.jcommune.web.validation; - -import static org.mockito.MockitoAnnotations.initMocks; -import static org.mockito.Mockito.when; - -import org.jtalks.jcommune.model.entity.UserContactType; -import org.jtalks.jcommune.service.UserContactsService; -import org.jtalks.jcommune.plugin.api.exceptions.NotFoundException; -import org.jtalks.jcommune.web.validation.annotations.ValidUserContact; -import org.jtalks.jcommune.web.validation.validators.ValidUserContactValidator; -import org.mockito.Mock; -import org.testng.Assert; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -import javax.validation.ConstraintValidatorContext; - -/** - * Test for {@link ValidUserContact} annotation constraint and - * {@link ValidUserContactValidator} implementation. - * - * @author Vyachelsav Mishcheryakov - */ -public class ValidUserContactValidatorTest { - - /** Valid value for tests */ - private final static String VALID_VALUE = "value"; - - private final static String WRONG_VALUE = "valu"; - - /** Pattern for valid value */ - private final static String VALID_PATTERN = "^value$"; - - /** Id of valid contact type */ - private final static long TYPE_ID_VALID = 1; - - /** Id of contact type with null pattern */ - private final static long TYPE_ID_NULL_PATTERN = 2; - - /** Id of non-existent contact type */ - private final static long TYPE_ID_NON_EXISTENT = -1; - - @Mock - private UserContactsService contactsService; - - @Mock - private ConstraintValidatorContext validatorContext; - - private ValidUserContactValidator validator; - - /** - * Class for testing constraint. - */ - @ValidUserContact(field = "value", storedTypeId = "typeId", message = "Values don't match") - public class SimpleTestObject { - String value; - long typeId; - - public SimpleTestObject(String value, long typeIdValid) { - this.value = value; - this.typeId = typeIdValid; - } - - public String getValue() { - - return value; - } - - public long getTypeId() { - return typeId; - } - } - - /** - * Class for testing constraint with non existing properties. - */ - @ValidUserContact(field = "aaa", storedTypeId = "bbb") - public class TestObjectBadProperties { - String value; - long typeId; - - public TestObjectBadProperties(String value, long typeIdValid) { - this.value = value; - this.typeId = typeIdValid; - } - - public String getValue() { - - return value; - } - - public long getTypeId() { - return typeId; - } - } - - /** - * Initialize validator with annotation got from <code>clazz</code> - * @param validator validator - * @param clazz target class for validation - * @return - */ - private void initializeValidator(ValidUserContactValidator validator, Class<?> clazz) { - ValidUserContact annotation = (ValidUserContact) - clazz.getDeclaredAnnotations()[0]; - validator.initialize(annotation); - } - - @BeforeMethod - public void setUp() throws NotFoundException { - initMocks(this); - validator = new ValidUserContactValidator(); - - UserContactType validContactType = new UserContactType(); - validContactType.setId(TYPE_ID_VALID); - validContactType.setValidationPattern(VALID_PATTERN); - UserContactType nullPatternContactType = new UserContactType(); - nullPatternContactType.setId(TYPE_ID_NULL_PATTERN); - nullPatternContactType.setValidationPattern(null); - when(contactsService.get(TYPE_ID_VALID)).thenReturn(validContactType); - when(contactsService.get(TYPE_ID_NULL_PATTERN)).thenReturn(nullPatternContactType); - when(contactsService.get(TYPE_ID_NON_EXISTENT)).thenThrow(new NotFoundException()); - validator = new ValidUserContactValidator(); - validator.setContactsService(contactsService); - } - - @Test - public void testValidatorSuccess() { - initializeValidator(validator, SimpleTestObject.class); - boolean result = validator.isValid( - new SimpleTestObject(VALID_VALUE, TYPE_ID_VALID), - validatorContext); - - Assert.assertEquals(result, true, "Validation errors"); - } - - @Test - public void testValidatorFail() { - initializeValidator(validator, SimpleTestObject.class); - boolean result = validator.isValid( - new SimpleTestObject(WRONG_VALUE, TYPE_ID_VALID), - validatorContext); - - Assert.assertEquals(result, false, "Validation without errors"); - } - - @Test(expectedExceptions = IllegalStateException.class) - public void testWrongField() { - initializeValidator(validator, TestObjectBadProperties.class); - validator.isValid(new TestObjectBadProperties("aaa", TYPE_ID_VALID), validatorContext); - } - - @Test(expectedExceptions = IllegalStateException.class) - public void testPatternObjectNotExist() { - initializeValidator(validator, TestObjectBadProperties.class); - validator.isValid(new TestObjectBadProperties("", TYPE_ID_NON_EXISTENT), validatorContext); - } - - @Test - public void testValueIsNull() { - initializeValidator(validator, SimpleTestObject.class); - - boolean result = validator.isValid( - new SimpleTestObject(null, TYPE_ID_VALID), - validatorContext); - - Assert.assertEquals(result, false, "Validation without errors"); - } - - @Test - public void testPatternIsNull() { - initializeValidator(validator, SimpleTestObject.class); - boolean result = validator.isValid( - new SimpleTestObject(VALID_VALUE, TYPE_ID_NULL_PATTERN), - validatorContext); - - Assert.assertEquals(result, true, "Validation errors"); - } - -} diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/validators/AtLeastOneNotEmptyValidatorTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/validators/AtLeastOneNotEmptyValidatorTest.java new file mode 100644 index 0000000000..5dd6de1a0c --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/validators/AtLeastOneNotEmptyValidatorTest.java @@ -0,0 +1,99 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.web.validation.validators; + +import org.jtalks.jcommune.web.validation.annotations.AtLeastOneNotEmpty; +import org.mockito.Mock; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * @author Mikhail Stryzhonok + */ +public class AtLeastOneNotEmptyValidatorTest { + + @Mock + private AtLeastOneNotEmpty annotation; + + @BeforeMethod + public void setUp() { + initMocks(this); + } + + @Test + public void validationShouldNotPassIfAllFieldsNull() { + AtLeastOneNotEmptyValidator validator = new AtLeastOneNotEmptyValidator(); + + when(annotation.fieldNames()).thenReturn(new String[]{"field1", "field2"}); + validator.initialize(annotation); + + assertFalse(validator.isValid(new TestBean(null, null), null)); + } + + @Test + public void validationShouldNotPassIfAllFieldsEmpty() { + AtLeastOneNotEmptyValidator validator = new AtLeastOneNotEmptyValidator(); + + when(annotation.fieldNames()).thenReturn(new String[]{"field1", "field2"}); + validator.initialize(annotation); + + assertFalse(validator.isValid(new TestBean(" ", " "), null)); + } + + @Test + public void validationShouldPassIfAtLeastOneFieldNotEmpty() { + AtLeastOneNotEmptyValidator validator = new AtLeastOneNotEmptyValidator(); + + when(annotation.fieldNames()).thenReturn(new String[]{"field1", "field2"}); + validator.initialize(annotation); + + assertTrue(validator.isValid(new TestBean("test", " "), null)); + } + + @Test + public void validationShouldPassIfAtLeastOneFieldNotNull() { + AtLeastOneNotEmptyValidator validator = new AtLeastOneNotEmptyValidator(); + + when(annotation.fieldNames()).thenReturn(new String[]{"field1", "field2"}); + validator.initialize(annotation); + + assertTrue(validator.isValid(new TestBean("test", null), null)); + } + + @Test + public void validationShouldPassIfAllFieldsNotEmpty() { + AtLeastOneNotEmptyValidator validator = new AtLeastOneNotEmptyValidator(); + + when(annotation.fieldNames()).thenReturn(new String[]{"field1", "field2"}); + validator.initialize(annotation); + + assertTrue(validator.isValid(new TestBean("test", "test"), null)); + } + + private static class TestBean { + private String field1; + private String field2; + + public TestBean(String field1, String field2) { + this.field1 = field1; + this.field2 = field2; + } + } +} diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/validators/ValidatorStubTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/validators/ValidatorStubTest.java new file mode 100644 index 0000000000..4ffb4b33a7 --- /dev/null +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/validation/validators/ValidatorStubTest.java @@ -0,0 +1,97 @@ +/** + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.jtalks.jcommune.web.validation.validators; + +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.Errors; +import org.testng.annotations.Test; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertTrue; + +/** + * @author Mikhail Stryzhonok + */ +public class ValidatorStubTest { + + @Test + public void supportsShouldReturnTrue() { + ValidatorStub validator = new ValidatorStub(); + + assertTrue(validator.supports(Object.class)); + } + + @Test + public void validateShouldRejectAllSpecifiedFields() { + String[] fields = new String[]{"field1", "field2", "field3"}; + ValidatorStub validator = new ValidatorStub(fields); + TestBean testObject = new TestBean(); + Errors errors = new BeanPropertyBindingResult(testObject, "object"); + + validator.validate(testObject, errors); + + assertEquals(3, errors.getErrorCount()); + for (String field : fields) { + assertTrue("Errors should contain field error for " + field, errors.hasFieldErrors(field)); + } + } + + @Test + public void validateShouldNotRejectFieldsIfNoErrorFieldSpecified() { + ValidatorStub validator = new ValidatorStub(); + TestBean testObject = new TestBean(); + Errors errors = new BeanPropertyBindingResult(testObject, "object"); + + validator.validate(testObject, errors); + + assertFalse(errors.hasErrors()); + } + + @Test + public void validateShouldSetGlobalErrorsIfBeanHasGlobalErrors() { + ValidatorStub validator = new ValidatorStub(); + validator.setGlobalError(); + TestBean testBean = new TestBean(); + Errors errors = new BeanPropertyBindingResult(testBean, "object"); + + validator.validate(testBean, errors); + + assertTrue(errors.hasGlobalErrors()); + } + + private static class TestBean { + private String field1; + private String field2; + private String field3; + private String field4; + + public String getField1() { + return field1; + } + + public String getField2() { + return field2; + } + + public String getField3() { + return field3; + } + + public String getField4() { + return field4; + } + } +} diff --git a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/view/RssViewerTest.java b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/view/RssViewerTest.java index 60fb84fad1..c4665c0aa1 100644 --- a/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/view/RssViewerTest.java +++ b/jcommune-view/jcommune-web-controller/src/test/java/org/jtalks/jcommune/web/view/RssViewerTest.java @@ -15,8 +15,8 @@ package org.jtalks.jcommune.web.view; -import com.sun.syndication.feed.rss.Channel; import com.sun.syndication.feed.rss.Item; +import com.sun.syndication.feed.rss.Channel; import org.jtalks.common.model.entity.Component; import org.jtalks.jcommune.model.entity.JCUser; import org.jtalks.jcommune.model.entity.Post; @@ -34,9 +34,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertTrue; +import static org.testng.Assert.*; /** * @author Andrey Kluev @@ -47,8 +45,7 @@ public class RssViewerTest { private Channel channel; private MockHttpServletRequest request; private MockHttpServletResponse response; - private Map<String, Object> model; - private Topic topic; + private Map<String, Object> newsComponents; @BeforeMethod protected void setUp() { @@ -58,34 +55,30 @@ protected void setUp() { rssViewer.setContentType("application/rss+xml;charset=UTF-8"); rssViewerMock = mock(RssViewer.class); channel = new Channel(); - model = new HashMap<String, Object>(); - List<Topic> topics = new ArrayList<Topic>(); - JCUser user = new JCUser("username", "email", "password"); - user.setSignature("Signature"); - Post post = new Post(user, "sagjalighjh eghjwhjslhjsdfhdfhljdfh"); - topic = new Topic(user, ""); - topic.addPost(post); - topic.setId(1L); - topics.add(topic); - topics.add(topic); - model.put("topics", topics); + newsComponents = getNewsComponents(); } - + @Test public void testBuildFeedItems() throws Exception { - List<Item> items = rssViewer.buildFeedItems(model, request, response); + List<Item> items = rssViewer.buildFeedItems(newsComponents, request, response); assertEquals(items.get(0).getAuthor(), "username"); - assertEquals(items.get(0).getComments(), "Signature"); assertEquals(response.getContentType(), rssViewer.getContentType()); } + @Test + public void testFeedBodyMustStripInvalidXMLSymbols() throws Exception { + newsComponents.put("topics", getTopicsWithXMLSpecChars()); + List<Item> items = rssViewer.buildFeedItems(newsComponents, request, response); + assertTrue(containtsOnlyValidXMLChars(items.get(0).getDescription().getValue())); + } + @Test public void testRedirect() throws IOException { - model.put("topics", null); + newsComponents.put("topics", null); - assertEquals(rssViewer.buildFeedItems(model, request, response), null); + assertEquals(rssViewer.buildFeedItems(newsComponents, request, response), null); } @Test @@ -95,9 +88,9 @@ public void rssShouldBeGeneratedWithMetaDataFromComponent() throws Exception { String description = "my description"; component.setName(name); component.setDescription(description); - model.put("forumComponent", component); + newsComponents.put("forumComponent", component); - rssViewer.buildFeedMetadata(model, channel, request); + rssViewer.buildFeedMetadata(newsComponents, channel, request); assertFalse(channel.equals(new Channel())); assertEquals(channel.getTitle(), name); assertEquals(channel.getDescription(), description); @@ -105,7 +98,7 @@ public void rssShouldBeGeneratedWithMetaDataFromComponent() throws Exception { @Test public void rssShouldBeGeneratedWithEmptyFeedMetaDataWhenThereIsNoComponent() { - rssViewer.buildFeedMetadata(model, channel, request); + rssViewer.buildFeedMetadata(newsComponents, channel, request); assertTrue(channel.getTitle().isEmpty()); assertTrue(channel.getDescription().isEmpty()); } @@ -113,11 +106,48 @@ public void rssShouldBeGeneratedWithEmptyFeedMetaDataWhenThereIsNoComponent() { @Test public void testRssFeed() throws Exception { - rssViewerMock.buildFeedMetadata(model, channel, request); - rssViewerMock.buildFeedItems(model, request, response); + rssViewerMock.buildFeedMetadata(newsComponents, channel, request); + rssViewerMock.buildFeedItems(newsComponents, request, response); - verify(rssViewerMock).buildFeedMetadata(model, channel, request); - verify(rssViewerMock).buildFeedItems(model, request, response); + verify(rssViewerMock).buildFeedMetadata(newsComponents, channel, request); + verify(rssViewerMock).buildFeedItems(newsComponents, request, response); } + private boolean containtsOnlyValidXMLChars(String stringToValidate) { + String pattern = "[^" + + "\u0009\r\n" + + "\u0020-\uD7FF" + + "\uE000-\uFFFD" + + "\ud800\udc00-\udbff\udfff" + + "]"; + String resultString = stringToValidate.replaceAll(pattern, ""); + return resultString.equals(stringToValidate); + } + + private List<Topic> getTopicsWithXMLSpecChars(){ + JCUser user = new JCUser("username", "email", "password"); + List<Topic> topicsWithSpecChars = new ArrayList<>(); + Post postWithSpecChars = new Post(user, "����\u000F���"); + Topic topicWithSpecChars = new Topic(user, ""); + topicWithSpecChars.addPost(postWithSpecChars); + topicWithSpecChars.setId(2L); + topicsWithSpecChars.add(topicWithSpecChars); + + return topicsWithSpecChars; + } + private Map<String, Object> getNewsComponents() { + Map<String, Object> newsComponents = new HashMap<>(); + List<Topic> topics = new ArrayList<>(); + JCUser user = new JCUser("username", "email", "password"); + user.setSignature("Signature"); + Post post = new Post(user, "sagjalighjh eghjwhjslhjsdfhdfhljdfh"); + Topic topic = new Topic(user, ""); + topic.addPost(post); + topic.setId(1L); + topics.add(topic); + topics.add(topic); + newsComponents.put("topics", topics); + + return newsComponents; + } } diff --git a/jcommune-view/jcommune-web-view/pom.xml b/jcommune-view/jcommune-web-view/pom.xml index ecfe90dc2c..06693c58da 100644 --- a/jcommune-view/jcommune-web-view/pom.xml +++ b/jcommune-view/jcommune-web-view/pom.xml @@ -1,10 +1,11 @@ <?xml version="1.0"?> -<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"> +<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"> <modelVersion>4.0.0</modelVersion> <parent> <artifactId>jcommune-view</artifactId> <groupId>org.jtalks.jcommune</groupId> - <version>2.14-SNAPSHOT</version> + <version>3.13-SNAPSHOT</version> </parent> <artifactId>jcommune-web-view</artifactId> <packaging>war</packaging> @@ -90,16 +91,7 @@ <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> </dependency> - <dependency> - <groupId>org.testng</groupId> - <artifactId>testng</artifactId> - <exclusions> - <exclusion> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - </exclusion> - </exclusions> - </dependency> + <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> @@ -116,34 +108,330 @@ <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> </dependency> + <dependency> + <groupId>org.spockframework</groupId> + <artifactId>spock-core</artifactId> + </dependency> + <dependency> + <groupId>org.codehaus.groovy</groupId> + <artifactId>groovy-all</artifactId> + </dependency> + <dependency> + <groupId>org.spockframework</groupId> + <artifactId>spock-spring</artifactId> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + </dependency> + <dependency> + <groupId>com.jayway.jsonpath</groupId> + <artifactId>json-path</artifactId> + </dependency> + <dependency> + <groupId>com.jayway.jsonpath</groupId> + <artifactId>json-path-assert</artifactId> + </dependency> + <dependency> + <groupId>io.qala.datagen</groupId> + <artifactId>qala-datagen</artifactId> + </dependency> + <dependency> + <groupId>org.unitils</groupId> + <artifactId>unitils-core</artifactId> + </dependency> </dependencies> <build> <plugins> <plugin> - <groupId>ro.isdc.wro4j</groupId> - <artifactId>wro4j-maven-plugin</artifactId> - <version>1.7.5</version> + <groupId>org.codehaus.gmavenplus</groupId> + <artifactId>gmavenplus-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.primefaces.extensions</groupId> + <artifactId>resources-optimizer-maven-plugin</artifactId> + <version>2.1.0</version> <executions> <execution> <phase>generate-resources</phase> + <id>optimize</id> <goals> - <goal>run</goal> + <goal>optimize</goal> </goals> </execution> </executions> <configuration> - <targetGroups>main,pm,cr,post,user,plugin,topic</targetGroups> - <minimize>true</minimize> - <destinationFolder>${basedir}/src/main/webapp/resources/wro</destinationFolder> - <cssDestinationFolder>${basedir}/src/main/webapp/resources/wro</cssDestinationFolder> - <jsDestinationFolder>${basedir}/src/main/webapp/resources/wro</jsDestinationFolder> - <contextFolder>${basedir}/src/main/webapp/</contextFolder> - <wroFile>${basedir}/wro.xml</wroFile> - <wroManagerFactory> - ro.isdc.wro.extensions.manager.standalone.GoogleStandaloneManagerFactory - </wroManagerFactory> - <ignoreMissingResources>false</ignoreMissingResources> + <inputDir>${basedir}/src/main/webapp</inputDir> + <languageIn>ECMASCRIPT5</languageIn> + <sourceMap> + <create>true</create> + <sourceMapRoot> + sourcemaps/ + </sourceMapRoot> + <outputDir> + ${project.build.directory}/${project.build.finalName}/resources/compressed/javascript/sourcemaps/ + </outputDir> + </sourceMap> + <resourcesSets> + <resourcesSet> + <includes> + <!--SCREEN - CSS--> + <include>resources/css/app/editor.css</include> + <include>resources/css/lib/fonts-googleapis-com.css</include> + <include>resources/css/lib/bootstrap.css</include> + <include>resources/css/lib/bootstrap-responsive.css</include> + <include>resources/css/lib/prettify.css</include> + <include>resources/css/lib/prettyPhoto.css</include> + <include>resources/css/lib/inline.css</include> + <include>resources/css/app/application.css</include> + + <!--LIBS - CSS--> + <include>/resources/css/lib/jquery.contextMenu.css</include> + <include>/resources/css/lib/jquery-ui.css</include> + <include>/resources/css/lib/chosen.css</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/css/main.css + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + + <resourcesSet> + <includes> + <!--LIBS - JS--> + <include>resources/javascript/lib/jquery/jquery-1.7.min.js</include> + <include>resources/javascript/lib/jquery/jquery.truncate.js</include> + <include>resources/javascript/lib/jquery/jquery-ui.min.js</include> + <include>resources/javascript/lib/jquery/jquery-ui-i18n.min.js</include> + <include>resources/javascript/lib/jquery/jquery.prettyPhoto.js</include> + <include>resources/javascript/lib/jquery/contextmenu/*.js</include> + <include>resources/javascript/lib/prettify/prettify.js</include> + <include>resources/javascript/lib/prettify/lang-*.js</include> + <include>resources/javascript/lib/wysiwyg-bbcode/*.js</include> + <include>resources/javascript/lib/*.js</include> + + <!--MAIN - JS--> + <include>resources/javascript/app/keymaps.js</include> + <include>resources/javascript/app/dialog.js</include> + <include>resources/javascript/app/mainLinksEditor.js</include> + <include>resources/javascript/app/URLBuilder.js</include> + <include>resources/javascript/app/registration.js</include> + <include>resources/javascript/app/signin.js</include> + <include>resources/javascript/app/global.js</include> + <include>resources/javascript/app/antimultipost.js</include> + <include>resources/javascript/app/errorUtils.js</include> + <include>resources/javascript/app/utils.js</include> + <include>resources/javascript/app/dropdown.js</include> + <include>resources/javascript/app/forumEffects.js</include> + <include>resources/javascript/app/topline.js</include> + <include>resources/javascript/app/search.js</include> + <include>resources/javascript/app/events.js</include> + <include>resources/javascript/app/banner.js</include> + <include>resources/javascript/app/forumAdministration.js</include> + <include>resources/javascript/app/editPermissions.js</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/javascript/main.js + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + + <resourcesSet> + <includes> + <!--PM - JS--> + <include>resources/javascript/app/privateMessages.js</include> + <include>resources/javascript/app/updateSaveButtonStateOnPmForm.js</include> + <include>resources/javascript/app/leaveConfirm.js</include> + <include>resources/javascript/app/contextMenu.js</include> + <include>resources/javascript/lib/purl.js</include> + <include>resources/javascript/app/pollPreview.js</include> + <include>resources/javascript/app/codeHighlighting.js</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/javascript/pm.js + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + + <resourcesSet> + <includes> + <!--CR - JS--> + <include>resources/javascript/app/leaveConfirm.js</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/javascript/cr.js + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + + <resourcesSet> + <includes> + <!--POST - JS--> + <include>resources/javascript/app/leaveConfirm.js</include> + <include>resources/javascript/app/bbeditorEffects.js</include> + <include>resources/javascript/app/contextMenu.js</include> + <include>resources/javascript/app/pollPreview.js</include> + <include>resources/javascript/lib/purl.js</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/javascript/post.js + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + + <resourcesSet> + <includes> + <!--USER - CSS--> + <include>resources/css/app/profile.css</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/css/user.css + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + + <resourcesSet> + <includes> + <!--USER - JS--> + <include>resources/javascript/app/avatarUpload.js</include> + <include>resources/javascript/app/contacts.js</include> + <include>resources/javascript/app/userProfileEffects.js</include> + <include>resources/javascript/app/contextMenu.js</include> + <include>resources/javascript/app/codeHighlighting.js</include> + <include>resources/javascript/app/registration.js</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/javascript/user.js + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + <resourcesSet> + <includes> + <!--USERS - JS--> + <include>resources/javascript/app/userSearch.js</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/javascript/users.js + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + <resourcesSet> + <includes> + <!--PLUGIN - JS--> + <include>resources/javascript/app/utils.js</include> + <include>resources/javascript/app/permissionService.js</include> + <include>resources/javascript/app/pluginConfiguration.js</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/javascript/plugin.js + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + <resourcesSet> + <includes> + <!--TOPIC - JS--> + <include>resources/javascript/app/datepicker.js</include> + <include>resources/javascript/app/pollPreview.js</include> + <include>resources/javascript/app/leaveConfirm.js</include> + <include>resources/javascript/app/contextMenu.js</include> + <include>resources/javascript/app/bbeditorEffects.js</include> + <include>resources/javascript/app/utils.js</include> + <include>resources/javascript/app/subscription.js</include> + <include>resources/javascript/app/moveTopic.js</include> + <include>resources/javascript/app/poll.js</include> + <include>resources/javascript/app/codeHighlighting.js</include> + <include>resources/javascript/app/permissionService.js</include> + <include>resources/javascript/lib/purl.js</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/javascript/topic.js + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + <resourcesSet> + <includes> + <!--POSTDRAFT - JS--> + <include>resources/javascript/app/draft.js</include> + <include>resources/javascript/app/postDraft.js</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/javascript/postDraft.js + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + <resourcesSet> + <includes> + <!--TOPICDRAFT - JS--> + <include>resources/javascript/app/draft.js</include> + <include>resources/javascript/app/topicDraft.js</include> + </includes> + + <aggregations> + <aggregation> + <outputFile> + ${project.build.directory}/${project.build.finalName}/resources/compressed/javascript/topicDraft.js + </outputFile> + <removeIncluded>false</removeIncluded> + </aggregation> + </aggregations> + </resourcesSet> + </resourcesSets> </configuration> </plugin> <plugin> @@ -183,7 +471,10 @@ as well as prepare-package phase and goal exploded in execution--> <useCache>true</useCache> <packagingExcludes> - resources/css/app/**,resources/css/lib/**,resources/javascript/**, + <!--Exclude compressed resources--> + **/resources/css/app/**, + **/resources/css/lib/**, + **/resources/javascript/**, <!-- Ignore jsp-classes compiled by jspc --> WEB-INF/classes/jsp/** </packagingExcludes> @@ -240,7 +531,7 @@ <!--Remove all new line characters(Windows and Unix)--> <replacement> <token>\r\n|\n</token> - <value /> + <value/> </replacement> <replacement> <!--Removes spaces, tabs and line breaks after " or ' --> @@ -289,7 +580,45 @@ </dependencies> </plugin> </plugins> - + <pluginManagement> + <plugins> + <plugin> + <groupId>org.codehaus.gmavenplus</groupId> + <artifactId>gmavenplus-plugin</artifactId> + <version>1.2</version> + <configuration> + <testSources> + <fileset> + <directory>${project.basedir}/src/test/java</directory> + <includes> + <include>**/*.groovy</include> + </includes> + </fileset> + </testSources> + <header>${project.name}</header> + <scope>package</scope> + </configuration> + <executions> + <execution> + <goals> + <goal>testCompile</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </pluginManagement> + <testResources> + <testResource> + <!-- WEB-INF folder marked as test resource directory to provide possibility to access to + spring-dispatcher-servlet.xml and security-context.xml from component tests--> + <directory>${project.basedir}/src/main/webapp/WEB-INF</directory> + </testResource> + <testResource> + <directory>${project.basedir}/src/test/resources</directory> + </testResource> + </testResources> + <testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory> <finalName>jcommune</finalName> </build> diff --git a/jcommune-view/jcommune-web-view/src/main/resources/log4j.xml b/jcommune-view/jcommune-web-view/src/main/resources/log4j.xml index 70140bd6ec..034bd1fb99 100644 --- a/jcommune-view/jcommune-web-view/src/main/resources/log4j.xml +++ b/jcommune-view/jcommune-web-view/src/main/resources/log4j.xml @@ -30,10 +30,10 @@ --> <appender name="fileAppender" class="org.apache.log4j.RollingFileAppender"> <param name="File" value="${catalina.home}/logs/jcommune.log"/> - <param name="MaxFileSize" value="5MB"/> + <param name="MaxFileSize" value="40MB"/> <param name="Encoding" value="UTF-8"/> <!--How many old log files to keep--> - <param name="MaxBackupIndex" value="5"/> + <param name="MaxBackupIndex" value="15"/> <layout class="org.apache.log4j.EnhancedPatternLayout"> <!-- The username is set by MDC feature in a web filter. Therefore we have username logged even in guts diff --git a/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/model/datasource.properties b/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/model/datasource.properties index ee29d95c3f..19dc84f0f1 100644 --- a/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/model/datasource.properties +++ b/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/model/datasource.properties @@ -2,10 +2,9 @@ #Other properties that can be read only from this properties file are using such.usual_convention jdbc.driverClassName=com.mysql.jdbc.Driver -JCOMMUNE_DB_URL=jdbc:mysql://localhost:3306/jtalks?characterEncoding=UTF-8 +JCOMMUNE_DB_URL=jdbc:mysql://localhost:3306/jtalks #JCOMMUNE_DB_USER=root #JCOMMUNE_DB_PASSWORD= -EH_CACHE_CONFIG=/org/jtalks/jcommune/model/entity/ehcache.xml encoding=utf-8 hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect hibernate.hbm2ddl.auto=validate diff --git a/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_en.properties b/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_en.properties index d924ea3de9..393f0c4639 100644 --- a/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_en.properties +++ b/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_en.properties @@ -7,8 +7,8 @@ label.forum=Forum label.topic=Topic label.author=Author label.date=Date -label.addtopic=New Topic -label.addtopic.tip=Create a plain discussion if you have a question to ask or a topic to discuss +label.addtopic=Start Discussion +label.addtopic.tip=Create a plain discussion if you have topic to discuss label.addCodeReview=New Code Review label.addCodeReview.tip=Post your smelly source code if you want others to share their opinion on it. Reviewers will be able to comment each line. label.click_language=Click to change language @@ -54,8 +54,10 @@ label.answer.options=Options label.answer_to=Answer to the topic label.answer.title_label=Answer! label.answer.font_size=Font size -label.answer.font_code=Add syntax highlight -label.answer.indent=Size of margin +label.answer.font_code=Insert code +label.answer.font_code.button=Code +label.answer.indent=Add indent +label.answer.indent.button=Size of margin label.answer.none=None label.answer.font_size.tiny=Tiny label.answer.font_size.small=Small @@ -69,7 +71,8 @@ label.answer.bold=Bold (Ctrl+B) label.answer.italic=Italic (Ctrl+I) label.answer.underline=Underlined (Ctrl+U) label.answer.striked=Striked (Ctrl+S) -label.answer.highlight=Add highlight +label.answer.highlight=Add background color +label.answer.highlight.button=Highlight label.answer.align_left=Align left label.answer.align_center=Align center label.answer.align_right=Align right @@ -85,10 +88,11 @@ label.yes=Yes label.cancel=Cancel label.ok=OK label.deletePostConfirmation=Are you sure you want to delete this Post? -label.deleteCommentConfirmation=Are you sure you want to delete this Comment? -label.deleteTopicConfirmation=Are you sure you want to delete the first post (topic would be deleted too)? +label.deleteCommentConfirmation=Are you sure you want to delete this comment? +label.deleteTopicConfirmation=Are you sure you want to delete the first post (topic will be deleted too)? label.deleteContactConfirmation=Delete this contact? label.deleteContactFailture=Due to an error contact has not been deleted +label.deleteCodeReviewConfirmation=Are you sure you want to delete code review (comments will be deleted too)? label.deletePMConfirmation=Are you sure you want to delete checked messages? label.deletePMGroupConfirmation=Are you sure you want to delete checked messages? %s message(s) will be deleted #login and register page @@ -109,10 +113,10 @@ label.firstname=First name label.lastname=Last name label.confirmation=Confirm password label.captcha=Enter a numbers from picture -label.tip.username=Username, 1-25 characters +label.tip.username=Username label.tip.email=Email -label.tip.password=Password, 1-50 characters -label.tip.confirmation=Repeat password +label.tip.password=Password +label.tip.confirmation=Confirm password label.tip.captcha=Captcha text label.tip.honeypot.captcha=Please leave this field empty label.login_error=Invalid username or password @@ -133,6 +137,7 @@ label.auto_logon=Remember me label.registration.success=A message with activation link was sent to the specified e-mail. label.registration.success.title=You have been successfully registered label.registration.failture=Sorry. Unexpected error has occurred during registration. Please try again later. +label.email.confirmation=Your account is not activated. If you didn't receive activation link, press {0}Send Activation Link Again{1}. label.honeypot.not.null=Invalid registration request. label.registration.connection.error=Authentication service is not available right now. Please, contact administrator or try later. label.authentication.connection.error=Authentication failed. Authentication service is not available right now. Please contact administrator or try later. @@ -277,11 +282,14 @@ label.back2main=go to forum main page label.404.title=Page not found label.404.detail=We're unable to find the page you are looking for label.404.checkurl=Please check is page address is valid or +label.405.title=Method not supported +label.405.detail=The method is not applicable to the resource specified in the request. +label.405.checkurl=Please check that page address is valid or method that you use is applicable for specified resource. label.500.title=Server error -label.500.detail=We're sorry\! The server encountered an internal error or misconfiguration and was unable to complete your request. +label.500.detail=We\'re sorry\! The server encountered an internal error or misconfiguration and was unable to complete your request. label.500.refresh=Please try again later or label.accessDenied.title=Access denied -label.accessDenied.detail=You don't have enough privilegies to access this resource or perform this operation. +label.accessDenied.detail=You don\'t have enough privilegies to access this resource or perform this operation. label.accessDenied.checkPrivilegies=Ensure you have enough privilegies or label.400.title=Bad request label.400.detail=I don't understand you. Given request can't be processed, since it have some error(s). @@ -317,11 +325,12 @@ label.poll.vote=Vote label.poll.option.vote.info= {0} - {1}% label.poll.title.with.ending=(Voting stops at {0}) label.poll.message.error=Please select an answer. +label.poll.deleteEndingDate=Delete Ending date #url/img form label.url.header=Please enter fields values to set URL: label.img.header=Please enter fields values to set image: label.url.text=Text to display -label.url=URL link +label.url=Specify a link label.url.required=Required field label.url.info=If this field is empty, then as the text to display will be the link @@ -397,6 +406,8 @@ label.addReviewComment=Add comment label.administration=Administration label.administration.enter=Administration Mode label.administration.exit=Exit Administration +label.administration.userGroups=User Groups +label.administration.groupUserList=Users in group label.forum.description=Forum description label.forum.title=Forum title label.titlePrefix=Prefix for the title of every page @@ -412,6 +423,7 @@ label.uploadFavIcon=Upload Fav Icon label.deleteFavIcon=Remove Fav Icon label.deleteIconConfirmation=Are you sure you want to reset fav icon to default? label.dummyTextBBCode=Insert your text here +label.sessionTimeout.hint=Session timeout #Plugins label.plugins=Plugins @@ -462,3 +474,67 @@ permissions.group.load.failed=Failed to load group lists user.security.message.changed_password=Your password has successfully changed copy.link.to.clipboard.popup.title = Copy link to clipboard + +label.saved.just.now=Saved just now +label.connection.lost=Connection to the server was lost, please save your text locally. +label.saved=Saved +label.ago=ago +label.seconds=seconds +label.minute=minute +label.minute.1.suffix= +label.hour=hour +label.minutes.2.4.suffix=s +label.hours.2.4.suffix=s +label.hours.more.than.4.suffix=s +label.minutes.more.than.4.suffix=s +label.hours.1.suffix= +label.11.minutes.suffix=s +label.11.hours.suffix=s +label.minute.one.at.the.end.suffix=s +label.hour.one.at.the.end.suffix=s +label.not.logged.in.error=It seems that you no longer have permissions to post in this branch. Save your text and reload page, probably you are no longer logged in. + +label.registration.success.1= +label.registration.success.2=s + +label.search.user=Search user +label.search.user.empty=No users found +label.user.groups=Groups +label.user.groups.select.placeHolder=Select a group... +label.user.group.add.error=Error on add group +label.user.group.delete.error=Error on delete group + +label.connection.lost.genericError=Connection to the server was lost +label.user.groups.no.matches=No matches found for +label.not.logged.in.genericError=We're sorry, an error has occurred. Please refresh the page or login and try again. +label.sessionTimeout=Session timeout +label.avatarMaxSize=Avatar max size +label.emailNotification=Email notifications +label.avatarMaxSize.hint=Avatar max size + +label.group.name=Name +label.group.numberOfMembers=Number of Members +label.group.creation=Create new +label.group.placeholder.name=Group name +label.group.placeholder.description=Group description +label.group.create.title=New group +label.group.edit.title=Edit +label.group.user.name=Username +label.group.user.email=Email + +label.group.delete.message=Are you sure you want to delete user group? Users will not be deleted. +label.group.add.user=Adding user + +label.spamProtection.title=Spam protection +label.spamRule.regex=Rule regex +label.spamRule.description=Rule description +label.spamProtection.column.rules=Rules +label.spamProtection.column.description=Descriptions +label.spamRule.delete=Are you sure you want to delete this rule? +label.spamProtection.block.message=Sorry, you use restricted email. If you are not a bot - write to administrator (email at the bottom of every page). +label.spamProtection.settings=Spam protection settings +label.spamRule.add=Add rule +label.spamRule.new=New Rule +label.spamRule.edit=Edit rule +label.spamProtection.column.enabled=Enabled + diff --git a/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_es.properties b/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_es.properties index fb27d5c0dc..791597dad8 100644 --- a/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_es.properties +++ b/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_es.properties @@ -7,8 +7,8 @@ label.forum=Foro label.topic=Tema label.author=Autor label.date=Fecha -label.addtopic=Nuevo Tema -label.addtopic.tip=Crear una nueva discusi\u00F3n si tienes una pregunta o un tema de discusi\u00F3n +label.addtopic=Comenzar una discusi\u00F3n +label.addtopic.tip=Crear una nueva discusi\u00F3n si tema de discusi\u00F3n label.addCodeReview=Nueva revisi\u00F3n de c\u00F3digo label.addCodeReview.tip=Muestra tu c\u00F3digo si quieres que otros compartan su opini\u00F3n sobre el. Los revisores podr\u00E1n revisar cada l\u00EDnea. label.click_language=Click para cambiar el idioma. @@ -109,10 +109,10 @@ label.firstname=Nombre label.lastname=Apellidos label.confirmation=Confirmar contrase\u00F1a label.captcha=Introduzca los n\u00FAmeros de la imagen -label.tip.username=Nombre de usuario, 1-25 caracteres +label.tip.username=Nombre de usuario label.tip.email=E-mail -label.tip.password=Contrase\u00F1a, 1-50 caracteres -label.tip.confirmation=Repetir contrase\u00F1a +label.tip.password=Contrase\u00F1a +label.tip.confirmation=Confirmar su contrase\u00F1a label.tip.captcha=Texto de verificaci\u00F3n label.tip.honeypot.captcha=Por favor no rellene este campo. label.login_error=Usuario o contrase\u00F1a incorrectos @@ -318,7 +318,7 @@ label.poll.message.error=Por favor selecciona una respuesta. label.url.header=Por favor rellena los valores de los campos para establecer la URL: label.img.header=Por favor rellena los valores de los campos para establecer la imagen: label.url.text=Texto a mostrar -label.url=URL del enlace +label.url=Especifique enlace label.url.required=Campo obligatorio label.url.info=Si este campo es vac\u00EDo, entonces el texto que se mostrar\u00E1 ser\u00E1 el propio enlace @@ -367,9 +367,9 @@ label.banner.add=A\u00F1adir label.banner.edit=Editar #links editor -label.linksEditor = Editor de elnaces externos +label.linksEditor = Editor de los enlaces externos label.title = T\u00EDtulo -label.hint = Ayuda +label.hint = Indirecta label.deleteMainLink=\u00BFEst\u00E1s seguro de que deseas eliminar el enlace {0}? label.link.error.save=Se ha producido un error inesperado al guardar el enlace. Por favor int\u00E9ntalo de nuevo m\u00E1s tarde. label.link.error.delete=Se ha producido un error inesperado al eliminar el enlace. Por favor int\u00E9ntalo de nuevo m\u00E1s tarde. @@ -392,6 +392,8 @@ label.addReviewComment=A\u00F1adir comentario label.administration=Administraci\u00F3n label.administration.enter=Modo Administraci\u00F3n label.administration.exit=Cerrar Administraci\u00F3n +label.administration.userGroups=User Groups +label.administration.groupUserList=Group User List label.forum.description=Descripci\u00F3n del foro label.forum.title=T\u00EDtulo del foro label.titlePrefix=Prefijo del t\u00EDtulo para cada p\u00E1gina @@ -462,3 +464,44 @@ label.branch.header.lastMessage.tooltip=Ver \u00FAltimo mensaje label.topic.section.in=en label.tips.close=Cerrar este tema label.tips.open=Reabrir este tema +label.deleteCodeReviewConfirmation=\u00BFEst\u00E1s seguro de que deseas eliminar la revisi\u00F3n de c\u00F3digo?(los comentarios tambi\u00E9n ser\u00E1n eliminados). +label.hours.1.suffix= +label.user.groups=Groups +label.connection.lost.genericError=Connection to the server was lost +label.user.group.delete.error=Error on delete group +label.user.group.add.error=Error on add group +label.user.groups.select.placeHolder=Select a group... +label.user.groups.no.matches=No matches found for +label.not.logged.in.genericError=We're sorry, an error has occurred. Please refresh the page or login and try again. +label.sessionTimeout=Session timeout +label.avatarMaxSize=Avatar max size +label.leavePageConfirmation= +label.emailNotification=Email notifications + +label.group.name=Name +label.group.numberOfMembers=Number of Members +label.group.creation=Create new +label.group.placeholder.name=Group name +label.group.placeholder.description=Group description +label.group.create.title=New group +label.group.edit.title=Edit +label.group.add.user=Adding user +label.405.title=Method not supported +label.405.detail=The method is not applicable to the resource specified in the request. +label.405.checkurl=Please check that page address is valid or method that you use is applicable for specified resource. +label.group.user.name=Username +label.group.user.email=Email +label.group.delete.message=Are you sure you want to delete user group? Users will not be deleted. +label.spamProtection.title=Spam protection +label.spamRule.regex=Rule regex +label.spamRule.description=Rule description +label.spamProtection.column.rules=Rules +label.spamProtection.column.description=Descriptions +label.spamRule.delete=Are you sure you want to delete this rule? +label.spamProtection.block.message=Sorry, you use restricted email. If you are not a bot - write to administrator (email at the bottom of every page). +label.spamProtection.settings=Spam protection settings +label.spamRule.add=Add rule +label.spamRule.new=New Rule +label.spamRule.edit=Edit rule +label.spamProtection.column.enabled=Enabled + diff --git a/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_ru.properties b/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_ru.properties index ea54dd0679..e0d72c1c98 100644 --- a/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_ru.properties +++ b/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_ru.properties @@ -7,8 +7,8 @@ label.forum=\u0424\u043E\u0440\u0443\u043C label.topic=\u0422\u0435\u043C\u0430 label.author=\u0410\u0432\u0442\u043E\u0440 label.date=\u0414\u0430\u0442\u0430 -label.addtopic=\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u0442\u0435\u043C\u0443 -label.addtopic.tip=\u0421\u043E\u0437\u0434\u0430\u0439 \u043E\u0431\u044B\u0447\u043D\u0443\u044E \u0434\u0438\u0441\u043A\u0443\u0441\u0441\u0438\u044E, \u0435\u0441\u043B\u0438 \u0445\u043E\u0447\u0435\u0448\u044C \u0437\u0430\u0434\u0430\u0442\u044C \u0432\u043E\u043F\u0440\u043E\u0441 \u0438\u043B\u0438 \u043E\u0431\u0441\u0443\u0434\u0438\u0442\u044C \u0438\u043D\u0442\u0435\u0440\u0435\u0441\u0443\u044E\u0449\u0443\u044E \u0442\u0435\u043C\u0443 +label.addtopic=\u041D\u0430\u0447\u0430\u0442\u044C \u041E\u0431\u0441\u0443\u0436\u0434\u0435\u043D\u0438\u0435 +label.addtopic.tip=\u0421\u043E\u0437\u0434\u0430\u0439 \u043E\u0431\u044B\u0447\u043D\u0443\u044E \u0434\u0438\u0441\u043A\u0443\u0441\u0441\u0438\u044E, \u0435\u0441\u043B\u0438 \u0445\u043E\u0447\u0435\u0448\u044C \u043E\u0431\u0441\u0443\u0434\u0438\u0442\u044C \u0438\u043D\u0442\u0435\u0440\u0435\u0441\u0443\u044E\u0449\u0443\u044E \u0442\u0435\u043C\u0443 label.addCodeReview=\u0420\u0435\u0446\u0435\u043D\u0437\u0438\u044F \u041A\u043E\u0434\u0430 label.addCodeReview.tip=\u041F\u043E\u043A\u0430\u0436\u0438 \u0432\u0441\u0435\u043C \u0441\u0432\u043E\u0439 \u0433-\u043A\u043E\u0434, \u0434\u0430\u0431\u044B \u0434\u0440\u0443\u0433\u0438\u0435 \u043C\u043E\u0433\u043B\u0438 \u0432\u044B\u0440\u0430\u0437\u0438\u0442\u044C \u0441\u0432\u043E\u0435 \u043C\u043D\u0435\u043D\u0438\u0435 \u043E \u043D\u0435\u043C. \u0420\u0435\u0446\u0435\u043D\u0437\u0435\u043D\u0442\u044B \u0441\u043C\u043E\u0433\u0443\u0442 \u043E\u0441\u0442\u0430\u0432\u043B\u044F\u0442\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 \u043A \u043A\u0430\u0436\u0434\u043E\u0439 \u0435\u0433\u043E \u0441\u0442\u0440\u043E\u043A\u0435. label.click_language=\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0434\u043B\u044F \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u044F\u0437\u044B\u043A\u0430 @@ -46,8 +46,10 @@ label.answer.options=\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 label.answer_to=\u041E\u0442\u0432\u0435\u0442 \u043D\u0430 \u0442\u0435\u043C\u0443 label.answer.title_label=\u041E\u0442\u0432\u0435\u0442\! label.answer.font_size=\u0420\u0430\u0437\u043C\u0435\u0440 \u0448\u0440\u0438\u0444\u0442\u0430 -label.answer.font_code=\u041A\u043E\u0434 -label.answer.indent=\u041E\u0442\u0441\u0442\u0443\u043F \u043F\u0430\u0440\u0430\u0433\u0440\u0430\u0444\u0430 +label.answer.font_code=\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043A\u043E\u0434 +label.answer.font_code.button=\u041A\u043E\u0434 +label.answer.indent=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C\u0020\u043E\u0442\u0441\u0442\u0443\u043F +label.answer.indent.button=\u041E\u0442\u0441\u0442\u0443\u043F \u043F\u0430\u0440\u0430\u0433\u0440\u0430\u0444\u0430 label.answer.none=\u041D\u0435 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D label.answer.font_size.tiny=\u041E\u0447\u0435\u043D\u044C \u043C\u0430\u043B\u0435\u043D\u044C\u043A\u0438\u0439 label.answer.font_size.small=\u041C\u0430\u043B\u044B\u0439 @@ -61,7 +63,8 @@ label.answer.bold=\u0416\u0438\u0440\u043D\u044B\u0439 (Ctrl+B) label.answer.italic=\u041A\u0443\u0440\u0441\u0438\u0432 (Ctrl+I) label.answer.underline=\u041F\u043E\u0434\u0447\u0435\u0440\u043A\u043D\u0443\u0442\u044B\u0439 (Ctrl+U) label.answer.striked=\u0417\u0430\u0447\u0435\u0440\u043A\u043D\u0443\u0442\u044B\u0439 (Ctrl+S) -label.answer.highlight=\u041F\u043E\u0434\u0441\u0432\u0435\u0442\u043A\u0430 +label.answer.highlight=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C\u0020\u0446\u0432\u0435\u0442\u0020\u0444\u043E\u043D\u0430 +label.answer.highlight.button=\u041F\u043E\u0434\u0441\u0432\u0435\u0442\u043A\u0430 \u0442\u0435\u043A\u0441\u0442\u0430 label.answer.align_left=\u0412\u044B\u0440\u043E\u0432\u043D\u044F\u0442\u044C \u043F\u043E \u043B\u0435\u0432\u043E\u043C\u0443 \u043A\u0440\u0430\u044E label.answer.align_center=\u0412\u044B\u0440\u043E\u0432\u043D\u044F\u0442\u044C \u043F\u043E \u0446\u0435\u043D\u0442\u0440\u0443 label.answer.align_right=\u0412\u044B\u0440\u043E\u0432\u043D\u044F\u0442\u044C \u043F\u043E \u043F\u0440\u0430\u0432\u043E\u043C\u0443 \u043A\u0440\u0430\u044E @@ -104,10 +107,10 @@ label.firstname=\u0418\u043C\u044F label.lastname=\u0424\u0430\u043C\u0438\u043B\u0438\u044F label.confirmation=\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u044C label.captcha=\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043B\u043E \u0441 \u043A\u0430\u0440\u0442\u0438\u043A\u0438 -label.tip.username=\u0418\u043C\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F, 1-25 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 +label.tip.username=\u0418\u043C\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F label.tip.email=\u0410\u0434\u0440\u0435\u0441 \u044D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u043E\u0439 \u043F\u043E\u0447\u0442\u044B -label.tip.password=\u041F\u0430\u0440\u043E\u043B\u044C, 1-50 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 -label.tip.confirmation=\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435 \u0432\u0432\u043E\u0434 \u043F\u0430\u0440\u043E\u043B\u044F +label.tip.password=\u041F\u0430\u0440\u043E\u043B\u044C +label.tip.confirmation=\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u044C label.tip.captcha=\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442 \u0441 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438 label.tip.honeypot.captcha=\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u043E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u044D\u0442\u043E \u043F\u043E\u043B\u0435 \u043F\u0443\u0441\u0442\u044B\u043C label.login_error=\u041D\u0435\u0432\u0435\u0440\u043D\u043E\u0435 \u0438\u043C\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \u0438\u043B\u0438 \u043F\u0430\u0440\u043E\u043B\u044C @@ -295,16 +298,17 @@ label.registration.success.2= label.registration.success=\u041D\u0430 \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u044B\u0439 e-mail \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E \u043F\u0438\u0441\u044C\u043C\u043E \u0441\u043E \u0441\u0441\u044B\u043B\u043A\u043E\u0439 \u0434\u043B\u044F \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438. label.registration.success.title=\u0412\u044B \u0431\u044B\u043B\u0438 \u0443\u0441\u043F\u0435\u0448\u043D\u043E \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u044B label.registration.failture=\u041F\u0440\u0438\u043D\u043E\u0441\u0438\u043C \u0438\u0437\u0432\u0438\u043D\u0435\u043D\u0438\u044F. \u041F\u0440\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0441\u043B\u0443\u0447\u0438\u043B\u0430\u0441\u044C \u043D\u0435\u043F\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043D\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u043F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430 \u0435\u0449\u0435 \u0440\u0430\u0437 \u043F\u043E\u0437\u0436\u0435. +label.email.confirmation=\u0412\u044B \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044E \u043D\u0430 \u0444\u043E\u0440\u0443\u043C\u0435. \u0415\u0441\u043B\u0438 \u0412\u044B \u043D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u0438 \u0441\u0441\u044B\u043B\u043A\u0443 \u0434\u043B\u044F \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438, \u043D\u0430\u0436\u043C\u0438\u0442\u0435 \u043D\u0430 {0}\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u0434\u043B\u044F \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u043E{1}. label.honeypot.not.null=\u041D\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0439 \u0437\u0430\u043F\u0440\u043E\u0441 \u043F\u0440\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438. label.registration.connection.error=\u0421\u0435\u0440\u0432\u0438\u0441 \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u0432 \u0434\u0430\u043D\u043D\u044B\u0439 \u043C\u043E\u043C\u0435\u043D\u0442 \u043D\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D. \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u043E\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044C \u043A \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0443 \u0438\u043B\u0438 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u043F\u043E\u0437\u0436\u0435. label.authentication.connection.error=\u041F\u043E\u043F\u044B\u0442\u043A\u0430 \u0432\u0445\u043E\u0434\u0430 \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C. \u0421\u0435\u0440\u0432\u0438\u0441 \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u0432 \u0434\u0430\u043D\u043D\u044B\u0439 \u043C\u043E\u043C\u0435\u043D\u0442 \u043D\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D. \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u043E\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044C \u043A \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0443 \u0438\u043B\u0438 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u043F\u043E\u0437\u0436\u0435. #Add contact label.contact.type=\u0422\u0438\u043F \u043A\u043E\u043D\u0442\u0430\u043A\u0442\u0430 label.contact.value=\u041A\u043E\u043D\u0442\u0430\u043A\u0442 -label.contact.value.info=\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043B\u044F \u0432\u044B\u0431\u0440\u0430\u043D\u043E\u0433\u043E \u043A\u043E\u043D\u0442\u0430\u043A\u0442\u0430 +label.contact.value.info=\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0434\u043B\u044F \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0433\u043E \u043A\u043E\u043D\u0442\u0430\u043A\u0442\u0430 label.activation.reason=\u041D\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044F \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0439 \u0430\u043A\u043A\u0430\u0443\u043D\u0442. \u0412\u043E\u0437\u043C\u043E\u0436\u043D\u043E \u043D\u0435\u0432\u0435\u0440\u043D\u043E \u0443\u043A\u0430\u0437\u0430\u043D \u0430\u0434\u0440\u0435\u0441 \u0438\u043B\u0438 \u0432\u0440\u0435\u043C\u044F \u0430\u043A\u0442\u0438\u0432\u0430\u0446\u0438\u0438 (24 \u0447\u0430\u0441\u0430 \u0441 \u043C\u043E\u043C\u0435\u043D\u0442\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438) \u0443\u0436\u0435 \u0438\u0441\u0442\u0435\u043A\u043B\u043E. label.activation.error=\u041E\u0448\u0438\u0431\u043A\u0430 \u0430\u043A\u0442\u0438\u0432\u0430\u0446\u0438\u0438 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430 - + #BB editor selected color label.selected.color=\u0412\u044B \u0432\u044B\u0431\u0440\u0430\u043B\u0438 \u0446\u0432\u0435\u0442 label.errors.not_empty = \u041D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C @@ -320,12 +324,13 @@ label.poll.vote=\u0413\u043E\u043B\u043E\u0441\u043E\u0432\u0430\u0442\u044C label.poll.option.vote.info={0} - {1}% label.poll.title.with.ending=(\u0413\u043E\u043B\u043E\u0441\u043E\u0432\u0430\u043D\u0438\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044F {0}) label.poll.message.error=\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043E\u0442\u0432\u0435\u0442. - +label.poll.deleteEndingDate=\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0414\u0430\u0442\u0443 \u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u044F + label.leavePageConfirmation=\u0421\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 \u043D\u0435\u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435. label.pm.folders=\u041F\u0430\u043F\u043A\u0438 placeholder.editor.content=\u0421\u043E\u0434\u0435\u0440\u0436\u0430\u043D\u0438\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F placeholder.codereview.editor.content=\u0412\u043F\u0438\u0448\u0438\u0442\u0435 \u0441\u0432\u043E\u0439 \u0448\u0435\u0434\u0435\u0432\u0440 \u0441\u044E\u0434\u0430 \u0431\u0435\u0437 \u043A\u0430\u043A\u0438\u0445-\u043B\u0438\u0431\u043E BB-\u043A\u043E\u0434\u043E\u0432, \u043E\u0431\u044B\u0447\u043D\u044B\u0439 \u0442\u0435\u043A\u0441\u0442 \u0437\u0434\u0435\u0441\u044C \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D, \u0442\u043E\u043B\u044C\u043A\u043E \u043A\u043E\u0434. \u0415\u0441\u043B\u0438 \u043D\u0443\u0436\u043D\u043E \u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043A\u0430\u043A\u043E\u0435-\u0442\u043E \u043F\u043E\u044F\u0441\u043D\u0435\u043D\u0438\u0435, \u043E\u043D\u043E \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u043E \u0432 \u0432\u0438\u0434\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0435\u0432 \u0432 \u043A\u043E\u0434\u0435. - + #tooltips label.tips.view_profile=\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0434\u043B\u044F \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430 \u043F\u0440\u043E\u0444\u0438\u043B\u044F label.tips.create_new_post=\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u043F\u043E\u0441\u0442 \u0432 \u044D\u0442\u043E\u0439 \u0442\u0435\u043C\u0435 @@ -391,6 +396,8 @@ label.addReviewComment=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u label.administration=\u0410\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 label.administration.enter=\u0420\u0435\u0436\u0438\u043C \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F label.administration.exit=\u041F\u043E\u043A\u0438\u043D\u0443\u0442\u044C \u0410\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 +label.administration.userGroups=\u0413\u0440\u0443\u043F\u043F\u044B \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439 +label.administration.groupUserList=\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 \u0432 \u0433\u0440\u0443\u043F\u043F\u0435 label.forum.description=\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u0444\u043E\u0440\u0443\u043C\u0430 label.forum.title=\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0444\u043E\u0440\u0443\u043C\u0430 label.titlePrefix=\u041F\u0440\u0435\u0444\u0438\u043A\u0441 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0430 \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0441\u0442\u0440\u0430\u043D\u0438\u0446 @@ -460,3 +467,64 @@ label.branch.header.lastMessage.tooltip=\u041F\u0435\u0440\u0435\u0439\u0442\u04 label.topic.section.in=\u0432 label.tips.close=\u0417\u0430\u043A\u0440\u044B\u0442\u044C \u0442\u0435\u043C\u0443 label.tips.open=\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0442\u0435\u043C\u0443 +label.deleteCodeReviewConfirmation=\u0412\u044B \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u0445\u043E\u0442\u0438\u0442\u0435 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0440\u0435\u0446\u0435\u043D\u0437\u0438\u044E \u043A\u043E\u0434\u0430 (\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 \u0442\u043E\u0436\u0435 \u0431\u0443\u0434\u0443\u0442 \u0443\u0434\u0430\u043B\u0435\u043D\u044B)? +label.saved.just.now=\u0427\u0435\u0440\u043D\u043E\u0432\u0438\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u043E \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D +label.connection.lost=\u0421\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C \u043F\u0440\u0435\u0440\u0432\u0430\u043D\u043E. \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0435 \u0412\u0430\u0448 \u0442\u0435\u043A\u0441\u0442 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E. +label.saved=\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E +label.ago=\u043D\u0430\u0437\u0430\u0434 +label.seconds=\u0441\u0435\u043A\u0443\u043D\u0434 +label.hour=\u0447\u0430\u0441 +label.minutes.2.4.suffix=\u044B +label.hours.2.4.suffix=\u0430 +label.hours.more.than.4.suffix=\u043E\u0432 +label.minute=\u043C\u0438\u043D\u0443\u0442 +label.minute.1.suffix=\u0443 +label.11.minutes.suffix= +label.11.hours.suffix=\u043E\u0432 +label.minute.one.at.the.end.suffix=\u0443 +label.hour.one.at.the.end.suffix= +label.minutes.more.than.4.suffix= +label.hours.1.suffix= +label.not.logged.in.error=\u041F\u043E\u0445\u043E\u0436\u0435, \u0447\u0442\u043E \u0443 \u0412\u0430\u0441 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0442 \u043F\u0440\u0430\u0432 \u043E\u0441\u0442\u0430\u0432\u043B\u044F\u0442\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0432 \u044D\u0442\u043E\u0439 \u0432\u0435\u0442\u043A\u0435. \u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442 \u0438 \u043F\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443 - \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E \u0432\u044B \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0437\u0430\u043B\u043E\u0433\u0438\u043D\u0435\u043D\u044B. +label.search.user=\u041F\u043E\u0438\u0441\u043A \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F +label.search.user.empty=\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B +label.user.groups=\u0413\u0440\u0443\u043F\u043F\u044B +label.connection.lost.genericError=\u041F\u043E\u0442\u0435\u0440\u044F\u043D\u043E \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C +label.user.group.delete.error=\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0438 \u0438\u0437 \u0433\u0440\u0443\u043F\u043F\u044B +label.user.group.add.error=\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u0438 \u0432 \u0433\u0440\u0443\u043F\u043F\u0443 +label.user.groups.select.placeHolder=\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0433\u0440\u0443\u043F\u043F\u044B... +label.user.groups.no.matches=\u041F\u043E\u0438\u0441\u043A \u043D\u0435 \u0434\u0430\u043B \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432 \u043F\u043E +label.not.logged.in.genericError=\u0418\u0437\u0432\u0438\u043D\u0438\u0442\u0435, \u043F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443 \u0438\u043B\u0438 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044C \u0438 \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u0441\u043D\u043E\u0432\u0430. +label.sessionTimeout=\u0422\u0430\u0439\u043C\u0430\u0443\u0442 \u0441\u0435\u0441\u0441\u0438\u0438 +label.avatarMaxSize=\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0430\u0432\u0430\u0442\u0430\u0440\u0430 +label.emailNotification=Email \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u044F +label.sessionTimeout.hint=\u0422\u0430\u0439\u043C\u0430\u0443\u0442 \u0441\u0435\u0441\u0441\u0438\u0438 +label.avatarMaxSize.hint=\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0430\u0432\u0430\u0442\u0430\u0440\u0430 + +label.group.name=\u0418\u043C\u044F +label.group.numberOfMembers=\u041A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u043E\u0432 +label.group.creation=\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u043D\u043E\u0432\u0443\u044E +label.group.placeholder.name=\u0418\u043C\u044F \u0433\u0440\u0443\u043F\u043F\u044B +label.group.placeholder.description=\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u0433\u0440\u0443\u043F\u043F\u044B +label.group.create.title=\u041D\u043E\u0432\u0430\u044F \u0433\u0440\u0443\u043F\u043F\u0430 +label.group.edit.title=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 +label.group.add.user=\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F +label.405.title=\u041C\u0435\u0442\u043E\u0434 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F +label.405.detail=\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u044B\u0439 \u0412\u0430\u043C\u0438 \u043C\u0435\u0442\u043E\u0434 \u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C \u043A \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u043E\u043C\u0443 \u0432 \u0437\u0430\u043F\u0440\u043E\u0441\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u0443. +label.405.checkurl=\u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u043F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435, \u0447\u0442\u043E \u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u0435\u043D \u0438\u043B\u0438 \u043C\u0435\u0442\u043E\u0434, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0432\u044B \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C \u0434\u043B\u044F \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u043E\u0433\u043E \u0440\u0435\u0441\u0443\u0440\u0441\u0430. +label.group.user.name=\u0418\u043C\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F +label.group.user.email=\u042D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u0430\u044F \u043F\u043E\u0447\u0442\u0430 +label.group.delete.message=\u0412\u044B \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u0445\u043E\u0442\u0438\u0442\u0435 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0433\u0440\u0443\u043F\u043F\u0443? \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 \u043D\u0435 \u0431\u0443\u0434\u0443\u0442 \u0443\u0434\u0430\u043B\u0435\u043D\u044B. +label.spamProtection.title=\u0417\u0430\u0449\u0438\u0442\u0430 \u043E\u0442 \u0441\u043F\u0430\u043C\u0430 +label.spamRule.regex=\u0420\u0435\u0433\u0443\u043B\u044F\u0440\u043D\u043E\u0435 \u0432\u044B\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u0434\u043B\u044F \u043F\u0440\u0430\u0432\u0438\u043B\u0430 +label.spamRule.description=\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 +label.spamProtection.column.rules=\u041F\u0440\u0430\u0432\u0438\u043B\u0430 +label.spamProtection.column.description=\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u044F +label.spamRule.delete=\u0412\u044B \u0443\u0432\u0435\u0440\u0435\u043D\u044B, \u0447\u0442\u043E \u0445\u043E\u0442\u0438\u0442\u0435 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u044D\u0442\u043E \u043F\u0440\u0430\u0432\u0438\u043B\u043E? +label.spamProtection.block.message=\u041F\u0440\u043E\u0441\u0442\u0438\u0442\u0435, \u0432\u044B \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0435 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u043D\u044B\u0439 \u0430\u0434\u0440\u0435\u0441 \u044D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u043E\u0439 \u043F\u043E\u0447\u0442\u044B. \u0415\u0441\u043B\u0438 \u0432\u044B \u043D\u0435 \u0431\u043E\u0442 - \u043D\u0430\u043F\u0438\u0448\u0438\u0442\u0435 \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0443 (\u0430\u0434\u0440\u0435\u0441 \u043F\u043E\u0447\u0442\u044B \u0432\u043D\u0438\u0437\u0443 \u043A\u0430\u0436\u0434\u043E\u0439 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B). +label.spamProtection.settings=\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0437\u0430\u0449\u0438\u0442\u044B \u043E\u0442 \u0441\u043F\u0430\u043C\u0430 +label.spamRule.add=\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0430\u0432\u0438\u043B\u043E +label.spamRule.new=\u041D\u043E\u0432\u043E\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u043E +label.spamRule.edit=\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0440\u0430\u0432\u0438\u043B\u043E +label.spamProtection.column.enabled=\u0412\u043A\u043B\u044E\u0447\u0435\u043D\u043E + diff --git a/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_uk.properties b/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_uk.properties index b749da0834..141eddb1d8 100644 --- a/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_uk.properties +++ b/jcommune-view/jcommune-web-view/src/main/resources/org/jtalks/jcommune/web/view/messages_uk.properties @@ -7,7 +7,7 @@ label.forum=\u0424\u043E\u0440\u0443\u043C label.topic=\u0422\u0435\u043C\u0430 label.author=\u0410\u0432\u0442\u043E\u0440 label.date=\u0414\u0430\u0442\u0430 -label.addtopic=\u0421\u0442\u0432\u043E\u0440\u0438\u0442\u0438 \u0442\u0435\u043C\u0443 +label.addtopic=\u041F\u043E\u0447\u0430\u0442\u0438 \u0414\u0438\u0441\u043A\u0443\u0441\u0456\u044E label.addCodeReview=\u0420\u0435\u0446\u0435\u043D\u0437\u0456\u044F \u041A\u043E\u0434\u0443 label.addCodeReview.tip=\u041F\u043E\u043A\u0430\u0436\u0438 \u0443\u0441\u0456\u043C \u0441\u0432\u0456\u0439 \u0433-\u043A\u043E\u0434, \u0449\u043E\u0431 \u0456\u043D\u0448\u0456 \u0437\u043C\u043E\u0433\u043B\u0438 \u0432\u0438\u0441\u043B\u043E\u0432\u0438\u0442\u0438 \u0441\u0432\u043E\u044E \u0434\u0443\u043C\u043A\u0443 \u043F\u0440\u043E \u043D\u044C\u043E\u0433\u043E. \u0420\u0435\u0446\u0435\u043D\u0437\u0435\u043D\u0442\u0438 \u0437\u043C\u043E\u0436\u0443\u0442\u044C \u0437\u0430\u043B\u0438\u0448\u0438\u0442\u0438 \u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u0456 \u0434\u043E \u043A\u043E\u0436\u043D\u043E\u0433\u043E \u0439\u043E\u0433\u043E \u0440\u044F\u0434\u043A\u0430. label.click_language=\u041D\u0430\u0442\u0438\u0441\u043D\u0456\u0442\u044C \u0434\u043B\u044F \u0437\u043C\u0456\u043D\u0438 \u043C\u043E\u0432\u0438 @@ -51,8 +51,10 @@ label.answer.options=\u041D\u0430\u043B\u0430\u0448\u0442\u0443\u0432\u0430\u043 label.answer_to=\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C \u043D\u0430 \u0442\u0435\u043C\u0443 label.answer.title_label=\u0412\u0456\u0434\u043F\u043E\u0432\u0456\u0434\u044C! label.answer.font_size=\u0420\u043E\u0437\u043C\u0456\u0440 \u0448\u0440\u0438\u0444\u0442\u0443 -label.answer.font_code=\u041A\u043E\u0434 -label.answer.indent=\u0412\u0456\u0434\u0441\u0442\u0443\u043F \u043F\u0430\u0440\u0430\u0433\u0440\u0430\u0444\u0430 +label.answer.font_code=\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u0438 \u043A\u043E\u0434 +label.answer.font_code.button=\u041A\u043E\u0434 +label.answer.indent=\u0414\u043E\u0434\u0430\u0442\u0438\u0020\u0432\u0456\u0434\u0441\u0442\u0443\u043F +label.answer.indent.button=\u0412\u0456\u0434\u0441\u0442\u0443\u043F \u043F\u0430\u0440\u0430\u0433\u0440\u0430\u0444\u0430 label.answer.none=\u041D\u0435 \u0432\u0438\u0437\u043D\u0430\u0447\u0435\u043D\u043E label.answer.font_size.tiny=\u0414\u0443\u0436\u0435 \u043C\u0430\u043B\u0438\u0439 label.answer.font_size.small=\u041C\u0430\u043B\u0438\u0439 @@ -66,7 +68,8 @@ label.answer.bold=\u0416\u0438\u0440\u043D\u0438\u0439 (Ctrl+B) label.answer.italic=\u041A\u0443\u0440\u0441\u0438\u0432 (Ctrl+I) label.answer.underline=\u041F\u0456\u0434\u043A\u0440\u0435\u0441\u043B\u0435\u043D\u0438\u0439 (Ctrl+U) label.answer.striked=\u0417\u0430\u043A\u0440\u0435\u0441\u043B\u0435\u043D\u0438\u0439 (Ctrl+S) -label.answer.highlight=\u041F\u0456\u0434\u0441\u0432\u0456\u0442\u043A\u0430 +label.answer.highlight=\u0414\u043E\u0434\u0430\u0442\u0438\u0020\u043A\u043E\u043B\u0456\u0440\u0020\u0444\u043E\u043D\u0443 +label.answer.highlight.button=\u041F\u0456\u0434\u0441\u0432\u0456\u0447\u0443\u0432\u0430\u043D\u043D\u044F \u0442\u0435\u043A\u0441\u0442\u0443 label.answer.align_left=\u0412\u0438\u0440\u0456\u0432\u043D\u044F\u0442\u0438 \u043F\u043E \u043B\u0456\u0432\u043E\u043C\u0443 \u043A\u0440\u0430\u044E label.answer.align_center=\u0412\u0438\u0440\u0456\u0432\u043D\u044F\u0442\u0438 \u043F\u043E \u0446\u0435\u043D\u0442\u0440\u0443 label.answer.align_right=\u0412\u0438\u0440\u0456\u0432\u043D\u044F\u0442\u0438 \u043F\u043E \u043F\u0440\u0430\u0432\u043E\u043C\u0443 \u043A\u0440\u0430\u044E @@ -106,10 +109,10 @@ label.firstname=\u0406\u043C'\u044F label.lastname=\u041F\u0440\u0456\u0437\u0432\u0438\u0449\u0435 label.confirmation=\u041F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C label.captcha=\u0412\u0432\u0435\u0434\u0456\u0442\u044C \u0447\u0438\u0441\u043B\u043E \u043D\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u0446\u0456 -label.tip.username=\u0406\u043C'\u044F \u043A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430, 1-25 \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 +label.tip.username=\u0406\u043C'\u044F \u043A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 label.tip.email=\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u043E\u0457 \u043F\u043E\u0448\u0442\u0438 -label.tip.password=\u041F\u0430\u0440\u043E\u043B\u044C, 1-50 \u0441\u0438\u043C\u0432\u043E\u043B\u0456\u0432 -label.tip.confirmation=\u041F\u043E\u0432\u0442\u043E\u0440\u0456\u0442\u044C \u0432\u0432\u0435\u0434\u0435\u043D\u043D\u044F \u043F\u0430\u0440\u043E\u043B\u044E +label.tip.password=\u041F\u0430\u0440\u043E\u043B\u044C +label.tip.confirmation=\u041F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C label.tip.captcha=\u0412\u0432\u0435\u0434\u0456\u0442\u044C \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044F \u0437 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0438 label.tip.honeypot.captcha=\u0411\u0443\u0434\u044C \u043B\u0430\u0441\u043A\u0430, \u0437\u0430\u043B\u0438\u0448\u0442\u0435 \u0446\u0435 \u043F\u043E\u043B\u0435 \u043F\u0443\u0441\u0442\u0438\u043C. label.login_error=\u041D\u0435\u0432\u0456\u0440\u043D\u0435 \u0456\u043C'\u044F \u043A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0430\u0431\u043E \u043F\u0430\u0440\u043E\u043B\u044C @@ -130,6 +133,7 @@ label.auto_logon=\u0417\u0430\u043F\u0430\u043C'\u044F\u0442\u0430\u0442\u0438 \ label.registration.success=\u041D\u0430 \u0432\u0430\u0448 e-mail \u0431\u0443\u0432 \u0432\u0456\u0434\u0456\u0441\u043B\u0430\u043D\u0438\u0439 \u043B\u0438\u0441\u0442 \u0437 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F\u043C \u0434\u043B\u044F \u043F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043D\u043D\u044F \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u0457. label.registration.success.title=\u0412\u0438 \u0431\u0443\u043B\u0438 \u0443\u0441\u043F\u0456\u0448\u043D\u043E \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043E\u0432\u0430\u043D\u0456 label.registration.failture=\u0412\u0438\u0431\u0430\u0447\u0442\u0435, \u043F\u0440\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044F \u0442\u0440\u0430\u043F\u0438\u043B\u0430\u0441\u044C \u043D\u0435\u043F\u0435\u0440\u0435\u0434\u0431\u0430\u0447\u0443\u0432\u0430\u043D\u0430 \u043F\u043E\u043C\u0438\u043B\u043A\u0430. \u0421\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u0431\u0443\u0434\u044C \u043B\u0430\u0441\u043A\u0430 \u0449\u0435 \u0440\u0430\u0437 \u043F\u0456\u0437\u043D\u0456\u0448\u0435. +label.email.confirmation=\u0412\u0438 \u043D\u0435 \u043F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044E \u043D\u0430 \u0444\u043E\u0440\u0443\u043C\u0456. \u042F\u043A\u0449\u043E \u0412\u0438 \u043D\u0435 \u043E\u0442\u0440\u0438\u043C\u0430\u043B\u0438 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F \u0434\u043B\u044F \u043F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043D\u043D\u044F \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u0456, \u043D\u0430\u0442\u0438\u0441\u043D\u0456\u0442\u044C \u043D\u0430 {0}\u0412\u0456\u0434\u043F\u0440\u0430\u0432\u0438\u0442\u0438 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F \u0434\u043B\u044F \u043F\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043D\u043D\u044F \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u0456 \u0449\u0435 \u0440\u0430\u0437{1}. label.honeypot.not.null=\u041D\u0435\u0432\u0456\u0440\u043D\u0438\u0439 \u0437\u0430\u043F\u0438\u0442 \u043F\u0456\u0434 \u0447\u0430\u0441 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u0457 label.registration.connection.error=\u0421\u0435\u0440\u0432\u0456\u0441 \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0456\u043A\u0430\u0446\u0456\u0457 \u043D\u0430 \u0434\u0430\u043D\u0438\u0439 \u043C\u043E\u043C\u0435\u043D\u0442 \u043D\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0438\u0439. \u0417\u0432\u0435\u0440\u043D\u0456\u0442\u044C\u0441\u044F \u0434\u043E \u0430\u0434\u043C\u0456\u043D\u0456\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430 \u0430\u0431\u043E \u0441\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u043F\u0456\u0437\u043D\u0456\u0448\u0435. label.authentication.connection.error=\u0421\u043F\u0440\u043E\u0431\u0430 \u0432\u0445\u043E\u0434\u0443 \u043D\u0435 \u0432\u0434\u0430\u043B\u0430\u0441\u044F. \u0421\u0435\u0440\u0432\u0456\u0441 \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0456\u043A\u0430\u0446\u0456\u0457 \u043D\u0430 \u0434\u0430\u043D\u0438\u0439 \u043C\u043E\u043C\u0435\u043D\u0442 \u043D\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0438\u0439. \u0417\u0432\u0435\u0440\u043D\u0456\u0442\u044C\u0441\u044F \u0434\u043E \u0430\u0434\u043C\u0456\u043D\u0456\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430 \u0430\u0431\u043E \u0441\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u043F\u0456\u0437\u043D\u0456\u0448\u0435. @@ -177,8 +181,8 @@ label.users=\u041A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456 label.newbies=\u0414\u043B\u044F \u043D\u043E\u0432\u0430\u0447\u043A\u0456\u0432 label.edit=\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438 label.save=\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438 -label.drafts=\u0427\u043E\u0440\u043D\u043E\u0432\u0438\u043A -label.drafts.empty=\u0427\u043E\u0440\u043D\u043E\u0432\u0438\u043A\u0438 \u0432\u0456\u0434\u0441\u0443\u0442\u043D\u0456. +label.drafts=\u0427\u0435\u0440\u043D\u0435\u0442\u043A\u0430 +label.drafts.empty=\u0427\u0435\u0440\u043D\u0435\u0442\u043A\u0438 \u0432\u0456\u0434\u0441\u0443\u0442\u043D\u0456. label.user=\u041A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447 label.lastlogin=\u041E\u0441\u0442\u0430\u043D\u043D\u0456\u0439 \u0441\u0435\u0430\u043D\u0441 #pagination @@ -309,6 +313,7 @@ label.poll.vote=\u0413\u043E\u043B\u043E\u0441\u0443\u0432\u0430\u043D\u043D\u04 label.poll.option.vote.info= {0} - {1}% label.poll.title.with.ending=(\u0413\u043E\u043B\u043E\u0441\u0443\u0432\u0430\u043D\u043D\u044F \u0437\u0430\u043A\u0456\u043D\u0447\u0438\u0442\u044C\u0441\u044F {0}) label.poll.message.error=\u0411\u0443\u0434\u044C \u043B\u0430\u0441\u043A\u0430, \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044C \u0432\u0430\u0440\u0456\u0430\u043D\u0442. +label.poll.deleteEndingDate=\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u0414\u0430\u0442\u0443 \u0437\u0430\u043A\u0456\u043D\u0447\u0435\u043D\u043D\u044F #edit action #todo duplicate keys #label.post.edit_title=\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438 \u043F\u043E\u0432\u0456\u0434\u043E\u043C\u043B\u0435\u043D\u043D\u044F @@ -360,7 +365,7 @@ label.topicYouDontHavePermissions=\u0423 \u0412\u0430\u0441 \u0431\u0456\u043B\u #keymaps label.keymaps.review =Ctrl + Enter \u0434\u043B\u044F \u0432\u0456\u0434\u043F\u0440\u0430\u0432\u043A\u0438, Esc \u0434\u043B\u044F \u0437\u0430\u043A\u0440\u0438\u0442\u0442\u044F \u0444\u043E\u0440\u043C\u0438 label.keymaps.post =Ctrl + Enter \u0434\u043B\u044F \u0432\u0456\u0434\u043F\u0440\u0430\u0432\u043A\u0438 \u0444\u043E\u0440\u043C\u0438 -label.addtopic.tip=\u0421\u0442\u0432\u043E\u0440\u0438 \u0437\u0432\u0438\u0447\u0430\u0439\u043D\u0443 \u0434\u0438\u0441\u043A\u0443\u0441\u0456\u044E, \u044F\u043A\u0449\u043E \u0445\u043E\u0447\u0435\u0448 \u0437\u0430\u0434\u0430\u0442\u0438 \u043F\u0438\u0442\u0430\u043D\u043D\u044F \u0430\u0431\u043E \u043E\u0431\u0433\u043E\u0432\u043E\u0440\u0438\u0442\u0438 \u0446i\u043A\u0430\u0432\u0443 \u0434\u043B\u044F \u0442\u0435\u0431\u0435 \u0442\u0435\u043C\u0443 +label.addtopic.tip=\u0421\u0442\u0432\u043E\u0440\u0438 \u0437\u0432\u0438\u0447\u0430\u0439\u043D\u0443 \u0434\u0438\u0441\u043A\u0443\u0441\u0456\u044E, \u044F\u043A\u0449\u043E \u0445\u043E\u0447\u0435\u0448 \u043E\u0431\u0433\u043E\u0432\u043E\u0440\u0438\u0442\u0438 \u0446\u0456\u043A\u0430\u0432\u0443 \u0434\u043B\u044F \u0442\u0435\u0431\u0435 \u0442\u0435\u043C\u0443 #banners label.banner.upload.dialog.header=\u0412\u0441\u0442\u0430\u0432\u0442\u0435 \u0441\u0432\u0456\u0439 HTML/JavaScript \u043A\u043E\u0434 \u0441\u044E\u0434\u0438: @@ -370,7 +375,7 @@ label.banner.add=\u0414\u043E\u0434\u0430\u0442\u0438 label.banner.edit=\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438 #links editor -label.linksEditor = \u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u044C +label.linksEditor = \u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0437\u043E\u0432\u043Di\u0448\u043Dix \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u044C label.title = \u041D\u0430\u0437\u0432\u0430 label.hint = \u041F\u0456\u0434\u043A\u0430\u0437\u043A\u0430 label.deleteMainLink=\u0412\u0438 \u0432\u043F\u0435\u0432\u043D\u0435\u043D\u0456, \u0449\u043E \u0445\u043E\u0447\u0435\u0442\u0435 \u0432\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u043Bi\u043D\u043A {0}? @@ -394,6 +399,8 @@ label.addReviewComment=\u0414\u043E\u0434\u0430\u0442\u0438 \u043A\u043E\u043C\u label.administration=\u0410\u0434\u043C\u0456\u043D\u0456\u0441\u0442\u0440\u0443\u0432\u0430\u043D\u043D\u044F label.administration.enter=\u0420\u0435\u0436\u0438\u043C \u0430\u0434\u043C\u0456\u043D\u0456\u0441\u0442\u0440\u0443\u0432\u0430\u043D\u043D\u044F label.administration.exit=\u0417\u0430\u043B\u0438\u0448\u0438\u0442\u0438 \u0410\u0434\u043C\u0456\u043D\u0456\u0441\u0442\u0440\u0443\u0432\u0430\u043D\u043D\u044F +label.administration.userGroups=\u0413\u0440\u0443\u043F\u0438 \u043A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456\u0432 +label.administration.groupUserList=\u041A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456 \u0432 \u0433\u0440\u0443\u043F\u0456 label.forum.description=\u041E\u043F\u0438\u0441 \u0444\u043E\u0440\u0443\u043C\u0443 label.forum.title=\u041D\u0430\u0437\u0432\u0430 \u0444\u043E\u0440\u0443\u043C\u0443 label.titlePrefix=\u041F\u0440\u0435\u0444\u0456\u043A\u0441 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0430 \u0434\u043B\u044F \u0432\u0441\u0456\u0445 \u0441\u0442\u043E\u0440\u0456\u043D\u043E\u043A @@ -408,6 +415,9 @@ label.uploadFavIcon=\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442 label.deleteFavIcon=\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u0437\u043D\u0430\u0447\u043E\u043A \u0441\u0430\u0439\u0442\u0443 label.deleteIconConfirmation=\u0412\u0438 \u0434\u0456\u0439\u0441\u043D\u043E \u0431\u0430\u0436\u0430\u0454\u0442\u0435 \u043F\u043E\u0432\u0435\u0440\u043D\u0443\u0442\u0438 \u0437\u043D\u0430\u0447\u043E\u043A \u0441\u0430\u0439\u0442\u0443 \u0437\u0430 \u0437\u0430\u043C\u043E\u0432\u0447\u0443\u0432\u0430\u043D\u043D\u044F\u043C? label.dummyTextBBCode = \u0412\u0432\u0435\u0434\u0456\u0442\u044C \u0432\u0430\u0448 \u0442\u0435\u043A\u0441\u0442 \u0442\u0443\u0442 +label.sessionTimeout=\u0422\u0430\u0439\u043C\u0430\u0443\u0442 \u0441\u0435\u0441\u0441\u0456\u0457 +label.avatarMaxSize=\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0438\u0439 \u0440\u043E\u0437\u043C\u0456\u0440 \u0430\u0432\u0430\u0442\u0430\u0440\u0443 +label.emailNotification=Email \u0441\u043F\u043E\u0432\u0456\u0449\u0435\u043D\u043D\u044F #Plugins label.plugins=\u041F\u043B\u0430\u0433\u0456\u043D\u0438 @@ -462,3 +472,63 @@ label.branch.header.lastMessage.tooltip=\u041F\u0435\u0440\u0435\u0439\u0442\u04 label.topic.section.in=\u0432 label.tips.close=\u0417\u0430\u043A\u0440\u0438\u0442\u0438 \u0442\u0435\u043C\u0443 label.tips.open=\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u0442\u0435\u043C\u0443 +label.deleteCodeReviewConfirmation=\u0412\u0438 \u0434\u0456\u0439\u0441\u043D\u043E \u0431\u0430\u0436\u0430\u0454\u0442\u0435 \u0432\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u0440\u0435\u0446\u0435\u043D\u0437\u0456\u044E \u043A\u043E\u0434\u0443 (\u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u0456 \u0442\u0430\u043A\u043E\u0436 \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043B\u0435\u043D\u043E)? +label.saved.just.now=\u0427\u0435\u0440\u043D\u0435\u0442\u043A\u0430 \u0449\u043E\u0439\u043D\u043E \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u0430 +label.connection.lost=\u0417`\u0454\u0434\u043D\u0430\u043D\u043D\u044F \u0437 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C \u0440\u043E\u0437\u0456\u0440\u0432\u0430\u043D\u043E.\u0411\u0443\u0434\u044C \u043B\u0430\u0441\u043A\u0430, \u0437\u0431\u0435\u0440\u0435\u0436\u0456\u0442\u044C \u0412\u0430\u0448 \u0442\u0435\u043A\u0441\u0442 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E. +label.saved=\u0417\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u043E +label.ago=\u0442\u043E\u043C\u0443 +label.seconds=\u0441\u0435\u043A\u0443\u043D\u0434 +label.hour=\u0433\u043E\u0434\u0438\u043D +label.minutes.2.4.suffix=\u0438 +label.hours.2.4.suffix=\u0438 +label.minutes.more.than.4.suffix= +label.hours.more.than.4.suffix= +label.minute=\u0445\u0432\u0438\u043B\u0438\u043D +label.minute.1.suffix=\u0443 +label.hours.1.suffix=\u0443 +label.11.minutes.suffix= +label.minute.one.at.the.end.suffix=\u0443 +label.hour.one.at.the.end.suffix=\u0443 +label.11.hours.suffix= +label.registration.success.2= +label.not.logged.in.error=\u0421\u0445\u043E\u0436\u0435, \u0449\u043E \u0432\u0438 \u043D\u0435 \u043C\u0430\u0454\u0442\u0435 \u0431\u0456\u043B\u044C\u0448\u0435 \u043F\u0440\u0430\u0432 \u0437\u0430\u043B\u0438\u0448\u0430\u0442\u0438 \u043F\u043E\u0432\u0456\u0434\u043E\u043C\u043B\u0435\u043D\u043D\u044F \u0443 \u0446\u044C\u043E\u043C\u0443 \u0440\u043E\u0437\u0434\u0456\u043B\u0456. \u0417\u0431\u0435\u0440\u0435\u0436\u0456\u0442\u044C \u0442\u0435\u043A\u0441\u0442 \u0442\u0430 \u043F\u0435\u0440\u0435\u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0442\u0435 \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0443 - \u043C\u043E\u0436\u043B\u0438\u0432\u043E \u0432\u0438 \u0431\u0456\u043B\u044C\u0448\u0435 \u043D\u0435 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D\u0456. +label.search.user=\u041F\u043E\u0448\u0443\u043A \u043A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 +label.search.user.empty=\u041A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456 \u043D\u0435 \u0437\u043D\u0430\u0439\u0434\u0435\u043D\u0456 +label.user.groups=\u0413\u0440\u0443\u043F\u0438 +label.connection.lost.genericError=\u0417'\u0454\u0434\u043D\u0430\u043D\u043D\u044F \u0437 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C \u0440\u043E\u0437\u0456\u0440\u0432\u0430\u043D\u043E +label.user.group.delete.error=\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0440\u0438 \u0432\u0438\u0434\u0430\u043B\u0435\u043D\u043D\u0456 \u0437 \u0433\u0440\u0443\u043F\u0438 +label.user.group.add.error=\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0440\u0438 \u0434\u043E\u0434\u0430\u0432\u0430\u043D\u043D\u0456 \u0434\u043E \u0433\u0440\u0443\u043F\u0438 +label.registration.success.1= +label.user.groups.select.placeHolder=\u0412\u0438\u0431\u0440\u0430\u0442\u0438 \u0433\u0440\u0443\u043F\u0438... +label.user.groups.no.matches=\u041F\u043E\u0448\u0443\u043A \u043D\u0435 \u0434\u0430\u0432 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0456\u0432 \u043F\u043E +label.not.logged.in.genericError=\u0412\u0438\u0431\u0430\u0447\u0442\u0435, \u0441\u0442\u0430\u043B\u0430\u0441\u044F \u043F\u043E\u043C\u0438\u043B\u043A\u0430. \u0411\u0443\u0434\u044C \u043B\u0430\u0441\u043A\u0430, \u043E\u043D\u043E\u0432\u0456\u0442\u044C \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0443 \u0430\u0431\u043E \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044C \u0456 \u0441\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u0437\u043D\u043E\u0432\u0443. +label.sessionTimeout.hint=\u0422\u0430\u0439\u043C\u0430\u0443\u0442 \u0441\u0435\u0441\u0456\u0457 +label.avatarMaxSize.hint=\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0438\u0439 \u0440\u043E\u0437\u043C\u0456\u0440 \u0430\u0432\u0430\u0442\u0430\u0440\u0443 + +label.group.name=\u0406\u043C'\u044F +label.group.numberOfMembers=\u041A\u0456\u043B\u044C\u043A\u0456\u0441\u0442\u044C \u0443\u0447\u0430\u0441\u043D\u0438\u043A\u0456\u0432 +label.group.creation=\u0421\u0442\u0432\u043E\u0440\u0438\u0442\u0438 \u043D\u043E\u0432\u0443 +label.group.placeholder.name=\u0406\u043C'\u044F \u0433\u0440\u0443\u043F\u0438 +label.group.placeholder.description=\u041E\u043F\u0438\u0441 \u0433\u0440\u0443\u043F\u0438 +label.group.create.title=\u041D\u043E\u0432\u0430 \u0433\u0440\u0443\u043F\u0430 +label.group.edit.title=\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u043D\u043D\u044F +label.group.add.user=\u0414\u043E\u0434\u0430\u0432\u0430\u043D\u043D\u044F \u043A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 +label.405.title=\u041C\u0435\u0442\u043E\u0434 \u043D\u0435 \u043F\u0456\u0434\u0442\u0440\u0438\u043C\u0443\u0454\u0442\u044C\u0441\u044F +label.405.detail=\u0412\u0438\u043A\u043E\u0440\u0438\u0441\u0442\u043E\u0432\u0443\u0432\u0430\u043D\u0438\u0439 \u0412\u0430\u043C\u0438 \u043C\u0435\u0442\u043E\u0434 \u043D\u0435 \u043C\u043E\u0436\u0435 \u0431\u0443\u0442\u0438 \u0437\u0430\u0441\u0442\u043E\u0441\u043E\u0432\u0430\u043D\u043E \u0434\u043E \u0437\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043E\u0433\u043E \u0432 \u0437\u0430\u043F\u0438\u0442\u0456 \u0440\u0435\u0441\u0443\u0440\u0441\u0443. +label.405.checkurl=\u0411\u0443\u0434\u044C \u043B\u0430\u0441\u043A\u0430, \u043F\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435, \u0449\u043E \u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0438 \u0434\u0456\u0439\u0441\u043D\u0430 \u0430\u0431\u043E \u043C\u0435\u0442\u043E\u0434, \u0449\u043E \u0432\u0438\u043A\u043E\u0440\u0438\u0441\u0442\u043E\u0432\u0443\u0454\u0442\u044C\u0441\u044F \u043C\u043E\u0436\u0435 \u0431\u0443\u0442\u0438 \u0437\u0430\u0441\u0442\u043E\u0441\u043E\u0432\u0430\u043D\u0438\u0439 \u0434\u043E \u0432\u043A\u0430\u0437\u0430\u043D\u043E\u0433\u043E \u0440\u0435\u0441\u0443\u0440\u0441\u0443. +label.group.user.name=\u0406\u043C'\u044F \u043A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 +label.group.user.email=\u0415\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u0430 \u043F\u043E\u0448\u0442\u0430 +label.group.delete.message=\u0412\u0438 \u0434\u0456\u0439\u0441\u043D\u043E \u0431\u0430\u0436\u0430\u0454\u0442\u0435 \u0432\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u0433\u0440\u0443\u043F\u0443? \u041A\u043E\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456 \u043D\u0435 \u0431\u0443\u0434\u0443\u0442\u044C \u0432\u0438\u0434\u0430\u043B\u0435\u043D\u0456. +label.spamProtection.title=\u0417\u0430\u0445\u0438\u0441\u0442 \u0432\u0456\u0434 \u0441\u043F\u0430\u043C\u0443 +label.spamRule.regex=\u0420\u0435\u0433\u0443\u043B\u044F\u0440\u043D\u0438\u0439 \u0432\u0438\u0440\u0430\u0437 \u0434\u043B\u044F \u043F\u0440\u0430\u0432\u0438\u043B\u0430 +label.spamRule.description=\u041E\u043F\u0438\u0441 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 +label.spamProtection.column.rules=\u041F\u0440\u0430\u0432\u0438\u043B\u0430 +label.spamProtection.column.description=\u041E\u043F\u0438\u0441\u0438 +label.spamRule.delete=\u0412\u0438 \u0432\u043F\u0435\u0432\u043D\u0435\u043D\u0456, \u0449\u043E \u0431\u0430\u0436\u0430\u0454\u0442\u0435 \u0432\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u0446\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u043E? +label.spamProtection.block.message=\u0412\u0438\u0431\u0430\u0447\u0442\u0435, \u0432\u0438 \u0432\u0438\u043A\u043E\u0440\u0438\u0441\u0442\u043E\u0432\u0443\u0454\u0442\u0435 \u0437\u0430\u0431\u043E\u0440\u043E\u043D\u0435\u043D\u0443 \u0430\u0434\u0440\u0435\u0441\u0443 \u0435\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u043E\u0457 \u043F\u043E\u0448\u0442\u0438. \u042F\u043A\u0449\u043E \u0432\u0438 \u043D\u0435 \u0431\u043E\u0442 - \u043D\u0430\u043F\u0438\u0448\u0456\u0442\u044C \u0430\u0434\u043C\u0456\u043D\u0456\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0443 (\u0430\u0434\u0440\u0435\u0441\u0430 \u043F\u043E\u0448\u0442\u0438 \u0432\u043D\u0438\u0437\u0443 \u043A\u043E\u0436\u043D\u043E\u0457 \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0438). +label.spamProtection.settings=\u041D\u0430\u043B\u0430\u0448\u0442\u0443\u0432\u0430\u043D\u043D\u044F \u0437\u0430\u0445\u0438\u0441\u0442\u0443 \u0432\u0456\u0434 \u0441\u043F\u0430\u043C\u0443 +label.spamRule.add=\u0414\u043E\u0434\u0430\u0442\u0438 \u043F\u0440\u0430\u0432\u0438\u043B\u043E +label.spamRule.new=\u041D\u043E\u0432\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u043E +label.spamRule.edit=\u0420\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438 \u043F\u0440\u0430\u0432\u0438\u043B\u043E +label.spamProtection.column.enabled=\u0412\u0432\u0456\u043C\u043A\u043D\u0435\u043D\u043E + diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/ajax/postPreview.jsp b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/ajax/postPreview.jsp index c69691045f..669b228655 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/ajax/postPreview.jsp +++ b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/ajax/postPreview.jsp @@ -29,7 +29,7 @@ </c:when> <c:otherwise> <json:property name="html" escapeXml="false"> - <jtalks:postContent text="${content}" signature="${signature}"/> + <jtalks:postContent text="${content}"/> </json:property> </c:otherwise> </c:choose> diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/branchPermissions.jsp b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/branchPermissions.jsp index 9e00bb9467..5aed06883e 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/branchPermissions.jsp +++ b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/branchPermissions.jsp @@ -31,14 +31,14 @@ <body> <div class="container"> <div class="permissions-branch-header"> - <h1>${branch.name}</h1> + <h1><c:out value="${branch.name}"/></h1> </div> <div class="permissions-branch-header"> <h3> <spring:message code="permissions.moderators"/>: <c:if test="${not empty branch.moderatorsGroup.name}"> - <span id="moderators-group-name">["${branch.moderatorsGroup.name}"]</span><spring:message code="permissions.group"/> + <span id="moderators-group-name">["<c:out value="${branch.moderatorsGroup.name}"/>]</span><spring:message code="permissions.group"/> </c:if> </h3> </div> @@ -46,16 +46,16 @@ <div class="permissions"> <c:forEach items="${permissions.permissions}" var="entry"> <div class="panel panel-primary"> - <div class="panel-heading">${entry.name}</div> + <div class="panel-heading"><c:out value="${entry.name}"/></div> <div class="panel-body"> <div class="pull-left permission-type permission-allowed"> <spring:message code="permissions.allowed"/> </div> <div class="pull-right edit-permission"> <a class="btn editAllowedPermission" - data-permission="${entry.mask}" - data-branch="${branch.id}" - data-permission-name="${entry.name}" + data-permission="<c:out value="${entry.mask}"/>" + data-branch="<c:out value="${branch.id}"/>" + data-permission-name="<c:out value="${entry.name}"/>" href="#"> <span><spring:message code="label.edit"/></span> </a> @@ -63,7 +63,7 @@ <div class="permissions-container"> <ul class="permissions-list"> <c:forEach items="${permissions.accessListMap[entry].allowed}" var="group"> - <li> ${group.name}</li> + <li><c:out value="${group.name}"/></li> </c:forEach> </ul> </div> @@ -75,9 +75,9 @@ </div> <div class="pull-right edit-permission"> <a class="btn editRestrictedPermission" - data-permission="${entry.mask}" - data-branch="${branch.id}" - data-permission-name="${entry.name}" + data-permission="<c:out value="${entry.mask}"/>" + data-branch="<c:out value="${branch.id}"/>" + data-permission-name="<c:out value="${entry.name}"/>" href="#"> <span><spring:message code="label.edit"/></span> </a> @@ -85,7 +85,7 @@ <div class="permissions-container"> <ul class="permissions-list"> <c:forEach items="${permissions.accessListMap[entry].restricted}" var="group"> - <li> ${group.name}</li> + <li><c:out value="${group.name}"/></li> </c:forEach> </ul> </div> diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/codeReviewForm.jsp b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/codeReviewForm.jsp index 029dfb6d85..5c54713903 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/codeReviewForm.jsp +++ b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/codeReviewForm.jsp @@ -23,7 +23,7 @@ <meta name="description" content="<c:out value="${topicDto.topic.branch.name}"/>"> <title> <c:out value="${cmpTitlePrefix}"/> - <c:out value="${topicDto.topic.branch.name}"/> - <spring:message code="label.addCodeReview"/> + <c:if test="${topicDto.topic.branch.name != null}"><c:out value="${topicDto.topic.branch.name}"/> - </c:if><spring:message code="label.addCodeReview"/> @@ -33,14 +33,16 @@ + + +
- +
@@ -56,7 +58,7 @@
"/> + value=""/> diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/editProfile.jsp b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/editProfile.jsp index 638b7b382d..117c033780 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/editProfile.jsp +++ b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/editProfile.jsp @@ -27,7 +27,7 @@ "> <c:out value="${cmpTitlePrefix}"/> - <spring:message code="label.user"/> - "${editedUser.username}" + <spring:message code="label.user"/> - <c:out value="${editedUser.username}"/> @@ -122,7 +122,7 @@ - + diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/editUserProfile.jsp b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/editUserProfile.jsp index a5405c13ea..0d74de562c 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/editUserProfile.jsp +++ b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/editUserProfile.jsp @@ -21,6 +21,9 @@ <%@ taglib prefix="jtalks" uri="http://www.jtalks.org/tags" %> + + + @@ -51,9 +53,9 @@
- + - + diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/pm/pmForm.jsp b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/pm/pmForm.jsp index a1efd573d2..8da73d1e9e 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/pm/pmForm.jsp +++ b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/pm/pmForm.jsp @@ -48,8 +48,7 @@ -
- +
diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/pm/showPm.jsp b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/pm/showPm.jsp index cfd435426c..8f1e97d8c6 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/pm/showPm.jsp +++ b/jcommune-view/jcommune-web-view/src/main/webapp/WEB-INF/jsp/pm/showPm.jsp @@ -82,7 +82,7 @@
- +
"; } content += "
"; return content; @@ -154,6 +153,8 @@ $(function () { } }); + fillGroupListWith(availableGroups, selectedGroups); + var selectAllAvailableFunc = function() { $("#groupListAvailable input[type='checkbox']").each(function() { $(this).prop('checked', $("#selectAllAvailable").prop('checked')); @@ -262,6 +263,7 @@ $(function () { $("#selectAllAlreadyAdded").bind('click', selectAllAlreadyAddedFunc); enableTooltipsOnLongNamesFor(selectedGroups); enableTooltipsOnLongNamesFor(availableGroups); + fillGroupListWith(availableGroups, selectedGroups); } /** @@ -277,6 +279,19 @@ $(function () { } } + /** + * Fills groups names with jQuery in manage permission dialog after dialog creation + * or when group list is modified. This is protection from XSS vulnerability + * when group name contain some script. + */ + function fillGroupListWith() { + Array.prototype.slice.call(arguments) + .forEach(function (groups) { + groups.forEach(function (group) { + jDialog.dialog.find('#span' + group.id).text(group.name); + }); + }) + } } } }); diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/errorUtils.js b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/errorUtils.js index 2222ad649b..dae17196de 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/errorUtils.js +++ b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/errorUtils.js @@ -20,9 +20,7 @@ ErrorUtils.patternForErrorRow = '${message}'; /** Add necessary classes to page elements to highlight errors for current design */ ErrorUtils.fixErrorHighlighting = function() { - if ($('.help-inline').closest('div.control-group').length) { - $('div.control-group:not(:has(.rememberme-lbl))').addClass('error'); - } + $('.help-inline').closest('div.control-group').addClass('error'); } /** Add required classes to highlight errors in specified input for current design diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/forumAdministration.js b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/forumAdministration.js index bbaef79640..44ea10c4a1 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/forumAdministration.js +++ b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/forumAdministration.js @@ -22,13 +22,32 @@ var REQUEST_ENTITY_TOO_LARGE = 413; $(function () { + $(this).find('.management-element').hide(); $("#cmpName").on('click', showForumConfigurationDialog); $("#cmpDescription").on('click', showForumConfigurationDialog); $("#forumLogo").on('click', showForumConfigurationDialog); $("#userDefinedCopyright").on('click', editCopyright); - $("[id^=branchLabel]").on('click', showBranchEditDialog); $("[id^=newBranch]").on('click', showNewBranchDialog); + $("[id^=newGroup], .edit-group").on('click', showGroupManagementDialog); + $("[id^=addUsersInGroup]").on('click', showSearchUsersDialog); + $(".delete-group").on('click', showDeleteGroupDialog); + $("#addSpamRuleBtn, .edit-spam-rule-btn").on('click', showSpamManagementDialog); + $(".delete-spam-rule-btn").on('click', showDeleteSpamRuleDialog); + $("[id^=status]").on('change', sendChangeSpamRuleStatusRequest); + $("[id^=group-], [id^=spam-rule-]").hover( + function () { + $(this).find('.management-element').show() + }, + function () { + $(this).find('.management-element').hide() + } + ); + $('.management-element').keypress(function (e) { + if(isEnterKeyPressed(e)){ + this.click(); + } + }); }); /** @@ -40,7 +59,7 @@ function showNewBranchDialog(e) { } /** - * Show dialog for edit or create branch. + * Show dialog for edit or create branch. * sectionId should be specified if new branch created. * @param {type} e * @param {type} sectionId @@ -143,7 +162,10 @@ function getCurrentAdminValues() { iconPreview: getFavIconUrl() || "", logo: null, icon: null, - copyright: $("#copyrightHolder").text() || "" + copyright: $("#copyrightHolder").text() || "", + sessionTimeout: $("#sessionTimeoutHolder").text() || "", + avatarMaxSize: $("#avatarMaxSizeHolder").text() || "", + emailNotification: $("#emailNotificationHolder").text() || "" } } @@ -217,8 +239,11 @@ function createAdministrationDialog() { ' + Utils.createFormElement($labelForumTitle, 'forumName', 'text', 'first dialog-input') + Utils.createFormElement($labelForumDescription, 'forumDescription', 'text', 'dialog-input') + Utils.createFormElement($labelTitlePrefix, 'forumTitlePrefix', 'text', 'first dialog-input') - + Utils.createFormElement($labelLogoTooltip, 'forumLogoTooltip', 'text', 'dialog-input') - + Utils.createFormElement($copyrightLabel, 'forumCopyright', 'text', 'dialog-input') + ' \ + + Utils.createFormElement($labelLogoTooltip, 'forumLogoTooltip', 'text', 'dialog-input') + + Utils.createFormElement($copyrightLabel, 'forumCopyright', 'text', 'dialog-input') + + Utils.createFormElement($labelSessionTimeout, 'forumSessionTimeout', 'text', 'dialog-input') + + Utils.createFormElement($labelAvatarMaxSize, 'forumAvatarMaxSize', 'text', 'dialog-input') + + Utils.createFormElement($labelEmailNotification, 'forumEmailNotification', 'checkbox', 'dialog-input', "", $labelEmailNotification) + ' \
'; var footerContent = ' \ @@ -234,7 +259,8 @@ function createAdministrationDialog() { maxHeight: 700, firstFocus: true, tabNavigation: ['#forumName', '#forumDescription', '#forumTitlePrefix', '#forumLogoTooltip', '#forumCopyright', - '#administrationSubmitButton', '#administrationCancelButton'], + '#forumSessionTimeout', '#forumAvatarMaxSize', '#forumEmailNotification', + '#administrationSubmitButton', '#administrationCancelButton'], handlers: { '#administrationSubmitButton': {'click': sendForumConfiguration}, '#administrationCancelButton': {'static':'close'} @@ -249,6 +275,10 @@ function createAdministrationDialog() { $('#forumTitlePrefix').tooltip(); $('#forumCopyright').attr('data-original-title', $copyrightHint); $('#forumCopyright').tooltip(); + $('#forumSessionTimeout').attr('data-original-title', $labelSessionTimeoutHint); + $('#forumSessionTimeout').tooltip(); + $('#forumAvatarMaxSize').attr('data-original-title', $labelAvatarMaxSizeHint); + $('#forumAvatarMaxSize').tooltip(); var tabFunc = function (e) { if (document.activeElement.id == jDialog.options.dialogId && (e.keyCode || e.charCode) == tabCode) { @@ -291,6 +321,9 @@ function fillAdminDialogInputs() { $('#logo').val(currentAdminValues.logo); $('#icon').val(currentAdminValues.icon); $('#forumCopyright').val(currentAdminValues.copyright); + $('#forumSessionTimeout').val(currentAdminValues.sessionTimeout); + $('#forumAvatarMaxSize').val(currentAdminValues.avatarMaxSize); + $('#forumEmailNotification').prop('checked', currentAdminValues.emailNotification.toString() === 'true'); } /* @@ -396,7 +429,47 @@ function createIconUploader() { } ); } +function showDeleteGroupDialog(event) { + event.preventDefault(); + var groupRow = $(this).closest('tr'); + var groupId = groupRow.attr("data-group-id"); + var footerContent = ' \ + \ + '; + jDialog.createDialog({ + type: jDialog.confirmType, + title: $labelDelete, + bodyMessage: $deleteGroupDialogMessage, + footerContent: footerContent, + tabNavigation: ['#delete-group-ok', '#delete-group-cancel', 'button.close'], + handlers: { + '#delete-group-ok': {'click': sendDeleteGroupRequest}, + '#delete-group-cancel': {'static':'close'} + } + }); + $('#delete-group-ok').focus(); + + function sendDeleteGroupRequest(event) { + event.preventDefault(); + $.ajax({ + url: $root + '/group/' + groupId, + type: 'DELETE', + async: false, + success: function (response) { + if (response.status === 'SUCCESS') { + location.reload(); + } + }, + error: function () { + jDialog.createDialog({ + type: jDialog.alertType, + bodyMessage: $labelError500Detail + }); + } + }); + } +} /* Adds handler for remove image button */ @@ -418,6 +491,11 @@ function addRestoreDefaultImageHandler(buttonId, defaultImageUrl, onSuccess) { jDialog.closeDialog(); }; + var cancel = function () { + createAdministrationDialog(); + return false; + }; + jDialog.createDialog({ type: jDialog.confirmType, bodyMessage : $labelDeleteLogoConfirmation, @@ -428,7 +506,7 @@ function addRestoreDefaultImageHandler(buttonId, defaultImageUrl, onSuccess) { tabNavigation: ['#restoreDefaultOk','#restoreDefaultCancel'], handlers: { '#restoreDefaultOk': {'click': submitFunc}, - '#restoreDefaultCancel': {'click': createAdministrationDialog} + '#restoreDefaultCancel': {'click': cancel} } }); @@ -476,7 +554,10 @@ function saveInputValues() { logoPreview: jDialog.dialog.find('#logoPreview').attr("src"), icon: jDialog.dialog.find('#icon').val(), iconPreview: jDialog.dialog.find('#iconPreview').attr("src"), - copyright: jDialog.dialog.find('#forumCopyright').val() + copyright: jDialog.dialog.find('#forumCopyright').val(), + sessionTimeout: jDialog.dialog.find('#forumSessionTimeout').val(), + avatarMaxSize: jDialog.dialog.find('#forumAvatarMaxSize').val(), + emailNotification: jDialog.dialog.find('#forumEmailNotification').prop('checked') } } @@ -497,6 +578,9 @@ function sendForumConfiguration(e) { componentInformation.icon = currentAdminValues.icon; componentInformation.titlePrefix = currentAdminValues.titlePrefix; componentInformation.copyright = currentAdminValues.copyright; + componentInformation.sessionTimeout = currentAdminValues.sessionTimeout; + componentInformation.avatarMaxSize = currentAdminValues.avatarMaxSize; + componentInformation.emailNotification = currentAdminValues.emailNotification; jDialog.dialog.find('*').attr('disabled', true); @@ -529,4 +613,363 @@ function sendForumConfiguration(e) { }); } }); -}; \ No newline at end of file +}; + +/** + * Show dialog for create or edit group. + * + * @param event + */ +function showGroupManagementDialog(event) { + event.preventDefault(); + // Create a new group or edit an existing? + var editMode = this.className.indexOf("edit-group") >= 0; + + if (editMode){ + // find row with group and extract all data that we need. + var groupRow = $(this).closest('tr'); + var groupId = groupRow.attr('data-group-id'); + var groupName = groupRow.attr('data-group-name'); + var groupDescription = groupRow.attr('data-group-description'); + } + var bodyContent = + Utils.createFormElement($labelGroupPlaceholderName, 'groupName', 'text', 'first dialog-input') + + Utils.createFormElement($labelGroupPlaceholderDescription, 'groupDescription', 'text', 'dialog-input') + + '
'; + + var footerContent = ' \ + \ + '; + + jDialog.createDialog({ + dialogId: 'groupCreateDialog', + title: editMode ? $labelGroupEditTitle : $labelGroupCreateTitle, + bodyContent: bodyContent, + footerContent: footerContent, + maxWidth: 350, + maxHeight: 500, + firstFocus: true, + tabNavigation: ['#groupName', '#groupDescription', + '#saveGroupButton', '#cancelGroupButton', 'button.close'], + handlers: { + '#saveGroupButton': {'click': sendNewGroup}, + '#cancelGroupButton': {'static': 'close'} + } + }); + + if (editMode) fillDialogInputFields([ + {id: '#groupName', value: groupName}, + {id: '#groupDescription', value: groupDescription}]); + + /** + * Handles submit request from groupManagementDialog by sending POST or PUT request, with params + * containing group information. + * + * @param event + */ + function sendNewGroup(event) { + event.preventDefault(); + + var groupInformation = {}; + groupInformation.id = groupId; + groupInformation.name = jDialog.dialog.find('#groupName').val(); + groupInformation.description = jDialog.dialog.find('#groupDescription').val(); + + jDialog.dialog.find('*').attr('disabled', true); + + $.ajax({ + url: $root + '/group/' + (editMode ? groupId : ''), + type: editMode ? 'PUT' : 'POST', + contentType: 'application/json', + async: false, + data: JSON.stringify(groupInformation), + success: function (response) { + if (response.status === 'SUCCESS') { + location.reload(); + } else { + if (response.result instanceof Array) { + jDialog.prepareDialog(jDialog.dialog); + jDialog.showErrors(jDialog.dialog, response.result, 'group', ''); + } else { + jDialog.createDialog({ + type: jDialog.alertType, + bodyMessage: response.result + }); + } + } + }, + error: function () { + jDialog.createDialog({ + type: jDialog.alertType, + bodyMessage: $labelError500Detail + }); + } + }); + } +} + +function showSpamManagementDialog(event) { + event.preventDefault(); + var spamRule = { + id: '', + regex: '', + description: '', + enabled: '' + }; + var editMode = this.className.indexOf("edit-spam-rule-btn") >= 0; + + var bodyContent = + Utils.createFormElement($spamProtectionRegexPlaceholder, 'spamRegex', 'text', 'first dialog-input') + + Utils.createFormElement($spamProtectionDescriptionPlaceholder, 'spamDescription', 'text', 'dialog-input') + + '
'; + + var footerContent = ' \ + \ + '; + + jDialog.createDialog({ + dialogId: 'spamProtectionDialog', + title: editMode ? $labelEditSpamRule : $labelNewSpamRule, + bodyContent: bodyContent, + footerContent: footerContent, + maxWidth: 350, + maxHeight: 500, + firstFocus: true, + tabNavigation: ['#spamRegex', '#spamDescription', + '#saveSpamRuleButton', '#cancelSpamRuleButton', 'button.close'], + handlers: { + '#saveSpamRuleButton': {'click': saveOrUpdateSpamRule}, + '#cancelSpamRuleButton': {'static': 'close'} + } + }); + + if (editMode) { + var row = $(this).closest('tr'); + spamRule = parseSpamRuleDataFrom(row); + fillDialogInputFields([ + {id: '#spamRegex', value: spamRule.regex}, + {id: '#spamDescription', value: spamRule.description} + ]); + } + + function saveOrUpdateSpamRule(event) { + event.preventDefault(); + spamRule.regex = jDialog.dialog.find('#spamRegex').val(); + spamRule.description = jDialog.dialog.find('#spamDescription').val(); + if (!editMode) spamRule.enabled = true; + + $.ajax({ + url: $root + '/api/spam-rules/' + (editMode ? spamRule.id : ''), + type: editMode ? 'PUT' : 'POST', + contentType: 'application/json', + async: false, + data: JSON.stringify(spamRule), + success: successHandler, + statusCode: {403: showAccessDeniedAlert} + }); + + function successHandler(response) { + if (response.status === 'FAIL' && response.result instanceof Array) { + jDialog.prepareDialog(jDialog.dialog); + jDialog.showErrors(jDialog.dialog, response.result, 'spam', ''); + } else { + location.reload(); + } + } + } +} + +function showAccessDeniedAlert() { + jDialog.createDialog({ + type: jDialog.alertType, + bodyMessage: $labelAccessDeniedMessage + }); +} + +function fillDialogInputFields(elements) { + elements.forEach(function (element) { + jDialog.dialog.find(element.id).val(element.value); + }); +} + +function showDeleteSpamRuleDialog(event) { + event.preventDefault(); + var spamRuleId = $(this).closest('tr').attr('data-rule-id'); + var footerContent = ' \ + \ + '; + + jDialog.createDialog({ + type: jDialog.confirmType, + title: $labelDelete, + bodyMessage: $labelDeleteSpamRule, + footerContent: footerContent, + tabNavigation: ['#delete-spam-rule-ok', '#delete-spam-rule-cancel', 'button.close'], + handlers: { + '#delete-spam-rule-ok': {'click': sendDeleteSpamRuleRequest}, + '#delete-spam-rule-cancel': {'static': 'close'} + } + }); + $('#delete-spam-rule-ok').focus(); + + function sendDeleteSpamRuleRequest(event) { + event.preventDefault(); + $.ajax({ + url: $root + '/api/spam-rules/' + spamRuleId, + type: 'DELETE', + async: false, + statusCode: {403: showAccessDeniedAlert}, + success: function (response) { + if (response.status === 'SUCCESS') { + $('#spam-rule-' + spamRuleId).remove(); + jDialog.closeDialog(); + } + } + }); + } +} +function sendChangeSpamRuleStatusRequest(event) { + event.preventDefault(); + var checkbox = $(this); + var row = checkbox.closest('tr'); + var spamRule = parseSpamRuleDataFrom(row); + $.ajax({ + url: $root + '/api/spam-rules/' + spamRule.id, + type: 'PUT', + contentType: 'application/json', + async: false, + data: JSON.stringify(spamRule), + statusCode: { + 403: function (result) { + showAccessDeniedAlert(result); + checkbox[0].checked = !checkbox[0].checked; + } + } + }); +} + +function parseSpamRuleDataFrom(row) { + var ruleId = row.attr('data-rule-id'); + return { + id: ruleId, + regex: $("#regex-" + ruleId)[0].textContent, + description: $("#description-" + ruleId)[0].textContent, + enabled: $("#status-" + ruleId)[0].checked + }; +} + +/** + * Shows dialog for searching users by any part of username or email. + * + * @param event + */ +function showSearchUsersDialog(event) { + event.preventDefault(); + + var bodyContent = ' \ +
\ + \ + \ +
'; + + jDialog.createDialog({ + dialogId: 'searchUserDialog', + title: $labelGroupAddUser, + bodyContent: bodyContent, + maxWidth: 400, + maxHeight: 500, + tabNavigation: ['#searchPattern', 'button.close'], + }); + + $('#searchPattern').on('keydown', function(event) { + if (event.keyCode === $.ui.keyCode.TAB + && $(this).autocomplete('instance').menu.active) { + event.preventDefault(); + } + }).autocomplete({ + source: function(request, response) { + ErrorUtils.removeErrorMessage('#searchPattern'); + var notInGroupId = $('#addUsersInGroup').attr('data-group-id'); + var pattern = request.term; + $.ajax({ + url: $root + '/user', + type: 'GET', + data: { + notInGroupId: notInGroupId, + pattern: pattern + }, + success: function(serverResponse) { + if (serverResponse.status === 'SUCCESS') { + response($.map(serverResponse.result, function(user) { + var username = user.username; + return { + userId: user.id, + label: [username, user.email].join(' / '), + value: username, + email: user.email + }; + })); + } else { + if (serverResponse.result instanceof Array) { + jDialog.prepareDialog(jDialog.dialog); + jDialog.showErrors(jDialog.dialog, serverResponse.result, 'search', ''); + } else { + jDialog.createDialog({ + type: jDialog.alertType, + bodyMessage: serverResponse.result + }); + } + } + } + }); + }, + focus: function(event) { + event.preventDefault(); + $('.ui-menu-item').removeClass('custom-selected-item'); + var uiActiveMenuItemElement = $("#ui-active-menuitem"); + uiActiveMenuItemElement.parent().addClass('custom-selected-item'); + uiActiveMenuItemElement.removeClass('ui-corner-all'); + }, + select: function(event, ui) { + event.preventDefault(); + this.value = ''; + addUserInGroup(ui.item); + }, + delay: 1000, + autoFocus: true, + minLength: 2 + }); + + /** + * Sends ajax POST request to update user group + * and then puts user on top of users table in the current group. + * + * @param item + */ + function addUserInGroup(item) { + event.preventDefault(); + var currentGroupId = $('#addUsersInGroup').attr('data-group-id'); + $.ajax({ + url: $root + '/user/' + item.userId + '/groups/' + currentGroupId, + type: 'POST', + success: function (serverResponse) { + if (serverResponse.status === 'SUCCESS') { + putUserOnTopOfUsersTable(item); + } + } + }); + + } + + /** + * Puts user on top of the users table in the current group. + * + * @param item + */ + function putUserOnTopOfUsersTable(item) { + var row = $(''); + row.append($('').text(item.value)); + row.append($('').text(item.email)); + $('#groupUserListTableData').prepend(row); + } +} diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/forumEffects.js b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/forumEffects.js index 4222357e9a..ab3be6aee0 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/forumEffects.js +++ b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/forumEffects.js @@ -23,5 +23,6 @@ jQuery(document).ready(function(){ jQuery("a").tooltip(); jQuery("span").tooltip(); jQuery('.btn').tooltip({placement: 'bottom'}); - jQuery('.script-has-tooltip').tooltip(); -}); \ No newline at end of file + jQuery('.script-has-tooltip').tooltip(); + jQuery('[data-toggle="tooltip"]').tooltip(); +}); diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/global.js b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/global.js index d251491c2d..71f87acbc9 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/global.js +++ b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/global.js @@ -18,7 +18,10 @@ $(document).ready(function () { //Sets timezone cookie for the server to show all the dates in a client timezone document.cookie = "GMT=" + new Date().getTimezoneOffset() + "; path=/"; // Initializes image previewing - $('a.pretty-photo').prettyPhoto(); + $('a.pretty-photo').prettyPhoto({ + deeplinking: false, + social_tools:false + }); // popups for individual post links $('a.postLink').each(function () { $(this).click(function (e) { @@ -144,19 +147,6 @@ $(document).ready(function () { }); - //redirect to external links in the body of posts (and signature, profile contacts) - $(document).delegate('.post-content-td a, .content a, #contacts a, .test-signature a, .pm_message_view a, #editorBBCODEdiv a', - 'mousedown', function (e) { - var tagName = $(e.target).prop("tagName").toLowerCase(); - var link = $(e.target); - //prettyPhoto img link - if (tagName == 'img' || tagName != 'link') { - link = $(link).closest("a"); - } - - link.attr('href', link.attr('href').replace('/out?url=', '')); - }); - $(window).resize(); // html5 placeholder emulation for old IE @@ -170,5 +160,17 @@ $(document).ready(function () { return dialog.outerWidth() / 2 * (-1) }); }); + + /* + The first element, which have attribute "autofocus", visible, not hidden, focusable, not in focus + receives focus during initial page load. + Made to support the set focus on the element with attribute "autofocus" in browsers that do not support HTML5. + + Note: there is a strange behavior when opening background tabs in Opera 12. + */ + $('[autofocus]') + .filter(':visible:not(:hidden):focusable:not(:focus)') + .eq(0) + .focus(); }); diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/keymaps.js b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/keymaps.js index 63f3d13504..0df0401442 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/keymaps.js +++ b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/keymaps.js @@ -119,34 +119,6 @@ Keymaps.linksEditor = function (e) { } } -Keymaps.linksEditorRemoveButton = function (e) { - if ((e.keyCode || e.charCode) == tabCode) { - e.preventDefault(); - $('#mainLinksEditor #cancelLink').focus(); - } -} - -Keymaps.linksEditorCancelButton = function (e) { - if ((e.keyCode || e.charCode) == tabCode) { - e.preventDefault(); - $('#mainLinksEditor button.close').focus(); - } -} - -Keymaps.linksEditorHintInput = function (e) { - if ((e.keyCode || e.charCode) == tabCode) { - e.preventDefault(); - $('#mainLinksEditor #saveLink').focus(); - } -} - -Keymaps.linksEditorSaveButton = function (e) { - if ((e.keyCode || e.charCode) == tabCode) { - e.preventDefault(); - $('#mainLinksEditor #cancelLink').focus(); - } -} - Keymaps.uploadBannerCancelButton = function (e) { if ((e.keyCode || e.charCode) == tabCode) { e.preventDefault(); @@ -168,7 +140,7 @@ Keymaps.defaultDialog = function (e) { //disable submit by enter if (e.keyCode == enterCode) { //if focus on button then do action of button, else click submit - if (!$(e.target).hasClass('btn')) { + if (!$(e.target).hasClass('btn') && !$(e.target).hasClass('close')) { e.preventDefault(); jDialog.dialog.find('.btn-primary:first').click(); } diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/mainLinksEditor.js b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/mainLinksEditor.js index 21a9ed96cc..076f3688bd 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/mainLinksEditor.js +++ b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/mainLinksEditor.js @@ -49,11 +49,10 @@ function showExternalLinksDialog() { idToExternalLinkMap[id] = externalLink; elements[i] = externalLink; - tabNavigationOrder.push("#editLink" + id); - tabNavigationOrder.push("#removeLink" + id); + tabNavigationOrder.push("#editLink" + id, "#removeLink" + id); }); - tabNavigationOrder.push('#addMainLink'); - tabNavigationOrder.push('button.close'); + tabNavigationOrder.push('#addMainLink', 'button.close', '#linkTitle', '#linkUrl', + '#linkHint', '#saveLink', '#removeLink', '#cancelLink'); var footerContent = '' + ' \ @@ -65,7 +64,11 @@ function showExternalLinksDialog() { '' + createLinksTableRows(elements) + ' \ ' + - "
"+ + "
" + + Utils.createFormElement($labelTitle, 'linkTitle', 'text', 'edit-links dialog-input') + + Utils.createFormElement($labelUrl, 'linkUrl', 'text', 'edit-links dialog-input') + + Utils.createFormElement($labelHint, 'linkHint', 'text', 'edit-links dialog-input') + + "
"+ ' '; var editButtonClick = function (e) { @@ -86,20 +89,6 @@ function showExternalLinksDialog() { toAction('add'); }; - var linksEditorCloseButtonInput = function (e) { - if ((e.keyCode || e.charCode) == tabCode) { - e.preventDefault(); - if ($('#mainLinksEditor #linkTitle:visible')[0]) { - $('#mainLinksEditor #linkTitle').focus(); - } else if ($('#mainLinksEditor #removeLink:visible')[0]) { - $('#mainLinksEditor #removeLink').focus(); - } - else { - $(tabNavigationOrder[0]).focus(); - } - } - } - jDialog.createDialog({ dialogId: 'mainLinksEditor', title: $labelLinksEditor, @@ -110,12 +99,7 @@ function showExternalLinksDialog() { tabNavigation: tabNavigationOrder, dialogKeydown: Keymaps.linksEditor, handlers: { - '#addMainLink': {'click': addButtonClick}, - 'button.close': {'keydown': linksEditorCloseButtonInput}, - '#mainLinksEditor #linkHint': {'keydown': Keymaps.linksEditorHintInput}, - '#mainLinksEditor #saveLink': {'keydown': Keymaps.linksEditorSaveButton}, - '#mainLinksEditor #cancelLink': {'keydown': Keymaps.linksEditorCancelButton}, - '#mainLinksEditor #removeLink': {'keydown': Keymaps.linksEditorRemoveButton} + '#addMainLink': {'click': addButtonClick} }, handlersDelegate: { '.icon-pencil': {'click': editButtonClick}, @@ -182,18 +166,12 @@ function listOfLinksVisible(visible) { }, '100'); } -function getLinkEditorFormElements() { - return Utils.createFormElement($labelTitle, 'linkTitle', 'text', 'edit-links dialog-input') + - Utils.createFormElement($labelUrl, 'linkUrl', 'text', 'edit-links dialog-input') + - Utils.createFormElement($labelHint, 'linkHint', 'text', 'edit-links dialog-input'); -} - function editLinksVisible(visible) { var intervalID = setInterval(function () { if ($('.edit-links')) { if (visible) { var link = getLinkById(actionId); - $("#linkEditorPlaceholder").html(getLinkEditorFormElements()); + $("#linkEditorPlaceholder").removeClass("hide-element"); $('#linkTitle').val(link.title); $('#linkUrl').val(link.url); $('#linkHint').val(link.hint); @@ -240,7 +218,7 @@ function editLinksVisible(visible) { }); } else { - $("#linkEditorPlaceholder").html(""); + $("#linkEditorPlaceholder").addClass("hide-element"); $('.edit-links').addClass("hide-element"); } clearInterval(intervalID) @@ -281,11 +259,11 @@ function addLinkVisible(visible) { var intervalID = setInterval(function () { if ($('.edit-links')) { if (visible) { - $("#linkEditorPlaceholder").html(getLinkEditorFormElements()); + $("#linkEditorPlaceholder").removeClass("hide-element"); + $('.edit-links').removeClass("hide-element"); $('#linkTitle').val(""); $('#linkUrl').val(""); $('#linkHint').val(""); - $('.edit-links').removeClass("hide-element"); $('#linkTitle').focus(); $('#saveLink').unbind('click').bind('click', function (e) { e.preventDefault(); @@ -328,7 +306,7 @@ function addLinkVisible(visible) { }); } else { - $("#linkEditorPlaceholder").html(""); + $("#linkEditorPlaceholder").addClass("hide-element"); $('.edit-links').addClass("hide-element"); } clearInterval(intervalID) diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/pollPreview.js b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/pollPreview.js index ba3bb0231c..3d364d6988 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/pollPreview.js +++ b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/pollPreview.js @@ -25,10 +25,18 @@ $(document).ready(function () { //setting proper datepicker locale, at current time there are not ukraine and spain datepicker locales, // so will be used only en and ru locales. - if ($localeCode == 'ru') { - $.datepicker.setDefaults($.datepicker.regional['ru']); - } else { - $.datepicker.setDefaults($.datepicker.regional[""]); + switch($localeCode) { + case "uk": + $.datepicker.setDefaults($.datepicker.regional.uk) + break; + case "es": + $.datepicker.setDefaults($.datepicker.regional.es) + break; + case "ru": + $.datepicker.setDefaults($.datepicker.regional.ru) + break; + default: + $.datepicker.setDefaults($.datepicker.regional[""]) } }); diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/postDraft.js b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/postDraft.js new file mode 100644 index 0000000000..250aa37e35 --- /dev/null +++ b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/postDraft.js @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2011 JTalks.org Team + * This library 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 2.1 of the License, or (at your option) any later version. + * This library 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 this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +(function (draft) { + 'use strict'; + + var SAVE_INTERVAL = 15 * 1000; + + var MIN_CONTENT_LENGTH = 2, + MAX_CONTENT_LENGTH = 20000; + + var CONTENT_ERROR_MESSAGE = $labelMessageSizeValidation + .replace('{min}', MIN_CONTENT_LENGTH).replace('{max}', MAX_CONTENT_LENGTH); + + /** + * Class for access to post draft API. + * + * @constructor + */ + function PostDraftApi() {} + + /** + * Saves or update draft. + * + * @param draft object with content of draft post + * @param async whether request is async (be default it is true) + * @returns {$.Deferred} + */ + PostDraftApi.prototype.save = function (draft, async) { + + async = async !== false; + + return $.ajax({ + url: baseUrl + '/posts/savedraft', + type: 'POST', + async: async, + data: JSON.stringify(draft), + contentType: 'application/json' + }); + }; + + /** + * Deletes draft. + * + * @param id the draft id + * @returns {jQuery.Deferred} + */ + PostDraftApi.prototype.remove = function (id) { + return $.ajax({ + url: baseUrl + '/drafts/' + id + '/delete' + }); + }; + + /** + * Class for work with current draft post + * + * @constructor + */ + function PostDraft(api, counter, popup) { + var self = this; + + this._api = api; + this._counter = counter; + this._popup = popup; + + this._topicId = $('#topicId').val(); + this._draftId = $('#draftId').val(); + + this._bodyText = $('#postBody'); + + /* + * Construct element for displaying error. If on the current page already + * there is such element generated on backend, we use it. + */ + var error = $('#bodyText\\.errors'), + defaultError = $('').hide(); + + this._bodyTextError = error.length ? error + : defaultError.clone().insertAfter(self._bodyText); + + this._bodyTextGroup = this._bodyText.parents('.control-group'); + + this._lastSavedDraftState = this._getDraftState(); + this._savingTimer = new draft.IntervalTimer(this.save.bind(this), SAVE_INTERVAL); + + this._bodyText.on('blur', this._onBlur.bind(this)); + this._bodyText.on('input', this._onInput.bind(this)); + } + + /** + * Checks whether there is enough data in post and it is valid, and if it so, + * tries to save it's draft, otherwise just returns failed promise. + * + * @param {boolean} [async] whether request is async (be default it is true) + * @returns {jQuery.Deferred} + */ + PostDraft.prototype.save = function (async) { + var self = this, + draft = this._getDraftState(); + + async = async !== false; + + if (enoughData(draft) && this._validate()) { + return this._api.save(draft, async) + .done(function (response) { + self._draftId = response.result; + + self._counter.restart(); + self._savingTimer.stop(); + self._popup.hide(); + + self._lastSavedDraftState = draft; + }) + .fail(function (xhr, status) { + if (status == 'timeout' || xhr.status == 0) { + if (xhr.status == 0) { + // Need it because firefox makes no difference between refused connection and + // aborted request + setTimeout(function () { + self._popup.show($labelConnectionLost); + }, 3000); + } else { + self._popup.show($labelConnectionLost); + } + } else if (xhr.status == 403) { + self._popup.show($labelNotLoggedInError); + } + }); + } else { + return $.Deferred(function (deferred) { + deferred.fail(); + }); + } + + function enoughData(draft) { + return (draft['bodyText'] && draft['bodyText'].length > 0); + } + }; + + /** + * Determines whether the post was changed from last saving. + * + * @returns {boolean} whether this draft was changed + */ + PostDraft.prototype.wasChanged = function () { + var currentState = this._getDraftState(), + previousState = this._lastSavedDraftState; + + return currentState['bodyText'] !== previousState['bodyText']; + }; + + /** + * Validates content of the post. + * Note: it checks only that values is not more than specified maximum. + * + * @returns {boolean} whether content is valid + * @private + */ + PostDraft.prototype._validate = function () { + var self = this, + draft = this._getDraftState(); + + if (draft['bodyText'] && (draft['bodyText'].length > MAX_CONTENT_LENGTH)) { + self._showError(CONTENT_ERROR_MESSAGE); + return false; + } else { + self._hideError(); + return true; + } + }; + + /** + * Shows error message and highlight it. + * + * @param message the message + * @private + */ + PostDraft.prototype._showError = function (message) { + this._bodyTextError.text(message).show(); + this._bodyTextGroup.addClass('error'); + }; + + /** + * Hides error message. + * + * @private + */ + PostDraft.prototype._hideError = function () { + this._bodyTextError.text('').hide(); + this._bodyTextGroup.removeClass('error'); + }; + + /** + * Collects data from fields and returns it as an object. + * + * @returns {*} + * @private + */ + PostDraft.prototype._getDraftState = function () { + return { + bodyText: this._bodyText.val(), + topicId: this._topicId + }; + }; + + PostDraft.prototype._onBlur = function () { + this._savingTimer.stop(); + if (this.wasChanged()) { + this.save(); + } + }; + + PostDraft.prototype._onInput = function (event) { + var self = this; + + // Remove draft if user emptied content (for instance by Ctrl-A and Backspace) + if ($(event.target).is(this._bodyText) && this._bodyText.val().length == 0) { + this._api.remove(this._draftId).then(function () { + self._savingTimer.stop(); + self._counter.stop(); + self._lastSavedDraftState = self._getDraftState(); + }); + } else { + this._savingTimer.start(); + this._validate(); + } + }; + + $(function () { + var postBody = $('#postBody'), + lastSavedTime = $('#savedMillis').val(); + + // Check that we are on the right page + if (postBody.length > 0) { + var api = new PostDraftApi(), + counter = new draft.Counter(), + popup = new draft.AlertMessagePopup(); + + counter.getElement().insertAfter(postBody); + popup.getElement().insertBefore(postBody); + + if (lastSavedTime) { + counter.start(Date.now() - lastSavedTime); + } + + var postDraft = new PostDraft(api, counter, popup); + + // Try to save draft when user leaves this page + window.addEventListener('beforeunload', function() { + if (postDraft.wasChanged()) { + postDraft.save(false); + } + }); + } + }); +})(draft); \ No newline at end of file diff --git a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/registration.js b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/registration.js index 2c17c63028..cbd3bf5264 100644 --- a/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/registration.js +++ b/jcommune-view/jcommune-web-view/src/main/webapp/resources/javascript/app/registration.js @@ -29,9 +29,14 @@ $(function () { var captchaContainer = $('.registration-page'); if (captchaContainer) { - captchaContainer.find('.captcha-refresh, .captcha-img').click(function (e) { - e.preventDefault(); - refreshCaptchaJsp(); + // add event handlers + captchaContainer.find('.captcha-img, .btn-captcha-refresh').on({ + click: refreshCaptchaJsp, + keypress: function(e) { + if (isEnterKeyPressed(e)) { + refreshCaptchaJsp(); + } + } }); } @@ -71,8 +76,13 @@ function signUp(e) { Utils.createFormElement($labelPassword, 'password', 'password', null, widthStyle) + Utils.createFormElement($labelPasswordConfirmation, 'passwordConfirm', 'password', null, widthStyle) + Utils.createFormElement($lableHoneypotCaptcha, 'honeypotCaptcha', 'text', 'hide-element', widthStyle); + var widthStyleForPlugin = widthStyle.split(":"); for (var pluginId in params) { - bodyContent += params[pluginId]; + bodyContent += $(params[pluginId]) + .find("input[id^='plugin']") + .css(widthStyleForPlugin[0], widthStyleForPlugin[1]) + .end() + .prop("outerHTML"); } var footerContent = '