Category: Java

Java Serialization – static nested, inner and anonymous classes

public class SerializationUtils {
    public static ByteArrayOutputStream serialize(Object object) throws IOException {
        System.out.print("Serializing: " + object);
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(bout);
        out.writeObject(object);
        out.close();
        return bout;
    }

    public static void deserialize(ByteArrayOutputStream bout) throws IOException, 
                                                           ClassNotFoundException {
        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream in = new ObjectInputStream(bin);
        Object readObject = in.readObject();
        System.out.println(", deserialized: " + readObject);
    }
}

Junit:

public class MySerializableClass implements Serializable {
}

public class MySerializationTest {
    private String myJunitStringField = "hello";

    static class MyStaticNestedSerializableClass implements Serializable {
        public int getLength(String s) {
            // Error: java: non-static variable this cannot be referenced from a static context
            //System.out.println(MySerializationTest.this);// static nested class does not have reference to outer class
            return s.length();
        }
    }

    interface MySerializableFunction<T, R> extends Function<T, R>, Serializable {}

    @Test
    public void testSerialization() throws IOException, ClassNotFoundException {
       deserialize(serialize(1));     // Serializing: 1, deserialized: 1
       deserialize(serialize("aaa")); // Serializing: aaa, deserialized: aaa
       deserialize(serialize(new MySerializableClass())); // Serializing: com.bawi.serialization.MySerializableClass@4520ebad, deserialized: com.bawi.serialization.MySerializableClass@4f933fd1
       deserialize(serialize(new MyStaticNestedSerializableClass())); // Serializing: com.bawi.serialization.MySerializationTest$MyStaticNestedSerializableClass@4520ebad, deserialized: com.bawi.serialization.MySerializationTest$MyStaticNestedSerializableClass@4f933fd1
       deserialize(serialize((MySerializableFunction<String, Integer>) s -> s.length()));       
    }

    static class MyStaticNestedNonSerializableClass {} // missing implements Serializable

    class MyInnerSerializableClass implements Serializable {
        public int getLength(String s) {
            System.out.println("myJunitStringField=" + myJunitStringField); // direct access to private outer class fields
            System.out.println(MySerializationTest.this); // reference to enclosing instance of the outer class
            return s.length();
        }
    }

    @Test//(expected = java.io.NotSerializableException.class)
    public void failWithNotSerializableException() throws IOException, ClassNotFoundException {
        // NotSerializableException: com.bawi.serialization.MySerializationTest$MyStaticNestedNonSerializableClass
//        deserialize(serialize(new MyStaticNestedNonSerializableClass()));

        // inner (non static nested) class has reference to non-serializable outer class (junit com.bawi.serialization.MySerializationTest)

        MyInnerSerializableClass myInnerSerializableClass = new MyInnerSerializableClass();
        myInnerSerializableClass.getLength("a"); // myJunitStringField=hello
                                                 // com.bawi.serialization.MySerializationTest@1324409e
        // java.io.NotSerializableException: com.bawi.serialization.MySerializationTest
//        deserialize(serialize(myInnerSerializableClass));

        class MyLocalSerializableClass implements Serializable {
            @Override
            public String toString() {
                return MySerializationTest.this + "";
            }
        }
        // java.io.NotSerializableException: com.bawi.serialization.MySerializationTest
//        deserialize(serialize(new MyLocalSerializableClass()));


        //anonymous is inner class: java.io.NotSerializableException: com.bawi.serialization.MySerializationTest
//        deserialize(serialize(new Serializable(){
//            @Override
//            public String toString() {
//                return MySerializationTest.this + "";
//            }
//        }));

        // like local classes, anonymous classes have access to local variables of enclosing scope that are effectively final
        // anonymous class definition (within {}) is an expression and must be part of the statement and end with semicolon
        MyStaticNestedSerializableClass myAnonymousInnerSerializableClass = new MyStaticNestedSerializableClass() {
            @Override
            public int getLength(String s) {
                System.out.println("myJunitStringField=" + myJunitStringField); // direct access to private outer class fields
                System.out.println(MySerializationTest.this);
                return s.length() + 2;
            }
        };
        myAnonymousInnerSerializableClass.getLength("aa");
        // java.io.NotSerializableException: com.bawi.serialization.MySerializationTest
//        deserialize(serialize(myAnonymousInnerSerializableClass));

        //java.io.NotSerializableException: com.bawi.serialization.MySerializationTest$$Lambda$1/1576861390
//        deserialize(serialize((Function<String, Integer>) s -> s.length()));
    }
}

If we make MySerializationTest implements Serializable then we will not get java.io.NotSerializableException: com.bawi.serialization.MySerializationTest

Generics with java collection

import java.util.Arrays;
import java.util.List;

public class MyGenerics {
    static class SuperClass {}
    static class SubClass extends SuperClass {}

    public static void main(String[] args) {
// get1:
        List<SuperClass> superClasses = get1(Arrays.asList(new SuperClass()));

        // Error: java: incompatible types: List<MyGenerics.SuperClass> cannot be converted to List<MyGenerics.SubClass>
        //List<SubClass> subClasses = get1(Arrays.asList(new SubClass()));

// get2 (none compiles):
        // Error: java: incompatible types: List<capture#1 of ? extends MyGenerics.SuperClass> cannot be converted to List<MyGenerics.SuperClass>
        //List<SuperClass> superClasses2 = get2(Arrays.asList(new SuperClass()));

        // Error: java: incompatible types: List<capture#1 of ? extends MyGenerics.SuperClass> cannot be converted to List<MyGenerics.SubClass>
        //List<SubClass> subClasses2 = get2(Arrays.asList(new SubClass()));

// get3: (all compiles):
        List<SuperClass> superClasses3 = get3(Arrays.asList(new SuperClass()));
        List<SubClass> subClasses3 = get3(Arrays.asList(new SubClass()));
        List<SuperClass> subClasses3_1 = get3(Arrays.asList(new SubClass()));
        List<SuperClass> subClasses3_2 = get3(Arrays.asList(new SubClass(), new SuperClass()));
    }

