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..7eb23fd59
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/dao/GroupDao.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.Group;
+
+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);
+ }
+}
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..d5fa6676a
--- /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(index -> users.get(index).getEmail())
+ .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..fe150292e
--- /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("/apps/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..e6aa92b8b
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,25 @@
+
+ 4.0.0
+
+ ru.javaops
+ masterjava
+ pom
+
+ 1.0-SNAPSHOT
+
+ MasterJava Root
+ https://github.com/JavaOPs/masterjava
+
+
+ parent
+ parent-web
+
+ common
+ persist
+ test
+
+ web
+ services
+
+
diff --git a/services/mail-api/pom.xml b/services/mail-api/pom.xml
new file mode 100644
index 000000000..25c53388e
--- /dev/null
+++ b/services/mail-api/pom.xml
@@ -0,0 +1,26 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent
+ ../../parent/pom.xml
+ 1.0-SNAPSHOT
+
+
+ mail-api
+ 1.0-SNAPSHOT
+ Mail API
+
+
+
+ ${project.groupId}
+ common
+ ${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..f30f68fc4
--- /dev/null
+++ b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/Addressee.java
@@ -0,0 +1,17 @@
+package ru.javaops.masterjava.service.mail;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * gkislin
+ * 15.11.2016
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Addressee {
+ private String email;
+ private String name;
+}
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..2ebaf8d76
--- /dev/null
+++ b/services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailService.java
@@ -0,0 +1,21 @@
+package ru.javaops.masterjava.service.mail;
+
+import javax.jws.WebMethod;
+import javax.jws.WebParam;
+import javax.jws.WebService;
+import java.util.List;
+
+/**
+ * gkislin
+ * 15.11.2016
+ */
+@WebService
+public interface MailService {
+
+ @WebMethod
+ void sendMail(
+ @WebParam(name = "to") List to,
+ @WebParam(name = "cc") List cc,
+ @WebParam(name = "subject") String subject,
+ @WebParam(name = "body") String body);
+}
\ 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..2b4be6bad
--- /dev/null
+++ b/services/mail-service/pom.xml
@@ -0,0 +1,26 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent-web
+ ../../parent-web/pom.xml
+ 1.0-SNAPSHOT
+
+
+ mail-service
+ 1.0-SNAPSHOT
+ war
+ Mail Service
+
+
+
+ ${project.groupId}
+ mail-api
+ ${project.version}
+
+
+
\ No newline at end of file
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..a6147371a
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailSender.java
@@ -0,0 +1,16 @@
+package ru.javaops.masterjava.service.mail;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+/**
+ * gkislin
+ * 15.11.2016
+ */
+@Slf4j
+public class MailSender {
+ static void sendMail(List to, List cc, String subject, String body) {
+ log.info("Send mail to \'" + to + "\' cc \'" + cc + "\' subject \'" + subject + (log.isDebugEnabled()?"\nbody=" + body:""));
+ }
+}
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..e26f302d2
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceExecutor.java
@@ -0,0 +1,141 @@
+package ru.javaops.masterjava.service.mail;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.*;
+import java.util.stream.Collectors;
+
+public class MailServiceExecutor {
+ private static final String OK = "OK";
+
+ 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 final ExecutorService mailExecutor = Executors.newFixedThreadPool(8);
+
+ public GroupResult sendToList(final String template, final Set emails) throws Exception {
+ final CompletionService completionService = new ExecutorCompletionService<>(mailExecutor);
+
+ List> futures = emails.stream()
+ .map(email -> completionService.submit(() -> sendToUser(template, email)))
+ .collect(Collectors.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);
+ }
+ }
+/*
+ for (Future future : futures) {
+ MailResult mailResult;
+ try {
+ mailResult = future.get(10, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ return cancelWithFail(INTERRUPTED_EXCEPTION);
+ } catch (ExecutionException e) {
+ return cancelWithFail(e.getCause().toString());
+ } catch (TimeoutException e) {
+ return cancelWithFail(INTERRUPTED_BY_TIMEOUT);
+ }
+ if (mailResult.isOk()) {
+ success++;
+ } else {
+ failed.add(mailResult);
+ if (failed.size() >= 5) {
+ return cancelWithFail(INTERRUPTED_BY_FAULTS_NUMBER);
+ }
+ }
+ }
+*/
+ return new GroupResult(success, failed, null);
+ }
+
+ private GroupResult cancelWithFail(String cause) {
+ futures.forEach(f -> f.cancel(true));
+ return new GroupResult(success, failed, cause);
+ }
+ }.call();
+ }
+
+ // dummy realization
+ public MailResult sendToUser(String template, String email) throws Exception {
+ try {
+ Thread.sleep(500); //delay
+ } catch (InterruptedException e) {
+ // log cancel;
+ return null;
+ }
+ return Math.random() < 0.7 ? MailResult.ok(email) : MailResult.error(email, "Error");
+ }
+
+ public static class MailResult {
+ private final String email;
+ private final String result;
+
+ private static MailResult ok(String email) {
+ return new MailResult(email, OK);
+ }
+
+ private static MailResult error(String email, String error) {
+ return new MailResult(email, error);
+ }
+
+ public boolean isOk() {
+ return OK.equals(result);
+ }
+
+ private MailResult(String email, String cause) {
+ this.email = email;
+ this.result = cause;
+ }
+
+ @Override
+ public String toString() {
+ return '(' + email + ',' + result + ')';
+ }
+ }
+
+ public static class GroupResult {
+ private final int success; // number of successfully sent email
+ private final List failed; // failed emails with causes
+ private final String failedCause; // global fail cause
+
+ public GroupResult(int success, List failed, String failedCause) {
+ this.success = success;
+ this.failed = failed;
+ this.failedCause = failedCause;
+ }
+
+ @Override
+ public String toString() {
+ return "Success: " + success + '\n' +
+ "Failed: " + failed.toString() + '\n' +
+ (failedCause == null ? "" : "Failed cause" + failedCause);
+ }
+ }
+}
\ 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..66cd43bea
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceImpl.java
@@ -0,0 +1,15 @@
+package ru.javaops.masterjava.service.mail;
+
+import javax.jws.WebService;
+import java.util.List;
+
+/**
+ * gkislin
+ * 15.11.2016
+ */
+@WebService(endpointInterface = "ru.javaops.masterjava.service.mail.MailService")
+public class MailServiceImpl implements MailService {
+ public void sendMail(List to, List cc, String subject, String body) {
+ MailSender.sendMail(to, cc, subject, body);
+ }
+}
\ No newline at end of file
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..4ef6f6d4f
--- /dev/null
+++ b/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServiceClient.java
@@ -0,0 +1,25 @@
+package ru.javaops.masterjava.service.mail;
+
+import com.google.common.collect.ImmutableList;
+
+import javax.xml.namespace.QName;
+import javax.xml.ws.Service;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * User: gkislin
+ * Date: 28.05.2014
+ */
+public class MailServiceClient {
+
+ public static void main(String[] args) throws MalformedURLException {
+ QName qname = new QName("http://mail.service.masterjava.javaops.ru/", "MailServiceImplService");
+ Service service = Service.create(
+ new URL("http://localhost:8080/mail/mailService?wsdl"),
+ new QName("http://mail.service.masterjava.javaops.ru/", "MailServiceImplService"));
+
+ MailService mailService = service.getPort(MailService.class);
+ mailService.sendMail(ImmutableList.of(new Addressee("gkislin@yandex.ru", null)), null, "Subject", "Body");
+ }
+}
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..88de00ae3
--- /dev/null
+++ b/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServicePublisher.java
@@ -0,0 +1,14 @@
+package ru.javaops.masterjava.service.mail;
+
+import javax.xml.ws.Endpoint;
+
+/**
+ * User: gkislin
+ * Date: 28.05.2014
+ */
+public class MailServicePublisher {
+
+ public static void main(String[] args) {
+ Endpoint.publish("http://localhost:8080/mail/mailService", new MailServiceImpl());
+ }
+}
diff --git a/services/pom.xml b/services/pom.xml
new file mode 100644
index 000000000..bb7824194
--- /dev/null
+++ b/services/pom.xml
@@ -0,0 +1,15 @@
+
+ 4.0.0
+
+ ru.javaops
+ services
+ pom
+ 1.0-SNAPSHOT
+
+ MasterJava Services
+
+ mail-api
+ mail-service
+
+
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..1d7e0c298
--- /dev/null
+++ b/web/export/pom.xml
@@ -0,0 +1,49 @@
+
+
+ 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
+
+
+
+
+ ${project.groupId}
+ persist
+ ${project.version}
+ test-jar
+ test
+
+
+
+
\ 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..d503ecea2
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/export/CityImporter.java
@@ -0,0 +1,35 @@
+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 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();
+
+ while (processor.startElement("City", "Cities")) {
+ 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..6e762a431
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/export/PayloadImporter.java
@@ -0,0 +1,31 @@
+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 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 cities = cityImporter.process(processor);
+ return userImporter.process(processor, cities, chunkSize);
+ }
+}
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..dcaf97ea0
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/export/UserImporter.java
@@ -0,0 +1,103 @@
+package ru.javaops.masterjava.export;
+
+import lombok.extern.slf4j.Slf4j;
+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.model.City;
+import ru.javaops.masterjava.persist.model.User;
+import ru.javaops.masterjava.persist.model.UserFlag;
+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.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ * gkislin
+ * 14.10.2016
+ */
+@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);
+
+ public List process(StaxStreamProcessor processor, Map cities, int chunkSize) throws XMLStreamException {
+ log.info("Start proseccing with chunkSize=" + chunkSize);
+
+ 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 {
+ List futures = new ArrayList<>();
+
+ int id = userDao.getSeqAndSkip(chunkSize);
+ List chunk = new ArrayList<>(chunkSize);
+ List 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 {
+ final UserFlag flag = UserFlag.valueOf(processor.getAttribute("flag"));
+ final String fullName = processor.getReader().getElementText();
+ final User user = new User(id++, fullName, email, flag, city.getId());
+ chunk.add(user);
+ 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) {
+ ChunkFuture chunkFuture = new ChunkFuture(chunk,
+ executorService.submit(() -> userDao.insertAndGetConflictEmails(chunk))
+ );
+ 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;
+ }
+
+
+ /**
+ * 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">
+ * <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>
+ *
+ *
+ *
+ */
+ @XmlAccessorType(XmlAccessType.FIELD)
+ @XmlType(name = "")
+ public static class Group {
+
+ @XmlAttribute(name = "name", required = true)
+ @XmlJavaTypeAdapter(CollapsedStringAdapter.class)
+ @XmlID
+ @XmlSchemaType(name = "ID")
+ protected String name;
+ @XmlAttribute(name = "type", required = true)
+ protected GroupType type;
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Gets the value of the type property.
+ *
+ * @return
+ * possible object is
+ * {@link GroupType }
+ *
+ */
+ public GroupType getType() {
+ return type;
+ }
+
+ /**
+ * Sets the value of the type property.
+ *
+ * @param value
+ * allowed object is
+ * {@link GroupType }
+ *
+ */
+ public void setType(GroupType value) {
+ this.type = value;
+ }
+
+ }
+
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/schema/User.java b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/User.java
new file mode 100644
index 000000000..9f19c0ad7
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/schema/User.java
@@ -0,0 +1,182 @@
+
+package ru.javaops.masterjava.xml.schema;
+
+import javax.xml.bind.annotation.*;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Java class for anonymous complex type.
+ *
+ *
The following schema fragment specifies the expected content contained within this class.
+ *
+ *
+ * <complexType>
+ * <simpleContent>
+ * <extension base="<http://www.w3.org/2001/XMLSchema>string">
+ * <attribute name="email" type="{http://javaops.ru}emailAddressType" />
+ * <attribute name="flag" use="required" type="{http://javaops.ru}flagType" />
+ * <attribute name="city" use="required" type="{http://www.w3.org/2001/XMLSchema}IDREF" />
+ * <attribute name="groupRefs" type="{http://www.w3.org/2001/XMLSchema}IDREFS" />
+ * </extension>
+ * </simpleContent>
+ * </complexType>
+ *
+ *
+ *
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlType(name = "", propOrder = {
+ "value"
+})
+@XmlRootElement(name = "User", namespace = "http://javaops.ru")
+public class User {
+
+ @XmlValue
+ protected String value;
+ @XmlAttribute(name = "email")
+ protected String email;
+ @XmlAttribute(name = "flag", required = true)
+ protected FlagType flag;
+ @XmlAttribute(name = "city", required = true)
+ @XmlIDREF
+ @XmlSchemaType(name = "IDREF")
+ protected Object city;
+ @XmlAttribute(name = "groupRefs")
+ @XmlIDREF
+ @XmlSchemaType(name = "IDREFS")
+ protected List groupRefs;
+
+ /**
+ * 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 email property.
+ *
+ * @return
+ * possible object is
+ * {@link String }
+ *
+ */
+ public String getEmail() {
+ return email;
+ }
+
+ /**
+ * Sets the value of the email property.
+ *
+ * @param value
+ * allowed object is
+ * {@link String }
+ *
+ */
+ public void setEmail(String value) {
+ this.email = value;
+ }
+
+ /**
+ * Gets the value of the flag property.
+ *
+ * @return
+ * possible object is
+ * {@link FlagType }
+ *
+ */
+ public FlagType getFlag() {
+ return flag;
+ }
+
+ /**
+ * Sets the value of the flag property.
+ *
+ * @param value
+ * allowed object is
+ * {@link FlagType }
+ *
+ */
+ public void setFlag(FlagType value) {
+ this.flag = value;
+ }
+
+ /**
+ * Gets the value of the city property.
+ *
+ * @return
+ * possible object is
+ * {@link Object }
+ *
+ */
+ public Object getCity() {
+ return city;
+ }
+
+ /**
+ * Sets the value of the city property.
+ *
+ * @param value
+ * allowed object is
+ * {@link Object }
+ *
+ */
+ public void setCity(Object value) {
+ this.city = value;
+ }
+
+ /**
+ * Gets the value of the groupRefs 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 groupRefs property.
+ *
+ *
+ * For example, to add a new item, do as follows:
+ *
+ * getGroupRefs().add(newItem);
+ *
+ *
+ *
+ *
+ * Objects of the following type(s) are allowed in the list
+ * {@link Object }
+ *
+ *
+ */
+ public List getGroupRefs() {
+ if (groupRefs == null) {
+ groupRefs = new ArrayList();
+ }
+ return this.groupRefs;
+ }
+
+ @Override
+ public String toString() {
+ return value + '(' + email + ')';
+ }
+
+
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/util/JaxbMarshaller.java b/web/export/src/main/java/ru/javaops/masterjava/xml/util/JaxbMarshaller.java
new file mode 100644
index 000000000..7401011e9
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/util/JaxbMarshaller.java
@@ -0,0 +1,43 @@
+package ru.javaops.masterjava.xml.util;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.PropertyException;
+import javax.xml.validation.Schema;
+import java.io.StringWriter;
+import java.io.Writer;
+
+/**
+ * @author GKislin
+ * Date: 18.11.2008
+ */
+public class JaxbMarshaller {
+ private Marshaller marshaller;
+
+ public JaxbMarshaller(JAXBContext ctx) throws JAXBException {
+ marshaller = ctx.createMarshaller();
+ marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+ marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
+ marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
+ }
+
+ public void setProperty(String prop, Object value) throws PropertyException {
+ marshaller.setProperty(prop, value);
+ }
+
+ public synchronized void setSchema(Schema schema) {
+ marshaller.setSchema(schema);
+ }
+
+ public String marshal(Object instance) throws JAXBException {
+ StringWriter sw = new StringWriter();
+ marshal(instance, sw);
+ return sw.toString();
+ }
+
+ public synchronized void marshal(Object instance, Writer writer) throws JAXBException {
+ marshaller.marshal(instance, writer);
+ }
+
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/util/JaxbParser.java b/web/export/src/main/java/ru/javaops/masterjava/xml/util/JaxbParser.java
new file mode 100644
index 000000000..5f8289b4b
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/util/JaxbParser.java
@@ -0,0 +1,89 @@
+package ru.javaops.masterjava.xml.util;
+
+import org.xml.sax.SAXException;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.PropertyException;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import java.io.*;
+
+
+/**
+ * Marshalling/Unmarshalling JAXB helper
+ * XML Facade
+ * @author Grigory Kislin
+ */
+public class JaxbParser {
+
+ protected JaxbMarshaller jaxbMarshaller;
+ protected JaxbUnmarshaller jaxbUnmarshaller;
+ protected Schema schema;
+
+ public JaxbParser(Class... classesToBeBound) {
+ try {
+ init(JAXBContext.newInstance(classesToBeBound));
+ } catch (JAXBException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ // http://stackoverflow.com/questions/30643802/what-is-jaxbcontext-newinstancestring-contextpath
+ public JaxbParser(String context) {
+ try {
+ init(JAXBContext.newInstance(context));
+ } catch (JAXBException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private void init(JAXBContext ctx) throws JAXBException {
+ jaxbMarshaller = new JaxbMarshaller(ctx);
+ jaxbUnmarshaller = new JaxbUnmarshaller(ctx);
+ }
+
+ // Unmarshaller
+ public T unmarshal(InputStream is) throws JAXBException {
+ return (T) jaxbUnmarshaller.unmarshal(is);
+ }
+
+ public T unmarshal(Reader reader) throws JAXBException {
+ return (T) jaxbUnmarshaller.unmarshal(reader);
+ }
+
+ public T unmarshal(String str) throws JAXBException {
+ return (T) jaxbUnmarshaller.unmarshal(str);
+ }
+
+ // Marshaller
+ public void setMarshallerProperty(String prop, Object value) {
+ try {
+ jaxbMarshaller.setProperty(prop, value);
+ } catch (PropertyException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public String marshal(Object instance) throws JAXBException {
+ return jaxbMarshaller.marshal(instance);
+ }
+
+ public void marshal(Object instance, Writer writer) throws JAXBException {
+ jaxbMarshaller.marshal(instance, writer);
+ }
+
+ public void setSchema(Schema schema) {
+ this.schema = schema;
+ jaxbUnmarshaller.setSchema(schema);
+ jaxbMarshaller.setSchema(schema);
+ }
+
+ public void validate(String str) throws IOException, SAXException {
+ validate(new StringReader(str));
+ }
+
+ public void validate(Reader reader) throws IOException, SAXException {
+ schema.newValidator().validate(new StreamSource(reader));
+ }
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/util/JaxbUnmarshaller.java b/web/export/src/main/java/ru/javaops/masterjava/xml/util/JaxbUnmarshaller.java
new file mode 100644
index 000000000..afc34a6e4
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/util/JaxbUnmarshaller.java
@@ -0,0 +1,37 @@
+package ru.javaops.masterjava.xml.util;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.validation.Schema;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+
+/**
+ * @author GKislin
+ * Date: 18.11.2008
+ */
+public class JaxbUnmarshaller {
+ private Unmarshaller unmarshaller;
+
+ public JaxbUnmarshaller(JAXBContext ctx) throws JAXBException {
+ unmarshaller = ctx.createUnmarshaller();
+ }
+
+ public synchronized void setSchema(Schema schema) {
+ unmarshaller.setSchema(schema);
+ }
+
+ public synchronized Object unmarshal(InputStream is) throws JAXBException {
+ return unmarshaller.unmarshal(is);
+ }
+
+ public synchronized Object unmarshal(Reader reader) throws JAXBException {
+ return unmarshaller.unmarshal(reader);
+ }
+
+ public Object unmarshal(String str) throws JAXBException {
+ return unmarshal(new StringReader(str));
+ }
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/util/Schemas.java b/web/export/src/main/java/ru/javaops/masterjava/xml/util/Schemas.java
new file mode 100644
index 000000000..711e9eabe
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/util/Schemas.java
@@ -0,0 +1,51 @@
+package ru.javaops.masterjava.xml.util;
+
+import com.google.common.io.Resources;
+import org.xml.sax.SAXException;
+
+import javax.xml.XMLConstants;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import java.io.File;
+import java.io.StringReader;
+import java.net.URL;
+
+
+/**
+ * @author Grigory Kislin
+ */
+public class Schemas {
+
+ // SchemaFactory is not thread-safe
+ private static final SchemaFactory SCHEMA_FACTORY = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+
+ public static synchronized Schema ofString(String xsd) {
+ try {
+ return SCHEMA_FACTORY.newSchema(new StreamSource(new StringReader(xsd)));
+ } catch (SAXException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static synchronized Schema ofClasspath(String resource) {
+ // http://digitalsanctum.com/2012/11/30/how-to-read-file-contents-in-java-the-easy-way-with-guava/
+ return ofURL(Resources.getResource(resource));
+ }
+
+ public static synchronized Schema ofURL(URL url) {
+ try {
+ return SCHEMA_FACTORY.newSchema(url);
+ } catch (SAXException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static synchronized Schema ofFile(File file) {
+ try {
+ return SCHEMA_FACTORY.newSchema(file);
+ } catch (SAXException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/util/StaxStreamProcessor.java b/web/export/src/main/java/ru/javaops/masterjava/xml/util/StaxStreamProcessor.java
new file mode 100644
index 000000000..b93e0bc02
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/util/StaxStreamProcessor.java
@@ -0,0 +1,83 @@
+package ru.javaops.masterjava.xml.util;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.events.XMLEvent;
+import java.io.InputStream;
+
+/**
+ * gkislin
+ * 23.09.2016
+ */
+public class StaxStreamProcessor implements AutoCloseable {
+ private static final XMLInputFactory FACTORY = XMLInputFactory.newInstance();
+
+ private final XMLStreamReader reader;
+
+ public StaxStreamProcessor(InputStream is) throws XMLStreamException {
+ reader = FACTORY.createXMLStreamReader(is);
+ }
+
+ public boolean startElement(String element, String parent) throws XMLStreamException {
+ while (reader.hasNext()) {
+ int event = reader.next();
+ if (parent != null && isElementEnd(event, parent)) {
+ return false;
+ }
+ if (isElementStart(event, element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isElementStart(int event, String el) {
+ return event == XMLEvent.START_ELEMENT && el.equals(reader.getLocalName());
+ }
+
+ private boolean isElementEnd(int event, String el) {
+ return event == XMLEvent.END_ELEMENT && el.equals(reader.getLocalName());
+ }
+
+ public XMLStreamReader getReader() {
+ return reader;
+ }
+
+ public String getAttribute(String name) throws XMLStreamException {
+ return reader.getAttributeValue(null, name);
+ }
+
+ public boolean doUntil(int stopEvent, String value) throws XMLStreamException {
+ while (reader.hasNext()) {
+ int event = reader.next();
+ if (event == stopEvent && value.equals(getValue(event))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public String getValue(int event) throws XMLStreamException {
+ return (event == XMLEvent.CHARACTERS) ? reader.getText() : reader.getLocalName();
+ }
+
+ public String getElementValue(String element) throws XMLStreamException {
+ return doUntil(XMLEvent.START_ELEMENT, element) ? reader.getElementText() : null;
+ }
+
+ public String getText() throws XMLStreamException {
+ return reader.getElementText();
+ }
+
+ @Override
+ public void close() {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (XMLStreamException e) {
+ // empty
+ }
+ }
+ }
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/util/XPathProcessor.java b/web/export/src/main/java/ru/javaops/masterjava/xml/util/XPathProcessor.java
new file mode 100644
index 000000000..d6456a5de
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/util/XPathProcessor.java
@@ -0,0 +1,62 @@
+package ru.javaops.masterjava.xml.util;
+
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * gkislin
+ * 23.09.2016
+ */
+public class XPathProcessor {
+ private static final DocumentBuilderFactory DOCUMENT_FACTORY = DocumentBuilderFactory.newInstance();
+ private static final DocumentBuilder DOCUMENT_BUILDER;
+
+ private static final XPathFactory XPATH_FACTORY = XPathFactory.newInstance();
+ private static final XPath XPATH = XPATH_FACTORY.newXPath();
+
+ static {
+ DOCUMENT_FACTORY.setNamespaceAware(true);
+ try {
+ DOCUMENT_BUILDER = DOCUMENT_FACTORY.newDocumentBuilder();
+ } catch (ParserConfigurationException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private final Document doc;
+
+ public XPathProcessor(InputStream is) {
+ try {
+ doc = DOCUMENT_BUILDER.parse(is);
+ } catch (SAXException | IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static synchronized XPathExpression getExpression(String exp) {
+ try {
+ return XPATH.compile(exp);
+ } catch (XPathExpressionException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public T evaluate(XPathExpression expression, QName type) {
+ try {
+ return (T) expression.evaluate(doc, type);
+ } catch (XPathExpressionException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+}
diff --git a/web/export/src/main/java/ru/javaops/masterjava/xml/util/XsltProcessor.java b/web/export/src/main/java/ru/javaops/masterjava/xml/util/XsltProcessor.java
new file mode 100644
index 000000000..4a395ba3e
--- /dev/null
+++ b/web/export/src/main/java/ru/javaops/masterjava/xml/util/XsltProcessor.java
@@ -0,0 +1,52 @@
+package ru.javaops.masterjava.xml.util;
+
+import javax.xml.transform.*;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * User: GKislin
+ * Date: 18.05.2009
+ */
+
+public class XsltProcessor {
+ private static TransformerFactory FACTORY = TransformerFactory.newInstance();
+ private final Transformer xformer;
+
+ public XsltProcessor(InputStream xslInputStream) {
+ this(new BufferedReader(new InputStreamReader(xslInputStream, StandardCharsets.UTF_8)));
+ }
+
+ public XsltProcessor(Reader xslReader) {
+ try {
+ Templates template = FACTORY.newTemplates(new StreamSource(xslReader));
+ xformer = template.newTransformer();
+ } catch (TransformerConfigurationException e) {
+ throw new IllegalStateException("XSLT transformer creation failed: " + e.toString(), e);
+ }
+ }
+
+ public String transform(InputStream xmlInputStream) throws TransformerException {
+ StringWriter out = new StringWriter();
+ transform(xmlInputStream, out);
+ return out.getBuffer().toString();
+ }
+
+ public void transform(InputStream xmlInputStream, Writer result) throws TransformerException {
+ transform(new BufferedReader(new InputStreamReader(xmlInputStream, StandardCharsets.UTF_8)), result);
+ }
+
+ public void transform(Reader sourceReader, Writer result) throws TransformerException {
+ xformer.transform(new StreamSource(sourceReader), new StreamResult(result));
+ }
+
+ public static String getXsltHeader(String xslt) {
+ return "\n";
+ }
+
+ public void setParameter(String name, String value) {
+ xformer.setParameter(name, value);
+ }
+}
diff --git a/web/export/src/main/resources/cities.xsl b/web/export/src/main/resources/cities.xsl
new file mode 100644
index 000000000..1c509124b
--- /dev/null
+++ b/web/export/src/main/resources/cities.xsl
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/export/src/main/resources/groups.xsl b/web/export/src/main/resources/groups.xsl
new file mode 100644
index 000000000..bdc88113a
--- /dev/null
+++ b/web/export/src/main/resources/groups.xsl
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+ groups
+
+
+
+
+ groups
+
+
+
+ Group
+ Type
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/export/src/main/resources/payload.xsd b/web/export/src/main/resources/payload.xsd
new file mode 100644
index 000000000..3d545ec54
--- /dev/null
+++ b/web/export/src/main/resources/payload.xsd
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/export/src/main/webapp/WEB-INF/templates/exception.html b/web/export/src/main/webapp/WEB-INF/templates/exception.html
new file mode 100644
index 000000000..6123126d2
--- /dev/null
+++ b/web/export/src/main/webapp/WEB-INF/templates/exception.html
@@ -0,0 +1,19 @@
+
+
+
+ Export XML
+
+
+
+
+
+
+
Application error:
+
exception.message
+
+
+
+
+
\ No newline at end of file
diff --git a/web/export/src/main/webapp/WEB-INF/templates/export.html b/web/export/src/main/webapp/WEB-INF/templates/export.html
new file mode 100644
index 000000000..704600594
--- /dev/null
+++ b/web/export/src/main/webapp/WEB-INF/templates/export.html
@@ -0,0 +1,22 @@
+
+
+
+ Export XML
+
+
+
+
+
\ No newline at end of file
diff --git a/web/export/src/main/webapp/WEB-INF/templates/result.html b/web/export/src/main/webapp/WEB-INF/templates/result.html
new file mode 100644
index 000000000..e8c63fbfa
--- /dev/null
+++ b/web/export/src/main/webapp/WEB-INF/templates/result.html
@@ -0,0 +1,14 @@
+
+
+
+ Failed users
+
+
+
+Failed users
+
+
+
\ No newline at end of file
diff --git a/web/export/src/test/java/ru/javaops/masterjava/MainXml.java b/web/export/src/test/java/ru/javaops/masterjava/MainXml.java
new file mode 100644
index 000000000..3f91cfe3e
--- /dev/null
+++ b/web/export/src/test/java/ru/javaops/masterjava/MainXml.java
@@ -0,0 +1,139 @@
+package ru.javaops.masterjava;
+
+import com.google.common.base.Splitter;
+import com.google.common.io.Resources;
+import j2html.tags.ContainerTag;
+import one.util.streamex.StreamEx;
+import ru.javaops.masterjava.xml.schema.ObjectFactory;
+import ru.javaops.masterjava.xml.schema.Payload;
+import ru.javaops.masterjava.xml.schema.Project;
+import ru.javaops.masterjava.xml.schema.User;
+import ru.javaops.masterjava.xml.util.JaxbParser;
+import ru.javaops.masterjava.xml.util.Schemas;
+import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
+import ru.javaops.masterjava.xml.util.XsltProcessor;
+
+import javax.xml.stream.events.XMLEvent;
+import java.io.InputStream;
+import java.io.Writer;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.google.common.base.Strings.nullToEmpty;
+import static j2html.TagCreator.*;
+
+
+/**
+ * User: gkislin
+ */
+public class MainXml {
+ private static final Comparator USER_COMPARATOR = Comparator.comparing(User::getValue).thenComparing(User::getEmail);
+
+ public static void main(String[] args) throws Exception {
+ if (args.length != 1) {
+ System.out.println("Required argument: projectName");
+ System.exit(1);
+ }
+ String projectName = args[0];
+ URL payloadUrl = Resources.getResource("payload.xml");
+
+ Set users = parseByJaxb(projectName, payloadUrl);
+ users.forEach(System.out::println);
+
+ String html = toHtml(users, projectName);
+ System.out.println(html);
+ try (Writer writer = Files.newBufferedWriter(Paths.get("out/users.html"))) {
+ writer.write(html);
+ }
+
+ System.out.println();
+ users = processByStax(projectName, payloadUrl);
+ users.forEach(System.out::println);
+
+ html = transform(projectName, payloadUrl);
+ try (Writer writer = Files.newBufferedWriter(Paths.get("out/groups.html"))) {
+ writer.write(html);
+ }
+ }
+
+ private static Set parseByJaxb(String projectName, URL payloadUrl) throws Exception {
+ JaxbParser parser = new JaxbParser(ObjectFactory.class);
+ parser.setSchema(Schemas.ofClasspath("payload.xsd"));
+ Payload payload;
+ try (InputStream is = payloadUrl.openStream()) {
+ payload = parser.unmarshal(is);
+ }
+
+ Project project = StreamEx.of(payload.getProjects().getProject())
+ .filter(p -> p.getName().equals(projectName))
+ .findAny()
+ .orElseThrow(() -> new IllegalArgumentException("Invalid project name '" + projectName + '\''));
+
+ final Set groups = new HashSet<>(project.getGroup()); // identity compare
+ return StreamEx.of(payload.getUsers().getUser())
+ .filter(u -> !Collections.disjoint(groups, u.getGroupRefs()))
+ .collect(Collectors.toCollection(() -> new TreeSet<>(USER_COMPARATOR)));
+ }
+
+ private static Set processByStax(String projectName, URL payloadUrl) throws Exception {
+
+ try (InputStream is = payloadUrl.openStream()) {
+ StaxStreamProcessor processor = new StaxStreamProcessor(is);
+ final Set groupNames = new HashSet<>();
+
+ while (processor.startElement("Project", "Projects")) {
+ if (projectName.equals(processor.getAttribute("name"))) {
+ while (processor.startElement("Group", "Project")) {
+ groupNames.add(processor.getAttribute("name"));
+ }
+ break;
+ }
+ }
+
+ if (groupNames.isEmpty()) {
+ throw new IllegalArgumentException("Invalid " + projectName + " or no groups");
+ }
+
+ // Users loop
+ Set users = new TreeSet<>(USER_COMPARATOR);
+
+ while (processor.startElement("User", null)) {
+ String groupRefs = processor.getAttribute("groupRefs");
+ if (!Collections.disjoint(groupNames, Splitter.on(' ').splitToList(nullToEmpty(groupRefs)))) {
+ User user = new User();
+ user.setEmail(processor.getAttribute("email"));
+ user.setValue(processor.getText());
+ users.add(user);
+ }
+ }
+ return users;
+ }
+ }
+
+ private static String toHtml(Set users, String projectName) {
+ final ContainerTag table = table().with(
+ tr().with(th("FullName"), th("email")))
+ .attr("border", "1")
+ .attr("cellpadding", "8")
+ .attr("cellspacing", "0");
+
+ users.forEach(u -> table.with(tr().with(td(u.getValue()), td(u.getEmail()))));
+
+ return html().with(
+ head().with(title(projectName + " users")),
+ body().with(h1(projectName + " users"), table)
+ ).render();
+ }
+
+ private static String transform(String projectName, URL payloadUrl) throws Exception {
+ URL xsl = Resources.getResource("groups.xsl");
+ try (InputStream xmlStream = payloadUrl.openStream(); InputStream xslStream = xsl.openStream()) {
+ XsltProcessor processor = new XsltProcessor(xslStream);
+ processor.setParameter("projectName", projectName);
+ return processor.transform(xmlStream);
+ }
+ }
+}
diff --git a/web/export/src/test/java/ru/javaops/masterjava/export/CityImporterTest.java b/web/export/src/test/java/ru/javaops/masterjava/export/CityImporterTest.java
new file mode 100644
index 000000000..4376358a4
--- /dev/null
+++ b/web/export/src/test/java/ru/javaops/masterjava/export/CityImporterTest.java
@@ -0,0 +1,28 @@
+package ru.javaops.masterjava.export;
+
+import com.google.common.io.Resources;
+import org.junit.Test;
+import ru.javaops.masterjava.persist.DBITestProvider;
+import ru.javaops.masterjava.persist.model.City;
+import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
+
+import java.util.Map;
+
+/**
+ * gkislin
+ * 04.09.2017
+ */
+public class CityImporterTest {
+ static {
+ DBITestProvider.initDBI();
+ }
+
+ @Test
+ public void process() throws Exception {
+ try (StaxStreamProcessor processor =
+ new StaxStreamProcessor(Resources.getResource("payload.xml").openStream())) {
+ Map cityMap = new CityImporter().process(processor);
+ System.out.println(cityMap);
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/export/src/test/java/ru/javaops/masterjava/xml/util/JaxbParserTest.java b/web/export/src/test/java/ru/javaops/masterjava/xml/util/JaxbParserTest.java
new file mode 100644
index 000000000..ab1e365e5
--- /dev/null
+++ b/web/export/src/test/java/ru/javaops/masterjava/xml/util/JaxbParserTest.java
@@ -0,0 +1,44 @@
+package ru.javaops.masterjava.xml.util;
+
+import com.google.common.io.Resources;
+import org.junit.Test;
+import ru.javaops.masterjava.xml.schema.CityType;
+import ru.javaops.masterjava.xml.schema.ObjectFactory;
+import ru.javaops.masterjava.xml.schema.Payload;
+
+import javax.xml.bind.JAXBElement;
+import javax.xml.namespace.QName;
+
+/**
+ * gkislin
+ * 23.09.2016
+ */
+public class JaxbParserTest {
+ private static final JaxbParser JAXB_PARSER = new JaxbParser(ObjectFactory.class);
+
+ static {
+ JAXB_PARSER.setSchema(Schemas.ofClasspath("payload.xsd"));
+ }
+
+ @Test
+ public void testPayload() throws Exception {
+// JaxbParserTest.class.getResourceAsStream("/city.xml")
+ Payload payload = JAXB_PARSER.unmarshal(
+ Resources.getResource("payload.xml").openStream());
+ String strPayload = JAXB_PARSER.marshal(payload);
+ JAXB_PARSER.validate(strPayload);
+ System.out.println(strPayload);
+ }
+
+ @Test
+ public void testCity() throws Exception {
+ JAXBElement cityElement = JAXB_PARSER.unmarshal(
+ Resources.getResource("city.xml").openStream());
+ CityType city = cityElement.getValue();
+ JAXBElement cityElement2 =
+ new JAXBElement<>(new QName("http://javaops.ru", "City"), CityType.class, city);
+ String strCity = JAXB_PARSER.marshal(cityElement2);
+ JAXB_PARSER.validate(strCity);
+ System.out.println(strCity);
+ }
+}
\ No newline at end of file
diff --git a/web/export/src/test/java/ru/javaops/masterjava/xml/util/StaxStreamProcessorTest.java b/web/export/src/test/java/ru/javaops/masterjava/xml/util/StaxStreamProcessorTest.java
new file mode 100644
index 000000000..229e2da64
--- /dev/null
+++ b/web/export/src/test/java/ru/javaops/masterjava/xml/util/StaxStreamProcessorTest.java
@@ -0,0 +1,40 @@
+package ru.javaops.masterjava.xml.util;
+
+import com.google.common.io.Resources;
+import org.junit.Test;
+
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.events.XMLEvent;
+
+/**
+ * gkislin
+ * 23.09.2016
+ */
+public class StaxStreamProcessorTest {
+ @Test
+ public void readCities() throws Exception {
+ try (StaxStreamProcessor processor =
+ new StaxStreamProcessor(Resources.getResource("payload.xml").openStream())) {
+ XMLStreamReader reader = processor.getReader();
+ while (reader.hasNext()) {
+ int event = reader.next();
+ if (event == XMLEvent.START_ELEMENT) {
+ if ("City".equals(reader.getLocalName())) {
+ System.out.println(reader.getElementText());
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ public void readCities2() throws Exception {
+ try (StaxStreamProcessor processor =
+ new StaxStreamProcessor(Resources.getResource("payload.xml").openStream())) {
+ String city;
+ while ((city = processor.getElementValue("City")) != null) {
+ System.out.println(city);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/export/src/test/java/ru/javaops/masterjava/xml/util/XPathProcessorTest.java b/web/export/src/test/java/ru/javaops/masterjava/xml/util/XPathProcessorTest.java
new file mode 100644
index 000000000..18b6efdc2
--- /dev/null
+++ b/web/export/src/test/java/ru/javaops/masterjava/xml/util/XPathProcessorTest.java
@@ -0,0 +1,30 @@
+package ru.javaops.masterjava.xml.util;
+
+import com.google.common.io.Resources;
+import org.junit.Test;
+import org.w3c.dom.NodeList;
+
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import java.io.InputStream;
+import java.util.stream.IntStream;
+
+/**
+ * gkislin
+ * 23.09.2016
+ */
+public class XPathProcessorTest {
+ @Test
+ public void getCities() throws Exception {
+ try (InputStream is =
+ Resources.getResource("payload.xml").openStream()) {
+ XPathProcessor processor = new XPathProcessor(is);
+ XPathExpression expression =
+ XPathProcessor.getExpression("/*[name()='Payload']/*[name()='Cities']/*[name()='City']/text()");
+ NodeList nodes = processor.evaluate(expression, XPathConstants.NODESET);
+ IntStream.range(0, nodes.getLength()).forEach(
+ i -> System.out.println(nodes.item(i).getNodeValue())
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/export/src/test/java/ru/javaops/masterjava/xml/util/XsltProcessorTest.java b/web/export/src/test/java/ru/javaops/masterjava/xml/util/XsltProcessorTest.java
new file mode 100644
index 000000000..403a14c87
--- /dev/null
+++ b/web/export/src/test/java/ru/javaops/masterjava/xml/util/XsltProcessorTest.java
@@ -0,0 +1,22 @@
+package ru.javaops.masterjava.xml.util;
+
+import com.google.common.io.Resources;
+import org.junit.Test;
+
+import java.io.InputStream;
+
+/**
+ * gkislin
+ * 23.09.2016
+ */
+public class XsltProcessorTest {
+ @Test
+ public void transform() throws Exception {
+ try (InputStream xslInputStream = Resources.getResource("cities.xsl").openStream();
+ InputStream xmlInputStream = Resources.getResource("payload.xml").openStream()) {
+
+ XsltProcessor processor = new XsltProcessor(xslInputStream);
+ System.out.println(processor.transform(xmlInputStream));
+ }
+ }
+}
diff --git a/web/export/src/test/resources/city.xml b/web/export/src/test/resources/city.xml
new file mode 100644
index 000000000..8b0abcf8a
--- /dev/null
+++ b/web/export/src/test/resources/city.xml
@@ -0,0 +1,4 @@
+Санкт-Петербург
+
\ No newline at end of file
diff --git a/web/export/src/test/resources/payload.xml b/web/export/src/test/resources/payload.xml
new file mode 100644
index 000000000..fadf8c64a
--- /dev/null
+++ b/web/export/src/test/resources/payload.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ Topjava
+
+
+
+
+
+ Masterjava
+
+
+
+
+ Санкт-Петербург
+ Москва
+ Киев
+ Минск
+
+
+ Full Name
+ Admin
+ Deleted
+ User1
+ User2
+ User3
+
+
\ No newline at end of file
diff --git a/web/pom.xml b/web/pom.xml
new file mode 100644
index 000000000..bf9ef21c3
--- /dev/null
+++ b/web/pom.xml
@@ -0,0 +1,16 @@
+
+ 4.0.0
+
+ ru.javaops
+ web
+ pom
+ 1.0-SNAPSHOT
+ MasterJava Web
+
+
+ common-web
+ export
+ webapp
+
+
diff --git a/web/webapp/pom.xml b/web/webapp/pom.xml
new file mode 100644
index 000000000..de3a56fc2
--- /dev/null
+++ b/web/webapp/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent-web
+ ../../parent-web/pom.xml
+ 1.0-SNAPSHOT
+
+
+ webapp
+ 1.0-SNAPSHOT
+ war
+ WebApp
+
+
+ webapp
+
+
+
+
+ ${project.groupId}
+ persist
+ ${project.version}
+
+
+
\ No newline at end of file
diff --git a/web/webapp/src/main/java/ru/javaops/masterjava/webapp/UsersServlet.java b/web/webapp/src/main/java/ru/javaops/masterjava/webapp/UsersServlet.java
new file mode 100644
index 000000000..5b587128f
--- /dev/null
+++ b/web/webapp/src/main/java/ru/javaops/masterjava/webapp/UsersServlet.java
@@ -0,0 +1,27 @@
+package ru.javaops.masterjava.webapp;
+
+import com.google.common.collect.ImmutableMap;
+import org.thymeleaf.context.WebContext;
+import ru.javaops.masterjava.persist.DBIProvider;
+import ru.javaops.masterjava.persist.dao.UserDao;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static ru.javaops.masterjava.common.web.ThymeleafListener.engine;
+
+@WebServlet("")
+public class UsersServlet extends HttpServlet {
+ private UserDao userDao = DBIProvider.getDao(UserDao.class);
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ final WebContext webContext = new WebContext(req, resp, req.getServletContext(), req.getLocale(),
+ ImmutableMap.of("users", userDao.getWithLimit(20)));
+ engine.process("users", webContext, resp.getWriter());
+ }
+}
diff --git a/web/webapp/src/main/webapp/WEB-INF/templates/users.html b/web/webapp/src/main/webapp/WEB-INF/templates/users.html
new file mode 100644
index 000000000..3991c8d07
--- /dev/null
+++ b/web/webapp/src/main/webapp/WEB-INF/templates/users.html
@@ -0,0 +1,27 @@
+
+
+
+ Users
+
+
+
+
+
+
+ Full Name
+ Email
+ Flag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file