() {
+- private final int[] tempColumn = new int[matrixSize];
++ new ForkJoinPool(threadNumber).submit(
++ () -> IntStream.range(0, matrixSize)
++ .parallel()
++ .forEach(row -> {
++ final int[] rowA = matrixA[row];
++ final int[] rowC = matrixC[row];
+
+- @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];
++ 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];
++ }
+ }
+- matrixC[j][i] = sum;
+- }
+- return null;
+- }
+- })
+- .collect(Collectors.toList());
++ })).get();
+
+- executor.invokeAll(tasks);
+ return matrixC;
+ }
+
+- public static int[][] concurrentMultiply2(int[][] matrixA, int[][] matrixB, ExecutorService executor) throws InterruptedException, ExecutionException {
++ public static int[][] concurrentMultiply(int[][] matrixA, int[][] matrixB, ExecutorService executor) throws InterruptedException, ExecutionException {
+ final int matrixSize = matrixA.length;
+ final int[][] matrixC = new int[matrixSize][];
+
+@@ -161,7 +66,30 @@
+ return matrixC;
+ }
+
+- // Optimized by https://habrahabr.ru/post/114797/
++ public static int[][] concurrentMultiply2(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];
+@@ -183,6 +111,25 @@
+ }
+ return matrixC;
+ }
++
++ public static int[][] singleThreadMultiplyOpt2(int[][] matrixA, int[][] matrixB) {
++ final int matrixSize = matrixA.length;
++ final int[][] matrixC = new int[matrixSize][matrixSize];
++
++ for (int row = 0; row < matrixSize; row++) {
++ final int[] rowA = matrixA[row];
++ final int[] rowC = matrixC[row];
++
++ 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];
++ }
++ }
++ }
++ return matrixC;
++ }
+
+ public static int[][] create(int size) {
+ int[][] matrix = new int[size][size];
+Index: src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java (date 1508268723000)
++++ src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java (revision )
+@@ -4,10 +4,6 @@
+ 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;
+@@ -30,7 +26,7 @@
+ singleThreadSum += duration;
+
+ start = System.currentTimeMillis();
+- final int[][] concurrentMatrixC = MatrixUtil.concurrentMultiply(matrixA, matrixB, executor);
++ final int[][] concurrentMatrixC = MatrixUtil.concurrentMultiplyStreams(matrixA, matrixB, Runtime.getRuntime().availableProcessors() - 1);
+ duration = (System.currentTimeMillis() - start) / 1000.;
+ out("Concurrent thread time, sec: %.3f", duration);
+ concurrentThreadSum += duration;
diff --git a/patch/Lesson2/2_04_JMH_Benchmark.patch b/patch/Lesson2/2_04_JMH_Benchmark.patch
new file mode 100644
index 000000000..d5bd75f16
--- /dev/null
+++ b/patch/Lesson2/2_04_JMH_Benchmark.patch
@@ -0,0 +1,113 @@
+Index: src/main/java/ru/javaops/masterjava/matrix/MatrixBenchmark.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/matrix/MatrixBenchmark.java (revision )
++++ src/main/java/ru/javaops/masterjava/matrix/MatrixBenchmark.java (revision )
+@@ -0,0 +1,70 @@
++package ru.javaops.masterjava.matrix;
++
++import org.openjdk.jmh.annotations.*;
++
++import java.util.concurrent.ExecutorService;
++import java.util.concurrent.Executors;
++import java.util.concurrent.TimeUnit;
++
++@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
++ private static final int MATRIX_SIZE = 1000;
++
++ @Param({"3", "4", "10"})
++ private int threadNumber;
++
++ private static int[][] matrixA;
++ private static int[][] matrixB;
++
++ @Setup
++ public void setUp() {
++ matrixA = MatrixUtil.create(MATRIX_SIZE);
++ matrixB = MatrixUtil.create(MATRIX_SIZE);
++ }
++
++ private ExecutorService executor;
++
++ // @Benchmark
++ public int[][] singleThreadMultiplyOpt() throws Exception {
++ return MatrixUtil.singleThreadMultiplyOpt(matrixA, matrixB);
++ }
++
++ // @Benchmark
++ public int[][] singleThreadMultiplyOpt2() throws Exception {
++ return MatrixUtil.singleThreadMultiplyOpt(matrixA, matrixB);
++ }
++
++ @Benchmark
++ public int[][] concurrentMultiplyStreams() throws Exception {
++ return MatrixUtil.concurrentMultiplyStreams(matrixA, matrixB, threadNumber);
++ }
++
++ // @Benchmark
++ public int[][] concurrentMultiply() throws Exception {
++ return MatrixUtil.concurrentMultiply(matrixA, matrixB, executor);
++ }
++
++ @Benchmark
++ public int[][] concurrentMultiply2() throws Exception {
++ return MatrixUtil.concurrentMultiply2(matrixA, matrixB, executor);
++ }
++
++ @Setup
++ public void setup() {
++ executor = Executors.newFixedThreadPool(threadNumber);
++ }
++
++ @TearDown
++ public void tearDown() {
++ executor.shutdown();
++ }
++}
+\ No newline at end of file
+Index: pom.xml
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- pom.xml (date 1508790464000)
++++ pom.xml (revision )
+@@ -24,7 +24,7 @@
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+- 3.1
++ 3.7.0
+
+ ${java.version}
+ ${java.version}
+@@ -34,6 +34,17 @@
+
+
+
++
++ org.openjdk.jmh
++ jmh-core
++ RELEASE
++
++
++ org.openjdk.jmh
++ jmh-generator-annprocess
++ RELEASE
++ provided
++
+
+
+
diff --git a/patch/Lesson2/2_05_JMH_main_jar.patch b/patch/Lesson2/2_05_JMH_main_jar.patch
new file mode 100644
index 000000000..1ad9083f5
--- /dev/null
+++ b/patch/Lesson2/2_05_JMH_main_jar.patch
@@ -0,0 +1,85 @@
+Index: src/main/java/ru/javaops/masterjava/matrix/MatrixBenchmark.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/matrix/MatrixBenchmark.java (date 1508792580000)
++++ src/main/java/ru/javaops/masterjava/matrix/MatrixBenchmark.java (revision )
+@@ -1,6 +1,11 @@
+ 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;
+@@ -33,6 +38,16 @@
+
+ private ExecutorService executor;
+
++ 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);
+Index: pom.xml
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- pom.xml (date 1508792580000)
++++ pom.xml (revision )
+@@ -30,6 +30,41 @@
+ ${java.version}
+
+
++
++ org.apache.maven.plugins
++ maven-shade-plugin
++ 3.1.0
++
++
++ package
++
++ shade
++
++
++ benchmarks
++
++
++ org.openjdk.jmh.Main
++
++
++
++
++
++ *:*
++
++ META-INF/*.SF
++ META-INF/*.DSA
++ META-INF/*.RSA
++
++
++
++
++
++
++
+
+
+
diff --git a/patch/Lesson2/2_06_xml_scheme.patch b/patch/Lesson2/2_06_xml_scheme.patch
new file mode 100644
index 000000000..9650668ee
--- /dev/null
+++ b/patch/Lesson2/2_06_xml_scheme.patch
@@ -0,0 +1,754 @@
+Index: src/main/java/ru/javaops/masterjava/xml/schema/ObjectFactory.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/schema/ObjectFactory.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/schema/ObjectFactory.java (revision )
+@@ -0,0 +1,85 @@
++
++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 Payload }
++ *
++ */
++ public Payload createPayload() {
++ return new Payload();
++ }
++
++ /**
++ * Create an instance of {@link User }
++ *
++ */
++ public User createUser() {
++ return new User();
++ }
++
++ /**
++ * 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);
++ }
++
++}
+Index: src/main/java/ru/javaops/masterjava/xml/schema/Payload.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/schema/Payload.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/schema/Payload.java (revision )
+@@ -0,0 +1,233 @@
++
++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">
++ * <all>
++ * <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>
++ * </all>
++ * </restriction>
++ * </complexContent>
++ * </complexType>
++ *
++ *
++ *
++ */
++@XmlAccessorType(XmlAccessType.FIELD)
++@XmlType(name = "", propOrder = {
++
++})
++@XmlRootElement(name = "Payload", namespace = "http://javaops.ru")
++public class Payload {
++
++ @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 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" 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;
++ }
++
++ }
++
++}
+Index: src/main/java/ru/javaops/masterjava/xml/schema/User.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/schema/User.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/schema/User.java (revision )
+@@ -0,0 +1,151 @@
++
++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.XmlElement;
++import javax.xml.bind.annotation.XmlIDREF;
++import javax.xml.bind.annotation.XmlRootElement;
++import javax.xml.bind.annotation.XmlSchemaType;
++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="email" type="{http://www.w3.org/2001/XMLSchema}string"/>
++ * <element name="fullName" type="{http://www.w3.org/2001/XMLSchema}string"/>
++ * </sequence>
++ * <attribute name="flag" use="required" type="{http://javaops.ru}flagType" />
++ * <attribute name="city" use="required" type="{http://www.w3.org/2001/XMLSchema}IDREF" />
++ * </restriction>
++ * </complexContent>
++ * </complexType>
++ *
++ *
++ *
++ */
++@XmlAccessorType(XmlAccessType.FIELD)
++@XmlType(name = "", propOrder = {
++ "email",
++ "fullName"
++})
++@XmlRootElement(name = "User", namespace = "http://javaops.ru")
++public class User {
++
++ @XmlElement(namespace = "http://javaops.ru", required = true)
++ protected String email;
++ @XmlElement(namespace = "http://javaops.ru", required = true)
++ protected String fullName;
++ @XmlAttribute(name = "flag", required = true)
++ protected FlagType flag;
++ @XmlAttribute(name = "city", required = true)
++ @XmlIDREF
++ @XmlSchemaType(name = "IDREF")
++ protected Object city;
++
++ /**
++ * 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 fullName property.
++ *
++ * @return
++ * possible object is
++ * {@link String }
++ *
++ */
++ public String getFullName() {
++ return fullName;
++ }
++
++ /**
++ * Sets the value of the fullName property.
++ *
++ * @param value
++ * allowed object is
++ * {@link String }
++ *
++ */
++ public void setFullName(String value) {
++ this.fullName = 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;
++ }
++
++}
+Index: src/main/java/ru/javaops/masterjava/xml/schema/FlagType.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/schema/FlagType.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/schema/FlagType.java (revision )
+@@ -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);
++ }
++
++}
+Index: src/test/resources/payload.xml
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/test/resources/payload.xml (revision )
++++ src/test/resources/payload.xml (revision )
+@@ -0,0 +1,23 @@
++
++
++
++ gmail@gmail.com
++ Full Name
++
++
++ admin@javaops.ru
++ Admin
++
++
++ mail@yandex.ru
++ Deleted
++
++
++
++ Санкт-Петербург
++ Киев
++ Минск
++
++
+\ No newline at end of file
+Index: src/main/java/ru/javaops/masterjava/xml/schema/CityType.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/schema/CityType.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/schema/CityType.java (revision )
+@@ -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;
++ }
++
++}
+Index: src/main/resources/payload.xsd
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/resources/payload.xsd (revision )
++++ src/main/resources/payload.xsd (revision )
+@@ -0,0 +1,56 @@
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
++
+\ No newline at end of file
diff --git a/patch/Lesson2/2_07_JAXB.patch b/patch/Lesson2/2_07_JAXB.patch
new file mode 100644
index 000000000..5dfafecc9
--- /dev/null
+++ b/patch/Lesson2/2_07_JAXB.patch
@@ -0,0 +1,342 @@
+Index: src/main/java/ru/javaops/masterjava/xml/util/JaxbMarshaller.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/util/JaxbMarshaller.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/util/JaxbMarshaller.java (revision )
+@@ -0,0 +1,39 @@
++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;
++
++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);
++ }
++
++}
+Index: src/main/java/ru/javaops/masterjava/xml/util/Schemas.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/util/Schemas.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/util/Schemas.java (revision )
+@@ -0,0 +1,48 @@
++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;
++
++
++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);
++ }
++ }
++}
+Index: src/test/java/ru/javaops/masterjava/xml/util/JaxbParserTest.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/test/java/ru/javaops/masterjava/xml/util/JaxbParserTest.java (revision )
++++ src/test/java/ru/javaops/masterjava/xml/util/JaxbParserTest.java (revision )
+@@ -0,0 +1,40 @@
++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;
++
++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
+Index: src/main/java/ru/javaops/masterjava/xml/util/JaxbParser.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/util/JaxbParser.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/util/JaxbParser.java (revision )
+@@ -0,0 +1,88 @@
++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
++ */
++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));
++ }
++}
+Index: src/main/java/ru/javaops/masterjava/xml/util/JaxbUnmarshaller.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/util/JaxbUnmarshaller.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/util/JaxbUnmarshaller.java (revision )
+@@ -0,0 +1,33 @@
++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;
++
++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));
++ }
++}
+Index: src/test/resources/city.xml
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/test/resources/city.xml (revision )
++++ src/test/resources/city.xml (revision )
+@@ -0,0 +1,4 @@
++Санкт-Петербург
++
+\ No newline at end of file
+Index: pom.xml
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- pom.xml (date 1508793189000)
++++ pom.xml (revision )
+@@ -32,6 +32,14 @@
+
+
+ org.apache.maven.plugins
++ maven-surefire-plugin
++ 2.20.1
++
++ -Dfile.encoding=UTF-8
++
++
++
++ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.1.0
+
+@@ -79,6 +87,17 @@
+ jmh-generator-annprocess
+ RELEASE
+ provided
++
++
++ com.google.guava
++ guava
++ 21.0
++
++
++ junit
++ junit
++ 4.12
++ test
+
+
+
diff --git a/patch/Lesson2/2_08_StAX.patch b/patch/Lesson2/2_08_StAX.patch
new file mode 100644
index 000000000..228cca7b1
--- /dev/null
+++ b/patch/Lesson2/2_08_StAX.patch
@@ -0,0 +1,109 @@
+Index: src/main/java/ru/javaops/masterjava/xml/util/StaxStreamProcessor.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/util/StaxStreamProcessor.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/util/StaxStreamProcessor.java (revision )
+@@ -0,0 +1,56 @@
++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;
++
++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 XMLStreamReader getReader() {
++ return reader;
++ }
++
++ public boolean doUntil(int stopEvent, String value) throws XMLStreamException {
++ while (reader.hasNext()) {
++ int event = reader.next();
++ if (event == stopEvent) {
++ if (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
++ }
++ }
++ }
++}
+Index: src/test/java/ru/javaops/masterjava/xml/util/StaxStreamProcessorTest.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/test/java/ru/javaops/masterjava/xml/util/StaxStreamProcessorTest.java (revision )
++++ src/test/java/ru/javaops/masterjava/xml/util/StaxStreamProcessorTest.java (revision )
+@@ -0,0 +1,36 @@
++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;
++
++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/patch/Lesson2/2_09_XPath.patch b/patch/Lesson2/2_09_XPath.patch
new file mode 100644
index 000000000..f662f3b82
--- /dev/null
+++ b/patch/Lesson2/2_09_XPath.patch
@@ -0,0 +1,101 @@
+Index: src/main/java/ru/javaops/masterjava/xml/util/XPathProcessor.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/util/XPathProcessor.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/util/XPathProcessor.java (revision )
+@@ -0,0 +1,58 @@
++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;
++
++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);
++ }
++ }
++}
+Index: src/test/java/ru/javaops/masterjava/xml/util/XPathProcessorTest.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/test/java/ru/javaops/masterjava/xml/util/XPathProcessorTest.java (revision )
++++ src/test/java/ru/javaops/masterjava/xml/util/XPathProcessorTest.java (revision )
+@@ -0,0 +1,26 @@
++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;
++
++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/patch/Lesson2/2_10_Xslt.patch b/patch/Lesson2/2_10_Xslt.patch
new file mode 100644
index 000000000..fceab9e55
--- /dev/null
+++ b/patch/Lesson2/2_10_Xslt.patch
@@ -0,0 +1,95 @@
+Index: src/main/java/ru/javaops/masterjava/xml/util/XsltProcessor.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/java/ru/javaops/masterjava/xml/util/XsltProcessor.java (revision )
++++ src/main/java/ru/javaops/masterjava/xml/util/XsltProcessor.java (revision )
+@@ -0,0 +1,43 @@
++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;
++
++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";
++ }
++}
+Index: src/test/java/ru/javaops/masterjava/xml/util/XsltProcessorTest.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/test/java/ru/javaops/masterjava/xml/util/XsltProcessorTest.java (revision )
++++ src/test/java/ru/javaops/masterjava/xml/util/XsltProcessorTest.java (revision )
+@@ -0,0 +1,18 @@
++package ru.javaops.masterjava.xml.util;
++
++import com.google.common.io.Resources;
++import org.junit.Test;
++
++import java.io.InputStream;
++
++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));
++ }
++ }
++}
+Index: src/main/resources/cities.xsl
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- src/main/resources/cities.xsl (revision )
++++ src/main/resources/cities.xsl (revision )
+@@ -0,0 +1,9 @@
++
++
++
++
++
++
++
++
++
+\ No newline at end of file
diff --git a/patch/Lesson6/6_1_HW5_model_sql.patch b/patch/Lesson6/6_1_HW5_model_sql.patch
new file mode 100644
index 000000000..5b4debf9f
--- /dev/null
+++ b/patch/Lesson6/6_1_HW5_model_sql.patch
@@ -0,0 +1,378 @@
+Index: persist/src/main/java/ru/javaops/masterjava/persist/model/RefEntity.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/model/RefEntity.java (revision )
++++ persist/src/main/java/ru/javaops/masterjava/persist/model/RefEntity.java (revision )
+@@ -0,0 +1,12 @@
++package ru.javaops.masterjava.persist.model;
++
++import lombok.*;
++
++@NoArgsConstructor
++@AllArgsConstructor
++@ToString
++@EqualsAndHashCode
++abstract public class RefEntity {
++ @Getter
++ @NonNull private String ref;
++}
+Index: persist/src/test/java/ru/javaops/masterjava/persist/UserTestData.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/test/java/ru/javaops/masterjava/persist/UserTestData.java (revision 7d61ae323c8f4b6707bc53ec46e4d50ca23c451c)
++++ persist/src/test/java/ru/javaops/masterjava/persist/UserTestData.java (revision )
+@@ -3,7 +3,7 @@
+ 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 ru.javaops.masterjava.persist.model.type.UserFlag;
+
+ import java.util.List;
+
+@@ -17,12 +17,12 @@
+ public static List FIST5_USERS;
+
+ public static void init() {
+- ADMIN = new User("Admin", "admin@javaops.ru", UserFlag.superuser);
+- DELETED = new User("Deleted", "deleted@yandex.ru", UserFlag.deleted);
+- FULL_NAME = new User("Full Name", "gmail@gmail.com", UserFlag.active);
+- USER1 = new User("User1", "user1@gmail.com", UserFlag.active);
+- USER2 = new User("User2", "user2@yandex.ru", UserFlag.active);
+- USER3 = new User("User3", "user3@yandex.ru", UserFlag.active);
++ ADMIN = new User("Admin", "admin@javaops.ru", UserFlag.superuser, null);
++ DELETED = new User("Deleted", "deleted@yandex.ru", UserFlag.deleted, null);
++ FULL_NAME = new User("Full Name", "gmail@gmail.com", UserFlag.active, null);
++ USER1 = new User("User1", "user1@gmail.com", UserFlag.active, null);
++ USER2 = new User("User2", "user2@yandex.ru", UserFlag.active, null);
++ USER3 = new User("User3", "user3@yandex.ru", UserFlag.active, null);
+ FIST5_USERS = ImmutableList.of(ADMIN, DELETED, FULL_NAME, USER1, USER2);
+ }
+
+Index: sql/databaseChangeLog.sql
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- sql/databaseChangeLog.sql (revision )
++++ sql/databaseChangeLog.sql (revision )
+@@ -0,0 +1,34 @@
++--liquibase formatted sql
++
++--changeset gkislin:1
++CREATE SEQUENCE common_seq START 100000;
++
++CREATE TABLE city (
++ ref TEXT PRIMARY KEY,
++ name TEXT NOT NULL
++);
++
++ALTER TABLE users
++ ADD COLUMN city_ref TEXT REFERENCES city (ref) ON UPDATE CASCADE;
++
++--changeset gkislin:2
++CREATE TABLE project (
++ id INTEGER PRIMARY KEY DEFAULT nextval('common_seq'),
++ name TEXT UNIQUE NOT NULL,
++ description TEXT
++);
++
++CREATE TYPE GROUP_TYPE AS ENUM ('REGISTERING', 'CURRENT', 'FINISHED');
++
++CREATE TABLE groups (
++ id INTEGER PRIMARY KEY DEFAULT nextval('common_seq'),
++ name TEXT UNIQUE NOT NULL,
++ type GROUP_TYPE NOT NULL,
++ project_id INTEGER NOT NULL REFERENCES project (id)
++);
++
++CREATE TABLE user_group (
++ user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
++ group_id INTEGER NOT NULL REFERENCES groups (id),
++ CONSTRAINT users_group_idx UNIQUE (user_id, group_id)
++);
+Index: persist/src/main/java/ru/javaops/masterjava/persist/model/Project.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/model/Project.java (revision )
++++ persist/src/main/java/ru/javaops/masterjava/persist/model/Project.java (revision )
+@@ -0,0 +1,14 @@
++package ru.javaops.masterjava.persist.model;
++
++import lombok.*;
++
++@Data
++@NoArgsConstructor
++@AllArgsConstructor
++@EqualsAndHashCode(callSuper = true)
++@ToString(callSuper = true)
++public class Project extends BaseEntity {
++
++ @NonNull private String name;
++ @NonNull private String description;
++}
+Index: persist/src/main/java/ru/javaops/masterjava/persist/dao/UserDao.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/dao/UserDao.java (revision 7d61ae323c8f4b6707bc53ec46e4d50ca23c451c)
++++ persist/src/main/java/ru/javaops/masterjava/persist/dao/UserDao.java (revision )
+@@ -33,23 +33,23 @@
+ return id;
+ }
+
+- @SqlUpdate("INSERT INTO users (full_name, email, flag) VALUES (:fullName, :email, CAST(:flag AS USER_FLAG)) ")
++ @SqlUpdate("INSERT INTO users (full_name, email, flag, city_ref) VALUES (:fullName, :email, CAST(:flag AS USER_FLAG), :cityRef) ")
+ @GetGeneratedKeys
+ abstract int insertGeneratedId(@BindBean User user);
+
+- @SqlUpdate("INSERT INTO users (id, full_name, email, flag) VALUES (:id, :fullName, :email, CAST(:flag AS USER_FLAG)) ")
++ @SqlUpdate("INSERT INTO users (id, full_name, email, flag, city_ref) VALUES (:id, :fullName, :email, CAST(:flag AS USER_FLAG), :cityRef) ")
+ 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")
++ @SqlUpdate("TRUNCATE users CASCADE")
+ @Override
+ public abstract void clean();
+
+ // https://habrahabr.ru/post/264281/
+- @SqlBatch("INSERT INTO users (id, full_name, email, flag) VALUES (:id, :fullName, :email, CAST(:flag AS USER_FLAG))" +
++ @SqlBatch("INSERT INTO users (id, full_name, email, flag, city_ref) VALUES (:id, :fullName, :email, CAST(:flag AS USER_FLAG), :cityRef)" +
+ "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);
+Index: config_templates/sql/initDB.sql
+===================================================================
+--- config_templates/sql/initDB.sql (revision 7d61ae323c8f4b6707bc53ec46e4d50ca23c451c)
++++ sql/initDB.sql (revision )
+@@ -1,0 +1,0 @@
+Index: persist/src/main/java/ru/javaops/masterjava/persist/model/City.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/model/City.java (revision )
++++ persist/src/main/java/ru/javaops/masterjava/persist/model/City.java (revision )
+@@ -0,0 +1,16 @@
++package ru.javaops.masterjava.persist.model;
++
++import lombok.*;
++
++@Data
++@NoArgsConstructor
++@EqualsAndHashCode(callSuper = true)
++@ToString(callSuper = true)
++public class City extends RefEntity {
++ public City(String ref, String name) {
++ super(ref);
++ this.name = name;
++ }
++
++ @NonNull private String name;
++}
+\ No newline at end of file
+Index: persist/src/main/java/ru/javaops/masterjava/persist/model/type/GroupType.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/model/type/GroupType.java (revision )
++++ persist/src/main/java/ru/javaops/masterjava/persist/model/type/GroupType.java (revision )
+@@ -0,0 +1,7 @@
++package ru.javaops.masterjava.persist.model.type;
++
++public enum GroupType {
++ REGISTERING,
++ CURRENT,
++ FINISHED;
++}
+Index: persist/src/main/java/ru/javaops/masterjava/persist/model/Group.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/model/Group.java (revision )
++++ persist/src/main/java/ru/javaops/masterjava/persist/model/Group.java (revision )
+@@ -0,0 +1,17 @@
++package ru.javaops.masterjava.persist.model;
++
++import com.bertoncelj.jdbi.entitymapper.Column;
++import lombok.*;
++import ru.javaops.masterjava.persist.model.type.GroupType;
++
++@Data
++@NoArgsConstructor
++@AllArgsConstructor
++@EqualsAndHashCode(callSuper = true)
++@ToString(callSuper = true)
++public class Group extends BaseEntity {
++
++ @NonNull private String name;
++ @NonNull private GroupType type;
++ @NonNull @Column("project_id") private int projectId;
++}
+Index: persist/src/main/java/ru/javaops/masterjava/persist/model/User.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/model/User.java (revision 7d61ae323c8f4b6707bc53ec46e4d50ca23c451c)
++++ persist/src/main/java/ru/javaops/masterjava/persist/model/User.java (revision )
+@@ -2,19 +2,23 @@
+
+ import com.bertoncelj.jdbi.entitymapper.Column;
+ import lombok.*;
++import ru.javaops.masterjava.persist.model.type.UserFlag;
+
+ @Data
+-@RequiredArgsConstructor
+-@EqualsAndHashCode(callSuper = true)
+ @NoArgsConstructor
++@AllArgsConstructor
++@EqualsAndHashCode(callSuper = true)
++@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_ref")
++ private @NonNull String cityRef;
+
+- public User(Integer id, String fullName, String email, UserFlag flag) {
+- this(fullName, email, flag);
++ public User(Integer id, String fullName, String email, UserFlag flag, String cityRef) {
++ this(fullName, email, flag, cityRef);
+ this.id=id;
+ }
+ }
+\ No newline at end of file
+Index: web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java (revision 7d61ae323c8f4b6707bc53ec46e4d50ca23c451c)
++++ web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java (revision )
+@@ -6,7 +6,7 @@
+ import ru.javaops.masterjava.persist.DBIProvider;
+ import ru.javaops.masterjava.persist.dao.UserDao;
+ import ru.javaops.masterjava.persist.model.User;
+-import ru.javaops.masterjava.persist.model.UserFlag;
++import ru.javaops.masterjava.persist.model.type.UserFlag;
+ import ru.javaops.masterjava.xml.schema.ObjectFactory;
+ import ru.javaops.masterjava.xml.util.JaxbParser;
+ import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
+@@ -59,7 +59,7 @@
+
+ while (processor.doUntil(XMLEvent.START_ELEMENT, "User")) {
+ ru.javaops.masterjava.xml.schema.User xmlUser = unmarshaller.unmarshal(processor.getReader(), ru.javaops.masterjava.xml.schema.User.class);
+- final User user = new User(id++, xmlUser.getValue(), xmlUser.getEmail(), UserFlag.valueOf(xmlUser.getFlag().value()));
++ final User user = new User(id++, xmlUser.getValue(), xmlUser.getEmail(), UserFlag.valueOf(xmlUser.getFlag().value()), null);
+ chunk.add(user);
+ if (chunk.size() == chunkSize) {
+ addChunkFutures(chunkFutures, chunk);
+Index: sql/lb_apply.bat
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- sql/lb_apply.bat (revision )
++++ sql/lb_apply.bat (revision )
+@@ -0,0 +1,8 @@
++set LB_HOME=c:\java\liquibase-3.5.3
++call %LB_HOME%\liquibase.bat --driver=org.postgresql.Driver ^
++--classpath=%LB_HOME%\lib ^
++--changeLogFile=databaseChangeLog.sql ^
++--url="jdbc:postgresql://localhost:5432/masterjava" ^
++--username=user ^
++--password=password ^
++migrate
+\ No newline at end of file
+Index: persist/src/main/java/ru/javaops/masterjava/persist/model/UserGroup.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/model/UserGroup.java (revision )
++++ persist/src/main/java/ru/javaops/masterjava/persist/model/UserGroup.java (revision )
+@@ -0,0 +1,15 @@
++package ru.javaops.masterjava.persist.model;
++
++import com.bertoncelj.jdbi.entitymapper.Column;
++import lombok.AllArgsConstructor;
++import lombok.Data;
++import lombok.NoArgsConstructor;
++import lombok.NonNull;
++
++@Data
++@NoArgsConstructor
++@AllArgsConstructor
++public class UserGroup {
++ @NonNull @Column("user_id") private Integer userId;
++ @NonNull @Column("group_id") private Integer groupId;
++}
+\ No newline at end of file
+Index: persist/src/main/java/ru/javaops/masterjava/persist/model/BaseEntity.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/model/BaseEntity.java (revision 7d61ae323c8f4b6707bc53ec46e4d50ca23c451c)
++++ persist/src/main/java/ru/javaops/masterjava/persist/model/BaseEntity.java (revision )
+@@ -1,30 +1,17 @@
+ package ru.javaops.masterjava.persist.model;
+
+-import lombok.*;
++import lombok.AllArgsConstructor;
++import lombok.Data;
++import lombok.NoArgsConstructor;
+
++@Data
+ @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;
+- }
+ }
+Index: persist/src/main/java/ru/javaops/masterjava/persist/model/UserFlag.java
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/model/UserFlag.java (revision 7d61ae323c8f4b6707bc53ec46e4d50ca23c451c)
++++ persist/src/main/java/ru/javaops/masterjava/persist/model/type/UserFlag.java (revision )
+@@ -1,4 +1,4 @@
+-package ru.javaops.masterjava.persist.model;
++package ru.javaops.masterjava.persist.model.type;
+
+ /**
+ * gkislin
diff --git a/patch/Lesson6/6_2_HW5_dao_test.patch b/patch/Lesson6/6_2_HW5_dao_test.patch
new file mode 100644
index 000000000..05148ef51
--- /dev/null
+++ b/patch/Lesson6/6_2_HW5_dao_test.patch
@@ -0,0 +1,633 @@
+Index: persist/src/main/java/ru/javaops/masterjava/persist/dao/UserGroupDao.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/dao/UserGroupDao.java (revision )
++++ persist/src/main/java/ru/javaops/masterjava/persist/dao/UserGroupDao.java (revision )
+@@ -0,0 +1,32 @@
++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;
++
++@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
+Index: persist/src/test/java/ru/javaops/masterjava/persist/dao/ProjectDaoTest.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/test/java/ru/javaops/masterjava/persist/dao/ProjectDaoTest.java (revision )
++++ persist/src/test/java/ru/javaops/masterjava/persist/dao/ProjectDaoTest.java (revision )
+@@ -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.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);
++ }
++
++ @BeforeClass
++ public static void init() throws Exception {
++ ProjectTestData.init();
++ }
++
++ @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
+Index: persist/src/main/java/ru/javaops/masterjava/persist/dao/CityDao.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/dao/CityDao.java (revision )
++++ persist/src/main/java/ru/javaops/masterjava/persist/dao/CityDao.java (revision )
+@@ -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.SqlBatch;
++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.City;
++
++import java.util.Collection;
++import java.util.List;
++import java.util.Map;
++
++import static java.util.function.Function.identity;
++
++@RegisterMapperFactory(EntityMapperFactory.class)
++public abstract class CityDao implements AbstractDao {
++
++ @SqlUpdate("TRUNCATE city CASCADE ")
++ @Override
++ public abstract void clean();
++
++ @SqlQuery("SELECT * FROM city")
++ public abstract List getAll();
++
++ public Map getAsMap() {
++ return StreamEx.of(getAll()).toMap(City::getRef, identity());
++ }
++
++ @SqlUpdate("INSERT INTO city (ref, name) VALUES (:ref, :name)")
++ public abstract void insert(@BindBean City city);
++
++ @SqlBatch("INSERT INTO city (ref, name) VALUES (:ref, :name)")
++ public abstract void insertBatch(@BindBean Collection cities);
++}
+Index: persist/src/test/java/ru/javaops/masterjava/persist/GroupTestData.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/test/java/ru/javaops/masterjava/persist/GroupTestData.java (revision )
++++ persist/src/test/java/ru/javaops/masterjava/persist/GroupTestData.java (revision )
+@@ -0,0 +1,53 @@
++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.type.GroupType.CURRENT;
++import static ru.javaops.masterjava.persist.model.type.GroupType.FINISHED;
++
++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.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();
++ }
++}
+Index: persist/src/test/java/ru/javaops/masterjava/persist/dao/CityDaoTest.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/test/java/ru/javaops/masterjava/persist/dao/CityDaoTest.java (revision )
++++ persist/src/test/java/ru/javaops/masterjava/persist/dao/CityDaoTest.java (revision )
+@@ -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.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);
++ }
++
++ @BeforeClass
++ public static void init() throws Exception {
++ CityTestData.init();
++ }
++
++ @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
+Index: persist/src/test/java/ru/javaops/masterjava/persist/CityTestData.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/test/java/ru/javaops/masterjava/persist/CityTestData.java (revision )
++++ persist/src/test/java/ru/javaops/masterjava/persist/CityTestData.java (revision )
+@@ -0,0 +1,36 @@
++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;
++
++public class CityTestData {
++ public static City KIEV;
++ public static City MINSK;
++ public static City MOSCOW;
++ public static City SPB;
++
++ public static Map CITIES;
++
++ public static void init() {
++ KIEV = new City("kiv", "Киев");
++ MINSK = new City("mnsk", "Минск");
++ MOSCOW = new City("mow", "Москва");
++ SPB = new City("spb", "Санкт-Петербург");
++ 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);
++ });
++ }
++}
+Index: persist/src/test/java/ru/javaops/masterjava/persist/dao/GroupDaoTest.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/test/java/ru/javaops/masterjava/persist/dao/GroupDaoTest.java (revision )
++++ persist/src/test/java/ru/javaops/masterjava/persist/dao/GroupDaoTest.java (revision )
+@@ -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
+Index: persist/src/main/java/ru/javaops/masterjava/persist/dao/ProjectDao.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/dao/ProjectDao.java (revision )
++++ persist/src/main/java/ru/javaops/masterjava/persist/dao/ProjectDao.java (revision )
+@@ -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);
++ }
++}
+Index: persist/src/test/java/ru/javaops/masterjava/persist/UserGroupTestData.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/test/java/ru/javaops/masterjava/persist/UserGroupTestData.java (revision )
++++ persist/src/test/java/ru/javaops/masterjava/persist/UserGroupTestData.java (revision )
+@@ -0,0 +1,41 @@
++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.GroupTestData.*;
++import static ru.javaops.masterjava.persist.dao.UserGroupDao.toUserGroups;
++
++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);
++ }
++}
+Index: persist/src/main/java/ru/javaops/masterjava/persist/dao/GroupDao.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/main/java/ru/javaops/masterjava/persist/dao/GroupDao.java (revision )
++++ persist/src/main/java/ru/javaops/masterjava/persist/dao/GroupDao.java (revision )
+@@ -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);
++ }
++}
+Index: persist/src/test/java/ru/javaops/masterjava/persist/ProjectTestData.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/test/java/ru/javaops/masterjava/persist/ProjectTestData.java (revision )
++++ persist/src/test/java/ru/javaops/masterjava/persist/ProjectTestData.java (revision )
+@@ -0,0 +1,34 @@
++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;
++
++public class ProjectTestData {
++ public static Project TOPJAVA;
++ public static Project MASTERJAVA;
++ public static Map PROJECTS;
++
++ public static int TOPJAVA_ID;
++ public static int MASTERJAVA_ID;
++
++ public static void init() {
++ TOPJAVA = new Project("topjava", "Topjava");
++ MASTERJAVA = new Project("masterjava", "Masterjava");
++ PROJECTS = ImmutableMap.of(
++ TOPJAVA.getName(), TOPJAVA,
++ MASTERJAVA.getName(), MASTERJAVA);
++ }
++
++ 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();
++ }
++}
+Index: persist/src/test/java/ru/javaops/masterjava/persist/dao/UserGroupDaoTest.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/test/java/ru/javaops/masterjava/persist/dao/UserGroupDaoTest.java (revision )
++++ persist/src/test/java/ru/javaops/masterjava/persist/dao/UserGroupDaoTest.java (revision )
+@@ -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
+Index: persist/src/test/java/ru/javaops/masterjava/persist/UserTestData.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/test/java/ru/javaops/masterjava/persist/UserTestData.java (date 1511204807000)
++++ persist/src/test/java/ru/javaops/masterjava/persist/UserTestData.java (revision )
+@@ -7,6 +7,8 @@
+
+ import java.util.List;
+
++import static ru.javaops.masterjava.persist.CityTestData.*;
++
+ public class UserTestData {
+ public static User ADMIN;
+ public static User DELETED;
+@@ -17,12 +19,15 @@
+ public static List FIST5_USERS;
+
+ public static void init() {
+- ADMIN = new User("Admin", "admin@javaops.ru", UserFlag.superuser, null);
+- DELETED = new User("Deleted", "deleted@yandex.ru", UserFlag.deleted, null);
+- FULL_NAME = new User("Full Name", "gmail@gmail.com", UserFlag.active, null);
+- USER1 = new User("User1", "user1@gmail.com", UserFlag.active, null);
+- USER2 = new User("User2", "user2@yandex.ru", UserFlag.active, null);
+- USER3 = new User("User3", "user3@yandex.ru", UserFlag.active, null);
++ CityTestData.init();
++ CityTestData.setUp();
++
++ ADMIN = new User("Admin", "admin@javaops.ru", UserFlag.superuser, SPB.getRef());
++ DELETED = new User("Deleted", "deleted@yandex.ru", UserFlag.deleted, SPB.getRef());
++ FULL_NAME = new User("Full Name", "gmail@gmail.com", UserFlag.active, KIEV.getRef());
++ USER1 = new User("User1", "user1@gmail.com", UserFlag.active, MOSCOW.getRef());
++ USER2 = new User("User2", "user2@yandex.ru", UserFlag.active, KIEV.getRef());
++ USER3 = new User("User3", "user3@yandex.ru", UserFlag.active, MINSK.getRef());
+ FIST5_USERS = ImmutableList.of(ADMIN, DELETED, FULL_NAME, USER1, USER2);
+ }
+
+Index: persist/src/test/java/ru/javaops/masterjava/persist/dao/AbstractDaoTest.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- persist/src/test/java/ru/javaops/masterjava/persist/dao/AbstractDaoTest.java (date 1511204807000)
++++ persist/src/test/java/ru/javaops/masterjava/persist/dao/AbstractDaoTest.java (revision )
+@@ -1,13 +1,32 @@
+ 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) {
diff --git a/patch/Lesson6/6_3_HW5_add_PayloadProcessor.patch b/patch/Lesson6/6_3_HW5_add_PayloadProcessor.patch
new file mode 100644
index 000000000..18d43e1b5
--- /dev/null
+++ b/patch/Lesson6/6_3_HW5_add_PayloadProcessor.patch
@@ -0,0 +1,123 @@
+Index: web/upload/src/main/java/ru/javaops/masterjava/upload/PayloadProcessor.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- web/upload/src/main/java/ru/javaops/masterjava/upload/PayloadProcessor.java (revision )
++++ web/upload/src/main/java/ru/javaops/masterjava/upload/PayloadProcessor.java (revision )
+@@ -0,0 +1,30 @@
++package ru.javaops.masterjava.upload;
++
++import lombok.AllArgsConstructor;
++import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
++
++import javax.xml.bind.JAXBException;
++import javax.xml.stream.XMLStreamException;
++import java.io.InputStream;
++import java.util.List;
++
++public class PayloadProcessor {
++ private final UserProcessor userProcessor = new UserProcessor();
++
++ @AllArgsConstructor
++ public static class FailedEmails {
++ public String emailsOrRange;
++ public String reason;
++
++ @Override
++ public String toString() {
++ return emailsOrRange + " : " + reason;
++ }
++ }
++
++
++ public List process(InputStream is, int chunkSize) throws XMLStreamException, JAXBException {
++ final StaxStreamProcessor processor = new StaxStreamProcessor(is);
++ return userProcessor.process(processor, chunkSize);
++ }
++}
+\ No newline at end of file
+Index: web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java (date 1511206086000)
++++ web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java (revision )
+@@ -1,12 +1,12 @@
+ package ru.javaops.masterjava.upload;
+
+-import lombok.AllArgsConstructor;
+ import lombok.extern.slf4j.Slf4j;
+ import lombok.val;
+ import ru.javaops.masterjava.persist.DBIProvider;
+ import ru.javaops.masterjava.persist.dao.UserDao;
+ import ru.javaops.masterjava.persist.model.User;
+ import ru.javaops.masterjava.persist.model.type.UserFlag;
++import ru.javaops.masterjava.upload.PayloadProcessor.FailedEmails;
+ import ru.javaops.masterjava.xml.schema.ObjectFactory;
+ import ru.javaops.masterjava.xml.util.JaxbParser;
+ import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
+@@ -14,7 +14,6 @@
+ import javax.xml.bind.JAXBException;
+ import javax.xml.stream.XMLStreamException;
+ import javax.xml.stream.events.XMLEvent;
+-import java.io.InputStream;
+ import java.util.ArrayList;
+ import java.util.LinkedHashMap;
+ import java.util.List;
+@@ -33,28 +32,16 @@
+
+ private ExecutorService executorService = Executors.newFixedThreadPool(NUMBER_THREADS);
+
+- @AllArgsConstructor
+- public static class FailedEmails {
+- public String emailsOrRange;
+- public String reason;
+-
+- @Override
+- public String toString() {
+- return emailsOrRange + " : " + reason;
+- }
+- }
+-
+ /*
+ * return failed users chunks
+ */
+- public List process(final InputStream is, int chunkSize) throws XMLStreamException, JAXBException {
++ public List process(final StaxStreamProcessor processor, int chunkSize) throws XMLStreamException, JAXBException {
+ log.info("Start processing with chunkSize=" + chunkSize);
+
+ Map>> chunkFutures = new LinkedHashMap<>(); // ordered map (emailRange -> chunk future)
+
+ int id = userDao.getSeqAndSkip(chunkSize);
+ List chunk = new ArrayList<>(chunkSize);
+- val processor = new StaxStreamProcessor(is);
+ val unmarshaller = jaxbParser.createUnmarshaller();
+
+ while (processor.doUntil(XMLEvent.START_ELEMENT, "User")) {
+Index: web/upload/src/main/java/ru/javaops/masterjava/upload/UploadServlet.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- web/upload/src/main/java/ru/javaops/masterjava/upload/UploadServlet.java (date 1511206086000)
++++ web/upload/src/main/java/ru/javaops/masterjava/upload/UploadServlet.java (revision )
+@@ -23,7 +23,7 @@
+ public class UploadServlet extends HttpServlet {
+ private static final int CHUNK_SIZE = 2000;
+
+- private final UserProcessor userProcessor = new UserProcessor();
++ private final PayloadProcessor payloadProcessor = new PayloadProcessor();
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+@@ -42,7 +42,7 @@
+ } else {
+ Part filePart = req.getPart("fileToUpload");
+ try (InputStream is = filePart.getInputStream()) {
+- List failed = userProcessor.process(is, chunkSize);
++ List failed = payloadProcessor.process(is, chunkSize);
+ log.info("Failed users: " + failed);
+ final WebContext webContext =
+ new WebContext(req, resp, req.getServletContext(), req.getLocale(),
diff --git a/patch/Lesson6/6_4_HW5_add_CityProcessor.patch b/patch/Lesson6/6_4_HW5_add_CityProcessor.patch
new file mode 100644
index 000000000..8975bb840
--- /dev/null
+++ b/patch/Lesson6/6_4_HW5_add_CityProcessor.patch
@@ -0,0 +1,173 @@
+Index: web/upload/src/main/java/ru/javaops/masterjava/upload/CityProcessor.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- web/upload/src/main/java/ru/javaops/masterjava/upload/CityProcessor.java (revision )
++++ web/upload/src/main/java/ru/javaops/masterjava/upload/CityProcessor.java (revision )
+@@ -0,0 +1,32 @@
++package ru.javaops.masterjava.upload;
++
++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;
++
++@Slf4j
++public class CityProcessor {
++ 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(ref, processor.getText()));
++ }
++ }
++ log.info("Insert batch " + newCities);
++ cityDao.insertBatch(newCities);
++ return cityDao.getAsMap();
++ }
++}
+Index: web/upload/src/test/resources/payload_bad.xml
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- web/upload/src/test/resources/payload_bad.xml (revision )
++++ web/upload/src/test/resources/payload_bad.xml (revision )
+@@ -0,0 +1,31 @@
++
++
++
++
++ Topjava
++
++
++
++
++
++ Masterjava
++
++
++
++
++ Санкт-Петербург
++ Москва
++ Киев
++ Минск
++
++
++ Full Name
++ Admin
++ Deleted
++ User1
++ User2
++ User3
++
++
+\ No newline at end of file
+Index: web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java (date 1511206679000)
++++ web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java (revision )
+@@ -4,6 +4,7 @@
+ import lombok.val;
+ 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.type.UserFlag;
+ import ru.javaops.masterjava.upload.PayloadProcessor.FailedEmails;
+@@ -35,7 +36,7 @@
+ /*
+ * return failed users chunks
+ */
+- public List process(final StaxStreamProcessor processor, int chunkSize) throws XMLStreamException, JAXBException {
++ public List process(final StaxStreamProcessor processor, Map cities, int chunkSize) throws XMLStreamException, JAXBException {
+ log.info("Start processing with chunkSize=" + chunkSize);
+
+ Map>> chunkFutures = new LinkedHashMap<>(); // ordered map (emailRange -> chunk future)
+@@ -43,15 +44,21 @@
+ int id = userDao.getSeqAndSkip(chunkSize);
+ List chunk = new ArrayList<>(chunkSize);
+ val unmarshaller = jaxbParser.createUnmarshaller();
++ List failed = new ArrayList<>();
+
+ while (processor.doUntil(XMLEvent.START_ELEMENT, "User")) {
++ String cityRef = processor.getAttribute("city"); // unmarshal doesn't get city ref
+ ru.javaops.masterjava.xml.schema.User xmlUser = unmarshaller.unmarshal(processor.getReader(), ru.javaops.masterjava.xml.schema.User.class);
+- final User user = new User(id++, xmlUser.getValue(), xmlUser.getEmail(), UserFlag.valueOf(xmlUser.getFlag().value()), null);
+- chunk.add(user);
+- if (chunk.size() == chunkSize) {
+- addChunkFutures(chunkFutures, chunk);
+- chunk = new ArrayList<>(chunkSize);
+- id = userDao.getSeqAndSkip(chunkSize);
++ if (cities.get(cityRef) == null) {
++ failed.add(new FailedEmails(xmlUser.getEmail(), "City '" + cityRef + "' is not present in DB"));
++ } else {
++ final User user = new User(id++, xmlUser.getValue(), xmlUser.getEmail(), UserFlag.valueOf(xmlUser.getFlag().value()), cityRef);
++ chunk.add(user);
++ if (chunk.size() == chunkSize) {
++ addChunkFutures(chunkFutures, chunk);
++ chunk = new ArrayList<>(chunkSize);
++ id = userDao.getSeqAndSkip(chunkSize);
++ }
+ }
+ }
+
+@@ -59,7 +66,6 @@
+ addChunkFutures(chunkFutures, chunk);
+ }
+
+- List failed = new ArrayList<>();
+ List allAlreadyPresents = new ArrayList<>();
+ chunkFutures.forEach((emailRange, future) -> {
+ try {
+Index: web/upload/src/main/java/ru/javaops/masterjava/upload/PayloadProcessor.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- web/upload/src/main/java/ru/javaops/masterjava/upload/PayloadProcessor.java (date 1511206679000)
++++ web/upload/src/main/java/ru/javaops/masterjava/upload/PayloadProcessor.java (revision )
+@@ -1,6 +1,7 @@
+ package ru.javaops.masterjava.upload;
+
+ import lombok.AllArgsConstructor;
++import lombok.val;
+ import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
+
+ import javax.xml.bind.JAXBException;
+@@ -9,6 +10,7 @@
+ import java.util.List;
+
+ public class PayloadProcessor {
++ private final CityProcessor cityProcessor = new CityProcessor();
+ private final UserProcessor userProcessor = new UserProcessor();
+
+ @AllArgsConstructor
+@@ -25,6 +27,7 @@
+
+ public List process(InputStream is, int chunkSize) throws XMLStreamException, JAXBException {
+ final StaxStreamProcessor processor = new StaxStreamProcessor(is);
+- return userProcessor.process(processor, chunkSize);
++ val cities = cityProcessor.process(processor);
++ return userProcessor.process(processor, cities, chunkSize);
+ }
+ }
+\ No newline at end of file
diff --git a/patch/Lesson6/6_5_web_services.patch b/patch/Lesson6/6_5_web_services.patch
new file mode 100644
index 000000000..80e22627b
--- /dev/null
+++ b/patch/Lesson6/6_5_web_services.patch
@@ -0,0 +1,145 @@
+Index: services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailService.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailService.java (revision )
++++ services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/MailService.java (revision )
+@@ -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
+Index: services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServiceClient.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServiceClient.java (revision )
++++ services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServiceClient.java (revision )
+@@ -0,0 +1,20 @@
++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;
++
++public class MailServiceClient {
++
++ public static void main(String[] args) throws MalformedURLException {
++ Service service = Service.create(
++ new URL("http://localhost:8080/mail/mailService?wsdl"),
++ new QName("http://mail.service.masterjava.javaops.ru/", "MailServiceImplService"));
++
++ MailService mailService = service.getPort(MailService.class);
++ mailService.sendMail(ImmutableList.of(new Addressee("masterjava@javaops.ru", null)), null, "Subject", "Body");
++ }
++}
+Index: services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServicePublisher.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServicePublisher.java (revision )
++++ services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServicePublisher.java (revision )
+@@ -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());
++ }
++}
+Index: services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailSender.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailSender.java (revision )
++++ services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailSender.java (revision )
+@@ -0,0 +1,12 @@
++package ru.javaops.masterjava.service.mail;
++
++import lombok.extern.slf4j.Slf4j;
++
++import java.util.List;
++
++@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 : ""));
++ }
++}
+Index: services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/Addressee.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/Addressee.java (revision )
++++ services/mail-api/src/main/java/ru/javaops/masterjava/service/mail/Addressee.java (revision )
+@@ -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;
++}
+Index: services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceImpl.java
+IDEA additional info:
+Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
+<+>UTF-8
+===================================================================
+--- services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceImpl.java (revision )
++++ services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceImpl.java (revision )
+@@ -0,0 +1,11 @@
++package ru.javaops.masterjava.service.mail;
++
++import javax.jws.WebService;
++import java.util.List;
++
++@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/persist/pom.xml b/persist/pom.xml
new file mode 100644
index 000000000..6efb20ca3
--- /dev/null
+++ b/persist/pom.xml
@@ -0,0 +1,46 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent
+ ../parent/pom.xml
+ 1.0-SNAPSHOT
+
+
+ persist
+ 1.0-SNAPSHOT
+ Persist
+
+
+
+ ${project.groupId}
+ common
+ ${project.version}
+
+
+ org.jdbi
+ jdbi
+ 2.78
+
+
+ com.bertoncelj.jdbi.entitymapper
+ jdbi-entity-mapper
+ 1.0.0
+
+
+ org.jdbi
+ jdbi
+
+
+
+
+ org.postgresql
+ postgresql
+ 42.1.4
+
+
+
\ No newline at end of file
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/DBIProvider.java b/persist/src/main/java/ru/javaops/masterjava/persist/DBIProvider.java
new file mode 100644
index 000000000..f3d115dcf
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/DBIProvider.java
@@ -0,0 +1,50 @@
+package ru.javaops.masterjava.persist;
+
+import lombok.extern.slf4j.Slf4j;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.logging.SLF4JLog;
+import org.skife.jdbi.v2.tweak.ConnectionFactory;
+import ru.javaops.masterjava.persist.dao.AbstractDao;
+
+import javax.naming.InitialContext;
+import javax.sql.DataSource;
+
+@Slf4j
+public class DBIProvider {
+
+ private volatile static ConnectionFactory connectionFactory = null;
+
+ private static class DBIHolder {
+ static final DBI jDBI;
+
+ static {
+ final DBI dbi;
+ if (connectionFactory != null) {
+ log.info("Init jDBI with connectionFactory");
+ dbi = new DBI(connectionFactory);
+ } else {
+ try {
+ log.info("Init jDBI with JNDI");
+ InitialContext ctx = new InitialContext();
+ dbi = new DBI((DataSource) ctx.lookup("java:/comp/env/jdbc/masterjava"));
+ } catch (Exception ex) {
+ throw new IllegalStateException("PostgreSQL initialization failed", ex);
+ }
+ }
+ jDBI = dbi;
+ jDBI.setSQLLog(new SLF4JLog());
+ }
+ }
+
+ public static void init(ConnectionFactory connectionFactory) {
+ DBIProvider.connectionFactory = connectionFactory;
+ }
+
+ public static DBI getDBI() {
+ return DBIHolder.jDBI;
+ }
+
+ public static T getDao(Class 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..8b3ee1099
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/dao/AbstractDao.java
@@ -0,0 +1,5 @@
+package ru.javaops.masterjava.persist.dao;
+
+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..5ebdb1afa
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/dao/CityDao.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.SqlBatch;
+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.City;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.function.Function.identity;
+
+@RegisterMapperFactory(EntityMapperFactory.class)
+public abstract class CityDao implements AbstractDao {
+
+ @SqlUpdate("TRUNCATE city CASCADE ")
+ @Override
+ public abstract void clean();
+
+ @SqlQuery("SELECT * FROM city")
+ public abstract List getAll();
+
+ public Map getAsMap() {
+ return StreamEx.of(getAll()).toMap(City::getRef, identity());
+ }
+
+ @SqlUpdate("INSERT INTO city (ref, name) VALUES (:ref, :name)")
+ public abstract void insert(@BindBean City city);
+
+ @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..c57c4f2e2
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/dao/UserDao.java
@@ -0,0 +1,65 @@
+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;
+
+@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_ref) VALUES (:fullName, :email, CAST(:flag AS USER_FLAG), :cityRef) ")
+ @GetGeneratedKeys
+ abstract int insertGeneratedId(@BindBean User user);
+
+ @SqlUpdate("INSERT INTO users (id, full_name, email, flag, city_ref) VALUES (:id, :fullName, :email, CAST(:flag AS USER_FLAG), :cityRef) ")
+ 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_ref) VALUES (:id, :fullName, :email, CAST(:flag AS USER_FLAG), :cityRef)" +
+ "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..2306a85e4
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/dao/UserGroupDao.java
@@ -0,0 +1,32 @@
+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;
+
+@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..e40f4dec8
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/BaseEntity.java
@@ -0,0 +1,17 @@
+package ru.javaops.masterjava.persist.model;
+
+import lombok.*;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+abstract public class BaseEntity {
+
+ @Getter
+ @Setter
+ protected Integer id;
+
+ public boolean isNew() {
+ return id == null;
+ }
+}
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..974f9868a
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/City.java
@@ -0,0 +1,16 @@
+package ru.javaops.masterjava.persist.model;
+
+import lombok.*;
+
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class City extends RefEntity {
+ public City(String ref, String name) {
+ super(ref);
+ this.name = name;
+ }
+
+ @NonNull private String name;
+}
\ 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..71a648731
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/Group.java
@@ -0,0 +1,17 @@
+package ru.javaops.masterjava.persist.model;
+
+import com.bertoncelj.jdbi.entitymapper.Column;
+import lombok.*;
+import ru.javaops.masterjava.persist.model.type.GroupType;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class Group extends BaseEntity {
+
+ @NonNull private String name;
+ @NonNull private GroupType type;
+ @NonNull @Column("project_id") private int projectId;
+}
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..c040121e4
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/Project.java
@@ -0,0 +1,14 @@
+package ru.javaops.masterjava.persist.model;
+
+import lombok.*;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class Project extends BaseEntity {
+
+ @NonNull private String name;
+ @NonNull private String description;
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/model/RefEntity.java b/persist/src/main/java/ru/javaops/masterjava/persist/model/RefEntity.java
new file mode 100644
index 000000000..249189604
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/RefEntity.java
@@ -0,0 +1,12 @@
+package ru.javaops.masterjava.persist.model;
+
+import lombok.*;
+
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
+@EqualsAndHashCode
+abstract public class RefEntity {
+ @Getter
+ @NonNull private String ref;
+}
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..c34f3020b
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/User.java
@@ -0,0 +1,24 @@
+package ru.javaops.masterjava.persist.model;
+
+import com.bertoncelj.jdbi.entitymapper.Column;
+import lombok.*;
+import ru.javaops.masterjava.persist.model.type.UserFlag;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@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_ref")
+ private @NonNull String cityRef;
+
+ public User(Integer id, String fullName, String email, UserFlag flag, String cityRef) {
+ this(fullName, email, flag, cityRef);
+ this.id=id;
+ }
+}
\ No newline at end of file
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..f647dfcff
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/UserGroup.java
@@ -0,0 +1,15 @@
+package ru.javaops.masterjava.persist.model;
+
+import com.bertoncelj.jdbi.entitymapper.Column;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.NonNull;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+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/java/ru/javaops/masterjava/persist/model/type/GroupType.java b/persist/src/main/java/ru/javaops/masterjava/persist/model/type/GroupType.java
new file mode 100644
index 000000000..8750d8613
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/type/GroupType.java
@@ -0,0 +1,7 @@
+package ru.javaops.masterjava.persist.model.type;
+
+public enum GroupType {
+ REGISTERING,
+ CURRENT,
+ FINISHED;
+}
diff --git a/persist/src/main/java/ru/javaops/masterjava/persist/model/type/UserFlag.java b/persist/src/main/java/ru/javaops/masterjava/persist/model/type/UserFlag.java
new file mode 100644
index 000000000..0aff2a46d
--- /dev/null
+++ b/persist/src/main/java/ru/javaops/masterjava/persist/model/type/UserFlag.java
@@ -0,0 +1,11 @@
+package ru.javaops.masterjava.persist.model.type;
+
+/**
+ * gkislin
+ * 13.10.2016
+ */
+public enum UserFlag {
+ active,
+ deleted,
+ superuser;
+}
diff --git a/persist/src/main/resources/persist.conf b/persist/src/main/resources/persist.conf
new file mode 100644
index 000000000..dc97cb39c
--- /dev/null
+++ b/persist/src/main/resources/persist.conf
@@ -0,0 +1,7 @@
+db {
+ url = "jdbc:postgresql://localhost:5432/masterjava"
+ user = postgres
+ password = password
+}
+
+include file("/apps/masterjava/config_templates/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..1e3fef700
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/CityTestData.java
@@ -0,0 +1,36 @@
+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;
+
+public class CityTestData {
+ public static City KIEV;
+ public static City MINSK;
+ public static City MOSCOW;
+ public static City SPB;
+
+ public static Map CITIES;
+
+ public static void init() {
+ KIEV = new City("kiv", "Киев");
+ MINSK = new City("mnsk", "Минск");
+ MOSCOW = new City("mow", "Москва");
+ SPB = new City("spb", "Санкт-Петербург");
+ 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..eb9605066
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/DBITestProvider.java
@@ -0,0 +1,24 @@
+package ru.javaops.masterjava.persist;
+
+import com.typesafe.config.Config;
+import ru.javaops.masterjava.config.Configs;
+
+import java.sql.DriverManager;
+
+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);
+ });
+ }
+}
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..2bbd47d7d
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/GroupTestData.java
@@ -0,0 +1,53 @@
+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.type.GroupType.CURRENT;
+import static ru.javaops.masterjava.persist.model.type.GroupType.FINISHED;
+
+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.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..cd45980d0
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/ProjectTestData.java
@@ -0,0 +1,34 @@
+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;
+
+public class ProjectTestData {
+ public static Project TOPJAVA;
+ public static Project MASTERJAVA;
+ public static Map PROJECTS;
+
+ public static int TOPJAVA_ID;
+ public static int MASTERJAVA_ID;
+
+ public static void init() {
+ TOPJAVA = new Project("topjava", "Topjava");
+ MASTERJAVA = new Project("masterjava", "Masterjava");
+ PROJECTS = ImmutableMap.of(
+ TOPJAVA.getName(), TOPJAVA,
+ MASTERJAVA.getName(), MASTERJAVA);
+ }
+
+ 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..06c2d4be9
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/UserGroupTestData.java
@@ -0,0 +1,41 @@
+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.GroupTestData.*;
+import static ru.javaops.masterjava.persist.dao.UserGroupDao.toUserGroups;
+
+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..ff1779c5e
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/UserTestData.java
@@ -0,0 +1,42 @@
+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.type.UserFlag;
+
+import java.util.List;
+
+import static ru.javaops.masterjava.persist.CityTestData.*;
+
+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.init();
+ CityTestData.setUp();
+
+ ADMIN = new User("Admin", "admin@javaops.ru", UserFlag.superuser, SPB.getRef());
+ DELETED = new User("Deleted", "deleted@yandex.ru", UserFlag.deleted, SPB.getRef());
+ FULL_NAME = new User("Full Name", "gmail@gmail.com", UserFlag.active, KIEV.getRef());
+ USER1 = new User("User1", "user1@gmail.com", UserFlag.active, MOSCOW.getRef());
+ USER2 = new User("User2", "user2@yandex.ru", UserFlag.active, KIEV.getRef());
+ USER3 = new User("User3", "user3@yandex.ru", UserFlag.active, MINSK.getRef());
+ 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..d4dbf19ab
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/dao/CityDaoTest.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.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);
+ }
+
+ @BeforeClass
+ public static void init() throws Exception {
+ CityTestData.init();
+ }
+
+ @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..b364e3f02
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/dao/ProjectDaoTest.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.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);
+ }
+
+ @BeforeClass
+ public static void init() throws Exception {
+ ProjectTestData.init();
+ }
+
+ @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..b5078d3b1
--- /dev/null
+++ b/persist/src/test/java/ru/javaops/masterjava/persist/dao/UserDaoTest.java
@@ -0,0 +1,49 @@
+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;
+
+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
index 39e811e08..c7e9c65d2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,41 +4,22 @@
ru.javaops
masterjava
- jar
+ pom
1.0-SNAPSHOT
- Master Java
+ Masterjava Root
https://github.com/JavaOPs/masterjava
-
- 1.8
- UTF-8
- UTF-8
-
+
+ parent
+ parent-web
-
- masterjava
- install
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.1
-
- ${java.version}
- ${java.version}
-
-
-
-
+ 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..ecfff61c6
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailSender.java
@@ -0,0 +1,12 @@
+package ru.javaops.masterjava.service.mail;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+@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..213d09f4f
--- /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);
+ }
+ }
+}
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..fd649330a
--- /dev/null
+++ b/services/mail-service/src/main/java/ru/javaops/masterjava/service/mail/MailServiceImpl.java
@@ -0,0 +1,11 @@
+package ru.javaops.masterjava.service.mail;
+
+import javax.jws.WebService;
+import java.util.List;
+
+@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..f2bfc79ad
--- /dev/null
+++ b/services/mail-service/src/test/java/ru/javaops/masterjava/service/mail/MailServiceClient.java
@@ -0,0 +1,20 @@
+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;
+
+public class MailServiceClient {
+
+ public static void main(String[] args) throws MalformedURLException {
+ Service service = Service.create(
+ new URL("http://localhost:8080/mail/mailService?wsdl"),
+ new QName("http://mail.service.masterjava.javaops.ru/", "MailServiceImplService"));
+
+ MailService mailService = service.getPort(MailService.class);
+ mailService.sendMail(ImmutableList.of(new Addressee("masterjava@javaops.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..62ffc5e38
--- /dev/null
+++ b/services/pom.xml
@@ -0,0 +1,15 @@
+
+ 4.0.0
+
+ ru.javaops
+ services
+ pom
+ 1.0-SNAPSHOT
+
+ Services
+
+ mail-api
+ mail-service
+
+
diff --git a/sql/databaseChangeLog.sql b/sql/databaseChangeLog.sql
new file mode 100644
index 000000000..60ec863d9
--- /dev/null
+++ b/sql/databaseChangeLog.sql
@@ -0,0 +1,34 @@
+--liquibase formatted sql
+
+--changeset gkislin:1
+CREATE SEQUENCE common_seq START 100000;
+
+CREATE TABLE city (
+ ref TEXT PRIMARY KEY,
+ name TEXT NOT NULL
+);
+
+ALTER TABLE users
+ ADD COLUMN city_ref TEXT REFERENCES city (ref) ON UPDATE CASCADE;
+
+--changeset gkislin:2
+CREATE TABLE project (
+ id INTEGER PRIMARY KEY DEFAULT nextval('common_seq'),
+ name TEXT UNIQUE NOT NULL,
+ description TEXT
+);
+
+CREATE TYPE GROUP_TYPE AS ENUM ('REGISTERING', 'CURRENT', 'FINISHED');
+
+CREATE TABLE groups (
+ id INTEGER PRIMARY KEY DEFAULT nextval('common_seq'),
+ name TEXT UNIQUE NOT NULL,
+ type GROUP_TYPE NOT NULL,
+ project_id INTEGER NOT NULL REFERENCES project (id)
+);
+
+CREATE TABLE user_group (
+ user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
+ group_id INTEGER NOT NULL REFERENCES groups (id),
+ CONSTRAINT users_group_idx UNIQUE (user_id, group_id)
+);
diff --git a/sql/initDB.sql b/sql/initDB.sql
new file mode 100644
index 000000000..287db70c0
--- /dev/null
+++ b/sql/initDB.sql
@@ -0,0 +1,16 @@
+DROP TABLE IF EXISTS users;
+DROP SEQUENCE IF EXISTS user_seq;
+DROP TYPE IF EXISTS user_flag;
+
+CREATE TYPE user_flag AS ENUM ('active', 'deleted', 'superuser');
+
+CREATE SEQUENCE user_seq START 100000;
+
+CREATE TABLE users (
+ id INTEGER PRIMARY KEY DEFAULT nextval('user_seq'),
+ full_name TEXT NOT NULL,
+ email TEXT NOT NULL,
+ flag user_flag NOT NULL
+);
+
+CREATE UNIQUE INDEX email_idx ON users (email);
diff --git a/sql/lb_apply.bat b/sql/lb_apply.bat
new file mode 100644
index 000000000..80f23598b
--- /dev/null
+++ b/sql/lb_apply.bat
@@ -0,0 +1,8 @@
+set LB_HOME=c:\java\liquibase-3.5.3
+call %LB_HOME%\liquibase.bat --driver=org.postgresql.Driver ^
+--classpath=%LB_HOME%\lib ^
+--changeLogFile=databaseChangeLog.sql ^
+--url="jdbc:postgresql://localhost:5432/masterjava" ^
+--username=user ^
+--password=password ^
+migrate
\ No newline at end of file
diff --git a/src/main/java/ru/javaops/masterjava/Main.java b/src/main/java/ru/javaops/masterjava/Main.java
deleted file mode 100644
index a849258c4..000000000
--- a/src/main/java/ru/javaops/masterjava/Main.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package ru.javaops.masterjava;
-
-/**
- * User: gkislin
- * Date: 05.08.2015
- *
- * @link http://caloriesmng.herokuapp.com/
- * @link https://github.com/JavaOPs/topjava
- */
-public class Main {
- public static void main(String[] args) {
- System.out.format("Hello MasterJava!");
- }
-}
diff --git a/src/main/java/ru/javaops/masterjava/matrix/MatrixUtil.java b/src/main/java/ru/javaops/masterjava/matrix/MatrixUtil.java
deleted file mode 100644
index 80a344ac2..000000000
--- a/src/main/java/ru/javaops/masterjava/matrix/MatrixUtil.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package ru.javaops.masterjava.matrix;
-
-import java.util.Random;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-
-/**
- * gkislin
- * 03.07.2016
- */
-public class MatrixUtil {
-
- // TODO implement parallel multiplication matrixA*matrixB
- public static int[][] concurrentMultiply(int[][] matrixA, int[][] matrixB, ExecutorService executor) throws InterruptedException, ExecutionException {
- final int matrixSize = matrixA.length;
- final int[][] matrixC = new int[matrixSize][matrixSize];
-
- return matrixC;
- }
-
- // TODO optimize by https://habrahabr.ru/post/114797/
- public static int[][] singleThreadMultiply(int[][] matrixA, int[][] matrixB) {
- final int matrixSize = matrixA.length;
- final int[][] matrixC = new int[matrixSize][matrixSize];
-
- for (int i = 0; i < matrixSize; i++) {
- for (int j = 0; j < matrixSize; j++) {
- int sum = 0;
- for (int k = 0; k < matrixSize; k++) {
- sum += matrixA[i][k] * matrixB[k][j];
- }
- matrixC[i][j] = 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;
- }
-}
diff --git a/src/main/java/ru/javaops/masterjava/service/MailService.java b/src/main/java/ru/javaops/masterjava/service/MailService.java
deleted file mode 100644
index 2b6aeb294..000000000
--- a/src/main/java/ru/javaops/masterjava/service/MailService.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package ru.javaops.masterjava.service;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
-public class MailService {
- 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";
-
- public GroupResult sendToList(final String template, final Set emails) throws Exception {
- return new GroupResult(0, Collections.emptyList(), null);
- }
-
-
- // 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/test/pom.xml b/test/pom.xml
new file mode 100644
index 000000000..1891ca791
--- /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.19
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ 1.19
+ provided
+
+
+
diff --git a/src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java b/test/src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java
similarity index 94%
rename from src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java
rename to test/src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java
index ec1c8a691..1f1380674 100644
--- a/src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java
+++ b/test/src/main/java/ru/javaops/masterjava/matrix/MainMatrix.java
@@ -4,10 +4,6 @@
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;
@@ -24,13 +20,13 @@ public static void main(String[] args) throws ExecutionException, InterruptedExc
while (count < 6) {
System.out.println("Pass " + count);
long start = System.currentTimeMillis();
- final int[][] matrixC = MatrixUtil.singleThreadMultiply(matrixA, matrixB);
+ 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.concurrentMultiply(matrixA, matrixB, executor);
+ final int[][] concurrentMatrixC = MatrixUtil.concurrentMultiplyStreams(matrixA, matrixB, Runtime.getRuntime().availableProcessors() - 1);
duration = (System.currentTimeMillis() - start) / 1000.;
out("Concurrent thread time, sec: %.3f", duration);
concurrentThreadSum += duration;
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..55827fe88
--- /dev/null
+++ b/test/src/main/java/ru/javaops/masterjava/matrix/MatrixBenchmark.java
@@ -0,0 +1,85 @@
+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;
+
+@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
+ private static final int MATRIX_SIZE = 1000;
+
+ @Param({"3", "4", "10"})
+ private int threadNumber;
+
+ private static int[][] matrixA;
+ private static int[][] matrixB;
+
+ @Setup
+ public void setUp() {
+ matrixA = MatrixUtil.create(MATRIX_SIZE);
+ matrixB = MatrixUtil.create(MATRIX_SIZE);
+ }
+
+ private ExecutorService executor;
+
+ 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[][] singleThreadMultiplyOpt2() throws Exception {
+ return MatrixUtil.singleThreadMultiplyOpt(matrixA, matrixB);
+ }
+
+ @Benchmark
+ public int[][] concurrentMultiplyStreams() throws Exception {
+ return MatrixUtil.concurrentMultiplyStreams(matrixA, matrixB, threadNumber);
+ }
+
+ // @Benchmark
+ public int[][] concurrentMultiply() throws Exception {
+ return MatrixUtil.concurrentMultiply(matrixA, matrixB, executor);
+ }
+
+ @Benchmark
+ public int[][] concurrentMultiply2() throws Exception {
+ return MatrixUtil.concurrentMultiply2(matrixA, matrixB, executor);
+ }
+
+ @Setup
+ public void setup() {
+ executor = Executors.newFixedThreadPool(threadNumber);
+ }
+
+ @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..9ec0ea887
--- /dev/null
+++ b/test/src/main/java/ru/javaops/masterjava/matrix/MatrixUtil.java
@@ -0,0 +1,157 @@
+package ru.javaops.masterjava.matrix;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.*;
+import java.util.stream.IntStream;
+
+public class MatrixUtil {
+
+ public static int[][] concurrentMultiplyStreams(int[][] matrixA, int[][] matrixB, int threadNumber)
+ throws InterruptedException, ExecutionException {
+
+ final int matrixSize = matrixA.length;
+ final int[][] matrixC = new int[matrixSize][matrixSize];
+
+ new ForkJoinPool(threadNumber).submit(
+ () -> IntStream.range(0, matrixSize)
+ .parallel()
+ .forEach(row -> {
+ final int[] rowA = matrixA[row];
+ final int[] rowC = matrixC[row];
+
+ 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];
+ }
+ }
+ })).get();
+
+ return matrixC;
+ }
+
+ public static int[][] concurrentMultiply(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[][] concurrentMultiply2(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[][] singleThreadMultiplyOpt2(int[][] matrixA, int[][] matrixB) {
+ final int matrixSize = matrixA.length;
+ final int[][] matrixC = new int[matrixSize][matrixSize];
+
+ for (int row = 0; row < matrixSize; row++) {
+ final int[] rowA = matrixA[row];
+ final int[] rowC = matrixC[row];
+
+ 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];
+ }
+ }
+ }
+ 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;
+ }
+}
diff --git a/web/common-web/pom.xml b/web/common-web/pom.xml
new file mode 100644
index 000000000..37ca1f642
--- /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.8.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/pom.xml b/web/pom.xml
new file mode 100644
index 000000000..ddb4a428b
--- /dev/null
+++ b/web/pom.xml
@@ -0,0 +1,16 @@
+
+ 4.0.0
+
+ ru.javaops
+ web
+ pom
+ 1.0-SNAPSHOT
+ Web
+
+
+ common-web
+ upload
+ webapp
+
+
diff --git a/web/upload/pom.xml b/web/upload/pom.xml
new file mode 100644
index 000000000..38f6b5c07
--- /dev/null
+++ b/web/upload/pom.xml
@@ -0,0 +1,47 @@
+
+
+ 4.0.0
+
+
+ ru.javaops
+ parent-web
+ ../../parent-web/pom.xml
+ 1.0-SNAPSHOT
+
+
+ upload
+ 1.0-SNAPSHOT
+ war
+ Upload
+
+
+ upload
+
+
+
+
+ ${project.groupId}
+ persist
+ ${project.version}
+
+
+ com.j2html
+ j2html
+ 1.2.0
+
+
+ com.google.guava
+ guava
+
+
+
+
+ ${project.groupId}
+ common-web
+ ${project.version}
+
+
+
+
diff --git a/web/upload/src/main/java/ru/javaops/masterjava/upload/CityProcessor.java b/web/upload/src/main/java/ru/javaops/masterjava/upload/CityProcessor.java
new file mode 100644
index 000000000..a288e08a7
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/upload/CityProcessor.java
@@ -0,0 +1,32 @@
+package ru.javaops.masterjava.upload;
+
+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;
+
+@Slf4j
+public class CityProcessor {
+ 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(ref, processor.getText()));
+ }
+ }
+ log.info("Insert batch " + newCities);
+ cityDao.insertBatch(newCities);
+ return cityDao.getAsMap();
+ }
+}
diff --git a/web/upload/src/main/java/ru/javaops/masterjava/upload/PayloadProcessor.java b/web/upload/src/main/java/ru/javaops/masterjava/upload/PayloadProcessor.java
new file mode 100644
index 000000000..38af0f35a
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/upload/PayloadProcessor.java
@@ -0,0 +1,33 @@
+package ru.javaops.masterjava.upload;
+
+import lombok.AllArgsConstructor;
+import lombok.val;
+import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
+
+import javax.xml.bind.JAXBException;
+import javax.xml.stream.XMLStreamException;
+import java.io.InputStream;
+import java.util.List;
+
+public class PayloadProcessor {
+ private final CityProcessor cityProcessor = new CityProcessor();
+ private final UserProcessor userProcessor = new UserProcessor();
+
+ @AllArgsConstructor
+ public static class FailedEmails {
+ public String emailsOrRange;
+ public String reason;
+
+ @Override
+ public String toString() {
+ return emailsOrRange + " : " + reason;
+ }
+ }
+
+
+ public List process(InputStream is, int chunkSize) throws XMLStreamException, JAXBException {
+ final StaxStreamProcessor processor = new StaxStreamProcessor(is);
+ val cities = cityProcessor.process(processor);
+ return userProcessor.process(processor, cities, chunkSize);
+ }
+}
\ No newline at end of file
diff --git a/web/upload/src/main/java/ru/javaops/masterjava/upload/UploadServlet.java b/web/upload/src/main/java/ru/javaops/masterjava/upload/UploadServlet.java
new file mode 100644
index 000000000..fafe55db0
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/upload/UploadServlet.java
@@ -0,0 +1,67 @@
+package ru.javaops.masterjava.upload;
+
+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(urlPatterns = "/", loadOnStartup = 1)
+@MultipartConfig(fileSizeThreshold = 1024 * 1024 * 10) //10 MB in memory limit
+@Slf4j
+public class UploadServlet extends HttpServlet {
+ private static final int CHUNK_SIZE = 2000;
+
+ private final PayloadProcessor payloadProcessor = new PayloadProcessor();
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ out(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 = payloadProcessor.process(is, chunkSize);
+ log.info("Failed users: " + failed);
+ final WebContext webContext =
+ new WebContext(req, resp, req.getServletContext(), req.getLocale(),
+ ImmutableMap.of("users", failed));
+ engine.process("result", webContext, resp.getWriter());
+ return;
+ }
+ }
+ } catch (Exception e) {
+ log.info(e.getMessage(), e);
+ message = e.toString();
+ }
+ out(req, resp, message, chunkSize);
+ }
+
+ private void out(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("upload", webContext, resp.getWriter());
+ }
+}
diff --git a/web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java b/web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java
new file mode 100644
index 000000000..170521ac9
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/upload/UserProcessor.java
@@ -0,0 +1,92 @@
+package ru.javaops.masterjava.upload;
+
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+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.type.UserFlag;
+import ru.javaops.masterjava.upload.PayloadProcessor.FailedEmails;
+import ru.javaops.masterjava.xml.schema.ObjectFactory;
+import ru.javaops.masterjava.xml.util.JaxbParser;
+import ru.javaops.masterjava.xml.util.StaxStreamProcessor;
+
+import javax.xml.bind.JAXBException;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.XMLEvent;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+@Slf4j
+public class UserProcessor {
+ private static final int NUMBER_THREADS = 4;
+
+ private static final JaxbParser jaxbParser = new JaxbParser(ObjectFactory.class);
+ private static UserDao userDao = DBIProvider.getDao(UserDao.class);
+
+ private ExecutorService executorService = Executors.newFixedThreadPool(NUMBER_THREADS);
+
+ /*
+ * return failed users chunks
+ */
+ public List process(final StaxStreamProcessor processor, Map cities, int chunkSize) throws XMLStreamException, JAXBException {
+ log.info("Start processing with chunkSize=" + chunkSize);
+
+ Map>> chunkFutures = new LinkedHashMap<>(); // ordered map (emailRange -> chunk future)
+
+ int id = userDao.getSeqAndSkip(chunkSize);
+ List chunk = new ArrayList<>(chunkSize);
+ val unmarshaller = jaxbParser.createUnmarshaller();
+ List failed = new ArrayList<>();
+
+ while (processor.doUntil(XMLEvent.START_ELEMENT, "User")) {
+ String cityRef = processor.getAttribute("city"); // unmarshal doesn't get city ref
+ ru.javaops.masterjava.xml.schema.User xmlUser = unmarshaller.unmarshal(processor.getReader(), ru.javaops.masterjava.xml.schema.User.class);
+ if (cities.get(cityRef) == null) {
+ failed.add(new FailedEmails(xmlUser.getEmail(), "City '" + cityRef + "' is not present in DB"));
+ } else {
+ final User user = new User(id++, xmlUser.getValue(), xmlUser.getEmail(), UserFlag.valueOf(xmlUser.getFlag().value()), cityRef);
+ chunk.add(user);
+ if (chunk.size() == chunkSize) {
+ addChunkFutures(chunkFutures, chunk);
+ chunk = new ArrayList<>(chunkSize);
+ id = userDao.getSeqAndSkip(chunkSize);
+ }
+ }
+ }
+
+ if (!chunk.isEmpty()) {
+ addChunkFutures(chunkFutures, chunk);
+ }
+
+ List allAlreadyPresents = new ArrayList<>();
+ chunkFutures.forEach((emailRange, future) -> {
+ try {
+ List alreadyPresentsInChunk = future.get();
+ log.info("{} successfully executed with already presents: {}", emailRange, alreadyPresentsInChunk);
+ allAlreadyPresents.addAll(alreadyPresentsInChunk);
+ } catch (InterruptedException | ExecutionException e) {
+ log.error(emailRange + " failed", e);
+ failed.add(new FailedEmails(emailRange, e.toString()));
+ }
+ });
+ if (!allAlreadyPresents.isEmpty()) {
+ failed.add(new FailedEmails(allAlreadyPresents.toString(), "already presents"));
+ }
+ return failed;
+ }
+
+ private void addChunkFutures(Map>> chunkFutures, List chunk) {
+ String emailRange = String.format("[%s-%s]", chunk.get(0).getEmail(), chunk.get(chunk.size() - 1).getEmail());
+ Future> future = executorService.submit(() -> userDao.insertAndGetConflictEmails(chunk));
+ chunkFutures.put(emailRange, future);
+ log.info("Submit chunk: " + emailRange);
+ }
+}
diff --git a/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/CityType.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/CityType.java
new file mode 100644
index 000000000..0615e8c4e
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/CityType.java
@@ -0,0 +1,87 @@
+package ru.javaops.masterjava.xml.schema;
+
+import javax.xml.bind.annotation.*;
+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/upload/src/main/java/ru/javaops/masterjava/xml/schema/FlagType.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/FlagType.java
new file mode 100644
index 000000000..fe225abde
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/FlagType.java
@@ -0,0 +1,53 @@
+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/upload/src/main/java/ru/javaops/masterjava/xml/schema/GroupType.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/GroupType.java
new file mode 100644
index 000000000..42673fe24
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/GroupType.java
@@ -0,0 +1,39 @@
+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/upload/src/main/java/ru/javaops/masterjava/xml/schema/ObjectFactory.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/ObjectFactory.java
new file mode 100644
index 000000000..c519c975d
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/ObjectFactory.java
@@ -0,0 +1,108 @@
+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/upload/src/main/java/ru/javaops/masterjava/xml/schema/Payload.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/Payload.java
new file mode 100644
index 000000000..73f672ec9
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/Payload.java
@@ -0,0 +1,327 @@
+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>
+ * <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/upload/src/main/java/ru/javaops/masterjava/xml/schema/Project.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/Project.java
new file mode 100644
index 000000000..14c20a08e
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/Project.java
@@ -0,0 +1,215 @@
+package ru.javaops.masterjava.xml.schema;
+
+import javax.xml.bind.annotation.*;
+import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+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>
+ * <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/upload/src/main/java/ru/javaops/masterjava/xml/schema/User.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/User.java
new file mode 100644
index 000000000..1959b96bd
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/schema/User.java
@@ -0,0 +1,179 @@
+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/upload/src/main/java/ru/javaops/masterjava/xml/util/JaxbMarshaller.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/JaxbMarshaller.java
new file mode 100644
index 000000000..507825dda
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/JaxbMarshaller.java
@@ -0,0 +1,42 @@
+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;
+
+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) {
+ try {
+ marshaller.setProperty(prop, value);
+ } catch (PropertyException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public 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 void marshal(Object instance, Writer writer) throws JAXBException {
+ marshaller.marshal(instance, writer);
+ }
+}
\ No newline at end of file
diff --git a/web/upload/src/main/java/ru/javaops/masterjava/xml/util/JaxbParser.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/JaxbParser.java
new file mode 100644
index 000000000..8aff55510
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/JaxbParser.java
@@ -0,0 +1,80 @@
+package ru.javaops.masterjava.xml.util;
+
+import org.xml.sax.SAXException;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+
+
+/**
+ * Marshalling/Unmarshalling JAXB facade
+ */
+public class JaxbParser {
+
+ private JAXBContext ctx;
+ protected Schema schema;
+
+ public JaxbParser(Class... classesToBeBound) {
+ try {
+ init(JAXBContext.newInstance(classesToBeBound));
+ } catch (JAXBException e) {
+ throw new IllegalStateException(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 IllegalStateException(e);
+ }
+ }
+
+ private void init(JAXBContext ctx) {
+ this.ctx = ctx;
+ }
+
+ // https://stackoverflow.com/a/7400735/548473
+ public JaxbMarshaller createMarshaller() {
+ try {
+ JaxbMarshaller marshaller = new JaxbMarshaller(ctx);
+ if (schema != null) {
+ marshaller.setSchema(schema);
+ }
+ return marshaller;
+ } catch (JAXBException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ // https://stackoverflow.com/a/7400735/548473
+ public JaxbUnmarshaller createUnmarshaller() {
+ try {
+ JaxbUnmarshaller unmarshaller = new JaxbUnmarshaller(ctx);
+ if (schema != null) {
+ unmarshaller.setSchema(schema);
+ }
+ return unmarshaller;
+ } catch (JAXBException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public void setSchema(Schema schema) {
+ this.schema = 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/upload/src/main/java/ru/javaops/masterjava/xml/util/JaxbUnmarshaller.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/JaxbUnmarshaller.java
new file mode 100644
index 000000000..f9f29c2c9
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/JaxbUnmarshaller.java
@@ -0,0 +1,38 @@
+package ru.javaops.masterjava.xml.util;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.validation.Schema;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+
+public class JaxbUnmarshaller {
+ private Unmarshaller unmarshaller;
+
+ public JaxbUnmarshaller(JAXBContext ctx) throws JAXBException {
+ unmarshaller = ctx.createUnmarshaller();
+ }
+
+ public void setSchema(Schema schema) {
+ unmarshaller.setSchema(schema);
+ }
+
+ public T unmarshal(InputStream is) throws JAXBException {
+ return (T) unmarshaller.unmarshal(is);
+ }
+
+ public T unmarshal(Reader reader) throws JAXBException {
+ return (T) unmarshaller.unmarshal(reader);
+ }
+
+ public T unmarshal(String str) throws JAXBException {
+ return (T) unmarshal(new StringReader(str));
+ }
+
+ public T unmarshal(XMLStreamReader reader, Class elementClass) throws JAXBException {
+ return unmarshaller.unmarshal(reader, elementClass).getValue();
+ }
+}
\ No newline at end of file
diff --git a/web/upload/src/main/java/ru/javaops/masterjava/xml/util/Schemas.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/Schemas.java
new file mode 100644
index 000000000..42f41df80
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/Schemas.java
@@ -0,0 +1,48 @@
+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;
+
+
+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/upload/src/main/java/ru/javaops/masterjava/xml/util/StaxStreamProcessor.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/StaxStreamProcessor.java
new file mode 100644
index 000000000..5878118c0
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/StaxStreamProcessor.java
@@ -0,0 +1,79 @@
+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;
+
+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/upload/src/main/java/ru/javaops/masterjava/xml/util/XPathProcessor.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/XPathProcessor.java
new file mode 100644
index 000000000..63baae5d1
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/XPathProcessor.java
@@ -0,0 +1,58 @@
+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;
+
+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/upload/src/main/java/ru/javaops/masterjava/xml/util/XsltProcessor.java b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/XsltProcessor.java
new file mode 100644
index 000000000..083febb00
--- /dev/null
+++ b/web/upload/src/main/java/ru/javaops/masterjava/xml/util/XsltProcessor.java
@@ -0,0 +1,47 @@
+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;
+
+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/upload/src/main/resources/cities.xsl b/web/upload/src/main/resources/cities.xsl
new file mode 100644
index 000000000..55380ad23
--- /dev/null
+++ b/web/upload/src/main/resources/cities.xsl
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/upload/src/main/resources/groups.xsl b/web/upload/src/main/resources/groups.xsl
new file mode 100644
index 000000000..afec4de2e
--- /dev/null
+++ b/web/upload/src/main/resources/groups.xsl
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+ groups
+
+
+
+
+ groups
+
+
+
+ Group
+ Type
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/upload/src/main/resources/payload.xsd b/web/upload/src/main/resources/payload.xsd
new file mode 100644
index 000000000..9dbee7b65
--- /dev/null
+++ b/web/upload/src/main/resources/payload.xsd
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/upload/src/main/webapp/WEB-INF/templates/exception.html b/web/upload/src/main/webapp/WEB-INF/templates/exception.html
new file mode 100644
index 000000000..2b5e6fe5a
--- /dev/null
+++ b/web/upload/src/main/webapp/WEB-INF/templates/exception.html
@@ -0,0 +1,20 @@
+
+
+
+ Application error
+
+
+
+
+
+
+
Application error:
+
+
exception.message
+
+
+
+
+
\ No newline at end of file
diff --git a/web/upload/src/main/webapp/WEB-INF/templates/result.html b/web/upload/src/main/webapp/WEB-INF/templates/result.html
new file mode 100644
index 000000000..7fe855cf3
--- /dev/null
+++ b/web/upload/src/main/webapp/WEB-INF/templates/result.html
@@ -0,0 +1,14 @@
+
+
+
+ Failed users
+
+
+
+Failed users
+
+
+
diff --git a/web/upload/src/main/webapp/WEB-INF/templates/upload.html b/web/upload/src/main/webapp/WEB-INF/templates/upload.html
new file mode 100644
index 000000000..c4c2641ee
--- /dev/null
+++ b/web/upload/src/main/webapp/WEB-INF/templates/upload.html
@@ -0,0 +1,22 @@
+
+
+
+ Upload XML
+
+
+
+
+
diff --git a/web/upload/src/test/java/ru/javaops/masterjava/MainXml.java b/web/upload/src/test/java/ru/javaops/masterjava/MainXml.java
new file mode 100644
index 000000000..ce2e3a902
--- /dev/null
+++ b/web/upload/src/test/java/ru/javaops/masterjava/MainXml.java
@@ -0,0 +1,136 @@
+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.*;
+
+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 static com.google.common.base.Strings.nullToEmpty;
+import static j2html.TagCreator.*;
+
+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);
+
+ 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);
+
+ 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);
+ JaxbUnmarshaller unmarshaller = parser.createUnmarshaller();
+ parser.setSchema(Schemas.ofClasspath("payload.xsd"));
+ Payload payload;
+ try (InputStream is = payloadUrl.openStream()) {
+ payload = unmarshaller.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()))
+ .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<>();
+
+ // Projects loop
+ projects:
+ 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);
+
+ JaxbParser parser = new JaxbParser(ObjectFactory.class);
+ JaxbUnmarshaller unmarshaller = parser.createUnmarshaller();
+ while (processor.doUntil(XMLEvent.START_ELEMENT, "User")) {
+ String groupRefs = processor.getAttribute("groupRefs");
+ if (!Collections.disjoint(groupNames, Splitter.on(' ').splitToList(nullToEmpty(groupRefs)))) {
+ User user = unmarshaller.unmarshal(processor.getReader(), User.class);
+ 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/upload/src/test/java/ru/javaops/masterjava/xml/util/JaxbParserTest.java b/web/upload/src/test/java/ru/javaops/masterjava/xml/util/JaxbParserTest.java
new file mode 100644
index 000000000..4754b17d6
--- /dev/null
+++ b/web/upload/src/test/java/ru/javaops/masterjava/xml/util/JaxbParserTest.java
@@ -0,0 +1,46 @@
+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;
+
+public class JaxbParserTest {
+ // https://google.github.io/styleguide/javaguide.html#s5.2.4-constant-names
+ private static final JaxbParser jaxbParser;
+ private static final JaxbMarshaller marshaller;
+ private static final JaxbUnmarshaller unmarshaller;
+
+ static {
+ jaxbParser = new JaxbParser(ObjectFactory.class);
+ jaxbParser.setSchema(Schemas.ofClasspath("payload.xsd"));
+ marshaller = jaxbParser.createMarshaller();
+ unmarshaller = jaxbParser.createUnmarshaller();
+ }
+
+ @Test
+ public void testPayload() throws Exception {
+// JaxbParserTest.class.getResourceAsStream("/city.xml")
+ Payload payload = unmarshaller.unmarshal(
+ Resources.getResource("payload.xml").openStream());
+ String strPayload = marshaller.marshal(payload);
+ jaxbParser.validate(strPayload);
+ System.out.println(strPayload);
+ }
+
+ @Test
+ public void testCity() throws Exception {
+ JAXBElement cityElement = unmarshaller.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 = marshaller.marshal(cityElement2);
+ jaxbParser.validate(strCity);
+ System.out.println(strCity);
+ }
+}
\ No newline at end of file
diff --git a/web/upload/src/test/java/ru/javaops/masterjava/xml/util/StaxStreamProcessorTest.java b/web/upload/src/test/java/ru/javaops/masterjava/xml/util/StaxStreamProcessorTest.java
new file mode 100644
index 000000000..dff3c7d5b
--- /dev/null
+++ b/web/upload/src/test/java/ru/javaops/masterjava/xml/util/StaxStreamProcessorTest.java
@@ -0,0 +1,36 @@
+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;
+
+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);
+ }
+ }
+ }
+}
diff --git a/web/upload/src/test/java/ru/javaops/masterjava/xml/util/XPathProcessorTest.java b/web/upload/src/test/java/ru/javaops/masterjava/xml/util/XPathProcessorTest.java
new file mode 100644
index 000000000..d5c26506e
--- /dev/null
+++ b/web/upload/src/test/java/ru/javaops/masterjava/xml/util/XPathProcessorTest.java
@@ -0,0 +1,26 @@
+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;
+
+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())
+ );
+ }
+ }
+}
diff --git a/web/upload/src/test/java/ru/javaops/masterjava/xml/util/XsltProcessorTest.java b/web/upload/src/test/java/ru/javaops/masterjava/xml/util/XsltProcessorTest.java
new file mode 100644
index 000000000..d7f42a699
--- /dev/null
+++ b/web/upload/src/test/java/ru/javaops/masterjava/xml/util/XsltProcessorTest.java
@@ -0,0 +1,18 @@
+package ru.javaops.masterjava.xml.util;
+
+import com.google.common.io.Resources;
+import org.junit.Test;
+
+import java.io.InputStream;
+
+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/upload/src/test/resources/city.xml b/web/upload/src/test/resources/city.xml
new file mode 100644
index 000000000..f6a4f9738
--- /dev/null
+++ b/web/upload/src/test/resources/city.xml
@@ -0,0 +1,4 @@
+Санкт-Петербург
+
diff --git a/web/upload/src/test/resources/payload.xml b/web/upload/src/test/resources/payload.xml
new file mode 100644
index 000000000..3cf63093c
--- /dev/null
+++ b/web/upload/src/test/resources/payload.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ Topjava
+
+
+
+
+
+ Masterjava
+
+
+
+
+ Санкт-Петербург
+ Москва
+ Киев
+ Минск
+
+
+ Full Name
+ Admin
+ Deleted
+ User1
+ User2
+ User3
+
+
diff --git a/web/upload/src/test/resources/payload_bad.xml b/web/upload/src/test/resources/payload_bad.xml
new file mode 100644
index 000000000..72a2e0422
--- /dev/null
+++ b/web/upload/src/test/resources/payload_bad.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ Topjava
+
+
+
+
+
+ Masterjava
+
+
+
+
+ Санкт-Петербург
+ Москва
+ Киев
+ Минск
+
+
+ Full Name
+ Admin
+ Deleted
+ User1
+ User2
+ User3
+
+
\ No newline at end of file
diff --git a/web/webapp/pom.xml b/web/webapp/pom.xml
new file mode 100644
index 000000000..b543f44ec
--- /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}
+
+
+
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..0ae713167
--- /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