    static List<SuperClass> get1(List<SuperClass> input) { return null; }

    static List<? extends SuperClass> get2(List<? extends SuperClass> input) { return null; }

    static <T extends SuperClass> List<T> get3(List<T> input) { return null; }
}

Java regex examples

Given xml

<?xml version="1.0" encoding="UTF-8" ?>
<MyRootElement xmlns="http://bawi.com/my-schema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://www.bawi.com/my-schema my-schema.xsd"
               xmlns:imp="http://bawi.com/my-imported-schema">

    <MyStringElement>Text</MyStringElement>
    <MyDuplicatedElement myStringAttribute="aaa">AAA</MyDuplicatedElement>

    <MyDuplicatedElementWithImportedType myImportedIntAttribute="abc">ABC</MyDuplicatedElementWithImportedType>

    <imp:MyDuplicatedElement myImportedIntAttribute="222">BBB</imp:MyDuplicatedElement>
    <MyDuplicatedElement xmlns="http://bawi.com/my-imported-schema" myImportedIntAttribute="333">CCC</MyDuplicatedElement>
    <i:MyDuplicatedElement xmlns:i="http://bawi.com/my-imported-schema" myImportedIntAttribute="444">DDD</i:MyDuplicatedElement>

    <MyElementWithIncludedType myIncludedIntAttribute="555">EEE</MyElementWithIncludedType>
</MyRootElement>

Let’s examine regex:

System.out.println("All MyDuplicatedElement elements including namespaces");
m = Pattern.compile("<(\\w+:)?\\bMyDuplicatedElement\\b.*?\\bMyDuplicatedElement>").matcher(xml);
while (m.find()) {
    System.out.println("found: " + m.group());
}
found: <MyDuplicatedElement myStringAttribute="aaa">AAA</MyDuplicatedElement>
found: <imp:MyDuplicatedElement myImportedIntAttribute="222">BBB</imp:MyDuplicatedElement>
found: <MyDuplicatedElement xmlns="http://bawi.com/my-imported-schema" myImportedIntAttribute="333">CCC</MyDuplicatedElement>
found: <i:MyDuplicatedElement xmlns:i="http://bawi.com/my-imported-schema" myImportedIntAttribute="444">DDD</i:MyDuplicatedElement>
System.out.println("Attributes for MyDuplicatedElement (attribute value without :)");
m = Pattern.compile("<(\\w+:)?\\bMyDuplicatedElement\\b.*?(\\w+=\"[^:\"]*\").*?\\bMyDuplicatedElement>").matcher(xml);
while (m.find()) {
    System.out.println("found: " + m.group(2));
}
Attributes for MyDuplicatedElement (attribute value without :)
found: myStringAttribute="aaa"
found: myImportedIntAttribute="222"
found: myImportedIntAttribute="333"
found: myImportedIntAttribute="444"
System.out.println("Attributes for MyDuplicatedElement (negative lookahead)");
m = Pattern.compile("<(\\w+:)?\\bMyDuplicatedElement\\b.*?(\\s+((?!xmlns)[\\w]+=\"[^\"]*\")).*?\\bMyDuplicatedElement>").matcher(xml);
while (m.find()) {
    System.out.println("found: " + m.group(3));
}
Attributes for MyDuplicatedElement (negative lookahead)
found: myStringAttribute="aaa"
found: myImportedIntAttribute="222"
found: myImportedIntAttribute="333"
found: myImportedIntAttribute="444"
System.out.println("Values of attributes for MyDuplicatedElement");
m = Pattern.compile("<(\\w+:)?MyDuplicatedElement.*?(\\w+=\"([^:\"]*)\").*?MyDuplicatedElement>").matcher(xml);
while (m.find()) {
    System.out.println("found: " + m.group(3));
}
Values of attributes for MyDuplicatedElement
found: aaa
found: abc
found: 333
found: 444
System.out.println("Attributes with name containing Attribute");
m = Pattern.compile("\\w+Attribute=\"[^\"]*\"").matcher(xml);
while (m.find()) {
    System.out.println(m.group());
}
Attributes with name containing Attribute
myStringAttribute="aaa"
myImportedIntAttribute="abc"
myImportedIntAttribute="222"
myImportedIntAttribute="333"
myImportedIntAttribute="444"
myIncludedIntAttribute="555"

Notes:
m.group() – return string matching whole pattern
m.group(1) – return string matching content of first (…) in regex
m.group(2) – return string matching content of second / more nested (…) in regex

\\w – word [A-Za-z_]
\\w+ – at least one word
\\b \\b – word boundary (characters inside must me of word type)
.* – any character zero more times
[^\”]* – zero or more chars different than double quote
.*? – shortest match of any characters, ? means 1 or none match, note without ? the pattern

m = Pattern.compile("<(\\w+:)?\\bMyDuplicatedElement\\b.*\\bMyDuplicatedElement>").matcher(xml);

returns longest match with is one long string:

<MyDuplicatedElement myStringAttribute="aaa">AAA</MyDuplicatedElement>    <MyDuplicatedElementWithImportedType myImportedIntAttribute="abc">ABC</MyDuplicatedElementWithImportedType>    <imp:MyDuplicatedElement myImportedIntAttribute="222">BBB</imp:MyDuplicatedElement>    <MyDuplicatedElement xmlns="http://bawi.com/my-imported-schema" myImportedIntAttribute="333">CCC</MyDuplicatedElement>    <i:MyDuplicatedElement xmlns:i="http://bawi.com/my-imported-schema" myImportedIntAttribute="444">DDD</i:MyDuplicatedElement> 

\\s+((?!xmlns)[\\w]+=\”[^\”]*\”) – negative lookahead – match all strings preceeded by whitespace and not starting with xmlns sequence

