daoClass) {
+ return DBIHolder.jDBI.onDemand(daoClass);
+ }
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/dao/AbstractDao.java b/persist/src/main/java/ru/javaops/masterjava/persist/dao/AbstractDao.java
new file mode 100644
index 000000000..f7e97e459
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/dao/AbstractDao.java
@@ -0,0 +1,11 @@
+package ru.javaops.masterjava.persist.dao;
+
+/**
+ * gkislin
+ * 27.10.2016
+ *
+ *
+ */
+public interface AbstractDao {
+ void clean();
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/dao/CityDao.java b/persist/src/main/java/ru/javaops/masterjava/persist/dao/CityDao.java
new file mode 100644
index 000000000..71fe79128
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/dao/CityDao.java
@@ -0,0 +1,38 @@
+package ru.javaops.masterjava.persist.dao;
+
+import com.bertoncelj.jdbi.entitymapper.EntityMapperFactory;
+import one.util.streamex.StreamEx;
+import org.skife.jdbi.v2.sqlobject.*;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapperFactory;
+import ru.javaops.masterjava.persist.model.City;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+@RegisterMapperFactory(EntityMapperFactory.class)
+public abstract class CityDao implements AbstractDao {
+
+ @SqlUpdate("TRUNCATE city CASCADE ")
+ @Override
+ public abstract void clean();
+
+ @SqlQuery("SELECT * FROM city ORDER BY name")
+ public abstract List getAll();
+
+ public Map getAsMap() {
+ return StreamEx.of(getAll()).toMap(City::getRef, c -> c);
+ }
+
+ @SqlUpdate("INSERT INTO city (ref, name) VALUES (:ref, :name)")
+ @GetGeneratedKeys
+ public abstract int insertGeneratedId(@BindBean City city);
+
+ public void insert(City city) {
+ int id = insertGeneratedId(city);
+ city.setId(id);
+ }
+
+ @SqlBatch("INSERT INTO city (ref, name) VALUES (:ref, :name)")
+ public abstract void insertBatch(@BindBean Collection cities);
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/dao/GroupDao.java b/persist/src/main/java/ru/javaops/masterjava/persist/dao/GroupDao.java
new file mode 100644
index 000000000..9fc78e2c7
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/dao/GroupDao.java
@@ -0,0 +1,38 @@
+package ru.javaops.masterjava.persist.dao;
+
+import com.bertoncelj.jdbi.entitymapper.EntityMapperFactory;
+import one.util.streamex.StreamEx;
+import org.skife.jdbi.v2.sqlobject.*;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapperFactory;
+import ru.javaops.masterjava.persist.model.Group;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+@RegisterMapperFactory(EntityMapperFactory.class)
+public abstract class GroupDao implements AbstractDao {
+
+ @SqlUpdate("TRUNCATE groups CASCADE ")
+ @Override
+ public abstract void clean();
+
+ @SqlQuery("SELECT * FROM groups ORDER BY name")
+ public abstract List getAll();
+
+ public Map getAsMap() {
+ return StreamEx.of(getAll()).toMap(Group::getName, g -> g);
+ }
+
+ @SqlUpdate("INSERT INTO groups (name, type, project_id) VALUES (:name, CAST(:type AS group_type), :projectId)")
+ @GetGeneratedKeys
+ public abstract int insertGeneratedId(@BindBean Group groups);
+
+ public void insert(Group groups) {
+ int id = insertGeneratedId(groups);
+ groups.setId(id);
+ }
+
+ @SqlBatch("INSERT INTO groups (name, type, project_id) VALUES (:name, CAST(:type AS group_type), :projectId)")
+ public abstract void insertBatch(@BindBean Collection groups);
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/dao/ProjectDao.java b/persist/src/main/java/ru/javaops/masterjava/persist/dao/ProjectDao.java
new file mode 100644
index 000000000..e5b8a9b1f
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/dao/ProjectDao.java
@@ -0,0 +1,37 @@
+package ru.javaops.masterjava.persist.dao;
+
+import com.bertoncelj.jdbi.entitymapper.EntityMapperFactory;
+import one.util.streamex.StreamEx;
+import org.skife.jdbi.v2.sqlobject.BindBean;
+import org.skife.jdbi.v2.sqlobject.GetGeneratedKeys;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapperFactory;
+import ru.javaops.masterjava.persist.model.Project;
+
+import java.util.List;
+import java.util.Map;
+
+@RegisterMapperFactory(EntityMapperFactory.class)
+public abstract class ProjectDao implements AbstractDao {
+
+ @SqlUpdate("TRUNCATE project CASCADE ")
+ @Override
+ public abstract void clean();
+
+ @SqlQuery("SELECT * FROM project ORDER BY name")
+ public abstract List getAll();
+
+ public Map getAsMap() {
+ return StreamEx.of(getAll()).toMap(Project::getName, g -> g);
+ }
+
+ @SqlUpdate("INSERT INTO project (name, description) VALUES (:name, :description)")
+ @GetGeneratedKeys
+ public abstract int insertGeneratedId(@BindBean Project project);
+
+ public void insert(Project project) {
+ int id = insertGeneratedId(project);
+ project.setId(id);
+ }
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/dao/UserDao.java b/persist/src/main/java/ru/javaops/masterjava/persist/dao/UserDao.java
new file mode 100644
index 000000000..2f138983c
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/dao/UserDao.java
@@ -0,0 +1,71 @@
+package ru.javaops.masterjava.persist.dao;
+
+import com.bertoncelj.jdbi.entitymapper.EntityMapperFactory;
+import one.util.streamex.IntStreamEx;
+import org.skife.jdbi.v2.sqlobject.*;
+import org.skife.jdbi.v2.sqlobject.customizers.BatchChunkSize;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapperFactory;
+import ru.javaops.masterjava.persist.DBIProvider;
+import ru.javaops.masterjava.persist.model.User;
+
+import java.util.List;
+
+/**
+ * gkislin
+ * 27.10.2016
+ *
+ *
+ */
+@RegisterMapperFactory(EntityMapperFactory.class)
+public abstract class UserDao implements AbstractDao {
+
+ public User insert(User user) {
+ if (user.isNew()) {
+ int id = insertGeneratedId(user);
+ user.setId(id);
+ } else {
+ insertWitId(user);
+ }
+ return user;
+ }
+
+ @SqlQuery("SELECT nextval('user_seq')")
+ abstract int getNextVal();
+
+ @Transaction
+ public int getSeqAndSkip(int step) {
+ int id = getNextVal();
+ DBIProvider.getDBI().useHandle(h -> h.execute("ALTER SEQUENCE user_seq RESTART WITH " + (id + step)));
+ return id;
+ }
+
+ @SqlUpdate("INSERT INTO users (full_name, email, flag, city_id) VALUES (:fullName, :email, CAST(:flag AS USER_FLAG), :cityId) ")
+ @GetGeneratedKeys
+ abstract int insertGeneratedId(@BindBean User user);
+
+ @SqlUpdate("INSERT INTO users (id, full_name, email, flag, city_id) VALUES (:id, :fullName, :email, CAST(:flag AS USER_FLAG), :cityId) ")
+ abstract void insertWitId(@BindBean User user);
+
+ @SqlQuery("SELECT * FROM users ORDER BY full_name, email LIMIT :it")
+ public abstract List getWithLimit(@Bind int limit);
+
+ // http://stackoverflow.com/questions/13223820/postgresql-delete-all-content
+ @SqlUpdate("TRUNCATE users CASCADE")
+ @Override
+ public abstract void clean();
+
+ // https://habrahabr.ru/post/264281/
+ @SqlBatch("INSERT INTO users (id, full_name, email, flag, city_id) VALUES (:id, :fullName, :email, CAST(:flag AS USER_FLAG), :cityId)" +
+ "ON CONFLICT DO NOTHING")
+// "ON CONFLICT (email) DO UPDATE SET full_name=:fullName, flag=CAST(:flag AS USER_FLAG)")
+ public abstract int[] insertBatch(@BindBean List users, @BatchChunkSize int chunkSize);
+
+
+ public List insertAndGetConflictEmails(List users) {
+ int[] result = insertBatch(users, users.size());
+ return IntStreamEx.range(0, users.size())
+ .filter(i -> result[i] == 0)
+ .mapToObj(users::get)
+ .toList();
+ }
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/dao/UserGroupDao.java b/persist/src/main/java/ru/javaops/masterjava/persist/dao/UserGroupDao.java
new file mode 100644
index 000000000..2ca4a8ed7
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/dao/UserGroupDao.java
@@ -0,0 +1,38 @@
+package ru.javaops.masterjava.persist.dao;
+
+import com.bertoncelj.jdbi.entitymapper.EntityMapperFactory;
+import one.util.streamex.StreamEx;
+import org.skife.jdbi.v2.sqlobject.*;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapperFactory;
+import ru.javaops.masterjava.persist.model.UserGroup;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * gkislin
+ * 27.10.2016
+ *
+ *
+ */
+@RegisterMapperFactory(EntityMapperFactory.class)
+public abstract class UserGroupDao implements AbstractDao {
+
+ @SqlUpdate("TRUNCATE user_group CASCADE")
+ @Override
+ public abstract void clean();
+
+ @SqlBatch("INSERT INTO user_group (user_id, group_id) VALUES (:userId, :groupId)")
+ public abstract void insertBatch(@BindBean List userGroups);
+
+ @SqlQuery("SELECT user_id FROM user_group WHERE group_id=:it")
+ public abstract Set getUserIds(@Bind int groupId);
+
+ public static List toUserGroups(int userId, Integer... groupIds) {
+ return StreamEx.of(groupIds).map(groupId -> new UserGroup(userId, groupId)).toList();
+ }
+
+ public static Set getByGroupId(int groupId, List userGroups) {
+ return StreamEx.of(userGroups).filter(ug -> ug.getGroupId() == groupId).map(UserGroup::getUserId).toSet();
+ }
+}
\ No newline at end of file
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/model/BaseEntity.java b/persist/src/main/java/ru/javaops/masterjava/persist/model/BaseEntity.java
new file mode 100644
index 000000000..8a840ad6c
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/BaseEntity.java
@@ -0,0 +1,34 @@
+package ru.javaops.masterjava.persist.model;
+
+import lombok.*;
+
+/**
+ * gkislin
+ * 28.10.2016
+ */
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
+abstract public class BaseEntity {
+
+ @Getter
+ @Setter
+ protected Integer id;
+
+ public boolean isNew() {
+ return id == null;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BaseEntity baseEntity = (BaseEntity) o;
+ return id != null && id.equals(baseEntity.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return id == null ? 0 : id;
+ }
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/model/City.java b/persist/src/main/java/ru/javaops/masterjava/persist/model/City.java
new file mode 100644
index 000000000..6c968930d
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/City.java
@@ -0,0 +1,21 @@
+package ru.javaops.masterjava.persist.model;
+
+import lombok.*;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@RequiredArgsConstructor
+@NoArgsConstructor
+@ToString(callSuper = true)
+public class City extends BaseEntity {
+
+ @NonNull
+ private String ref;
+ @NonNull
+ private String name;
+
+ public City(Integer id, String ref, String name) {
+ this(ref, name);
+ this.id = id;
+ }
+}
\ No newline at end of file
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/model/Group.java b/persist/src/main/java/ru/javaops/masterjava/persist/model/Group.java
new file mode 100644
index 000000000..2482b1fa2
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/Group.java
@@ -0,0 +1,21 @@
+package ru.javaops.masterjava.persist.model;
+
+import com.bertoncelj.jdbi.entitymapper.Column;
+import lombok.*;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@RequiredArgsConstructor
+@NoArgsConstructor
+@ToString(callSuper = true)
+public class Group extends BaseEntity {
+
+ @NonNull private String name;
+ @NonNull private GroupType type;
+ @NonNull @Column("project_id") private int projectId;
+
+ public Group(Integer id, String name, GroupType type, int projectId) {
+ this(name, type, projectId);
+ this.id = id;
+ }
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/model/GroupType.java b/persist/src/main/java/ru/javaops/masterjava/persist/model/GroupType.java
new file mode 100644
index 000000000..ab80bfc76
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/GroupType.java
@@ -0,0 +1,7 @@
+package ru.javaops.masterjava.persist.model;
+
+public enum GroupType {
+ REGISTERING,
+ CURRENT,
+ FINISHED;
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/model/Project.java b/persist/src/main/java/ru/javaops/masterjava/persist/model/Project.java
new file mode 100644
index 000000000..e3bcb6dcc
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/Project.java
@@ -0,0 +1,19 @@
+package ru.javaops.masterjava.persist.model;
+
+import lombok.*;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@RequiredArgsConstructor
+@NoArgsConstructor
+@ToString(callSuper = true)
+public class Project extends BaseEntity {
+
+ @NonNull private String name;
+ @NonNull private String description;
+
+ public Project(Integer id, String name, String description) {
+ this(name, description);
+ this.id = id;
+ }
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/model/User.java b/persist/src/main/java/ru/javaops/masterjava/persist/model/User.java
new file mode 100644
index 000000000..3eb24a74c
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/User.java
@@ -0,0 +1,23 @@
+package ru.javaops.masterjava.persist.model;
+
+import com.bertoncelj.jdbi.entitymapper.Column;
+import lombok.*;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@RequiredArgsConstructor
+@NoArgsConstructor
+@ToString(callSuper = true)
+public class User extends BaseEntity {
+ @Column("full_name")
+ private @NonNull String fullName;
+ private @NonNull String email;
+ private @NonNull UserFlag flag;
+ @Column("city_id")
+ private @NonNull Integer cityId;
+
+ public User(Integer id, String fullName, String email, UserFlag flag, Integer cityId) {
+ this(fullName, email, flag, cityId);
+ this.id=id;
+ }
+}
\ No newline at end of file
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/model/UserFlag.java b/persist/src/main/java/ru/javaops/masterjava/persist/model/UserFlag.java
new file mode 100644
index 000000000..bc2f69183
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/UserFlag.java
@@ -0,0 +1,11 @@
+package ru.javaops.masterjava.persist.model;
+
+/**
+ * gkislin
+ * 13.10.2016
+ */
+public enum UserFlag {
+ active,
+ deleted,
+ superuser;
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/model/UserGroup.java b/persist/src/main/java/ru/javaops/masterjava/persist/model/UserGroup.java
new file mode 100644
index 000000000..d7eaee36b
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/UserGroup.java
@@ -0,0 +1,12 @@
+package ru.javaops.masterjava.persist.model;
+
+import com.bertoncelj.jdbi.entitymapper.Column;
+import lombok.*;
+
+@Data
+@RequiredArgsConstructor
+@NoArgsConstructor
+public class UserGroup {
+ @NonNull @Column("user_id") private Integer userId;
+ @NonNull @Column("group_id") private Integer groupId;
+}
\ No newline at end of file
diff --git a/persist/src/main/resources/persist.conf b/persist/src/main/resources/persist.conf
new file mode 100644
index 000000000..64dda08be
--- /dev/null
+++ b/persist/src/main/resources/persist.conf
@@ -0,0 +1,7 @@
+db {
+ url = "jdbc:postgresql://localhost:5432/masterjava"
+ user = user
+ password = password
+}
+
+include required(file("/home/konst/work/masterjava/config/persist.conf"))
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/CityTestData.java b/persist/src/test/java/ru/javaops/masterjava/persist/CityTestData.java
new file mode 100644
index 000000000..084b5cd1a
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/CityTestData.java
@@ -0,0 +1,32 @@
+package ru.javaops.masterjava.persist;
+
+import com.google.common.collect.ImmutableMap;
+import ru.javaops.masterjava.persist.dao.CityDao;
+import ru.javaops.masterjava.persist.model.City;
+
+import java.util.Map;
+
+/**
+ * gkislin
+ * 14.11.2016
+ */
+public class CityTestData {
+ public static final City KIEV = new City("kiv", "Киев");
+ public static final City MINSK = new City("mnsk", "Минск");
+ public static final City MOSCOW = new City("mow", "Москва");
+ public static final City SPB = new City("spb", "Санкт-Петербург");
+
+ public static final Map CITIES = ImmutableMap.of(
+ KIEV.getRef(), KIEV,
+ MINSK.getRef(), MINSK,
+ MOSCOW.getRef(), MOSCOW,
+ SPB.getRef(), SPB);
+
+ public static void setUp() {
+ CityDao dao = DBIProvider.getDao(CityDao.class);
+ dao.clean();
+ DBIProvider.getDBI().useTransaction((conn, status) -> {
+ CITIES.values().forEach(dao::insert);
+ });
+ }
+}
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/DBITestProvider.java b/persist/src/test/java/ru/javaops/masterjava/persist/DBITestProvider.java
new file mode 100644
index 000000000..b391650fc
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/DBITestProvider.java
@@ -0,0 +1,28 @@
+package ru.javaops.masterjava.persist;
+
+import com.typesafe.config.Config;
+import ru.javaops.masterjava.config.Configs;
+
+import java.sql.DriverManager;
+
+/**
+ * gkislin
+ * 27.10.2016
+ */
+public class DBITestProvider {
+ public static void initDBI() {
+ Config db = Configs.getConfig("persist.conf","db");
+ initDBI(db.getString("url"), db.getString("user"), db.getString("password"));
+ }
+
+ public static void initDBI(String dbUrl, String dbUser, String dbPassword) {
+ DBIProvider.init(() -> {
+ try {
+ Class.forName("org.postgresql.Driver");
+ } catch (ClassNotFoundException e) {
+ throw new IllegalStateException("PostgreSQL driver not found", e);
+ }
+ return DriverManager.getConnection(dbUrl, dbUser, dbPassword);
+ });
+ }
+}
\ No newline at end of file
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/GroupTestData.java b/persist/src/test/java/ru/javaops/masterjava/persist/GroupTestData.java
new file mode 100644
index 000000000..01aba6bb9
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/GroupTestData.java
@@ -0,0 +1,55 @@
+package ru.javaops.masterjava.persist;
+
+import com.google.common.collect.ImmutableMap;
+import ru.javaops.masterjava.persist.dao.GroupDao;
+import ru.javaops.masterjava.persist.model.Group;
+
+import java.util.Map;
+
+import static ru.javaops.masterjava.persist.ProjectTestData.MASTERJAVA_ID;
+import static ru.javaops.masterjava.persist.ProjectTestData.TOPJAVA_ID;
+import static ru.javaops.masterjava.persist.model.GroupType.CURRENT;
+import static ru.javaops.masterjava.persist.model.GroupType.FINISHED;
+
+/**
+ * gkislin
+ * 14.11.2016
+ */
+public class GroupTestData {
+ public static Group TOPJAVA_06;
+ public static Group TOPJAVA_07;
+ public static Group TOPJAVA_08;
+ public static Group MASTERJAVA_01;
+ public static Map GROUPS;
+
+ public static int TOPJAVA_06_ID;
+ public static int TOPJAVA_07_ID;
+ public static int TOPJAVA_08_ID;
+ public static int MASTERJAVA_01_ID;
+
+
+ public static void init() {
+ ProjectTestData.setUp();
+ TOPJAVA_06 = new Group("topjava06", FINISHED, TOPJAVA_ID);
+ TOPJAVA_07 = new Group("topjava07", FINISHED, TOPJAVA_ID);
+ TOPJAVA_08 = new Group("topjava08", CURRENT, TOPJAVA_ID);
+ MASTERJAVA_01 = new Group("masterjava01", CURRENT, MASTERJAVA_ID);
+ GROUPS = ImmutableMap.of(
+ TOPJAVA_06.getName(), TOPJAVA_06,
+ TOPJAVA_07.getName(), TOPJAVA_07,
+ TOPJAVA_08.getName(), TOPJAVA_08,
+ MASTERJAVA_01.getName(), MASTERJAVA_01);
+ }
+
+ public static void setUp() {
+ GroupDao dao = DBIProvider.getDao(GroupDao.class);
+ dao.clean();
+ DBIProvider.getDBI().useTransaction((conn, status) -> {
+ GROUPS.values().forEach(dao::insert);
+ });
+ TOPJAVA_06_ID = TOPJAVA_06.getId();
+ TOPJAVA_07_ID = TOPJAVA_07.getId();
+ TOPJAVA_08_ID = TOPJAVA_08.getId();
+ MASTERJAVA_01_ID = MASTERJAVA_01.getId();
+ }
+}
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/ProjectTestData.java b/persist/src/test/java/ru/javaops/masterjava/persist/ProjectTestData.java
new file mode 100644
index 000000000..b26b49178
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/ProjectTestData.java
@@ -0,0 +1,32 @@
+package ru.javaops.masterjava.persist;
+
+import com.google.common.collect.ImmutableMap;
+import ru.javaops.masterjava.persist.dao.ProjectDao;
+import ru.javaops.masterjava.persist.model.Project;
+
+import java.util.Map;
+
+/**
+ * gkislin
+ * 14.11.2016
+ */
+public class ProjectTestData {
+ public static final Project TOPJAVA = new Project("topjava", "Topjava");
+ public static final Project MASTERJAVA = new Project("masterjava", "Masterjava");
+ public static final Map PROJECTS = ImmutableMap.of(
+ TOPJAVA.getName(), TOPJAVA,
+ MASTERJAVA.getName(), MASTERJAVA);
+
+ public static int TOPJAVA_ID;
+ public static int MASTERJAVA_ID;
+
+ public static void setUp() {
+ ProjectDao dao = DBIProvider.getDao(ProjectDao.class);
+ dao.clean();
+ DBIProvider.getDBI().useTransaction((conn, status) -> {
+ PROJECTS.values().forEach(dao::insert);
+ });
+ TOPJAVA_ID = TOPJAVA.getId();
+ MASTERJAVA_ID = MASTERJAVA.getId();
+ }
+}
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/UserGroupTestData.java b/persist/src/test/java/ru/javaops/masterjava/persist/UserGroupTestData.java
new file mode 100644
index 000000000..77f64ea59
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/UserGroupTestData.java
@@ -0,0 +1,45 @@
+package ru.javaops.masterjava.persist;
+
+import ru.javaops.masterjava.persist.dao.UserGroupDao;
+import ru.javaops.masterjava.persist.model.UserGroup;
+
+import java.util.List;
+import java.util.Set;
+
+import static ru.javaops.masterjava.persist.dao.UserGroupDao.toUserGroups;
+import static ru.javaops.masterjava.persist.GroupTestData.*;
+
+/**
+ * gkislin
+ * 14.11.2016
+ */
+public class UserGroupTestData {
+
+ public static List USER_GROUPS;
+
+ public static void init() {
+ UserTestData.init();
+ UserTestData.setUp();
+
+ GroupTestData.init();
+ GroupTestData.setUp();
+
+ USER_GROUPS = toUserGroups(UserTestData.ADMIN.getId(), TOPJAVA_07_ID, TOPJAVA_08_ID, MASTERJAVA_01_ID);
+ USER_GROUPS.addAll(toUserGroups(UserTestData.FULL_NAME.getId(), TOPJAVA_07_ID, MASTERJAVA_01_ID));
+ USER_GROUPS.addAll(toUserGroups(UserTestData.USER1.getId(), TOPJAVA_06_ID, MASTERJAVA_01_ID));
+ USER_GROUPS.add(new UserGroup(UserTestData.USER2.getId(), MASTERJAVA_01_ID));
+ USER_GROUPS.add(new UserGroup(UserTestData.USER3.getId(), MASTERJAVA_01_ID));
+ }
+
+ public static void setUp() {
+ UserGroupDao dao = DBIProvider.getDao(UserGroupDao.class);
+ dao.clean();
+ DBIProvider.getDBI().useTransaction((conn, status) -> {
+ dao.insertBatch(USER_GROUPS);
+ });
+ }
+
+ public static Set getByGroupId(int groupId) {
+ return UserGroupDao.getByGroupId(groupId, USER_GROUPS);
+ }
+}
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/UserTestData.java b/persist/src/test/java/ru/javaops/masterjava/persist/UserTestData.java
new file mode 100644
index 000000000..31c83da80
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/UserTestData.java
@@ -0,0 +1,44 @@
+package ru.javaops.masterjava.persist;
+
+import com.google.common.collect.ImmutableList;
+import ru.javaops.masterjava.persist.dao.UserDao;
+import ru.javaops.masterjava.persist.model.User;
+import ru.javaops.masterjava.persist.model.UserFlag;
+
+import java.util.List;
+
+import static ru.javaops.masterjava.persist.CityTestData.*;
+
+/**
+ * gkislin
+ * 14.11.2016
+ */
+public class UserTestData {
+ public static User ADMIN;
+ public static User DELETED;
+ public static User FULL_NAME;
+ public static User USER1;
+ public static User USER2;
+ public static User USER3;
+ public static List FIST5_USERS;
+
+ public static void init() {
+ CityTestData.setUp();
+ ADMIN = new User("Admin", "admin@javaops.ru", UserFlag.superuser, SPB.getId());
+ DELETED = new User("Deleted", "deleted@yandex.ru", UserFlag.deleted, SPB.getId());
+ FULL_NAME = new User("Full Name", "gmail@gmail.com", UserFlag.active, KIEV.getId());
+ USER1 = new User("User1", "user1@gmail.com", UserFlag.active, MOSCOW.getId());
+ USER2 = new User("User2", "user2@yandex.ru", UserFlag.active, KIEV.getId());
+ USER3 = new User("User3", "user3@yandex.ru", UserFlag.active, MINSK.getId());
+ FIST5_USERS = ImmutableList.of(ADMIN, DELETED, FULL_NAME, USER1, USER2);
+ }
+
+ public static void setUp() {
+ UserDao dao = DBIProvider.getDao(UserDao.class);
+ dao.clean();
+ DBIProvider.getDBI().useTransaction((conn, status) -> {
+ FIST5_USERS.forEach(dao::insert);
+ dao.insert(USER3);
+ });
+ }
+}
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/dao/AbstractDaoTest.java b/persist/src/test/java/ru/javaops/masterjava/persist/dao/AbstractDaoTest.java
new file mode 100644
index 000000000..3e8a891a5
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/dao/AbstractDaoTest.java
@@ -0,0 +1,35 @@
+package ru.javaops.masterjava.persist.dao;
+
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import ru.javaops.masterjava.persist.DBIProvider;
+import ru.javaops.masterjava.persist.DBITestProvider;
+
+@Slf4j
+public abstract class AbstractDaoTest {
+ static {
+ DBITestProvider.initDBI();
+ }
+
+ @Rule
+ public TestRule testWatcher = new TestWatcher() {
+ @Override
+ protected void starting(Description description) {
+ log.info("\n\n+++ Start " + description.getDisplayName());
+ }
+
+ @Override
+ protected void finished(Description description) {
+ log.info("\n+++ Finish " + description.getDisplayName() + '\n');
+ }
+ };
+
+ protected DAO dao;
+
+ protected AbstractDaoTest(Class daoClass) {
+ this.dao = DBIProvider.getDao(daoClass);
+ }
+}
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/dao/CityDaoTest.java b/persist/src/test/java/ru/javaops/masterjava/persist/dao/CityDaoTest.java
new file mode 100644
index 000000000..02d771475
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/dao/CityDaoTest.java
@@ -0,0 +1,30 @@
+package ru.javaops.masterjava.persist.dao;
+
+import org.junit.Before;
+import org.junit.Test;
+import ru.javaops.masterjava.persist.CityTestData;
+import ru.javaops.masterjava.persist.model.City;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static ru.javaops.masterjava.persist.CityTestData.CITIES;
+
+public class CityDaoTest extends AbstractDaoTest {
+
+ public CityDaoTest() {
+ super(CityDao.class);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ CityTestData.setUp();
+ }
+
+ @Test
+ public void getAll() throws Exception {
+ final Map cities = dao.getAsMap();
+ assertEquals(CITIES, cities);
+ System.out.println(cities.values());
+ }
+}
\ No newline at end of file
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/dao/GroupDaoTest.java b/persist/src/test/java/ru/javaops/masterjava/persist/dao/GroupDaoTest.java
new file mode 100644
index 000000000..467fad4b0
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/dao/GroupDaoTest.java
@@ -0,0 +1,36 @@
+package ru.javaops.masterjava.persist.dao;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import ru.javaops.masterjava.persist.GroupTestData;
+import ru.javaops.masterjava.persist.model.Group;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static ru.javaops.masterjava.persist.GroupTestData.GROUPS;
+
+public class GroupDaoTest extends AbstractDaoTest {
+
+ public GroupDaoTest() {
+ super(GroupDao.class);
+ }
+
+ @BeforeClass
+ public static void init() throws Exception {
+ GroupTestData.init();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ GroupTestData.setUp();
+ }
+
+ @Test
+ public void getAll() throws Exception {
+ final Map projects = dao.getAsMap();
+ assertEquals(GROUPS, projects);
+ System.out.println(projects.values());
+ }
+}
\ No newline at end of file
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/dao/ProjectDaoTest.java b/persist/src/test/java/ru/javaops/masterjava/persist/dao/ProjectDaoTest.java
new file mode 100644
index 000000000..1577859b0
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/dao/ProjectDaoTest.java
@@ -0,0 +1,30 @@
+package ru.javaops.masterjava.persist.dao;
+
+import org.junit.Before;
+import org.junit.Test;
+import ru.javaops.masterjava.persist.ProjectTestData;
+import ru.javaops.masterjava.persist.model.Project;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static ru.javaops.masterjava.persist.ProjectTestData.PROJECTS;
+
+public class ProjectDaoTest extends AbstractDaoTest {
+
+ public ProjectDaoTest() {
+ super(ProjectDao.class);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ ProjectTestData.setUp();
+ }
+
+ @Test
+ public void getAll() throws Exception {
+ final Map projects = dao.getAsMap();
+ assertEquals(PROJECTS, projects);
+ System.out.println(projects.values());
+ }
+}
\ No newline at end of file
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/dao/UserDaoTest.java b/persist/src/test/java/ru/javaops/masterjava/persist/dao/UserDaoTest.java
new file mode 100644
index 000000000..1dd6ed408
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/dao/UserDaoTest.java
@@ -0,0 +1,53 @@
+package ru.javaops.masterjava.persist.dao;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import ru.javaops.masterjava.persist.UserTestData;
+import ru.javaops.masterjava.persist.model.User;
+
+import java.util.List;
+
+import static ru.javaops.masterjava.persist.UserTestData.FIST5_USERS;
+
+/**
+ * gkislin
+ * 27.10.2016
+ */
+public class UserDaoTest extends AbstractDaoTest {
+
+ public UserDaoTest() {
+ super(UserDao.class);
+ }
+
+ @BeforeClass
+ public static void init() throws Exception {
+ UserTestData.init();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ UserTestData.setUp();
+ }
+
+ @Test
+ public void getWithLimit() {
+ List users = dao.getWithLimit(5);
+ Assert.assertEquals(FIST5_USERS, users);
+ }
+
+ @Test
+ public void insertBatch() throws Exception {
+ dao.clean();
+ dao.insertBatch(FIST5_USERS, 3);
+ Assert.assertEquals(5, dao.getWithLimit(100).size());
+ }
+
+ @Test
+ public void getSeqAndSkip() throws Exception {
+ int seq1 = dao.getSeqAndSkip(5);
+ int seq2 = dao.getSeqAndSkip(1);
+ Assert.assertEquals(5, seq2 - seq1);
+ }
+}
\ No newline at end of file
diff --git a/persist/src/test/java/ru/javaops/masterjava/persist/dao/UserGroupDaoTest.java b/persist/src/test/java/ru/javaops/masterjava/persist/dao/UserGroupDaoTest.java
new file mode 100644
index 000000000..a46f6dbd3
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/dao/UserGroupDaoTest.java
@@ -0,0 +1,39 @@
+package ru.javaops.masterjava.persist.dao;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import ru.javaops.masterjava.persist.UserGroupTestData;
+
+import java.util.Set;
+
+import static ru.javaops.masterjava.persist.GroupTestData.MASTERJAVA_01_ID;
+import static ru.javaops.masterjava.persist.GroupTestData.TOPJAVA_07_ID;
+import static ru.javaops.masterjava.persist.UserGroupTestData.getByGroupId;
+
+public class UserGroupDaoTest extends AbstractDaoTest {
+
+ public UserGroupDaoTest() {
+ super(UserGroupDao.class);
+ }
+
+ @BeforeClass
+ public static void init() throws Exception {
+ UserGroupTestData.init();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ UserGroupTestData.setUp();
+ }
+
+ @Test
+ public void getAll() throws Exception {
+ Set userIds = dao.getUserIds(MASTERJAVA_01_ID);
+ Assert.assertEquals(getByGroupId(MASTERJAVA_01_ID), userIds);
+
+ userIds = dao.getUserIds(TOPJAVA_07_ID);
+ Assert.assertEquals(getByGroupId(TOPJAVA_07_ID), userIds);
+ }
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 000000000..0e177638d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,31 @@
+
+ 4.0.0
+
+ ru.javaops
+ masterjava
+ pom
+
+ 1.0-SNAPSHOT
+
+ Master Java
+ https://github.com/JavaOPs/masterjava
+
+
+ parent
+ parent-web
+
+ common
+ persist
+ test
+
+ services/akka-remote
+ services/common-ws
+ services/mail-api
+ services/mail-service
+
+ web/common-web
+ web/webapp
+ web/export
+
+
diff --git a/services/akka-remote/pom.xml b/services/akka-remote/pom.xml
new file mode 100644
index 000000000..1034bd1ad
--- /dev/null
+++ b/services/akka-remote/pom.xml
@@ -0,0 +1,46 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent-web
+ ../../parent-web/pom.xml
+ 1.0-SNAPSHOT
+
+
+ akka-remote
+ 1.0-SNAPSHOT
+ Akka Remote
+
+
+
+ ${project.groupId}
+ common
+ ${project.version}
+
+
+
+ com.typesafe.akka
+ akka-remote_2.12
+ 2.5.1
+
+
+ com.typesafe
+ config
+
+
+ org.scala-lang
+ scala-library
+
+
+
+
+ org.scala-lang
+ scala-library
+ 2.12.2
+
+
+
\ No newline at end of file
diff --git a/services/akka-remote/src/main/java/ru/javaops/masterjava/akka/AkkaActivator.java b/services/akka-remote/src/main/java/ru/javaops/masterjava/akka/AkkaActivator.java
new file mode 100644
index 000000000..20d5d1f98
--- /dev/null
+++ b/services/akka-remote/src/main/java/ru/javaops/masterjava/akka/AkkaActivator.java
@@ -0,0 +1,64 @@
+package ru.javaops.masterjava.akka;
+
+import akka.actor.*;
+import akka.japi.Creator;
+import akka.util.Timeout;
+import lombok.extern.slf4j.Slf4j;
+import ru.javaops.masterjava.config.Configs;
+import scala.concurrent.ExecutionContext;
+import scala.concurrent.duration.Duration;
+
+import java.util.concurrent.TimeUnit;
+
+@Slf4j
+public class AkkaActivator {
+ private static final String AKKA_CONF = "akka.conf";
+
+ private ActorSystem system;
+
+ private AkkaActivator(String actorSystemName, String nodeName) {
+ log.info("Start AKKA System {} : {}", actorSystemName, nodeName);
+ system = ActorSystem.create(actorSystemName, Configs.getAppConfig(AKKA_CONF).getConfig(nodeName));
+ }
+
+ public static AkkaActivator start(String actorSystemName, String configName) {
+ return new AkkaActivator(actorSystemName, configName);
+ }
+
+ public void startTypedActor(Class typedClass, String name, Creator creator) {
+ log.info("Start AKKA typed actor: {}", name);
+ TypedActor.get(system).typedActorOf(
+ new TypedProps(typedClass, creator).withTimeout(new Timeout(Duration.create(20, TimeUnit.SECONDS))), name);
+ }
+
+ public ActorRef startActor(Class actorClass, String name) {
+ log.info("Start AKKA actor: {}", name);
+ return system.actorOf(Props.create(actorClass), name);
+ }
+
+ public ActorRef startActor(Props props) {
+ log.info("Start new AKKA actor");
+ return system.actorOf(props);
+ }
+
+ public T getTypedRef(Class typedClass, String path) {
+ log.info("Get typed reference with path={}", path);
+ return TypedActor.get(system).typedActorOf(new TypedProps(typedClass), system.actorFor(path));
+ }
+
+ public ActorRef getActorRef(String path) {
+ log.info("Get actor reference with path={}", path);
+ return system.actorFor(path);
+ }
+
+ public ExecutionContext getExecutionContext() {
+ return system.dispatcher();
+ }
+
+ public void shutdown() {
+ if (system != null) {
+ log.info("Akka system shutdown");
+ system.terminate();
+ }
+ }
+}
diff --git a/services/akka-remote/src/main/resources/akka-common.conf b/services/akka-remote/src/main/resources/akka-common.conf
new file mode 100644
index 000000000..f63aac186
--- /dev/null
+++ b/services/akka-remote/src/main/resources/akka-common.conf
@@ -0,0 +1,12 @@
+akka {
+ actor {
+ provider = "akka.remote.RemoteActorRefProvider"
+ }
+
+ remote {
+ netty.tcp {
+ hostname = "127.0.0.1"
+ maximum-frame-size = 10000000b
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/common-ws/pom.xml b/services/common-ws/pom.xml
new file mode 100644
index 000000000..9a38ba13a
--- /dev/null
+++ b/services/common-ws/pom.xml
@@ -0,0 +1,83 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent
+ ../../parent/pom.xml
+ 1.0-SNAPSHOT
+
+
+ common-ws
+ 1.0-SNAPSHOT
+ Common Web Services
+
+
+
+ ${project.groupId}
+ common
+ ${project.version}
+
+
+
+ com.sun.xml.ws
+ jaxws-rt
+ 2.2.10
+
+
+ org.jvnet.mimepull
+ mimepull
+
+
+ javax.xml.bind
+ jaxb-api
+
+
+ javax.annotation
+ javax.annotation-api
+
+
+ org.jvnet.staxex
+ stax-ex
+
+
+ javax.xml.soap
+ javax.xml.soap-api
+
+
+
+
+
+ org.jvnet.mimepull
+ mimepull
+ 1.9.4
+
+
+
+ javax.activation
+ activation
+ 1.1.1
+
+
+ org.jvnet.staxex
+ stax-ex
+ 1.7.7
+
+
+ javax.activation
+ activation
+
+
+
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
\ No newline at end of file
diff --git a/services/common-ws/src/main/java/ru/javaops/web/AuthUtil.java b/services/common-ws/src/main/java/ru/javaops/web/AuthUtil.java
new file mode 100644
index 000000000..4e3422276
--- /dev/null
+++ b/services/common-ws/src/main/java/ru/javaops/web/AuthUtil.java
@@ -0,0 +1,33 @@
+package ru.javaops.web;
+
+import lombok.extern.slf4j.Slf4j;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.bind.DatatypeConverter;
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.net.HttpHeaders.AUTHORIZATION;
+
+@Slf4j
+public class AuthUtil {
+
+ public static String encodeBasicAuthHeader(String name, String passw) {
+ String authString = name + ":" + passw;
+ return "Basic " + DatatypeConverter.printBase64Binary(authString.getBytes());
+ }
+
+ public static int checkBasicAuth(Map> headers, String basicAuthCredentials) {
+ List autHeaders = headers.get(AUTHORIZATION);
+ if ((autHeaders == null || autHeaders.isEmpty())) {
+ log.warn("Unauthorized access");
+ return HttpServletResponse.SC_UNAUTHORIZED;
+ } else {
+ if (!autHeaders.get(0).equals(basicAuthCredentials)) {
+ log.warn("Wrong password access");
+ return HttpServletResponse.SC_FORBIDDEN;
+ }
+ return 0;
+ }
+ }
+}
diff --git a/services/common-ws/src/main/java/ru/javaops/web/FaultInfo.java b/services/common-ws/src/main/java/ru/javaops/web/FaultInfo.java
new file mode 100644
index 000000000..d3525c5b9
--- /dev/null
+++ b/services/common-ws/src/main/java/ru/javaops/web/FaultInfo.java
@@ -0,0 +1,22 @@
+package ru.javaops.web;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import ru.javaops.masterjava.ExceptionType;
+
+import javax.xml.bind.annotation.XmlType;
+
+@Data
+@RequiredArgsConstructor
+@NoArgsConstructor
+@XmlType(namespace = "http://common.javaops.ru/")
+public class FaultInfo {
+ private @NonNull ExceptionType type;
+
+ @Override
+ public String toString() {
+ return type.toString();
+ }
+}
diff --git a/services/common-ws/src/main/java/ru/javaops/web/Statistics.java b/services/common-ws/src/main/java/ru/javaops/web/Statistics.java
new file mode 100644
index 000000000..e42fa1790
--- /dev/null
+++ b/services/common-ws/src/main/java/ru/javaops/web/Statistics.java
@@ -0,0 +1,23 @@
+package ru.javaops.web;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * gkislin
+ * 09.01.2017
+ */
+@Slf4j
+public class Statistics {
+ public enum RESULT {
+ SUCCESS, FAIL
+ }
+
+ public static void count(String payload, long startTime, RESULT result) {
+ long now = System.currentTimeMillis();
+ int ms = (int) (now - startTime);
+ log.info(payload + " " + result.name() + " execution time(ms): " + ms);
+ // place for statistics staff
+
+ }
+
+}
diff --git a/services/common-ws/src/main/java/ru/javaops/web/WebStateException.java b/services/common-ws/src/main/java/ru/javaops/web/WebStateException.java
new file mode 100644
index 000000000..8f1ffc4e0
--- /dev/null
+++ b/services/common-ws/src/main/java/ru/javaops/web/WebStateException.java
@@ -0,0 +1,35 @@
+package ru.javaops.web;
+
+
+import com.google.common.base.Throwables;
+import ru.javaops.masterjava.ExceptionType;
+
+import javax.xml.ws.WebFault;
+
+@WebFault(name = "webStateException", targetNamespace = "http://common.javaops.ru/")
+public class WebStateException extends Exception {
+ private FaultInfo faultInfo;
+
+ public WebStateException(String message, FaultInfo faultInfo) {
+ super(message);
+ this.faultInfo = faultInfo;
+ }
+
+ public WebStateException(Exception e) {
+ this(ExceptionType.SYSTEM, e);
+ }
+
+ public WebStateException(ExceptionType type, Throwable cause) {
+ super(Throwables.getRootCause(cause).toString(), cause);
+ this.faultInfo = new FaultInfo(type);
+ }
+
+ public FaultInfo getFaultInfo() {
+ return faultInfo;
+ }
+
+ @Override
+ public String toString() {
+ return faultInfo.toString() + '\n' + super.toString();
+ }
+}
diff --git a/services/common-ws/src/main/java/ru/javaops/web/WsClient.java b/services/common-ws/src/main/java/ru/javaops/web/WsClient.java
new file mode 100644
index 000000000..0242b8a98
--- /dev/null
+++ b/services/common-ws/src/main/java/ru/javaops/web/WsClient.java
@@ -0,0 +1,109 @@
+package ru.javaops.web;
+
+import com.typesafe.config.Config;
+import org.slf4j.event.Level;
+import ru.javaops.masterjava.ExceptionType;
+import ru.javaops.masterjava.config.Configs;
+import ru.javaops.web.handler.SoapLoggingHandlers;
+
+import javax.xml.namespace.QName;
+import javax.xml.ws.Binding;
+import javax.xml.ws.BindingProvider;
+import javax.xml.ws.Service;
+import javax.xml.ws.WebServiceFeature;
+import javax.xml.ws.handler.Handler;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+
+public class WsClient {
+ private static Config HOSTS;
+
+ private final Class serviceClass;
+ private final Service service;
+ private HostConfig hostConfig;
+
+ public static class HostConfig {
+ public final String endpoint;
+ public final Level serverDebugLevel;
+ public final String user;
+ public final String password;
+ public final String authHeader;
+ public final SoapLoggingHandlers.ClientHandler clientLoggingHandler;
+
+ public HostConfig(Config config, String endpointAddress) {
+ endpoint = config.getString("endpoint") + endpointAddress;
+ serverDebugLevel = config.getEnum(Level.class, "server.debugLevel");
+
+// https://github.com/typesafehub/config/issues/282
+ if (!config.getIsNull("user") && !config.getIsNull("password")) {
+ user = config.getString("user");
+ password = config.getString("password");
+ authHeader = AuthUtil.encodeBasicAuthHeader(user, password);
+ } else {
+ user = password = authHeader = null;
+ }
+ clientLoggingHandler = config.getIsNull("client.debugLevel") ? null :
+ new SoapLoggingHandlers.ClientHandler(config.getEnum(Level.class, "client.debugLevel"));
+ }
+
+ public boolean hasAuthorization() {
+ return authHeader != null;
+ }
+
+ public boolean hasHandler() {
+ return clientLoggingHandler != null;
+ }
+ }
+
+ static {
+ HOSTS = Configs.getConfig("hosts.conf", "hosts");
+ }
+
+ public WsClient(URL wsdlUrl, QName qname, Class serviceClass) {
+ this.serviceClass = serviceClass;
+ this.service = Service.create(wsdlUrl, qname);
+ }
+
+ public void init(String host, String endpointAddress) {
+ this.hostConfig = new HostConfig(
+ HOSTS.getConfig(host).withFallback(Configs.getConfig("defaults.conf")), endpointAddress);
+ }
+
+ public HostConfig getHostConfig() {
+ return hostConfig;
+ }
+
+ // Post is not thread-safe (http://stackoverflow.com/a/10601916/548473)
+ public T getPort(WebServiceFeature... features) {
+ T port = service.getPort(serviceClass, features);
+ BindingProvider bp = (BindingProvider) port;
+ Map requestContext = bp.getRequestContext();
+ requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, hostConfig.endpoint);
+ if (hostConfig.hasAuthorization()) {
+ setAuth(port, hostConfig.user, hostConfig.password);
+ }
+ if (hostConfig.hasHandler()) {
+ setHandler(port, hostConfig.clientLoggingHandler);
+ }
+ return port;
+ }
+
+ public static void setAuth(T port, String user, String password) {
+ Map requestContext = ((BindingProvider) port).getRequestContext();
+ requestContext.put(BindingProvider.USERNAME_PROPERTY, user);
+ requestContext.put(BindingProvider.PASSWORD_PROPERTY, password);
+ }
+
+ public static void setHandler(T port, Handler handler) {
+ Binding binding = ((BindingProvider) port).getBinding();
+ List handlerList = binding.getHandlerChain();
+ handlerList.add(handler);
+ binding.setHandlerChain(handlerList);
+ }
+
+ public static WebStateException getWebStateException(Exception e) {
+ return (e instanceof WebStateException) ?
+ (WebStateException) e : new WebStateException(ExceptionType.NETWORK, e);
+ }
+}
diff --git a/services/common-ws/src/main/java/ru/javaops/web/handler/SoapBaseHandler.java b/services/common-ws/src/main/java/ru/javaops/web/handler/SoapBaseHandler.java
new file mode 100644
index 000000000..ad2b18779
--- /dev/null
+++ b/services/common-ws/src/main/java/ru/javaops/web/handler/SoapBaseHandler.java
@@ -0,0 +1,23 @@
+package ru.javaops.web.handler;
+
+import com.sun.xml.ws.api.handler.MessageHandler;
+import com.sun.xml.ws.api.handler.MessageHandlerContext;
+
+import javax.xml.namespace.QName;
+import javax.xml.ws.handler.MessageContext;
+import java.util.Set;
+
+public abstract class SoapBaseHandler implements MessageHandler {
+
+ public Set getHeaders() {
+ return null;
+ }
+
+ @Override
+ public void close(MessageContext context) {
+ }
+
+ protected static boolean isOutbound(MessageHandlerContext context) {
+ return (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
+ }
+}
diff --git a/services/common-ws/src/main/java/ru/javaops/web/handler/SoapLoggingHandlers.java b/services/common-ws/src/main/java/ru/javaops/web/handler/SoapLoggingHandlers.java
new file mode 100644
index 000000000..cb29cb346
--- /dev/null
+++ b/services/common-ws/src/main/java/ru/javaops/web/handler/SoapLoggingHandlers.java
@@ -0,0 +1,143 @@
+package ru.javaops.web.handler;
+
+
+import com.sun.xml.txw2.output.IndentingXMLStreamWriter;
+import com.sun.xml.ws.api.handler.MessageHandlerContext;
+import com.sun.xml.ws.api.message.Message;
+import com.sun.xml.ws.api.streaming.XMLStreamWriterFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.event.Level;
+
+import javax.xml.stream.XMLStreamWriter;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.EnumMap;
+import java.util.Map;
+
+/**
+ * Refactored from:
+ *
+ * @see {http://weblogs.java.net/blog/ramapulavarthi/archive/2007/12/extend_your_web.html
+ * http://fisheye5.cenqua.com/browse/jax-ws-sources/jaxws-ri/samples/efficient_handler/src/efficient_handler/common/LoggingHandler.java?r=MAIN}
+ *
+ * This simple LoggingHandler will log the contents of incoming
+ * and outgoing messages. This is implemented as a MessageHandler
+ * for better performance over SOAPHandler.
+ */
+@Slf4j
+public abstract class SoapLoggingHandlers extends SoapBaseHandler {
+
+ private final Level loggingLevel;
+
+ protected SoapLoggingHandlers(Level loggingLevel) {
+ this.loggingLevel = loggingLevel;
+ }
+
+ private static final Map HANDLER_MAP = new EnumMap(Level.class) {
+ {
+ put(Level.TRACE, HANDLER.DEBUG);
+ put(Level.DEBUG, HANDLER.DEBUG);
+ put(Level.INFO, HANDLER.INFO);
+ put(Level.WARN, HANDLER.ERROR);
+ put(Level.ERROR, HANDLER.ERROR);
+ }
+ };
+
+ protected enum HANDLER {
+ NONE {
+ @Override
+ public void handleFault(MessageHandlerContext mhc) {
+ }
+
+ @Override
+ public void handleMessage(MessageHandlerContext mhc, boolean isRequest) {
+ }
+ },
+ ERROR {
+ private static final String REQUEST_MSG = "REQUEST_MSG";
+
+ public void handleFault(MessageHandlerContext context) {
+ log.error("Fault SOAP request:\n" + getMessageText(((Message) context.get(REQUEST_MSG))));
+ }
+
+ public void handleMessage(MessageHandlerContext context, boolean isRequest) {
+ if (isRequest) {
+ context.put(REQUEST_MSG, context.getMessage().copy());
+ }
+ }
+ },
+ INFO {
+ public void handleFault(MessageHandlerContext context) {
+ ERROR.handleFault(context);
+ }
+
+ public void handleMessage(MessageHandlerContext context, boolean isRequest) {
+ ERROR.handleMessage(context, isRequest);
+ log.info((isRequest ? "SOAP request: " : "SOAP response: ") + context.getMessage().getPayloadLocalPart());
+ }
+ },
+ DEBUG {
+ public void handleFault(MessageHandlerContext context) {
+ log.error("Fault SOAP message:\n" + getMessageText(context.getMessage().copy()));
+ }
+
+ public void handleMessage(MessageHandlerContext context, boolean isRequest) {
+ log.info((isRequest ? "SOAP request:\n" : "SOAP response:\n") + getMessageText(context.getMessage().copy()));
+ }
+ };
+
+ public abstract void handleMessage(MessageHandlerContext mhc, boolean isRequest);
+
+ public abstract void handleFault(MessageHandlerContext mhc);
+
+ protected static String getMessageText(Message msg) {
+ try {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ XMLStreamWriter writer = XMLStreamWriterFactory.create(out, "UTF-8");
+ IndentingXMLStreamWriter wrap = new IndentingXMLStreamWriter(writer);
+ msg.writeTo(wrap);
+ return out.toString(StandardCharsets.UTF_8.name());
+ } catch (Exception e) {
+ log.warn("Coudn't get SOAP message for logging", e);
+ return null;
+ }
+ }
+ }
+
+ abstract protected boolean isRequest(boolean isOutbound);
+
+ @Override
+ public boolean handleMessage(MessageHandlerContext mhc) {
+ HANDLER_MAP.get(loggingLevel).handleMessage(mhc, isRequest(isOutbound(mhc)));
+ return true;
+ }
+
+ @Override
+ public boolean handleFault(MessageHandlerContext mhc) {
+ HANDLER_MAP.get(loggingLevel).handleFault(mhc);
+ return true;
+ }
+
+ public static class ClientHandler extends SoapLoggingHandlers {
+ public ClientHandler(Level loggingLevel) {
+ super(loggingLevel);
+ }
+
+ @Override
+ protected boolean isRequest(boolean isOutbound) {
+ return isOutbound;
+ }
+ }
+
+ public static class ServerHandler extends SoapLoggingHandlers {
+
+ public ServerHandler(Level loggingLevel) {
+ super(loggingLevel);
+ }
+
+ @Override
+ protected boolean isRequest(boolean isOutbound) {
+ return !isOutbound;
+ }
+ }
+}
diff --git a/services/common-ws/src/main/java/ru/javaops/web/handler/SoapServerSecurityHandler.java b/services/common-ws/src/main/java/ru/javaops/web/handler/SoapServerSecurityHandler.java
new file mode 100644
index 000000000..5855630dc
--- /dev/null
+++ b/services/common-ws/src/main/java/ru/javaops/web/handler/SoapServerSecurityHandler.java
@@ -0,0 +1,43 @@
+package ru.javaops.web.handler;
+
+import com.sun.xml.ws.api.handler.MessageHandlerContext;
+import lombok.extern.slf4j.Slf4j;
+import ru.javaops.web.AuthUtil;
+
+import javax.xml.ws.handler.MessageContext;
+import java.util.List;
+import java.util.Map;
+
+import static ru.javaops.web.AuthUtil.encodeBasicAuthHeader;
+
+@Slf4j
+abstract public class SoapServerSecurityHandler extends SoapBaseHandler {
+
+ private String authHeader;
+
+ public SoapServerSecurityHandler(String user, String password) {
+ this(encodeBasicAuthHeader(user, password));
+ }
+
+ public SoapServerSecurityHandler(String authHeader) {
+ this.authHeader = authHeader;
+ }
+
+ @Override
+ public boolean handleMessage(MessageHandlerContext ctx) {
+ if (!isOutbound(ctx) && authHeader != null) {
+ Map> headers = (Map>) ctx.get(MessageContext.HTTP_REQUEST_HEADERS);
+ int code = AuthUtil.checkBasicAuth(headers, authHeader);
+ if (code != 0) {
+ ctx.put(MessageContext.HTTP_RESPONSE_CODE, code);
+ throw new SecurityException();
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean handleFault(MessageHandlerContext context) {
+ return true;
+ }
+}
diff --git a/services/common-ws/src/main/java/ru/javaops/web/handler/SoapStatisticHandler.java b/services/common-ws/src/main/java/ru/javaops/web/handler/SoapStatisticHandler.java
new file mode 100644
index 000000000..db2c9e68b
--- /dev/null
+++ b/services/common-ws/src/main/java/ru/javaops/web/handler/SoapStatisticHandler.java
@@ -0,0 +1,30 @@
+package ru.javaops.web.handler;
+
+import com.sun.xml.ws.api.handler.MessageHandlerContext;
+import ru.javaops.web.Statistics;
+
+public class SoapStatisticHandler extends SoapBaseHandler {
+
+ private static final String PAYLOAD = "PAYLOAD";
+ private static final String START_TIME = "START_TIME";
+
+ public boolean handleMessage(MessageHandlerContext context) {
+ if (isOutbound(context)) {
+ count(context, Statistics.RESULT.SUCCESS);
+ } else {
+ String payload = context.getMessage().getPayloadLocalPart();
+ context.put(PAYLOAD, payload);
+ context.put(START_TIME, System.currentTimeMillis());
+ }
+ return true;
+ }
+
+ public boolean handleFault(MessageHandlerContext context) {
+ count(context, Statistics.RESULT.FAIL);
+ return true;
+ }
+
+ private void count(MessageHandlerContext context, Statistics.RESULT result) {
+ Statistics.count((String) context.get(PAYLOAD), (Long) context.get(START_TIME), result);
+ }
+}
\ No newline at end of file
diff --git a/services/mail-api/pom.xml b/services/mail-api/pom.xml
new file mode 100644
index 000000000..cb76585c5
--- /dev/null
+++ b/services/mail-api/pom.xml
@@ -0,0 +1,48 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent
+ ../../parent/pom.xml
+ 1.0-SNAPSHOT
+
+
+ mail-api
+ 1.0-SNAPSHOT
+ Mail API
+
+
+
+
+ ${masterjava.config}
+
+ wsdl/mailService.wsdl
+ wsdl/common.xsd
+
+
+
+
+
+
+
+ ${project.groupId}
+ common-ws
+ ${project.version}
+
+
+ commons-io
+ commons-io
+ 2.5
+
+
+ ${project.groupId}
+ akka-remote
+ ${project.version}
+
+
+
+
\ No newline at end of file
diff --git a/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/Addressee.java b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/Addressee.java
new file mode 100644
index 000000000..babe171b5
--- /dev/null
+++ b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/Addressee.java
@@ -0,0 +1,50 @@
+package ru.javaops.masterjava.service.mail;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlValue;
+
+@Getter
+@AllArgsConstructor
+@NoArgsConstructor
+@XmlAccessorType(XmlAccessType.FIELD)
+public class Addressee {
+ @XmlAttribute
+ private String email;
+ @XmlValue
+ private String name;
+
+ public Addressee(String email) {
+ email = email.trim();
+ int idx = email.indexOf('<');
+ if (idx == -1) {
+ this.email = email;
+ } else {
+ this.name = email.substring(0, idx).trim();
+ this.email = email.substring(idx + 1, email.length() - 1).trim();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return name == null ? email : name + " <" + email + '>';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Addressee addressee = (Addressee) o;
+ return email.equals(addressee.email);
+ }
+
+ @Override
+ public int hashCode() {
+ return email.hashCode();
+ }
+}
diff --git a/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/Attach.java b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/Attach.java
new file mode 100644
index 000000000..c9e7cd377
--- /dev/null
+++ b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/Attach.java
@@ -0,0 +1,22 @@
+package ru.javaops.masterjava.service.mail;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import javax.activation.DataHandler;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlMimeType;
+
+@Getter
+@AllArgsConstructor
+@NoArgsConstructor
+@XmlAccessorType(XmlAccessType.FIELD)
+public class Attach {
+ // http://stackoverflow.com/questions/12250423/jax-ws-datahandler-getname-is-blank-when-called-from-client-side
+ protected String name;
+
+ @XmlMimeType("application/octet-stream")
+ private DataHandler dataHandler;
+}
diff --git a/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/GroupResult.java b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/GroupResult.java
new file mode 100644
index 000000000..19f0ae508
--- /dev/null
+++ b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/GroupResult.java
@@ -0,0 +1,31 @@
+package ru.javaops.masterjava.service.mail;
+
+import com.google.common.base.Throwables;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class GroupResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private int success; // number of successfully sent email
+ private List failed; // failed emails with causes
+ private String failedCause; // global fail cause
+
+ public GroupResult(Exception e) {
+ this(-1, null, Throwables.getRootCause(e).toString());
+ }
+
+ @Override
+ public String toString() {
+ return "Success: " + success + '\n' +
+ (failed == null ? "" : "Failed: " + failed.toString() + '\n') +
+ (failedCause == null ? "" : "Failed cause: " + failedCause);
+ }
+}
\ No newline at end of file
diff --git a/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailRemoteService.java b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailRemoteService.java
new file mode 100644
index 000000000..c7be717a6
--- /dev/null
+++ b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailRemoteService.java
@@ -0,0 +1,8 @@
+package ru.javaops.masterjava.service.mail;
+
+import ru.javaops.masterjava.service.mail.util.MailUtils;
+
+public interface MailRemoteService {
+
+ scala.concurrent.Future sendBulk(MailUtils.MailObject mailObject);
+}
\ No newline at end of file
diff --git a/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailResult.java b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailResult.java
new file mode 100644
index 000000000..64927f156
--- /dev/null
+++ b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailResult.java
@@ -0,0 +1,30 @@
+package ru.javaops.masterjava.service.mail;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.NonNull;
+
+import java.io.Serializable;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class MailResult implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ public static final String OK = "OK";
+
+ private @NonNull
+ String email;
+ private String result;
+
+ public boolean isOk() {
+ return OK.equals(result);
+ }
+
+ @Override
+ public String toString() {
+ return '\'' + email + "' result '" + result + '\'';
+ }
+}
diff --git a/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailService.java b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailService.java
new file mode 100644
index 000000000..6e51b1926
--- /dev/null
+++ b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailService.java
@@ -0,0 +1,33 @@
+package ru.javaops.masterjava.service.mail;
+
+import ru.javaops.web.WebStateException;
+
+import javax.jws.WebMethod;
+import javax.jws.WebParam;
+import javax.jws.WebService;
+import java.util.List;
+import java.util.Set;
+
+@WebService(targetNamespace = "http://mail.javaops.ru/")
+//@SOAPBinding(
+// style = SOAPBinding.Style.DOCUMENT,
+// use= SOAPBinding.Use.LITERAL,
+// parameterStyle = SOAPBinding.ParameterStyle.WRAPPED)
+public interface MailService {
+
+ @WebMethod
+ String sendToGroup(
+ @WebParam(name = "to") Set to,
+ @WebParam(name = "cc") Set cc,
+ @WebParam(name = "subject") String subject,
+ @WebParam(name = "body") String body,
+ @WebParam(name = "attaches") List attaches) throws WebStateException;
+
+ @WebMethod
+ GroupResult sendBulk(
+ @WebParam(name = "to") Set to,
+ @WebParam(name = "subject") String subject,
+ @WebParam(name = "body") String body,
+ @WebParam(name = "attaches") List attaches) throws WebStateException;
+
+}
\ No newline at end of file
diff --git a/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailWSClient.java b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailWSClient.java
new file mode 100644
index 000000000..568e7b94b
--- /dev/null
+++ b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailWSClient.java
@@ -0,0 +1,59 @@
+package ru.javaops.masterjava.service.mail;
+
+import com.google.common.io.Resources;
+import lombok.extern.slf4j.Slf4j;
+import ru.javaops.web.WebStateException;
+import ru.javaops.web.WsClient;
+
+import javax.xml.namespace.QName;
+import javax.xml.ws.soap.MTOMFeature;
+import java.util.List;
+import java.util.Set;
+
+@Slf4j
+public class MailWSClient {
+ private static final WsClient WS_CLIENT;
+
+ static {
+ WS_CLIENT = new WsClient(Resources.getResource("wsdl/mailService.wsdl"),
+ new QName("http://mail.javaops.ru/", "MailServiceImplService"),
+ MailService.class);
+
+ WS_CLIENT.init("mail", "/mail/mailService?wsdl");
+ }
+
+
+ public static String sendToGroup(final Set to, final Set cc, final String subject, final String body, List attaches) throws WebStateException {
+ log.info("Send mail to '" + to + "' cc '" + cc + "' subject '" + subject + (log.isDebugEnabled() ? "\nbody=" + body : ""));
+ String status;
+ try {
+ status = getPort().sendToGroup(to, cc, subject, body, attaches);
+ log.info("Sent with status: " + status);
+ } catch (Exception e) {
+ log.error("sendToGroup failed", e);
+ throw WsClient.getWebStateException(e);
+ }
+ return status;
+ }
+
+ public static GroupResult sendBulk(final Set to, final String subject, final String body, List attaches) throws WebStateException {
+ log.info("Send mail to '" + to + "' subject '" + subject + (log.isDebugEnabled() ? "\nbody=" + body : ""));
+ GroupResult result;
+ try {
+ result = getPort().sendBulk(to, subject, body, attaches);
+ } catch (WebStateException e) {
+ log.error("sendBulk failed", e);
+ throw WsClient.getWebStateException(e);
+ }
+ log.info("Sent with result: " + result);
+ return result;
+ }
+
+ private static MailService getPort() {
+ return WS_CLIENT.getPort(new MTOMFeature(1024));
+ }
+
+ public static WsClient.HostConfig getHostConfig() {
+ return WS_CLIENT.getHostConfig();
+ }
+}
diff --git a/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/util/MailUtils.java b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/util/MailUtils.java
new file mode 100644
index 000000000..25f41d869
--- /dev/null
+++ b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/util/MailUtils.java
@@ -0,0 +1,70 @@
+package ru.javaops.masterjava.service.mail.util;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.sun.istack.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.apache.commons.io.input.CloseShieldInputStream;
+import ru.javaops.masterjava.service.mail.Addressee;
+import ru.javaops.masterjava.service.mail.Attach;
+
+import javax.activation.DataHandler;
+import javax.activation.DataSource;
+import java.io.*;
+import java.util.AbstractMap.SimpleImmutableEntry;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class MailUtils {
+
+ public static Set split(String addressees) {
+ Iterable split = Splitter.on(',').trimResults().omitEmptyStrings().split(addressees);
+ return ImmutableSet.copyOf(Iterables.transform(split, Addressee::new));
+ }
+
+ @Data
+ @AllArgsConstructor
+ public static class MailObject implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private @NotNull String users;
+ private String subject;
+ private @NotNull String body;
+ // http://stackoverflow.com/questions/521171/a-java-collection-of-value-pairs-tuples
+ private List> attaches;
+ }
+
+ public static List getAttaches(List> attaches) {
+ return attaches.stream().map(a -> getAttach(a.getKey(), a.getValue())).collect(Collectors.toList());
+ }
+
+ public static Attach getAttach(String name, byte[] attachData) {
+ return new Attach(name, new DataHandler((ProxyDataSource) () -> new ByteArrayInputStream(attachData)));
+ }
+
+ public static Attach getAttach(String name, InputStream inputStream) {
+ // http://stackoverflow.com/questions/2830561/how-to-convert-an-inputstream-to-a-datahandler
+ // http://stackoverflow.com/a/5924019/548473
+ return new Attach(name, new DataHandler((ProxyDataSource) () -> new CloseShieldInputStream(inputStream)));
+ }
+
+ public interface ProxyDataSource extends DataSource {
+ @Override
+ default OutputStream getOutputStream() throws IOException {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ default String getContentType() {
+ return "application/octet-stream";
+ }
+
+ @Override
+ default String getName() {
+ return "";
+ }
+ }
+}
diff --git a/services/mail-api/src/test/java/ru/javaops/masterjava/service/mail/MailWSClientMain.java b/services/mail-api/src/test/java/ru/javaops/masterjava/service/mail/MailWSClientMain.java
new file mode 100644
index 000000000..6ec2b76d7
--- /dev/null
+++ b/services/mail-api/src/test/java/ru/javaops/masterjava/service/mail/MailWSClientMain.java
@@ -0,0 +1,25 @@
+package ru.javaops.masterjava.service.mail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.activation.DataHandler;
+import java.io.File;
+
+@Slf4j
+public class MailWSClientMain {
+ public static void main(String[] args) {
+ ImmutableSet addressees = ImmutableSet.of(
+ new Addressee("Мастер Java "));
+
+ try {
+ String state = MailWSClient.sendToGroup(addressees, ImmutableSet.of(), "Subject", "Body", ImmutableList.of(
+ new Attach("version.html", new DataHandler(new File("config_templates/version.html").toURI().toURL()))
+ ));
+ System.out.println(state);
+ } catch (Throwable e) {
+ log.error(e.toString(), e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/mail-service/pom.xml b/services/mail-service/pom.xml
new file mode 100644
index 000000000..e3516583f
--- /dev/null
+++ b/services/mail-service/pom.xml
@@ -0,0 +1,118 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent-web
+ ../../parent-web/pom.xml
+ 1.0-SNAPSHOT
+
+
+ mail-service
+ 1.0-SNAPSHOT
+ war
+ Mail Service
+
+
+ mail
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+ 1.8
+
+
+ prepare-package
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${project.groupId}
+ mail-api
+ ${project.version}
+
+
+ org.apache.commons
+ commons-email
+ 1.4
+
+
+ javax.activation
+ activation
+
+
+
+
+
+ ${project.groupId}
+ persist
+ ${project.version}
+
+
+ ${project.groupId}
+ persist
+ ${project.version}
+ test-jar
+ test
+
+
+
+
+ org.glassfish.jersey.containers
+ jersey-container-servlet
+ 2.25.1
+
+
+ org.glassfish.jersey.media
+ jersey-media-moxy
+ 2.25.1
+
+
+ org.glassfish.jersey.ext
+ jersey-bean-validation
+ 2.25.1
+
+
+ org.glassfish.jersey.media
+ jersey-media-multipart
+ 2.25.1
+
+
+ org.jvnet.mimepull
+ mimepull
+
+
+
+
+
+ org.apache.activemq
+ activemq-all
+ 5.14.5
+ provided
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
\ No newline at end of file
diff --git a/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailConfig.java b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailConfig.java
new file mode 100644
index 000000000..4b00e0276
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailConfig.java
@@ -0,0 +1,67 @@
+package ru.javaops.masterjava.service.mail;
+
+import com.typesafe.config.Config;
+import org.apache.commons.mail.DefaultAuthenticator;
+import org.apache.commons.mail.Email;
+import org.apache.commons.mail.EmailException;
+import org.apache.commons.mail.HtmlEmail;
+import ru.javaops.masterjava.config.Configs;
+
+import javax.mail.Authenticator;
+import java.nio.charset.StandardCharsets;
+
+public class MailConfig {
+ private static final MailConfig INSTANCE =
+ new MailConfig(Configs.getConfig("mail.conf", "mail"));
+
+ final private String host;
+ final private int port;
+ final private boolean useSSL;
+ final private boolean useTLS;
+ final private boolean debug;
+ final private String username;
+ final private Authenticator auth;
+ final private String fromName;
+
+ private MailConfig(Config conf) {
+ host = conf.getString("host");
+ port = conf.getInt("port");
+ username = conf.getString("username");
+ auth = new DefaultAuthenticator(username, conf.getString("password"));
+ useSSL = conf.getBoolean("useSSL");
+ useTLS = conf.getBoolean("useTLS");
+ debug = conf.getBoolean("debug");
+ fromName = conf.getString("fromName");
+ }
+
+ public T prepareEmail(T email) throws EmailException {
+ email.setFrom(username, fromName);
+ email.setHostName(host);
+ if (useSSL) {
+ email.setSslSmtpPort(String.valueOf(port));
+ } else {
+ email.setSmtpPort(port);
+ }
+ email.setSSLOnConnect(useSSL);
+ email.setStartTLSEnabled(useTLS);
+ email.setDebug(debug);
+ email.setAuthenticator(auth);
+ email.setCharset(StandardCharsets.UTF_8.name());
+ return email;
+ }
+
+ public static HtmlEmail createHtmlEmail() throws EmailException {
+ return INSTANCE.prepareEmail(new HtmlEmail());
+ }
+
+ @Override
+ public String toString() {
+ return "\nhost='" + host + '\'' +
+ "\nport=" + port +
+ "\nuseSSL=" + useSSL +
+ "\nuseTLS=" + useTLS +
+ "\ndebug=" + debug +
+ "\nusername='" + username + '\'' +
+ "\nfromName='" + fromName + '\'';
+ }
+}
diff --git a/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailHandlers.java b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailHandlers.java
new file mode 100644
index 000000000..ec8521f7c
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailHandlers.java
@@ -0,0 +1,18 @@
+package ru.javaops.masterjava.service.mail;
+
+import ru.javaops.web.handler.SoapLoggingHandlers;
+import ru.javaops.web.handler.SoapServerSecurityHandler;
+
+public class MailHandlers {
+ public static class SecurityHandler extends SoapServerSecurityHandler {
+ public SecurityHandler() {
+ super(MailWSClient.getHostConfig().authHeader);
+ }
+ }
+
+ public static class LoggingHandler extends SoapLoggingHandlers.ServerHandler {
+ public LoggingHandler() {
+ super(MailWSClient.getHostConfig().serverDebugLevel);
+ }
+ }
+}
diff --git a/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailSender.java b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailSender.java
new file mode 100644
index 000000000..1da70a781
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailSender.java
@@ -0,0 +1,68 @@
+package ru.javaops.masterjava.service.mail;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import ru.javaops.masterjava.ExceptionType;
+import ru.javaops.masterjava.persist.DBIProvider;
+import ru.javaops.masterjava.service.mail.persist.MailCase;
+import ru.javaops.masterjava.service.mail.persist.MailCaseDao;
+import ru.javaops.web.WebStateException;
+
+import javax.mail.internet.MimeUtility;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Set;
+
+@Slf4j
+public class MailSender {
+ private static final MailCaseDao MAIL_CASE_DAO = DBIProvider.getDao(MailCaseDao.class);
+
+ static MailResult sendTo(Addressee to, String subject, String body, List attaches) throws WebStateException {
+ val state = sendToGroup(ImmutableSet.of(to), ImmutableSet.of(), subject, body, attaches);
+ return new MailResult(to.getEmail(), state);
+ }
+
+ static String sendToGroup(Set to, Set cc, String subject, String body, List attaches) throws WebStateException {
+ log.info("Send mail to \'" + to + "\' cc \'" + cc + "\' subject \'" + subject + '\'' + (log.isDebugEnabled() ? "\nbody=" + body : ""));
+ String state = MailResult.OK;
+ try {
+ val email = MailConfig.createHtmlEmail();
+ email.setSubject(subject);
+ email.setHtmlMsg(body);
+ for (Addressee addressee : to) {
+ email.addTo(addressee.getEmail(), addressee.getName());
+ }
+ for (Addressee addressee : cc) {
+ email.addCc(addressee.getEmail(), addressee.getName());
+ }
+ for (Attach attach : attaches) {
+ email.attach(attach.getDataHandler().getDataSource(), encodeWord(attach.getName()), null);
+ }
+
+ // https://yandex.ru/blog/company/66296
+ email.setHeaders(ImmutableMap.of("List-Unsubscribe", ""));
+ email.send();
+ } catch (Exception e) {
+ log.error(e.getMessage(), e);
+ state = e.getMessage();
+ }
+ try {
+ MAIL_CASE_DAO.insert(MailCase.of(to, cc, subject, body, state));
+ } catch (Exception e) {
+ log.error("Mail history saving exception", e);
+ throw new WebStateException(ExceptionType.DATA_BASE, e);
+ }
+ log.info("Sent with state: " + state);
+ return state;
+ }
+
+ public static String encodeWord(String word) throws UnsupportedEncodingException {
+ if (word == null) {
+ return null;
+ }
+ return MimeUtility.encodeWord(word, StandardCharsets.UTF_8.name(), null);
+ }
+}
diff --git a/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceExecutor.java b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceExecutor.java
new file mode 100644
index 000000000..b3b7ab784
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceExecutor.java
@@ -0,0 +1,94 @@
+package ru.javaops.masterjava.service.mail;
+
+import akka.dispatch.Futures;
+import lombok.extern.slf4j.Slf4j;
+import one.util.streamex.StreamEx;
+import ru.javaops.masterjava.service.mail.util.MailUtils;
+import ru.javaops.masterjava.service.mail.util.MailUtils.MailObject;
+import ru.javaops.web.WebStateException;
+import scala.concurrent.ExecutionContext;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.*;
+
+@Slf4j
+public class MailServiceExecutor {
+
+ private static final String INTERRUPTED_BY_FAULTS_NUMBER = "+++ Interrupted by faults number";
+ private static final String INTERRUPTED_BY_TIMEOUT = "+++ Interrupted by timeout";
+ private static final String INTERRUPTED_EXCEPTION = "+++ InterruptedException";
+
+ private static final ExecutorService mailExecutor = Executors.newFixedThreadPool(8);
+
+ public static GroupResult sendBulk(final MailObject mailObject) {
+ return sendBulk(MailUtils.split(mailObject.getUsers()),
+ mailObject.getSubject(), mailObject.getBody(), MailUtils.getAttaches(mailObject.getAttaches()));
+ }
+
+ public static GroupResult sendBulk(final Set addressees, final String subject, final String body, List attaches) {
+ final CompletionService completionService = new ExecutorCompletionService<>(mailExecutor);
+
+ List> futures = StreamEx.of(addressees)
+ .map(addressee -> completionService.submit(() -> MailSender.sendTo(addressee, subject, body, attaches)))
+ .toList();
+
+ return new Callable() {
+ private int success = 0;
+ private List failed = new ArrayList<>();
+
+ @Override
+ public GroupResult call() {
+ while (!futures.isEmpty()) {
+ try {
+ Future future = completionService.poll(10, TimeUnit.SECONDS);
+ if (future == null) {
+ return cancelWithFail(INTERRUPTED_BY_TIMEOUT);
+ }
+ futures.remove(future);
+ MailResult mailResult = future.get();
+ if (mailResult.isOk()) {
+ success++;
+ } else {
+ failed.add(mailResult);
+ if (failed.size() >= 5) {
+ return cancelWithFail(INTERRUPTED_BY_FAULTS_NUMBER);
+ }
+ }
+ } catch (ExecutionException e) {
+ return cancelWithFail(e.getCause().toString());
+ } catch (InterruptedException e) {
+ return cancelWithFail(INTERRUPTED_EXCEPTION);
+ }
+ }
+ GroupResult groupResult = new GroupResult(success, failed, null);
+ log.info("groupResult: {}", groupResult);
+ return groupResult;
+ }
+
+ private GroupResult cancelWithFail(String cause) {
+ futures.forEach(f -> f.cancel(true));
+ return new GroupResult(success, failed, cause);
+ }
+ }.call();
+ }
+
+ public static scala.concurrent.Future sendAsyncWithReply(MailObject mailObject, ExecutionContext ec) {
+ // http://doc.akka.io/docs/akka/current/java/futures.html
+ return Futures.future(() -> sendBulk(mailObject), ec);
+ }
+
+ public static void sendAsync(MailObject mailObject) {
+ Set addressees = MailUtils.split(mailObject.getUsers());
+ addressees.forEach(addressee ->
+ mailExecutor.submit(() -> {
+ try {
+ MailSender.sendTo(addressee, mailObject.getSubject(), mailObject.getBody(), MailUtils.getAttaches(mailObject.getAttaches()));
+ } catch (WebStateException e) {
+ // already logged
+ }
+ })
+ );
+ }
+}
\ No newline at end of file
diff --git a/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceImpl.java b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceImpl.java
new file mode 100644
index 000000000..3bad1b536
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceImpl.java
@@ -0,0 +1,44 @@
+package ru.javaops.masterjava.service.mail;
+
+import ru.javaops.web.WebStateException;
+
+import javax.jws.HandlerChain;
+import javax.jws.WebService;
+import javax.xml.ws.soap.MTOM;
+import java.util.List;
+import java.util.Set;
+
+@WebService(endpointInterface = "ru.javaops.masterjava.service.mail.MailService", targetNamespace = "http://mail.javaops.ru/"
+// , wsdlLocation = "WEB-INF/wsdl/mailService.wsdl"
+)
+//@StreamingAttachment(parseEagerly=true, memoryThreshold=40000L)
+@MTOM
+@HandlerChain(file = "mailWsHandlers.xml")
+public class MailServiceImpl implements MailService {
+
+// @Resource
+// private WebServiceContext wsContext;
+
+ @Override
+ public String sendToGroup(Set to, Set cc, String subject, String body, List attaches) throws WebStateException {
+/*
+ MessageContext mCtx = wsContext.getMessageContext();
+ Map> headers = (Map>) mCtx.get(MessageContext.HTTP_REQUEST_HEADERS);
+
+ HttpServletRequest request = (HttpServletRequest) mCtx.get(MessageContext.SERVLET_REQUEST);
+ HttpServletResponse response = (HttpServletResponse) mCtx.get(MessageContext.SERVLET_RESPONSE);
+
+ int code = AuthUtil.checkBasicAuth(headers, MailWSClient.AUTH_HEADER);
+ if (code != 0) {
+ mCtx.put(MessageContext.HTTP_RESPONSE_CODE, code);
+ throw new SecurityException();
+ }
+*/
+ return MailSender.sendToGroup(to, cc, subject, body, attaches);
+ }
+
+ @Override
+ public GroupResult sendBulk(Set to, String subject, String body, List attaches) throws WebStateException {
+ return MailServiceExecutor.sendBulk(to, subject, body, attaches);
+ }
+}
\ No newline at end of file
diff --git a/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/listeners/AkkaMailListener.java b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/listeners/AkkaMailListener.java
new file mode 100644
index 000000000..a107db375
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/listeners/AkkaMailListener.java
@@ -0,0 +1,49 @@
+package ru.javaops.masterjava.service.mail.listeners;
+
+import akka.actor.AbstractActor;
+import akka.japi.Creator;
+import lombok.extern.slf4j.Slf4j;
+import ru.javaops.masterjava.akka.AkkaActivator;
+import ru.javaops.masterjava.service.mail.GroupResult;
+import ru.javaops.masterjava.service.mail.MailRemoteService;
+import ru.javaops.masterjava.service.mail.MailServiceExecutor;
+import ru.javaops.masterjava.service.mail.util.MailUtils;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.annotation.WebListener;
+
+@WebListener
+@Slf4j
+public class AkkaMailListener implements ServletContextListener {
+ private AkkaActivator akkaActivator;
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce) {
+ akkaActivator = AkkaActivator.start("MailService", "mail-service");
+ akkaActivator.startTypedActor(MailRemoteService.class, "mail-remote-service",
+ (Creator) () ->
+ mailObject -> MailServiceExecutor.sendAsyncWithReply(mailObject, akkaActivator.getExecutionContext()));
+ akkaActivator.startActor(MailActor.class, "mail-actor");
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce) {
+ akkaActivator.shutdown();
+ }
+
+ public static class MailActor extends AbstractActor {
+ @Override
+ public Receive createReceive() {
+ return receiveBuilder().match(MailUtils.MailObject.class,
+ mailObject -> {
+ log.info("Receive mail form webappActor");
+ GroupResult groupResult = MailServiceExecutor.sendBulk(mailObject);
+ log.info("Send result to webappActor");
+ sender().tell(groupResult, self());
+ })
+ .build();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/listeners/JmsMailListener.java b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/listeners/JmsMailListener.java
new file mode 100644
index 000000000..4b29b7079
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/listeners/JmsMailListener.java
@@ -0,0 +1,67 @@
+package ru.javaops.masterjava.service.mail.listeners;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.activemq.ActiveMQConnectionFactory;
+import ru.javaops.masterjava.service.mail.MailServiceExecutor;
+import ru.javaops.masterjava.service.mail.util.MailUtils.MailObject;
+
+import javax.jms.*;
+import javax.naming.InitialContext;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.annotation.WebListener;
+
+@WebListener
+@Slf4j
+public class JmsMailListener implements ServletContextListener {
+ private Thread listenerThread = null;
+ private QueueConnection connection;
+
+ @Override
+ public void contextInitialized(ServletContextEvent sce) {
+ try {
+ InitialContext initCtx = new InitialContext();
+ ActiveMQConnectionFactory connectionFactory =
+ (ActiveMQConnectionFactory) initCtx.lookup("java:comp/env/jms/ConnectionFactory");
+ connectionFactory.setTrustAllPackages(true);
+ connection = connectionFactory.createQueueConnection();
+ QueueSession queueSession = connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
+ Queue queue = (Queue) initCtx.lookup("java:comp/env/jms/queue/MailQueue");
+ QueueReceiver receiver = queueSession.createReceiver(queue);
+ connection.start();
+ log.info("Listen JMS messages ...");
+ listenerThread = new Thread(() -> {
+ try {
+ while (!Thread.interrupted()) {
+ Message m = receiver.receive();
+ if (m instanceof ObjectMessage) {
+ ObjectMessage om = (ObjectMessage) m;
+ MailObject mailObject = (MailObject) om.getObject();
+ log.info("Received MailObject {}", mailObject);
+ MailServiceExecutor.sendAsync(mailObject);
+ }
+ }
+ } catch (Exception e) {
+ log.error("Receiving messages failed: " + e.getMessage(), e);
+ }
+ });
+ listenerThread.start();
+ } catch (Exception e) {
+ log.error("JMS failed: " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void contextDestroyed(ServletContextEvent sce) {
+ if (connection != null) {
+ try {
+ connection.close();
+ } catch (JMSException ex) {
+ log.warn("Couldn't close JMSConnection: ", ex);
+ }
+ }
+ if (listenerThread != null) {
+ listenerThread.interrupt();
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/persist/MailCase.java b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/persist/MailCase.java
new file mode 100644
index 000000000..2286a3966
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/persist/MailCase.java
@@ -0,0 +1,28 @@
+package ru.javaops.masterjava.service.mail.persist;
+
+import com.bertoncelj.jdbi.entitymapper.Column;
+import com.google.common.base.Joiner;
+import lombok.*;
+import ru.javaops.masterjava.persist.model.BaseEntity;
+import ru.javaops.masterjava.service.mail.Addressee;
+
+import java.util.Date;
+import java.util.Set;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@EqualsAndHashCode // compare without id
+@ToString
+public class MailCase extends BaseEntity {
+ private @Column("list_to") String listTo;
+ private @Column("list_cc") String listCc;
+ private String subject;
+ private String body;
+ private String state;
+ private Date date;
+
+ public static MailCase of(Set to, Set cc, String subject, String body, String state) {
+ return new MailCase(Joiner.on(", ").join(to), Joiner.on(", ").join(cc), subject, body, state, new Date());
+ }
+}
\ No newline at end of file
diff --git a/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/persist/MailCaseDao.java b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/persist/MailCaseDao.java
new file mode 100644
index 000000000..d2915c53c
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/persist/MailCaseDao.java
@@ -0,0 +1,24 @@
+package ru.javaops.masterjava.service.mail.persist;
+
+import com.bertoncelj.jdbi.entitymapper.EntityMapperFactory;
+import org.skife.jdbi.v2.sqlobject.*;
+import org.skife.jdbi.v2.sqlobject.customizers.RegisterMapperFactory;
+import ru.javaops.masterjava.persist.dao.AbstractDao;
+
+import java.util.Date;
+import java.util.List;
+
+@RegisterMapperFactory(EntityMapperFactory.class)
+public abstract class MailCaseDao implements AbstractDao {
+
+ @SqlUpdate("TRUNCATE mail_hist")
+ @Override
+ public abstract void clean();
+
+ @SqlQuery("SELECT * FROM mail_hist WHERE date>=:after ORDER BY date DESC")
+ public abstract List getAfter(@Bind("after") Date date);
+
+ @SqlUpdate("INSERT INTO mail_hist (list_to, list_cc, subject, body, state, date) VALUES (:listTo, :listCc, :subject, :body, :state, :date)")
+ @GetGeneratedKeys
+ public abstract int insert(@BindBean MailCase mails);
+}
diff --git a/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/rest/MailRS.java b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/rest/MailRS.java
new file mode 100644
index 000000000..5337826e6
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/rest/MailRS.java
@@ -0,0 +1,57 @@
+package ru.javaops.masterjava.service.mail.rest;
+
+
+import com.google.common.collect.ImmutableList;
+import org.glassfish.jersey.media.multipart.BodyPartEntity;
+import org.glassfish.jersey.media.multipart.FormDataBodyPart;
+import org.glassfish.jersey.media.multipart.FormDataParam;
+import org.hibernate.validator.constraints.NotBlank;
+import ru.javaops.masterjava.service.mail.Attach;
+import ru.javaops.masterjava.service.mail.GroupResult;
+import ru.javaops.masterjava.service.mail.MailServiceExecutor;
+import ru.javaops.masterjava.service.mail.util.MailUtils;
+import ru.javaops.web.WebStateException;
+
+import javax.activation.DataHandler;
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+
+@Path("/")
+public class MailRS {
+ @GET
+ @Path("test")
+ @Produces(MediaType.TEXT_PLAIN)
+ public String test() {
+ return "Test";
+ }
+
+ @POST
+ @Path("/send")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ public GroupResult send(@NotBlank @FormDataParam("users") String users,
+ @FormDataParam("subject") String subject,
+ @NotBlank @FormDataParam("body") String body,
+ @FormDataParam("attach") FormDataBodyPart attachBodyPart) throws WebStateException {
+
+ final List attaches;
+ String attachName = attachBodyPart.getContentDisposition().getFileName();
+
+ if (attachName.isEmpty()) {
+ attaches = ImmutableList.of();
+ } else {
+ try {
+// UTF-8 encoding workaround: https://java.net/jira/browse/JERSEY-3032
+ String utf8name = new String(attachName.getBytes("ISO8859_1"), "UTF-8");
+ BodyPartEntity bodyPartEntity = ((BodyPartEntity) attachBodyPart.getEntity());
+
+ attaches = ImmutableList.of(new Attach(utf8name, new DataHandler((MailUtils.ProxyDataSource) bodyPartEntity::getInputStream)));
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ return MailServiceExecutor.sendBulk(MailUtils.split(users), subject, body, attaches);
+ }
+}
\ No newline at end of file
diff --git a/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/rest/MailRestConfig.java b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/rest/MailRestConfig.java
new file mode 100644
index 000000000..0a1fa5cc5
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/rest/MailRestConfig.java
@@ -0,0 +1,19 @@
+package ru.javaops.masterjava.service.mail.rest;
+
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.slf4j.bridge.SLF4JBridgeHandler;
+
+import javax.ws.rs.ApplicationPath;
+
+@ApplicationPath("rest")
+public class MailRestConfig extends ResourceConfig {
+
+ public MailRestConfig() {
+ // Set Jersey log to SLF4J instead of JUL
+ // http://stackoverflow.com/questions/4121722
+ SLF4JBridgeHandler.install();
+ packages("ru.javaops.masterjava.service.mail.rest");
+ register(MultiPartFeature.class);
+ }
+}
\ No newline at end of file
diff --git a/services/mail-service/src/main/resources/logback.xml b/services/mail-service/src/main/resources/logback.xml
new file mode 100644
index 000000000..be7988a95
--- /dev/null
+++ b/services/mail-service/src/main/resources/logback.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+ ${LOG_DIR}/mail.log
+
+ UTF-8
+ %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] %-5level %logger{0} [%file:%line] - %msg%n
+
+
+
+ ${LOG_DIR}/archived/mail.%d{yyyy-MM-dd}.%i.log
+
+
+ 5MB
+
+
+
+
+
+
+ UTF-8
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} [%file:%line] - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/services/mail-service/src/main/resources/mail.conf b/services/mail-service/src/main/resources/mail.conf
new file mode 100644
index 000000000..0537e61e5
--- /dev/null
+++ b/services/mail-service/src/main/resources/mail.conf
@@ -0,0 +1,12 @@
+mail {
+ host: smtp.yandex.ru
+ port: 465
+ username: "user@yandex.ru"
+ password: password
+ useSSL: true
+ useTLS: false
+ debug: false
+ fromName: MasterJava
+}
+
+include required(file("/home/konst/work/masterjava/config/mail.conf"))
\ No newline at end of file
diff --git a/services/mail-service/src/main/resources/mailWsHandlers.xml b/services/mail-service/src/main/resources/mailWsHandlers.xml
new file mode 100644
index 000000000..14ac001d0
--- /dev/null
+++ b/services/mail-service/src/main/resources/mailWsHandlers.xml
@@ -0,0 +1,12 @@
+
+
+
+ MailLoggingHandler
+ ru.javaops.masterjava.service.mail.MailHandlers$LoggingHandler
+
+
+ SoapStatisticHandler
+ ru.javaops.web.handler.SoapStatisticHandler
+
+
+
\ No newline at end of file
diff --git a/services/mail-service/src/main/webapp/WEB-INF/sun-jaxws.xml b/services/mail-service/src/main/webapp/WEB-INF/sun-jaxws.xml
new file mode 100644
index 000000000..763d86504
--- /dev/null
+++ b/services/mail-service/src/main/webapp/WEB-INF/sun-jaxws.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/services/mail-service/src/main/webapp/WEB-INF/web.xml b/services/mail-service/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 000000000..5a4bd7190
--- /dev/null
+++ b/services/mail-service/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ mailService
+ /mailService
+
+
+ tomcat
+
+
+
+
+ BASIC
+ Tomcat basic auth
+
+
+
+ tomcat
+
+
diff --git a/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServiceClient.java b/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServiceClient.java
new file mode 100644
index 000000000..722112b14
--- /dev/null
+++ b/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServiceClient.java
@@ -0,0 +1,40 @@
+package ru.javaops.masterjava.service.mail;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import ru.javaops.web.WebStateException;
+
+import javax.activation.DataHandler;
+import javax.xml.namespace.QName;
+import javax.xml.ws.Service;
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+
+public class MailServiceClient {
+
+ public static void main(String[] args) throws MalformedURLException {
+ Service service = Service.create(
+ new URL("http://localhost:8080/mail/mailService?wsdl"),
+ new QName("http://mail.javaops.ru/", "MailServiceImplService"));
+
+ MailService mailService = service.getPort(MailService.class);
+
+ ImmutableSet addressees = ImmutableSet.of(
+ new Addressee("Мастер Java "));
+
+ List attaches = ImmutableList.of(
+ new Attach("version.html", new DataHandler(new File("config_templates/version.html").toURI().toURL())));
+
+ try {
+ String status = mailService.sendToGroup(addressees, ImmutableSet.of(), "Bulk email subject", "Bulk email body", attaches);
+ System.out.println(status);
+
+ GroupResult groupResult = mailService.sendBulk(addressees, "Individual mail subject", "Individual mail body", attaches);
+ System.out.println(groupResult);
+ } catch (WebStateException e) {
+ System.out.println(e);
+ }
+ }
+}
diff --git a/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServicePublisher.java b/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServicePublisher.java
new file mode 100644
index 000000000..1d77f0f27
--- /dev/null
+++ b/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServicePublisher.java
@@ -0,0 +1,25 @@
+package ru.javaops.masterjava.service.mail;
+
+import com.google.common.collect.ImmutableList;
+import ru.javaops.masterjava.config.Configs;
+import ru.javaops.masterjava.persist.DBITestProvider;
+
+import javax.xml.transform.Source;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.ws.Endpoint;
+import java.util.List;
+
+public class MailServicePublisher {
+
+ public static void main(String[] args) {
+ DBITestProvider.initDBI();
+
+ Endpoint endpoint = Endpoint.create(new MailServiceImpl());
+ List metadata = ImmutableList.of(
+ new StreamSource(Configs.getConfigFile("wsdl/mailService.wsdl")),
+ new StreamSource(Configs.getConfigFile("wsdl/common.xsd")));
+
+ endpoint.setMetadata(metadata);
+ endpoint.publish("http://localhost:8080/mail/mailService");
+ }
+}
diff --git a/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/persist/MailCaseDaoTest.java b/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/persist/MailCaseDaoTest.java
new file mode 100644
index 000000000..2f6253501
--- /dev/null
+++ b/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/persist/MailCaseDaoTest.java
@@ -0,0 +1,22 @@
+package ru.javaops.masterjava.service.mail.persist;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import ru.javaops.masterjava.persist.dao.AbstractDaoTest;
+
+public class MailCaseDaoTest extends AbstractDaoTest {
+ public MailCaseDaoTest() {
+ super(MailCaseDao.class);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MailCaseTestData.setUp();
+ }
+
+ @Test
+ public void getAll() throws Exception {
+ Assert.assertEquals(MailCaseTestData.MAIL_CASES, dao.getAfter(MailCaseTestData.DATE_FROM));
+ }
+}
\ No newline at end of file
diff --git a/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/persist/MailCaseTestData.java b/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/persist/MailCaseTestData.java
new file mode 100644
index 000000000..3ba350895
--- /dev/null
+++ b/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/persist/MailCaseTestData.java
@@ -0,0 +1,48 @@
+package ru.javaops.masterjava.service.mail.persist;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import ru.javaops.masterjava.persist.DBIProvider;
+import ru.javaops.masterjava.service.mail.Addressee;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * gkislin
+ * 26.11.2016
+ */
+public class MailCaseTestData {
+ private static final Instant now = Instant.now();
+ static final Date DATE_FROM = Date.from(now.minus(Duration.ofDays(1)));
+
+ static final List MAIL_CASES = ImmutableList.of(
+ MailCase.of(
+ ImmutableSet.of(
+ new Addressee("ИмяTo1 Фамилия1 "),
+ new Addressee("Имя2 Фамилия2 ")),
+ ImmutableSet.of(
+ new Addressee("ИмяCc1 Фамилия1 "),
+ new Addressee("ИмяCc2 Фамилия2 ")),
+ "subject1", "body1", "state1"
+ ),
+ new MailCase("toMail2@ya.ru", null, "subject2", "body2", "state2",
+ Date.from(now.minus(Duration.ofMinutes(1)))),
+ new MailCase(null, "ccMail3@ya.ru", "subject3", "body3", "state3", DATE_FROM)
+ );
+
+ static final MailCase MAIL_CASE_EXCLUDED =
+ new MailCase("toMail4@ya.ru", "ccMail4@ya.ru", "subject4", "body4", "state4",
+ Date.from(now.minus(Duration.ofDays(2))));
+
+ public static void setUp() {
+ MailCaseDao dao = DBIProvider.getDao(MailCaseDao.class);
+ dao.clean();
+ DBIProvider.getDBI().useTransaction((conn, status) -> {
+ MAIL_CASES.forEach(dao::insert);
+ dao.insert(MAIL_CASE_EXCLUDED);
+ });
+ }
+}
diff --git a/services/pom.xml b/services/pom.xml
new file mode 100644
index 000000000..9bc70b43a
--- /dev/null
+++ b/services/pom.xml
@@ -0,0 +1,17 @@
+
+ 4.0.0
+
+ ru.javaops
+ services
+ pom
+ 1.0-SNAPSHOT
+
+ MasterJava Services
+
+ akka-remote
+ common-ws
+ mail-api
+ mail-service
+
+
diff --git a/sql/databaseChangeLog.sql b/sql/databaseChangeLog.sql
new file mode 100644
index 000000000..af0b27954
--- /dev/null
+++ b/sql/databaseChangeLog.sql
@@ -0,0 +1,49 @@
+--liquibase formatted sql
+
+--changeset gkislin:1
+CREATE SEQUENCE common_seq START 100000;
+
+CREATE TABLE city (
+ id INTEGER PRIMARY KEY DEFAULT nextval('common_seq'),
+ ref TEXT UNIQUE,
+ name TEXT NOT NULL
+);
+
+ALTER TABLE users
+ ADD COLUMN city_id INTEGER REFERENCES city (id);
+
+--changeset gkislin:2
+CREATE TABLE project (
+ id INTEGER PRIMARY KEY DEFAULT nextval('common_seq'),
+ name TEXT NOT NULL UNIQUE,
+ description TEXT
+);
+
+CREATE TYPE GROUP_TYPE AS ENUM ('REGISTERING', 'CURRENT', 'FINISHED');
+
+CREATE TABLE groups (
+ id INTEGER PRIMARY KEY DEFAULT nextval('common_seq'),
+ name TEXT NOT NULL UNIQUE,
+ type GROUP_TYPE NOT NULL,
+ project_id INTEGER NOT NULL REFERENCES project (id)
+);
+
+CREATE TABLE user_group (
+ user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+ group_id INTEGER NOT NULL REFERENCES groups (id),
+ CONSTRAINT users_group_idx UNIQUE (user_id, group_id)
+);
+
+--changeset gkislin:3
+CREATE TABLE mail_hist (
+ id SERIAL PRIMARY KEY,
+ list_to TEXT NULL,
+ list_cc TEXT NULL,
+ subject TEXT NULL,
+ body TEXT NULL,
+ state TEXT NOT NULL,
+ date TIMESTAMP NOT NULL
+);
+
+COMMENT ON TABLE mail_hist IS 'История отправки email';
+COMMENT ON COLUMN mail_hist.date IS 'Время отправки';
\ No newline at end of file
diff --git a/sql/initDB.sql b/sql/initDB.sql
new file mode 100644
index 000000000..09b463d69
--- /dev/null
+++ b/sql/initDB.sql
@@ -0,0 +1,16 @@
+DROP TABLE IF EXISTS users;
+DROP SEQUENCE IF EXISTS user_seq;
+DROP TYPE IF EXISTS user_flag;
+
+CREATE TYPE user_flag AS ENUM ('active', 'deleted', 'superuser');
+
+CREATE SEQUENCE user_seq START 100000;
+
+CREATE TABLE users (
+ id INTEGER PRIMARY KEY DEFAULT nextval('user_seq'),
+ full_name TEXT NOT NULL,
+ email TEXT NOT NULL,
+ flag user_flag NOT NULL
+);
+
+CREATE UNIQUE INDEX email_idx ON users (email);
\ No newline at end of file
diff --git a/sql/lb_apply.bat b/sql/lb_apply.bat
new file mode 100644
index 000000000..80f23598b
--- /dev/null
+++ b/sql/lb_apply.bat
@@ -0,0 +1,8 @@
+set LB_HOME=c:\java\liquibase-3.5.3
+call %LB_HOME%\liquibase.bat --driver=org.postgresql.Driver ^
+--classpath=%LB_HOME%\lib ^
+--changeLogFile=databaseChangeLog.sql ^
+--url="jdbc:postgresql://localhost:5432/masterjava" ^
+--username=user ^
+--password=password ^
+migrate
\ No newline at end of file
diff --git a/test/pom.xml b/test/pom.xml
new file mode 100644
index 000000000..bfab1855d
--- /dev/null
+++ b/test/pom.xml
@@ -0,0 +1,76 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent
+ ../parent/pom.xml
+ 1.0-SNAPSHOT
+
+
+ test
+ 1.0-SNAPSHOT
+ Test
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 2.2
+
+
+ package
+
+ shade
+
+
+ benchmarks
+
+
+ org.openjdk.jmh.Main
+
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
+
+ ${project.groupId}
+ common
+ ${project.version}
+
+
+ org.openjdk.jmh
+ jmh-core
+ 1.15
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ 1.15
+ provided
+
+
+
\ No newline at end of file
diff --git a/test/src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java b/test/src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java
new file mode 100644
index 000000000..4f30e499a
--- /dev/null
+++ b/test/src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java
@@ -0,0 +1,52 @@
+package ru.javaops.masterjava.matrix;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * gkislin
+ * 03.07.2016
+ */
+public class MainMatrix {
+ private static final int MATRIX_SIZE = 1000;
+ private static final int THREAD_NUMBER = 10;
+
+ private final static ExecutorService executor = Executors.newFixedThreadPool(MainMatrix.THREAD_NUMBER);
+
+ public static void main(String[] args) throws ExecutionException, InterruptedException {
+ final int[][] matrixA = MatrixUtil.create(MATRIX_SIZE);
+ final int[][] matrixB = MatrixUtil.create(MATRIX_SIZE);
+
+ double singleThreadSum = 0.;
+ double concurrentThreadSum = 0.;
+ int count = 1;
+ while (count < 6) {
+ System.out.println("Pass " + count);
+ long start = System.currentTimeMillis();
+ final int[][] matrixC = MatrixUtil.singleThreadMultiplyOpt(matrixA, matrixB);
+ double duration = (System.currentTimeMillis() - start) / 1000.;
+ out("Single thread time, sec: %.3f", duration);
+ singleThreadSum += duration;
+
+ start = System.currentTimeMillis();
+ final int[][] concurrentMatrixC = MatrixUtil.concurrentMultiply2(matrixA, matrixB, executor);
+ duration = (System.currentTimeMillis() - start) / 1000.;
+ out("Concurrent thread time, sec: %.3f", duration);
+ concurrentThreadSum += duration;
+
+ if (!MatrixUtil.compare(matrixC, concurrentMatrixC)) {
+ System.err.println("Comparison failed");
+ break;
+ }
+ count++;
+ }
+ executor.shutdown();
+ out("\nAverage single thread time, sec: %.3f", singleThreadSum / 5.);
+ out("Average concurrent thread time, sec: %.3f", concurrentThreadSum / 5.);
+ }
+
+ private static void out(String format, double ms) {
+ System.out.println(String.format(format, ms));
+ }
+}
diff --git a/test/src/main/java/ru/javaops/masterjava/matrix/MatrixBenchmark.java b/test/src/main/java/ru/javaops/masterjava/matrix/MatrixBenchmark.java
new file mode 100644
index 000000000..80f1558ea
--- /dev/null
+++ b/test/src/main/java/ru/javaops/masterjava/matrix/MatrixBenchmark.java
@@ -0,0 +1,77 @@
+package ru.javaops.masterjava.matrix;
+
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+import org.openjdk.jmh.runner.options.TimeValue;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * gkislin
+ * 23.09.2016
+ */
+@Warmup(iterations = 10)
+@Measurement(iterations = 10)
+@BenchmarkMode({Mode.SingleShotTime})
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Benchmark)
+@Threads(1)
+@Fork(10)
+@Timeout(time = 5, timeUnit = TimeUnit.MINUTES)
+public class MatrixBenchmark {
+ // Matrix size
+ @Param({"1000"})
+ private int matrixSize;
+
+ private static final int THREAD_NUMBER = 10;
+ private final static ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUMBER);
+
+ private static int[][] matrixA;
+ private static int[][] matrixB;
+
+ @Setup
+ public void setUp() {
+ matrixA = MatrixUtil.create(matrixSize);
+ matrixB = MatrixUtil.create(matrixSize);
+ }
+
+ public static void main(String[] args) throws RunnerException {
+ Options options = new OptionsBuilder()
+ .include(MatrixBenchmark.class.getSimpleName())
+ .threads(1)
+ .forks(10)
+ .timeout(TimeValue.minutes(5))
+ .build();
+ new Runner(options).run();
+ }
+
+// @Benchmark
+ public int[][] singleThreadMultiplyOpt() throws Exception {
+ return MatrixUtil.singleThreadMultiplyOpt(matrixA, matrixB);
+ }
+
+// @Benchmark
+ public int[][] concurrentMultiplyStreams() throws Exception {
+ return MatrixUtil.concurrentMultiplyStreams(matrixA, matrixB, executor);
+ }
+
+ @Benchmark
+ public int[][] concurrentMultiply2() throws Exception {
+ return MatrixUtil.concurrentMultiply2(matrixA, matrixB, executor);
+ }
+
+ @Benchmark
+ public int[][] concurrentMultiply3() throws Exception {
+ return MatrixUtil.concurrentMultiply3(matrixA, matrixB, executor);
+ }
+
+ @TearDown
+ public void tearDown() {
+ executor.shutdown();
+ }
+}
diff --git a/test/src/main/java/ru/javaops/masterjava/matrix/MatrixUtil.java b/test/src/main/java/ru/javaops/masterjava/matrix/MatrixUtil.java
new file mode 100644
index 000000000..39f077898
--- /dev/null
+++ b/test/src/main/java/ru/javaops/masterjava/matrix/MatrixUtil.java
@@ -0,0 +1,154 @@
+package ru.javaops.masterjava.matrix;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * gkislin
+ * 03.07.2016
+ */
+public class MatrixUtil {
+
+ public static int[][] concurrentMultiplyStreams(int[][] matrixA, int[][] matrixB, ExecutorService executor)
+ throws InterruptedException, ExecutionException {
+
+ final int matrixSize = matrixA.length;
+ final int[][] matrixC = new int[matrixSize][matrixSize];
+
+ List> tasks = IntStream.range(0, matrixSize)
+ .parallel()
+ .mapToObj(i -> new Callable() {
+ private final int[] tempColumn = new int[matrixSize];
+
+ @Override
+ public Void call() throws Exception {
+ for (int c = 0; c < matrixSize; c++) {
+ tempColumn[c] = matrixB[c][i];
+ }
+ for (int j = 0; j < matrixSize; j++) {
+ int row[] = matrixA[j];
+ int sum = 0;
+ for (int k = 0; k < matrixSize; k++) {
+ sum += tempColumn[k] * row[k];
+ }
+ matrixC[j][i] = sum;
+ }
+ return null;
+ }
+ })
+ .collect(Collectors.toList());
+
+ executor.invokeAll(tasks);
+ return matrixC;
+ }
+
+ public static int[][] concurrentMultiply2(int[][] matrixA, int[][] matrixB, ExecutorService executor) throws InterruptedException, ExecutionException {
+ final int matrixSize = matrixA.length;
+ final int[][] matrixC = new int[matrixSize][];
+
+ final int[][] matrixBT = new int[matrixSize][matrixSize];
+ for (int i = 0; i < matrixSize; i++) {
+ for (int j = 0; j < matrixSize; j++) {
+ matrixBT[i][j] = matrixB[j][i];
+ }
+ }
+
+ List> tasks = new ArrayList<>(matrixSize);
+ for (int j = 0; j < matrixSize; j++) {
+ final int row = j;
+ tasks.add(() -> {
+ final int[] rowC = new int[matrixSize];
+ for (int col = 0; col < matrixSize; col++) {
+ final int[] rowA = matrixA[row];
+ final int[] columnB = matrixBT[col];
+ int sum = 0;
+ for (int k = 0; k < matrixSize; k++) {
+ sum += rowA[k] * columnB[k];
+ }
+ rowC[col] = sum;
+ }
+ matrixC[row] = rowC;
+ return null;
+ });
+ }
+ executor.invokeAll(tasks);
+ return matrixC;
+ }
+
+ public static int[][] concurrentMultiply3(int[][] matrixA, int[][] matrixB, ExecutorService executor) throws InterruptedException {
+ final int matrixSize = matrixA.length;
+ final int[][] matrixC = new int[matrixSize][matrixSize];
+ final CountDownLatch latch = new CountDownLatch(matrixSize);
+
+ for (int row = 0; row < matrixSize; row++) {
+ final int[] rowA = matrixA[row];
+ final int[] rowC = matrixC[row];
+
+ executor.submit(() -> {
+ for (int idx = 0; idx < matrixSize; idx++) {
+ final int elA = rowA[idx];
+ final int[] rowB = matrixB[idx];
+ for (int col = 0; col < matrixSize; col++) {
+ rowC[col] += elA * rowB[col];
+ }
+ }
+ latch.countDown();
+ });
+ }
+ latch.await();
+ return matrixC;
+ }
+
+ public static int[][] singleThreadMultiplyOpt(int[][] matrixA, int[][] matrixB) {
+ final int matrixSize = matrixA.length;
+ final int[][] matrixC = new int[matrixSize][matrixSize];
+
+ for (int col = 0; col < matrixSize; col++) {
+ final int[] columnB = new int[matrixSize];
+ for (int k = 0; k < matrixSize; k++) {
+ columnB[k] = matrixB[k][col];
+ }
+
+ for (int row = 0; row < matrixSize; row++) {
+ int sum = 0;
+ final int[] rowA = matrixA[row];
+ for (int k = 0; k < matrixSize; k++) {
+ sum += rowA[k] * columnB[k];
+ }
+ matrixC[row][col] = sum;
+ }
+ }
+ return matrixC;
+ }
+
+ public static int[][] create(int size) {
+ int[][] matrix = new int[size][size];
+ Random rn = new Random();
+
+ for (int i = 0; i < size; i++) {
+ for (int j = 0; j < size; j++) {
+ matrix[i][j] = rn.nextInt(10);
+ }
+ }
+ return matrix;
+ }
+
+ public static boolean compare(int[][] matrixA, int[][] matrixB) {
+ final int matrixSize = matrixA.length;
+ for (int i = 0; i < matrixSize; i++) {
+ for (int j = 0; j < matrixSize; j++) {
+ if (matrixA[i][j] != matrixB[i][j]) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/web/common-web/pom.xml b/web/common-web/pom.xml
new file mode 100644
index 000000000..da4f46174
--- /dev/null
+++ b/web/common-web/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent
+ ../../parent/pom.xml
+ 1.0-SNAPSHOT
+
+
+ common-web
+ 1.0-SNAPSHOT
+ Common Web
+
+
+
+ ${project.groupId}
+ common
+ ${project.version}
+
+
+ javax.servlet
+ javax.servlet-api
+ 3.1.0
+ provided
+
+
+
+ org.thymeleaf
+ thymeleaf
+ 3.0.3.RELEASE
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+
\ No newline at end of file
diff --git a/web/common-web/src/main/java/ru/javaops/masterjava/common/web/ThymeleafListener.java b/web/common-web/src/main/java/ru/javaops/masterjava/common/web/ThymeleafListener.java
new file mode 100644
index 000000000..16948aa44
--- /dev/null
+++ b/web/common-web/src/main/java/ru/javaops/masterjava/common/web/ThymeleafListener.java
@@ -0,0 +1,20 @@
+package ru.javaops.masterjava.common.web;
+
+import org.thymeleaf.TemplateEngine;
+
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.annotation.WebListener;
+
+@WebListener
+public class ThymeleafListener implements ServletContextListener {
+
+ public static TemplateEngine engine;
+
+ public void contextInitialized(ServletContextEvent sce) {
+ engine = ThymeleafUtil.getTemplateEngine(sce.getServletContext());
+ }
+
+ public void contextDestroyed(ServletContextEvent sce) {
+ }
+}
diff --git a/web/common-web/src/main/java/ru/javaops/masterjava/common/web/ThymeleafUtil.java b/web/common-web/src/main/java/ru/javaops/masterjava/common/web/ThymeleafUtil.java
new file mode 100644
index 000000000..bf87ed3eb
--- /dev/null
+++ b/web/common-web/src/main/java/ru/javaops/masterjava/common/web/ThymeleafUtil.java
@@ -0,0 +1,24 @@
+package ru.javaops.masterjava.common.web;
+
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.templatemode.TemplateMode;
+import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
+
+import javax.servlet.ServletContext;
+
+public class ThymeleafUtil {
+
+ private ThymeleafUtil() {
+ }
+
+ public static TemplateEngine getTemplateEngine(ServletContext context) {
+ final ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(context);
+ templateResolver.setTemplateMode(TemplateMode.HTML);
+ templateResolver.setPrefix("/WEB-INF/templates/");
+ templateResolver.setSuffix(".html");
+ templateResolver.setCacheTTLMs(1000L);
+ final TemplateEngine engine = new TemplateEngine();
+ engine.setTemplateResolver(templateResolver);
+ return engine;
+ }
+}
diff --git a/web/export/pom.xml b/web/export/pom.xml
new file mode 100644
index 000000000..3196e8a05
--- /dev/null
+++ b/web/export/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent-web
+ ../../parent-web/pom.xml
+ 1.0-SNAPSHOT
+
+
+ export
+ 1.0-SNAPSHOT
+ war
+ Export
+
+
+ export
+
+
+
+
+ ${project.groupId}
+ persist
+ ${project.version}
+
+
+ com.j2html
+ j2html
+ 0.7
+
+
+ com.google.guava
+ guava
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/export/src/main/java/ru/javaops/masterjava/export/CityImporter.java b/web/export/src/main/java/ru/javaops/masterjava/export/CityImporter.java
new file mode 100644
index 000000000..f6b4e8be3
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/export/CityImporter.java
@@ -0,0 +1,38 @@
+package ru.javaops.masterjava.export;
+
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import ru.javaops.masterjava.persist.DBIProvider;
+import ru.javaops.masterjava.persist.dao.CityDao;
+import ru.javaops.masterjava.persist.model.City;
+import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.XMLEvent;
+import java.util.ArrayList;
+import java.util.Map;
+
+/**
+ * gkislin
+ * 15.11.2016
+ */
+@Slf4j
+public class CityImporter {
+ private final CityDao cityDao = DBIProvider.getDao(CityDao.class);
+ public Map process(StaxStreamProcessor processor) throws XMLStreamException {
+ val map = cityDao.getAsMap();
+ val newCities = new ArrayList();
+ String element;
+
+ while ((element = processor.doUntilAny(XMLEvent.START_ELEMENT, "City", "Users")) != null) {
+ if (element.equals("Users")) break;
+ val ref = processor.getAttribute("id");
+ if (!map.containsKey(ref)) {
+ newCities.add(new City(null, ref, processor.getText()));
+ }
+ }
+ log.info("Insert batch " + newCities);
+ cityDao.insertBatch(newCities);
+ return cityDao.getAsMap();
+ }
+}
\ No newline at end of file
diff --git a/web/export/src/main/java/ru/javaops/masterjava/export/PayloadImporter.java b/web/export/src/main/java/ru/javaops/masterjava/export/PayloadImporter.java
new file mode 100644
index 000000000..1879a6018
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/export/PayloadImporter.java
@@ -0,0 +1,33 @@
+package ru.javaops.masterjava.export;
+
+import lombok.Value;
+import lombok.val;
+import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
+
+import javax.xml.stream.XMLStreamException;
+import java.io.InputStream;
+import java.util.List;
+
+public class PayloadImporter {
+ private final ProjectGroupImporter projectGroupImporter = new ProjectGroupImporter();
+ private final CityImporter cityImporter = new CityImporter();
+ private final UserImporter userImporter = new UserImporter();
+
+ @Value
+ public static class FailedEmail {
+ public String emailOrRange;
+ public String reason;
+
+ @Override
+ public String toString() {
+ return emailOrRange + " : " + reason;
+ }
+ }
+
+ public List process(InputStream is, int chunkSize) throws XMLStreamException {
+ final StaxStreamProcessor processor = new StaxStreamProcessor(is);
+ val groups = projectGroupImporter.process(processor);
+ val cities = cityImporter.process(processor);
+ return userImporter.process(processor, groups, cities, chunkSize);
+ }
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/export/ProjectGroupImporter.java b/web/export/src/main/java/ru/javaops/masterjava/export/ProjectGroupImporter.java
new file mode 100644
index 000000000..d9b2c9159
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/export/ProjectGroupImporter.java
@@ -0,0 +1,53 @@
+package ru.javaops.masterjava.export;
+
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import ru.javaops.masterjava.persist.DBIProvider;
+import ru.javaops.masterjava.persist.dao.GroupDao;
+import ru.javaops.masterjava.persist.dao.ProjectDao;
+import ru.javaops.masterjava.persist.model.Group;
+import ru.javaops.masterjava.persist.model.GroupType;
+import ru.javaops.masterjava.persist.model.Project;
+import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.XMLEvent;
+import java.util.ArrayList;
+import java.util.Map;
+
+@Slf4j
+public class ProjectGroupImporter {
+ private final ProjectDao projectDao = DBIProvider.getDao(ProjectDao.class);
+ private final GroupDao groupDao = DBIProvider.getDao(GroupDao.class);
+
+ public Map process(StaxStreamProcessor processor) throws XMLStreamException {
+ val projectMap = projectDao.getAsMap();
+ val groupMap = groupDao.getAsMap();
+ String element;
+
+ val newGroups = new ArrayList();
+ Project project = null;
+ while ((element = processor.doUntilAny(XMLEvent.START_ELEMENT, "Project", "Group", "Cities")) != null) {
+ if (element.equals("Cities")) break;
+ if (element.equals("Project")) {
+ val name = processor.getAttribute("name");
+ val description = processor.getElementValue("description");
+ project = projectMap.get(name);
+ if (project == null) {
+ project = new Project(name, description);
+ log.info("Insert project " + project);
+ projectDao.insert(project);
+ }
+ } else {
+ val name = processor.getAttribute("name");
+ if (!groupMap.containsKey(name)) {
+ // project here already assigned, as it located in xml before Group
+ newGroups.add(new Group(name, GroupType.valueOf(processor.getAttribute("type")), project.getId()));
+ }
+ }
+ }
+ log.info("Insert groups " + newGroups);
+ groupDao.insertBatch(newGroups);
+ return groupDao.getAsMap();
+ }
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/export/UploadServlet.java b/web/export/src/main/java/ru/javaops/masterjava/export/UploadServlet.java
new file mode 100644
index 000000000..9c0147f9b
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/export/UploadServlet.java
@@ -0,0 +1,68 @@
+package ru.javaops.masterjava.export;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.extern.slf4j.Slf4j;
+import org.thymeleaf.context.WebContext;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.MultipartConfig;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.Part;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import static ru.javaops.masterjava.common.web.ThymeleafListener.engine;
+
+@WebServlet("/")
+@MultipartConfig
+@Slf4j
+public class UploadServlet extends HttpServlet {
+ private static final int CHUNK_SIZE = 2000;
+
+ private final PayloadImporter payloadImporter = new PayloadImporter();
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ outExport(req, resp, "", CHUNK_SIZE);
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ String message;
+ int chunkSize = CHUNK_SIZE;
+ try {
+// http://docs.oracle.com/javaee/6/tutorial/doc/glraq.html
+ chunkSize = Integer.parseInt(req.getParameter("chunkSize"));
+ if (chunkSize < 1) {
+ message = "Chunk Size must be > 1";
+ } else {
+ Part filePart = req.getPart("fileToUpload");
+ try (InputStream is = filePart.getInputStream()) {
+ List failed = payloadImporter.process(is, chunkSize);
+ log.info("Failed users: " + failed);
+ final WebContext webContext =
+ new WebContext(req, resp, req.getServletContext(), req.getLocale(),
+ ImmutableMap.of("failed", failed));
+ engine.process("result", webContext, resp.getWriter());
+ return;
+ }
+ }
+ } catch (Exception e) {
+ log.info(e.getMessage(), e);
+ message = e.toString();
+ }
+ outExport(req, resp, message, chunkSize);
+ }
+
+ private void outExport(HttpServletRequest req, HttpServletResponse resp, String message, int chunkSize) throws IOException {
+ resp.setCharacterEncoding("utf-8");
+ final WebContext webContext =
+ new WebContext(req, resp, req.getServletContext(), req.getLocale(),
+ ImmutableMap.of("message", message, "chunkSize", chunkSize));
+ engine.process("export", webContext, resp.getWriter());
+ }
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/export/UserImporter.java b/web/export/src/main/java/ru/javaops/masterjava/export/UserImporter.java
new file mode 100644
index 000000000..5ce55a638
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/export/UserImporter.java
@@ -0,0 +1,126 @@
+package ru.javaops.masterjava.export;
+
+import com.google.common.base.Splitter;
+import lombok.Value;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+import one.util.streamex.StreamEx;
+import ru.javaops.masterjava.export.PayloadImporter.FailedEmail;
+import ru.javaops.masterjava.persist.DBIProvider;
+import ru.javaops.masterjava.persist.dao.UserDao;
+import ru.javaops.masterjava.persist.dao.UserGroupDao;
+import ru.javaops.masterjava.persist.model.*;
+import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.XMLEvent;
+import java.util.*;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+@Slf4j
+public class UserImporter {
+
+ private static final int NUMBER_THREADS = 4;
+ private final ExecutorService executorService = Executors.newFixedThreadPool(NUMBER_THREADS);
+ private final UserDao userDao = DBIProvider.getDao(UserDao.class);
+ private UserGroupDao userGroupDao = DBIProvider.getDao(UserGroupDao.class);
+
+ public List process(StaxStreamProcessor processor, Map groups, Map cities, int chunkSize) throws XMLStreamException {
+ log.info("Start proseccing with chunkSize=" + chunkSize);
+
+ @Value
+ class ChunkItem {
+ private User user;
+ private StreamEx userGroups;
+ }
+
+ return new Callable>() {
+ class ChunkFuture {
+ String emailRange;
+ Future> future;
+
+ public ChunkFuture(List chunk, Future> future) {
+ this.future = future;
+ this.emailRange = chunk.get(0).getEmail();
+ if (chunk.size() > 1) {
+ this.emailRange += '-' + chunk.get(chunk.size() - 1).getEmail();
+ }
+ }
+ }
+
+ @Override
+ public List call() throws XMLStreamException {
+ val futures = new ArrayList();
+ int id = userDao.getSeqAndSkip(chunkSize);
+ List chunk = new ArrayList<>(chunkSize);
+ val failed = new ArrayList();
+
+ while (processor.doUntil(XMLEvent.START_ELEMENT, "User")) {
+ final String email = processor.getAttribute("email");
+ String cityRef = processor.getAttribute("city");
+ City city = cities.get(cityRef);
+ if (city == null) {
+ failed.add(new FailedEmail(email, "City '" + cityRef + "' is not present in DB"));
+ } else {
+ val groupRefs = processor.getAttribute("groupRefs");
+ List groupNames = (groupRefs == null) ?
+ Collections.emptyList() :
+ Splitter.on(' ').splitToList(groupRefs);
+
+ if (!groups.keySet().containsAll(groupNames)) {
+ failed.add(new FailedEmail(email, "One of group from '" + groupRefs + "' is not present in DB"));
+ } else {
+ final UserFlag flag = UserFlag.valueOf(processor.getAttribute("flag"));
+ final String fullName = processor.getText();
+ final User user = new User(id++, fullName, email, flag, city.getId());
+ StreamEx userGroups = StreamEx.of(groupNames).map(name -> new UserGroup(user.getId(), groups.get(name).getId()));
+ chunk.add(new ChunkItem(user, userGroups));
+ if (chunk.size() == chunkSize) {
+ futures.add(submit(chunk));
+ chunk = new ArrayList<>(chunkSize);
+ id = userDao.getSeqAndSkip(chunkSize);
+ }
+ }
+ }
+ }
+
+ if (!chunk.isEmpty()) {
+ futures.add(submit(chunk));
+ }
+
+ futures.forEach(cf -> {
+ try {
+ failed.addAll(StreamEx.of(cf.future.get()).map(email -> new FailedEmail(email, "already present")).toList());
+ log.info(cf.emailRange + " successfully executed");
+ } catch (Exception e) {
+ log.error(cf.emailRange + " failed", e);
+ failed.add(new FailedEmail(cf.emailRange, e.toString()));
+ }
+ });
+ return failed;
+ }
+
+ private ChunkFuture submit(List chunk) {
+ val users = StreamEx.of(chunk).map(ChunkItem::getUser).toList();
+ ChunkFuture chunkFuture = new ChunkFuture(
+ users,
+ executorService.submit(() -> {
+ List alreadyPresents = userDao.insertAndGetConflictEmails(users);
+ Set alreadyPresentsIds = StreamEx.of(alreadyPresents).map(User::getId).toSet();
+ userGroupDao.insertBatch(
+ StreamEx.of(chunk).flatMap(ChunkItem::getUserGroups)
+ .filter(ug -> !alreadyPresentsIds.contains(ug.getUserId()))
+ .toList()
+ );
+ return StreamEx.of(alreadyPresents).map(User::getEmail).toList();
+ })
+ );
+ log.info("Submit " + chunkFuture.emailRange);
+ return chunkFuture;
+ }
+ }.call();
+ }
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/schema/CityType.java b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/CityType.java
new file mode 100644
index 000000000..029e352cb
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/CityType.java
@@ -0,0 +1,94 @@
+
+package ru.javaops.masterjava.xml.schema;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlID;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+
+/**
+ * Java class for cityType complex type.
+ *
+ *
The following schema fragment specifies the expected content contained within this class.
+ *
+ *
+ * <complexType name="cityType">
+ * <simpleContent>
+ * <extension base="<http://www.w3.org/2001/XMLSchema>string">
+ * <attribute name="id" use="required" type="{http://www.w3.org/2001/XMLSchema}ID" />
+ * </extension>
+ * </simpleContent>
+ * </complexType>
+ *
+ *
+ *
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlType(name = "cityType", namespace = "http://javaops.ru", propOrder = {
+ "value"
+})
+public class CityType {
+
+ @XmlValue
+ protected String value;
+ @XmlAttribute(name = "id", required = true)
+ @XmlJavaTypeAdapter(CollapsedStringAdapter.class)
+ @XmlID
+ @XmlSchemaType(name = "ID")
+ protected String id;
+
+ /**
+ * Gets the value of the value property.
+ *
+ * @return
+ * possible object is
+ * {@link String }
+ *
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Sets the value of the value property.
+ *
+ * @param value
+ * allowed object is
+ * {@link String }
+ *
+ */
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Gets the value of the id property.
+ *
+ * @return
+ * possible object is
+ * {@link String }
+ *
+ */
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * Sets the value of the id property.
+ *
+ * @param value
+ * allowed object is
+ * {@link String }
+ *
+ */
+ public void setId(String value) {
+ this.id = value;
+ }
+
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/schema/FlagType.java b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/FlagType.java
new file mode 100644
index 000000000..eda39fa9a
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/FlagType.java
@@ -0,0 +1,54 @@
+
+package ru.javaops.masterjava.xml.schema;
+
+import javax.xml.bind.annotation.XmlEnum;
+import javax.xml.bind.annotation.XmlEnumValue;
+import javax.xml.bind.annotation.XmlType;
+
+
+/**
+ * Java class for flagType.
+ *
+ *
The following schema fragment specifies the expected content contained within this class.
+ *
+ *
+ * <simpleType name="flagType">
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}string">
+ * <enumeration value="active"/>
+ * <enumeration value="deleted"/>
+ * <enumeration value="superuser"/>
+ * </restriction>
+ * </simpleType>
+ *
+ *
+ */
+@XmlType(name = "flagType", namespace = "http://javaops.ru")
+@XmlEnum
+public enum FlagType {
+
+ @XmlEnumValue("active")
+ ACTIVE("active"),
+ @XmlEnumValue("deleted")
+ DELETED("deleted"),
+ @XmlEnumValue("superuser")
+ SUPERUSER("superuser");
+ private final String value;
+
+ FlagType(String v) {
+ value = v;
+ }
+
+ public String value() {
+ return value;
+ }
+
+ public static FlagType fromValue(String v) {
+ for (FlagType c: FlagType.values()) {
+ if (c.value.equals(v)) {
+ return c;
+ }
+ }
+ throw new IllegalArgumentException(v);
+ }
+
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/schema/GroupType.java b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/GroupType.java
new file mode 100644
index 000000000..d5041640b
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/GroupType.java
@@ -0,0 +1,40 @@
+
+package ru.javaops.masterjava.xml.schema;
+
+import javax.xml.bind.annotation.XmlEnum;
+import javax.xml.bind.annotation.XmlType;
+
+
+/**
+ * Java class for groupType.
+ *
+ *
The following schema fragment specifies the expected content contained within this class.
+ *
+ *
+ * <simpleType name="groupType">
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}string">
+ * <enumeration value="REGISTERING"/>
+ * <enumeration value="CURRENT"/>
+ * <enumeration value="FINISHED"/>
+ * </restriction>
+ * </simpleType>
+ *
+ *
+ */
+@XmlType(name = "groupType", namespace = "http://javaops.ru")
+@XmlEnum
+public enum GroupType {
+
+ REGISTERING,
+ CURRENT,
+ FINISHED;
+
+ public String value() {
+ return name();
+ }
+
+ public static GroupType fromValue(String v) {
+ return valueOf(v);
+ }
+
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/schema/ObjectFactory.java b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/ObjectFactory.java
new file mode 100644
index 000000000..bfb393299
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/ObjectFactory.java
@@ -0,0 +1,109 @@
+
+package ru.javaops.masterjava.xml.schema;
+
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.annotation.XmlElementDecl;
+import javax.xml.bind.annotation.XmlRegistry;
+import javax.xml.namespace.QName;
+
+
+/**
+ * This object contains factory methods for each
+ * Java content interface and Java element interface
+ * generated in the ru.javaops.masterjava.xml.schema package.
+ * An ObjectFactory allows you to programatically
+ * construct new instances of the Java representation
+ * for XML content. The Java representation of XML
+ * content can consist of schema derived interfaces
+ * and classes representing the binding of schema
+ * type definitions, element declarations and model
+ * groups. Factory methods for each of these are
+ * provided in this class.
+ *
+ */
+@XmlRegistry
+public class ObjectFactory {
+
+ private final static QName _City_QNAME = new QName("http://javaops.ru", "City");
+
+ /**
+ * Create a new ObjectFactory that can be used to create new instances of schema derived classes for package: ru.javaops.masterjava.xml.schema
+ *
+ */
+ public ObjectFactory() {
+ }
+
+ /**
+ * Create an instance of {@link Project }
+ *
+ */
+ public Project createProject() {
+ return new Project();
+ }
+
+ /**
+ * Create an instance of {@link Payload }
+ *
+ */
+ public Payload createPayload() {
+ return new Payload();
+ }
+
+ /**
+ * Create an instance of {@link Project.Group }
+ *
+ */
+ public Project.Group createProjectGroup() {
+ return new Project.Group();
+ }
+
+ /**
+ * Create an instance of {@link User }
+ *
+ */
+ public User createUser() {
+ return new User();
+ }
+
+ /**
+ * Create an instance of {@link Payload.Projects }
+ *
+ */
+ public Payload.Projects createPayloadProjects() {
+ return new Payload.Projects();
+ }
+
+ /**
+ * Create an instance of {@link Payload.Cities }
+ *
+ */
+ public Payload.Cities createPayloadCities() {
+ return new Payload.Cities();
+ }
+
+ /**
+ * Create an instance of {@link Payload.Users }
+ *
+ */
+ public Payload.Users createPayloadUsers() {
+ return new Payload.Users();
+ }
+
+ /**
+ * Create an instance of {@link CityType }
+ *
+ */
+ public CityType createCityType() {
+ return new CityType();
+ }
+
+ /**
+ * Create an instance of {@link JAXBElement }{@code <}{@link CityType }{@code >}}
+ *
+ */
+ @XmlElementDecl(namespace = "http://javaops.ru", name = "City")
+ public JAXBElement createCity(CityType value) {
+ return new JAXBElement(_City_QNAME, CityType.class, null, value);
+ }
+
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/schema/Payload.java b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/Payload.java
new file mode 100644
index 000000000..f4a8070e9
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/Payload.java
@@ -0,0 +1,332 @@
+
+package ru.javaops.masterjava.xml.schema;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+
+/**
+ * Java class for anonymous complex type.
+ *
+ *
The following schema fragment specifies the expected content contained within this class.
+ *
+ *
+ * <complexType>
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <sequence>
+ * <element name="Projects">
+ * <complexType>
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <sequence maxOccurs="unbounded">
+ * <element ref="{http://javaops.ru}Project"/>
+ * </sequence>
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ * </element>
+ * <element name="Cities">
+ * <complexType>
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <sequence maxOccurs="unbounded">
+ * <element ref="{http://javaops.ru}City"/>
+ * </sequence>
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ * </element>
+ * <element name="Users">
+ * <complexType>
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <sequence maxOccurs="unbounded" minOccurs="0">
+ * <element ref="{http://javaops.ru}User"/>
+ * </sequence>
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ * </element>
+ * </sequence>
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ *
+ *
+ *
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlType(name = "", propOrder = {
+ "projects",
+ "cities",
+ "users"
+})
+@XmlRootElement(name = "Payload", namespace = "http://javaops.ru")
+public class Payload {
+
+ @XmlElement(name = "Projects", namespace = "http://javaops.ru", required = true)
+ protected Payload.Projects projects;
+ @XmlElement(name = "Cities", namespace = "http://javaops.ru", required = true)
+ protected Payload.Cities cities;
+ @XmlElement(name = "Users", namespace = "http://javaops.ru", required = true)
+ protected Payload.Users users;
+
+ /**
+ * Gets the value of the projects property.
+ *
+ * @return
+ * possible object is
+ * {@link Payload.Projects }
+ *
+ */
+ public Payload.Projects getProjects() {
+ return projects;
+ }
+
+ /**
+ * Sets the value of the projects property.
+ *
+ * @param value
+ * allowed object is
+ * {@link Payload.Projects }
+ *
+ */
+ public void setProjects(Payload.Projects value) {
+ this.projects = value;
+ }
+
+ /**
+ * Gets the value of the cities property.
+ *
+ * @return
+ * possible object is
+ * {@link Payload.Cities }
+ *
+ */
+ public Payload.Cities getCities() {
+ return cities;
+ }
+
+ /**
+ * Sets the value of the cities property.
+ *
+ * @param value
+ * allowed object is
+ * {@link Payload.Cities }
+ *
+ */
+ public void setCities(Payload.Cities value) {
+ this.cities = value;
+ }
+
+ /**
+ * Gets the value of the users property.
+ *
+ * @return
+ * possible object is
+ * {@link Payload.Users }
+ *
+ */
+ public Payload.Users getUsers() {
+ return users;
+ }
+
+ /**
+ * Sets the value of the users property.
+ *
+ * @param value
+ * allowed object is
+ * {@link Payload.Users }
+ *
+ */
+ public void setUsers(Payload.Users value) {
+ this.users = value;
+ }
+
+
+ /**
+ * Java class for anonymous complex type.
+ *
+ *
The following schema fragment specifies the expected content contained within this class.
+ *
+ *
+ * <complexType>
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <sequence maxOccurs="unbounded">
+ * <element ref="{http://javaops.ru}City"/>
+ * </sequence>
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ *
+ *
+ *
+ */
+ @XmlAccessorType(XmlAccessType.FIELD)
+ @XmlType(name = "", propOrder = {
+ "city"
+ })
+ public static class Cities {
+
+ @XmlElement(name = "City", namespace = "http://javaops.ru", required = true)
+ protected List city;
+
+ /**
+ * Gets the value of the city property.
+ *
+ *
+ * This accessor method returns a reference to the live list,
+ * not a snapshot. Therefore any modification you make to the
+ * returned list will be present inside the JAXB object.
+ * This is why there is not a set method for the city property.
+ *
+ *
+ * For example, to add a new item, do as follows:
+ *
+ * getCity().add(newItem);
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list
+ * {@link CityType }
+ *
+ *
+ */
+ public List getCity() {
+ if (city == null) {
+ city = new ArrayList();
+ }
+ return this.city;
+ }
+
+ }
+
+
+ /**
+ * Java class for anonymous complex type.
+ *
+ *
The following schema fragment specifies the expected content contained within this class.
+ *
+ *
+ * <complexType>
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <sequence maxOccurs="unbounded">
+ * <element ref="{http://javaops.ru}Project"/>
+ * </sequence>
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ *
+ *
+ *
+ */
+ @XmlAccessorType(XmlAccessType.FIELD)
+ @XmlType(name = "", propOrder = {
+ "project"
+ })
+ public static class Projects {
+
+ @XmlElement(name = "Project", namespace = "http://javaops.ru", required = true)
+ protected List project;
+
+ /**
+ * Gets the value of the project property.
+ *
+ *
+ * This accessor method returns a reference to the live list,
+ * not a snapshot. Therefore any modification you make to the
+ * returned list will be present inside the JAXB object.
+ * This is why there is not a set method for the project property.
+ *
+ *
+ * For example, to add a new item, do as follows:
+ *
+ * getProject().add(newItem);
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list
+ * {@link Project }
+ *
+ *
+ */
+ public List getProject() {
+ if (project == null) {
+ project = new ArrayList();
+ }
+ return this.project;
+ }
+
+ }
+
+
+ /**
+ * Java class for anonymous complex type.
+ *
+ *
The following schema fragment specifies the expected content contained within this class.
+ *
+ *
+ * <complexType>
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <sequence maxOccurs="unbounded" minOccurs="0">
+ * <element ref="{http://javaops.ru}User"/>
+ * </sequence>
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ *
+ *
+ *
+ */
+ @XmlAccessorType(XmlAccessType.FIELD)
+ @XmlType(name = "", propOrder = {
+ "user"
+ })
+ public static class Users {
+
+ @XmlElement(name = "User", namespace = "http://javaops.ru")
+ protected List user;
+
+ /**
+ * Gets the value of the user property.
+ *
+ *
+ * This accessor method returns a reference to the live list,
+ * not a snapshot. Therefore any modification you make to the
+ * returned list will be present inside the JAXB object.
+ * This is why there is not a set method for the user property.
+ *
+ *
+ * For example, to add a new item, do as follows:
+ *
+ * getUser().add(newItem);
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list
+ * {@link User }
+ *
+ *
+ */
+ public List getUser() {
+ if (user == null) {
+ user = new ArrayList();
+ }
+ return this.user;
+ }
+
+ }
+
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/schema/Project.java b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/Project.java
new file mode 100644
index 000000000..7e9cd961a
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/Project.java
@@ -0,0 +1,223 @@
+
+package ru.javaops.masterjava.xml.schema;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlID;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+
+/**
+ * Java class for anonymous complex type.
+ *
+ *
The following schema fragment specifies the expected content contained within this class.
+ *
+ *
+ * <complexType>
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <sequence>
+ * <element name="description" type="{http://www.w3.org/2001/XMLSchema}string"/>
+ * <sequence maxOccurs="unbounded">
+ * <element name="Group">
+ * <complexType>
+ * <complexContent>
+ * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ * <attribute name="name" use="required" type="{http://www.w3.org/2001/XMLSchema}ID" />
+ * <attribute name="type" use="required" type="{http://javaops.ru}groupType" />
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ * </element>
+ * </sequence>
+ * </sequence>
+ * <attribute name="name" use="required" type="{http://www.w3.org/2001/XMLSchema}string" />
+ * </restriction>
+ * </complexContent>
+ * </complexType>
+ *
+ *
+ *
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlType(name = "", propOrder = {
+ "description",
+ "group"
+})
+@XmlRootElement(name = "Project", namespace = "http://javaops.ru")
+public class Project {
+
+ @XmlElement(namespace = "http://javaops.ru", required = true)
+ protected String description;
+ @XmlElement(name = "Group", namespace = "http://javaops.ru", required = true)
+ protected List group;
+ @XmlAttribute(name = "name", required = true)
+ protected String name;
+
+ /**
+ * Gets the value of the description property.
+ *
+ * @return
+ * possible object is
+ * {@link String }
+ *
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Sets the value of the description property.
+ *
+ * @param value
+ * allowed object is
+ * {@link String }
+ *
+ */
+ public void setDescription(String value) {
+ this.description = value;
+ }
+
+ /**
+ * Gets the value of the group property.
+ *
+ *
+ * This accessor method returns a reference to the live list,
+ * not a snapshot. Therefore any modification you make to the
+ * returned list will be present inside the JAXB object.
+ * This is why there is not a set method for the group property.
+ *
+ *
+ * For example, to add a new item, do as follows:
+ *
+ * getGroup().add(newItem);
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list
+ * {@link Project.Group }
+ *
+ *
+ */
+ public List getGroup() {
+ if (group == null) {
+ group = new ArrayList();
+ }
+ return this.group;
+ }
+
+ /**
+ * Gets the value of the name property.
+ *
+ * @return
+ * possible object is
+ * {@link String }
+ *
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the value of the name property.
+ *
+ * @param value
+ * allowed object is
+ * {@link String }
+ *
+ */
+ public void setName(String value) {
+ this.name = value;
+ }
+
+
+ /**
+ *