http forward and redirect in servlet with Intellij dynamic code classes reload to Jetty

1. Create simplest project with HttpServlet and JSP.

me@MacBook:~/dev/my-servlet$ find . -type f | grep -v .idea | grep -v target | grep -v *.iml
./pom.xml
./src/main/webapp/index.html
./src/main/webapp/redirected.jsp
./src/main/webapp/forwarded.jsp
./src/main/java/com/bawi/servlet/MyServlet.java

./pom.xml:

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.bawi</groupId>
    <artifactId>my-servlet</artifactId>
    <packaging>war</packaging>
    <version>0.1-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.1</version>
            </plugin>

            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>9.4.35.v20201120</version>
            </plugin>

        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

./src/main/java/com/bawi/servlet/MyServlet.java:

package com.bawi.servlet;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
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 java.util.Date;

@WebServlet(urlPatterns = {"/do"})
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String action = req.getParameter("action");
        if (action == null) {
            resp.setContentType("text/plain;charset=UTF-8");
            ServletOutputStream out = resp.getOutputStream();
            out.print("Hello from MyServlet!" +  new Date());
            return;
        }

        switch (action) {
            case "forward": {
                ServletContext servletContext = getServletContext();
                RequestDispatcher dispatcher = servletContext.getRequestDispatcher("/forwarded.jsp");
                dispatcher.forward(req, resp);
                break;
            }
            case "redirect": {
                resp.sendRedirect(req.getContextPath() + "/redirected.jsp");
                break;
            }
        }
    }
}

./src/main/webapp/index.html:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Home page</title>
    </head>
    <body>
        <p>This is home page.</p>
        <p>Call <a href="/do?action=forward">forward</a></p>
        <p>Call <a href="/do?action=redirect">redirect</a></p>
    </body>
</html>

./src/main/webapp/forwarded.jsp:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Forwarded page</title>
    </head>
    <body>
        <p>Action parameter: <%= request.getParameter("action") %>. This is forwarded page. Go to <a href="/">home</a></p>
    </body>
</html>

./src/main/webapp/redirected.jsp:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Redirected page</title>
    </head>
    <body>
        <p>Action parameter: <%= request.getParameter("action") %>. This is redirected page. Go to <a href="/">home</a></p>
    </body>
</html>
me@MacBook:~/dev/my-servlet$ mvn jetty:run
[INFO] webAppSourceDirectory not set. Trying src/main/webapp
[INFO] Reload Mechanic: automatic
[INFO] nonBlocking:false
[INFO] Classes = /Users/me/dev/my-servlet/target/classes
[INFO] Configuring Jetty for project: my-servlet
[INFO] Logging initialized @3943ms to org.eclipse.jetty.util.log.Slf4jLog
[INFO] Context path = /
[INFO] Tmp directory = /Users/me/dev/my-servlet/target/tmp
[INFO] Web defaults = org/eclipse/jetty/webapp/webdefault.xml
[INFO] Web overrides =  none
[INFO] web.xml file = null
[INFO] Webapp directory = /Users/me/dev/my-servlet/src/main/webapp
[INFO] jetty-9.4.35.v20201120; built: 2020-11-20T21:17:03.964Z; git: bdc54f03a5e0a7e280fab27f55c3c75ee8da89fb; jvm 1.8.0_241-b07
[INFO] Scanning elapsed time=26ms
[INFO] DefaultSessionIdManager workerName=node0
[INFO] No SessionScavenger set, using defaults
[INFO] node0 Scavenging every 660000ms
[INFO] Started o.e.j.m.p.JettyWebAppContext@760cf594{/,file:///Users/me/dev/my-servlet/src/main/webapp/,AVAILABLE}{file:///Users/me/dev/my-servlet/src/main/webapp/}
[INFO] Started ServerConnector@254f906e{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
[INFO] Started @4422ms
[INFO] Started Jetty Server

2. Testing

curl http://localhost:8080/do
Hello from MyServlet!Wed Dec 02 15:09:17 CET 2020

forward
Note action parameter is passed automatically

me@MacBook:~$ curl -vv http://localhost:8080/do?action=forward
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /do?action=forward HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Wed, 02 Dec 2020 14:11:26 GMT
< Content-Type: text/html;charset=utf-8
< Set-Cookie: JSESSIONID=node0vruwimbjjsy5uwywzhy51gy81.node0; Path=/
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< Content-Length: 248
< Server: Jetty(9.4.35.v20201120)
< 
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Forwarded page</title>
    </head>
    <body>
        <p>Action parameter: forward. This is forwarded page. Go to <a href="/">home</a></p>
    </body>
* Connection #0 to host localhost left intact
</html>* Closing connection 0

redirect
Note response code is 302 and redirect location is returned.

me@MacBook:~$ curl -vv http://localhost:8080/do?action=redirect
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /do?action=redirect HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 302 Found
< Date: Wed, 02 Dec 2020 14:11:39 GMT
< Location: http://localhost:8080/redirected.jsp
< Content-Length: 0
< Server: Jetty(9.4.35.v20201120)
< 
* Connection #0 to host localhost left intact
* Closing connection 0

With option to follow redirect location:
Note action parameter is NOT passed to second request.

me@MacBook:~$ curl -v -L http://localhost:8080/do?action=redirect
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /do?action=redirect HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 302 Found
< Date: Wed, 02 Dec 2020 17:14:59 GMT
< Location: http://localhost:8080/redirected.jsp
< Content-Length: 0
< Server: Jetty(9.4.35.v20201120)
< 
* Connection #0 to host localhost left intact
* Issue another request to this URL: 'http://localhost:8080/redirected.jsp'
* Found bundle for host localhost: 0x7fe33a818310 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) with host localhost
* Connected to localhost (::1) port 8080 (#0)
> GET /redirected.jsp HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Wed, 02 Dec 2020 17:14:59 GMT
< Content-Type: text/html;charset=utf-8
< Set-Cookie: JSESSIONID=node0gd5vxmsu6xgkh3ltwabj2jh810.node0; Path=/
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< Content-Length: 247
< Server: Jetty(9.4.35.v20201120)
< 
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Redirected page</title>
    </head>
    <body>
        <p>Action parameter: null. This is redirected page. Go to <a href="/">home</a></p>
    </body>
* Connection #0 to host localhost left intact
</html>* Closing connection 0
browser automatically makes second request to redirected page

3. Automatically reload classes and resources when debugging (change is seen after first request)

jetty server with update classes and resources
add exploded war
overwrite root context to / instead of default artifact name and version
me@MacBook:~$ curl  http://localhost:8080/do
Hello from MyServlet!Wed Dec 02 18:47:14 CET 2020

Now modify servlet java code

first request to trigger reloading

me@MacBook:~$ curl  http://localhost:8080/do
Hello from MyServlet!Wed Dec 02 18:48:03 CET 2020

second request returns modified response

me@MacBook:~$ curl  http://localhost:8080/do
Hello from MyServlet! - modifiedWed Dec 02 18:48:07 CET 2020

Intellij caller, method and type hierarchy + UML classes diagram

public interface MyIface {
    Number getNumericValue() throws RuntimeException;
}

public class MyIfaceImpl implements MyIface {
    @Override
    public Number getNumericValue() throws RuntimeException {
        System.out.println("MyIfaceImpl: in getNumericValue");
        return null;
    }
}

public abstract class MyAbstractClass implements MyIface {
    public abstract Number doInGet();

    @Override
    public Integer getNumericValue() throws IllegalArgumentException { // OK
        System.out.println("MyAbstractClass: in getNumericValue");
        doInGet();
        return null;
    }

    /*@Override
    public Long getNumericValue() { // OK (neither RuntimeException nor Exception does not need to included in overriding method signature)
        return null;
    }*/

    /*@Override
    public Number getNumericValue() throws Exception {  //Error:(7, 20) java: getNumericValue() in myhierarchy.MyAbstractClass cannot implement getNumericValue() in myhierarchy.MyIface overridden method does not throw java.lang.Exception
        return null;
    }*/

    /*@Override
    public Object getNumericValue() throws RuntimeException {  //Error:(17, 19) java: getNumericValue() in myhierarchy.MyAbstractClass cannot implement getNumericValue() in myhierarchy.MyIface         return type java.lang.Object is not compatible with java.lang.Number
        return null;
    }*/
}

public class MyAbstractClassImpl extends MyAbstractClass {
    @Override
    public Number doInGet() {
        System.out.println("MyImpl: in doInGet");
        return null;
    }
}

public class AllCaller {
    public static void main(String[] args) {
        MyIface myIface = new MyIfaceImpl();
        myIface.getNumericValue();
        MyAbstractClass myAbstractClass = new MyAbstractClassImpl();
        myAbstractClass.getNumericValue();
    }
}

import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;

public class RandomCallerApp {
    public static void main(String[] args) {
        Supplier myIfaceSupplier = get();
        System.out.println("supplier created, value lazy");
        myIfaceSupplier.get().getNumericValue();
    }

    private static Supplier get() {
        List myIfaces = Arrays.asList(new MyIfaceImpl(), new MyAbstractClassImpl());
        int i = new Random().nextInt(myIfaces.size());
        return () -> myIfaces.get(i);
    }
}

 

Ctrl+Alt+Shift+U – generate UML

Screenshot 2020-04-26 at 08.18.55

My shortcut:
F3 – go to declaration or view usages
Ctrl+Shift+C – caller hierarchy

Screenshot 2020-04-26 at 08.14.51

My shortcut:
F4 or Ctrl+Shift+I – navigate/choose method implementations
Ctrl+Shift+M – method hierarchy

Screenshot 2020-04-26 at 08.12.15

My shortcut: Ctrl+Shift+G – find usages:

Screenshot 2020-04-26 at 08.43.13

 

 

 

 

Scala for Java developers


class MyClass {
  var myname: String = _ // unitialize to default value // var so that cannot be changed
  var myseq: Seq[Any] = Nil // empty seq
  def mytrim: String = myname.trim
  
}
  
case class Person(age: Int, name: String)
  
object PersonImplicits {
  implicit class PersonHelper(person: Person) {
    def describe: String = s"Person[${person.age},${person.name}]"
  }
}
  
  
object MyClass {
  def main(args: Array[String]): Unit = {
 
    println(sum(1, 10, i => i)) // 55 pass directly function body
 
    println(sum(1, 10, square)) // 385 pass method calculating a square
 
    def square(i: Int): Int = i * i // declare method after referencing it
  
    import java.util
  
    val javaList: util.List[String] = new util.ArrayList[String] // import java jdk List and ArrayList
    javaList.add("a")
  
    import scala.collection.mutable
    import scala.collection.JavaConverters._
    val scalaSeq: mutable.Seq[String] = javaList.asScala // convert java javaList to scala mutable seq
  
    // alternative ways to println elements
    scalaSeq.foreach(s => println(s))
    scalaSeq.foreach(println(_))
    scalaSeq.foreach(println _)
    scalaSeq.foreach(println)
  
    val list: List[String] = scalaSeq.toList // convert from seq to list
    scalaSeq.foreach(println)
  
    val seq: Seq[String] = Seq("a", "b", "c") // scala Seq is a trait so equivalent to Java List interface
    seq.foreach(println)
  
    val immutableList = List(1, 2 ,3) // scala list is immutable and is an abstract class (that is extended by Nil == empty list)
    immutableList.map(10 * _).foreach(println)
  
    val ints: mutable.ListBuffer[Int] = mutable.ListBuffer(11, 22, 33)
    ints += 44 // mutable ListBuffer allows adding elements via +=, Seq does not
  
    val immutableMap: Map[String, Int] = Map("a" -> 1, "b" -> 2)
    println("map:" + immutableMap("a")) // get value by key = "a"
  
    val newImmutableMap: Map[String, Int] = immutableMap + ("c" -> 3) // create a new map based on existing and new entry
    newImmutableMap.foreach(e => println(e._1 + ":" + e._2))
  
    val mutableMap = mutable.Map("aa" -> 10)
    mutableMap += "bb" -> 20
    mutableMap.foreach{ case(k, v) => println(k + ":" + v) }
  
    printOptional(Some("a")) // value=a
    printOptional(None)      // value=empty
  
    println(divide(12,4,3))  // result 1
    println(divide(b=4,a=12,c=3)) // same result 1: alternatively with named params and different order
  
    println(divide(12,4)) // result 3: since third param has default value then no need to specify it unless overwrite
  
    val clazz = new MyClass
    clazz.myname = " aaa bbb "
    println(s"'${clazz.mytrim}'")
    clazz.myseq = Seq(12)
  
    matchcase(0)
    matchcase(1)
  
    import PersonImplicits._
    println(Person(18, "me").describe) // implicitly add describe method
  }
  
  def printOptional (optionalValue: Option[String]): Unit = {
    val value = if (optionalValue.isDefined) optionalValue.get else "empty"
    println("value=" + value)
  }
  
  def divide (a: Int, b: Int, c: Int = 1): Int = a / b / c    // value of last statement is the method return value
  
  def matchcase (n: Int): Unit = {
    n match {
      case -1 | 0 => println("non positive")
      case other => println("positive " + other)
    }
  }
 
  def sum(lb: Int, ub: Int, fun: Int => Int) = { // define a method (before referencing it) accepting function as 3rd arg
    var sum = 0 // declare mutable variable (val is immutable)
    for (el <- lb to ub) { // iterate by 1 from lb to ub
      sum += fun(el)
    }
    sum
  }
}

Hibernate n+1 selects and lazy loading vs eager

Assuming a shop can have many products we want to display shops along with products:
1) with one select query using join – we need to annotate in @OneToMany fetchType to EAGER to produce:

Hibernate:
select
this_.id as id1_1_1_,
this_.shipment_address as shipment2_1_1_,
products2_.shop_id as shop_id4_0_3_,
products2_.id as id1_0_3_,
products2_.id as id1_0_0_,
products2_.name as name2_0_0_,
products2_.price as price3_0_0_,
products2_.shop_id as shop_id4_0_0_
from
Shop this_
left outer join
Product products2_
on this_.id=products2_.shop_id

when calling

            Session session = sessionFactory.openSession();
            Criteria criteria = session.createCriteria(Shop.class).setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
            List list = criteria.list();

2) when we set fetch type to LAZY (default) then we get one select for shops and optionally additional selects for products for particular shop(s):

Hibernate:
select
this_.id as id1_1_0_,
this_.shipment_address as shipment2_1_0_
from
Shop this_

when retrieving shops
and when we want to retrieve products for particular shop(s)

            Shop shop = shops.get(0);
            List products = shop.getProducts();

then we get:

Hibernate:
select
products0_.shop_id as shop_id4_0_0_,
products0_.id as id1_0_0_,
products0_.id as id1_0_1_,
products0_.name as name2_0_1_,
products0_.price as price3_0_1_,
products0_.shop_id as shop_id4_0_1_
from
Product products0_
where
products0_.shop_id=?

for code

@Entity
public class Shop extends BaseEntity {

    @Column(name = "shipment_address", nullable = false)
    private String shipmentAddress;

    @OneToMany(mappedBy = "shop", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List products = new ArrayList<>();

    Shop() {
        // default constructor needed for ORM
    }

    public Shop(String shipmentAddress, Product... products) {
        this.shipmentAddress = shipmentAddress;
        for (Product product : products) {
            product.addShop(this);
            this.products.add(product);
        }
    }

    public List getProducts() {
        return products;
    }

    @Override
    public String toString() {
        return "Shop{" +
                super.toString() +
                ", shipmentAddress='" + shipmentAddress + '\'' +
                //", products=" + products +
                '}';
    }
}

@Entity
public class Product extends BaseEntity {

    @Column
    private String name;

    @Column
    private BigDecimal price;

    @ManyToOne
    @JoinColumn(name = "shop_id", foreignKey = @ForeignKey(name = "fk_product_shop"))
    private Shop shop;

    Product() {
        // default constructor needed for ORM
    }

    public Product(String name, BigDecimal price) {
        this.name = name;
        this.price = price;
    }

    void addShop(Shop shop) {
        this.shop = shop;
    }

    @Override
    public String toString() {
        return "Product{" +
                super.toString() +
                ", name='" + name + '\'' +
                ", price=" + price +
                ", shop.id=" + shop.getId() + '}';
    }
}

Unions and default value in apache avro serialization and deserialization

Initial avro schema (user.avsc) defines a User record with a name field only.

{
  "namespace": "com.bawi.avro.model",
  "type": "record",
  "name": "User",
  "fields": [
    {
      "name": "name",
      "type": "string"
    }
  ]
}

Maven pom.xml defines avro dependency

        <dependency>
            <groupId>org.apache.avro</groupId>
            <artifactId>avro</artifactId>
            <version>1.8.1</version>
        </dependency>

so we can serialize the User data in Java to disc to user.avro file

        Schema schema = new Schema.Parser().parse(new File("user.avsc"));
        File avroFile = new File("target/user.avro");
        GenericRecord user = new GenericData.Record(schema);
        user.put("name", "Alyssa");
        DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<>(schema);
        DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<>(datumWriter);
        dataFileWriter.create(schema, avroFile);
        dataFileWriter.append(user);
        dataFileWriter.close();

we can read (deserialize) User using the same schema from the disc either by Java

        DatumReader<GenericRecord> datumReader = new GenericDatumReader<>(schema);
        DataFileReader<GenericRecord> dataFileReader = new DataFileReader<>(avroFile, datumReader);
        GenericRecord user2 = null;
        while (dataFileReader.hasNext()) {
            user2 = dataFileReader.next(user2);
            System.out.println(user2);
        }

or by using avro-utils jar that can be downloaded by maven when declared maven test dependency:

        <dependency>
            <groupId>org.apache.avro</groupId>
            <artifactId>avro-tools</artifactId>
            <version>1.8.1</version>
            <scope>test</scope>
        </dependency>

and running with ‘tojson’ argument

me@MacBook:~/dev/my-projects/my-avro$ java -jar /Users/me/.m2/repository/org/apache/avro/avro-tools/1.8.1/avro-tools-1.8.1.jar tojson users.avro 
{"name":"Alyssa"}

Then we will add a new favorite_number element to the schema:

{
  "namespace": "com.bawi.avro.model",
  "type": "record",
  "name": "User",
  "fields": [
    {
      "name": "name",
      "type": "string"
    },
    {
      "name": "favorite_number",
      "type": "int"
    }
  ]
}

but not yet write favourite_number in the Java code.

When trying to write we get

org.apache.avro.file.DataFileWriter$AppendWriteException: java.lang.NullPointerException: null of int in field favorite_number of com.bawi.avro.model.User

since the favorite_number field is required by the avro schema but was not written by the writer.

Add a union of null and int value fixes the writing problem (union of int and null also works)

    {
      "name": "favorite_number",
      "type": [
        "null",
        "int"
      ]
    }

or

    {
      "name": "favorite_number",
      "type": [
        "int",
        "null"
      ]
    }

If written avro file has schema with favorite_number and it is written as null then it will always be read as null irregardless how the read schema looks like (default value affects only reading fields that were not defined in schema used for writing so the null values were not written, only schema used for reading should define that field (including default), schema used for writing should not define that field at all)

Lets assume different scenario where the write schema has only name field (without favorite_number):

{
  "namespace": "com.bawi.avro.model",
  "type": "record",
  "name": "User",
  "fields": [
    {
      "name": "name",
      "type": "string"
    }
  ]
}

and we write only name field into avro

Lets assume we want favorite_number to be set to -1 (with lets say new requirement to always populate in java code the favorite_number since we do not want to check for null for favorite_number fields when reading avro/hive table on the top of avro). Then lets modify the read schema to include default -1:
user_with_default_favourite_number.avsc:

{
  "namespace": "com.bawi.avro.model",
  "type": "record",
  "name": "User",
  "fields": [
    {
      "name": "name",
      "type": "string"
    },
    {
      "name": "favorite_number",
      "type": [
        "int",
        "null"
      ],
      "default": -1
    }
  ]
}

with

File file2 = new File("user_with_default_favourite_number.avsc");
Schema schema2 = new Schema.Parser().parse(file2);
DatumReader<GenericRecord> datumReader = new GenericDatumReader<>(schema2);

and the output is:

{"name": "Alyssa", "favorite_number": -1}

If we change read schema for favorite_number to invalid:

    {
      "name": "favorite_number",
      "type": [
        "null",
        "int"
      ],
      "default": -1
    }

then we get:

org.apache.avro.AvroTypeException: Non-null default value for null type: -1

so if default non-null value is given then null in union needs to on second place.

If we want to have "default": null then on the first place in the union needs to be null:

    {
      "name": "favorite_number",
      "type": [
        "null",
        "int"
      ],
      "default": null
    }

since for invalid:

    {
      "name": "favorite_number",
      "type": [
        "int",
        "null"
      ],
      "default": null
    }

we will get

org.apache.avro.AvroTypeException: Non-numeric default value for int: null

as described in https://avro.apache.org/docs/1.7.7/spec.html#Unions

Multiple approaches to concurrent processing in Java

Goal: Lets assume we want to execute 2 long running operations: download content and parse it and we want to do it for the list of resources.

The blog points multiple approaches:
1) Thread(s)
2) ExecutorService to submit Callable and return blocking Future get()
3) ExecutionCompletionService implementing CompletionService to submit Callable and returning Future.  CompletionService has take or poll methods waiting and returning first completed task (returned Future’s get() is not blocking)
4) Parallel streams
5) CompletableFuture

public class TestConcurrent {

    private static final Logger LOGGER = LoggerFactory.getLogger(TestConcurrent.class);

    public static void main(String[] args) {
        LOGGER.info("Started");
        long startMillis = System.currentTimeMillis();

        // for each of the resource: download it and parse it and aggregate results to list:
        List<Integer> results = ...

        long stopMillis = System.currentTimeMillis();
        LOGGER.info("Finished in {} seconds, results={}", (stopMillis - startMillis) / 1000, results);
    }

Lets define the download and parse methods and simulate long running execution using configurable TimeUnit.SECONDS.sleep(seconds).

static int download(int id, int seconds) {
    LOGGER.info("Downloading {} and sleeping {} second(s)", id, seconds);
    try {
        TimeUnit.SECONDS.sleep(seconds);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    LOGGER.info("Downloaded {}", id);
    return id;
}

static int parse(int id, int seconds) {
    LOGGER.info("Parsing {} and sleeping {} second(s)", id, seconds);
    try {
        TimeUnit.SECONDS.sleep(seconds);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    LOGGER.info("Parsed {}", id);
    return id;
}

In order to make the scenario even more representative between approaches lets define number of resources as 3 and each of the resource needed to be downloaded before it can be parsed. Moreover, 1st resource will be downloaded in 3 seconds, 2nd in two seconds and 3rd in 3 seconds. Conversely, it takes 1 second to parse 1st resource, 2 seconds to parse 2nd resource and 3 seconds to parse 3rd resource.

Obviously we do not want to execute the tasks sequentially using the same thread as it would take too long (12 seconds)

private static List<Integer> mapSequentiallyWithOnlyMainThread() {
    List<Integer> numbers = Arrays.asList(1, 2, 3);

    return numbers
        .stream()
        .map(n -> download(n, numbers.size() - n + 1))
        .map(n -> parse(n, n))
        .collect(Collectors.toList());
}
17:54:06.281 [main] Started
17:54:06.391 [main] Downloading 1 and sleeping 3 second(s)
17:54:09.394 [main] Downloaded 1
17:54:09.394 [main] Parsing 1 and sleeping 1 second(s)
17:54:10.394 [main] Parsed 1
17:54:10.394 [main] Downloading 2 and sleeping 2 second(s)
17:54:12.394 [main] Downloaded 2
17:54:12.394 [main] Parsing 2 and sleeping 2 second(s)
17:54:14.394 [main] Parsed 2
17:54:14.394 [main] Downloading 3 and sleeping 1 second(s)
17:54:15.394 [main] Downloaded 3
17:54:15.395 [main] Parsing 3 and sleeping 3 second(s)
17:54:18.395 [main] Parsed 3
17:54:18.395 [main] Finished in 12 seconds, results=[1, 2, 3]

So lets use multiple threads.

  1. Threads
    private static List<Integer> threads() {
        List<Integer> numbers = Arrays.asList(1, 2, 3);
    
        return numbers
            .stream()
            .map(n -> startThreadAndJoin(() -> download(n, numbers.size() - n + 1)))
            .map(n -> startThreadAndJoin(() -> parse(n, n)))
            .collect(Collectors.toList());
    }
    
    private static Integer startThreadAndJoin(Supplier<Integer> supplier) {
        AtomicInteger value = new AtomicInteger();
        Thread thread = new Thread(() -> {
            value.set(supplier.get());
        });
        thread.start();
        try {
            thread.join();
            return value.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }
    18:02:35.132 [main] Started
    18:02:35.244 [Thread-0] Downloading 1 and sleeping 3 second(s)
    18:02:38.246 [Thread-0] Downloaded 1
    18:02:38.248 [Thread-1] Parsing 1 and sleeping 1 second(s)
    18:02:39.248 [Thread-1] Parsed 1
    18:02:39.249 [Thread-2] Downloading 2 and sleeping 2 second(s)
    18:02:41.250 [Thread-2] Downloaded 2
    18:02:41.251 [Thread-3] Parsing 2 and sleeping 2 second(s)
    18:02:43.251 [Thread-3] Parsed 2
    18:02:43.252 [Thread-4] Downloading 3 and sleeping 1 second(s)
    18:02:44.252 [Thread-4] Downloaded 3
    18:02:44.253 [Thread-5] Parsing 3 and sleeping 3 second(s)
    18:02:47.253 [Thread-5] Parsed 3
    18:02:47.253 [main] Finished in 12 seconds, results=[1, 2, 3]
  2. ExecutorService
    private static List<Integer> executorServiceGet() {
        List<Integer> numbers = Arrays.asList(1, 2, 3);
    
        ExecutorService executorService = Executors.newFixedThreadPool(numbers.size());
        List<Future<Integer>> downloadedFutures = numbers
            .stream()
            .map(n -> {
                Callable<Integer> callable = () -> download(n, numbers.size() - n + 1);
                return executorService.submit(callable);
            })
            .collect(Collectors.toList());
    
        List<Future<Integer>> parsedFutures = numbers
            .stream()
            .map(n -> {
                Future<Integer> future = downloadedFutures.get(n - 1);
                try {
                    Integer value = future.get();
                    return executorService.submit(() -> parse(value, n));
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
                return null;
            })
            .collect(Collectors.toList());
    
        return parsedFutures
            .stream()
            .map(future -> {
                try {
                    return future.get();
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
                return null;
            })
            .collect(Collectors.toList());
    }
    
    18:10:11.988 [main] Started
    18:10:12.106 [pool-1-thread-1] Downloading 1 and sleeping 3 second(s)
    18:10:12.110 [pool-1-thread-2] Downloading 2 and sleeping 2 second(s)
    18:10:12.111 [pool-1-thread-3] Downloading 3 and sleeping 1 second(s)
    18:10:13.111 [pool-1-thread-3] Downloaded 3
    18:10:14.111 [pool-1-thread-2] Downloaded 2
    18:10:15.109 [pool-1-thread-1] Downloaded 1
    18:10:15.111 [pool-1-thread-3] Parsing 1 and sleeping 1 second(s)
    18:10:15.112 [pool-1-thread-2] Parsing 2 and sleeping 2 second(s)
    18:10:15.112 [pool-1-thread-1] Parsing 3 and sleeping 3 second(s)
    18:10:16.112 [pool-1-thread-3] Parsed 1
    18:10:17.113 [pool-1-thread-2] Parsed 2
    18:10:18.112 [pool-1-thread-1] Parsed 3
    18:10:18.112 [main] Finished in 6 seconds, results=[1, 2, 3]
  3. ExecutionCompletionService
    private static List<Integer> executionCompletionServiceTakeAndGet() {
        List<Integer> numbers = Arrays.asList(1, 2, 3);
    
        ExecutorService executorService = Executors.newFixedThreadPool(numbers.size());
        ExecutorCompletionService<Integer> executorCompletionService = new ExecutorCompletionService<>(executorService);
    
        numbers
            .stream()
            .forEach(n -> executorCompletionService.submit(() -> download(n, numbers.size() - n + 1)));
    
        numbers
            .stream()
            .forEach(ignored -> {
                try {
                    Future<Integer>future = executorCompletionService.take();
                    Integer value = future.get();
                    executorCompletionService.submit(() -> parse(value, value));
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            });
    
        return numbers
            .stream()
            .map(ignore -> {
                try {
                    Future<Integer> future = executorCompletionService.take();
                    return future.get();
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
                return null;
            })
            .collect(Collectors.toList());
    }
    
    18:18:16.548 [main] Started
    18:18:16.655 [pool-1-thread-2] Downloading 2 and sleeping 2 second(s)
    18:18:16.655 [pool-1-thread-3] Downloading 3 and sleeping 1 second(s)
    18:18:16.655 [pool-1-thread-1] Downloading 1 and sleeping 3 second(s)
    18:18:17.658 [pool-1-thread-3] Downloaded 3
    18:18:17.660 [pool-1-thread-3] Parsing 3 and sleeping 3 second(s)
    18:18:18.658 [pool-1-thread-2] Downloaded 2
    18:18:18.658 [pool-1-thread-2] Parsing 2 and sleeping 2 second(s)
    18:18:19.658 [pool-1-thread-1] Downloaded 1
    18:18:19.658 [pool-1-thread-1] Parsing 1 and sleeping 1 second(s)
    18:18:20.658 [pool-1-thread-2] Parsed 2
    18:18:20.658 [pool-1-thread-1] Parsed 1
    18:18:20.660 [pool-1-thread-3] Parsed 3
    18:18:20.660 [main] Finished in 4 seconds, results=[2, 1, 3]
  4. Parallel streams
    private static List<Integer> mapParallel() {
        List<Integer> numbers = Arrays.asList(1, 2, 3);
    
        return numbers
            .parallelStream()
            .map(n -> download(n, numbers.size() - n + 1))
            .map(n -> parse(n, n))
            .collect(Collectors.toList());
    }
    18:23:51.300 [main] Started
    18:23:51.413 [main] Downloading 2 and sleeping 2 second(s)
    18:23:51.413 [ForkJoinPool.commonPool-worker-1] Downloading 1 and sleeping 3 second(s)
    18:23:51.413 [ForkJoinPool.commonPool-worker-2] Downloading 3 and sleeping 1 second(s)
    18:23:52.416 [ForkJoinPool.commonPool-worker-2] Downloaded 3
    18:23:52.416 [ForkJoinPool.commonPool-worker-2] Parsing 3 and sleeping 3 second(s)
    18:23:53.416 [main] Downloaded 2
    18:23:53.416 [main] Parsing 2 and sleeping 2 second(s)
    18:23:54.416 [ForkJoinPool.commonPool-worker-1] Downloaded 1
    18:23:54.416 [ForkJoinPool.commonPool-worker-1] Parsing 1 and sleeping 1 second(s)
    18:23:55.416 [main] Parsed 2
    18:23:55.416 [ForkJoinPool.commonPool-worker-1] Parsed 1
    18:23:55.416 [ForkJoinPool.commonPool-worker-2] Parsed 3
    18:23:55.416 [main] Finished in 4 seconds, results=[1, 2, 3]
  5. CompletableFuture
    private static List<Integer> completableFuture() {
        List<Integer> numbers = Arrays.asList(1, 2, 3);
    
        List<CompletableFuture<Integer>> cfs = numbers
            .stream()
            .map(n ->
                CompletableFuture.supplyAsync(() -> download(n, numbers.size() - n + 1))
                    .thenApply(id -> parse(id, n)))
            .collect(Collectors.toList());
    
        CompletableFuture<List<Integer>> allResultsCF = CompletableFuture.allOf(cfs.toArray(new CompletableFuture[numbers.size()]))
            .thenApply(v -> cfs
                .stream()
                .map(CompletableFuture::join)
                .collect(Collectors.toList())
            );
    
        return allResultsCF.join();
    
18:25:25.722 [main] Started
18:25:25.867 [ForkJoinPool.commonPool-worker-1] Downloading 1 and sleeping 3 second(s)
18:25:25.869 [ForkJoinPool.commonPool-worker-3] Downloading 3 and sleeping 1 second(s)
18:25:25.868 [ForkJoinPool.commonPool-worker-2] Downloading 2 and sleeping 2 second(s)
18:25:26.871 [ForkJoinPool.commonPool-worker-3] Downloaded 3
18:25:26.872 [ForkJoinPool.commonPool-worker-3] Parsing 3 and sleeping 3 second(s)
18:25:27.871 [ForkJoinPool.commonPool-worker-2] Downloaded 2
18:25:27.871 [ForkJoinPool.commonPool-worker-2] Parsing 2 and sleeping 2 second(s)
18:25:28.871 [ForkJoinPool.commonPool-worker-1] Downloaded 1
18:25:28.871 [ForkJoinPool.commonPool-worker-1] Parsing 1 and sleeping 1 second(s)
18:25:29.871 [ForkJoinPool.commonPool-worker-1] Parsed 1
18:25:29.871 [ForkJoinPool.commonPool-worker-2] Parsed 2
18:25:29.872 [ForkJoinPool.commonPool-worker-3] Parsed 3
18:25:29.874 [main] Finished in 4 seconds, results=[1, 2, 3]

If we provide our own pool we get:

CompletableFuture.supplyAsync(() -> download(n, numbers.size() - n + 1), executorService)
    .thenApplyAsync(id -> parse(id, n), executorService)
18:31:47.902 [main] Started
18:31:47.995 [pool-1-thread-1] Downloading 1 and sleeping 3 second(s)
18:31:47.997 [pool-1-thread-3] Downloading 3 and sleeping 1 second(s)
18:31:47.997 [pool-1-thread-2] Downloading 2 and sleeping 2 second(s)
18:31:48.999 [pool-1-thread-3] Downloaded 3
18:31:48.999 [pool-1-thread-3] Parsing 3 and sleeping 3 second(s)
18:31:49.999 [pool-1-thread-2] Downloaded 2
18:31:49.999 [pool-1-thread-2] Parsing 2 and sleeping 2 second(s)
18:31:50.999 [pool-1-thread-1] Downloaded 1
18:31:50.999 [pool-1-thread-1] Parsing 1 and sleeping 1 second(s)
18:31:51.999 [pool-1-thread-1] Parsed 1
18:31:51.999 [pool-1-thread-3] Parsed 3
18:31:51.999 [pool-1-thread-2] Parsed 2
18:31:52.001 [main] Finished in 4 seconds, results=[1, 2, 3]