/*
 * Bach - Java Shell Builder
 * Copyright (C) 2019 Christian Stein
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.sormuras.bach;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.text.SimpleDateFormat;
import java.util.Locale;

/*BODY*/
/** File transfer. */
/*STATIC*/ class Transfer {

  final PrintWriter out, err;
  final HttpClient client;

  Transfer(PrintWriter out, PrintWriter err) {
    this(out, err, HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build());
  }

  Transfer(PrintWriter out, PrintWriter err, HttpClient client) {
    this.out = out;
    this.err = err;
    this.client = client;
  }

  Path getFile(Path path, URI uri) {
    var request = HttpRequest.newBuilder(uri).GET();
    if (Files.exists(path)) {
      try {
        var etagBytes = (byte[]) Files.getAttribute(path, "user:etag");
        var etag = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(etagBytes)).toString();
        request.setHeader("If-None-Match", etag);
      } catch (Exception e) {
        err.println("Couldn't get 'user:etag' file attribute: " + e);
      }
    }
    try {
      var handler = HttpResponse.BodyHandlers.ofFile(path);
      var response = client.send(request.build(), handler);
      if (response.statusCode() == 200) {
        var etagHeader = response.headers().firstValue("etag");
        if (etagHeader.isPresent()) {
          try {
            var etag = etagHeader.get();
            Files.setAttribute(path, "user:etag", StandardCharsets.UTF_8.encode(etag));
          } catch (Exception e) {
            err.println("Couldn't set 'user:etag' file attribute: " + e);
          }
        }
        var lastModifiedHeader = response.headers().firstValue("last-modified");
        if (lastModifiedHeader.isPresent()) {
          try {
            var format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
            var millis = format.parse(lastModifiedHeader.get()).getTime(); // 0 means "unknown"
            var fileTime = FileTime.fromMillis(millis == 0 ? System.currentTimeMillis() : millis);
            Files.setLastModifiedTime(path, fileTime);
          } catch (Exception e) {
            err.println("Couldn't set last modified file attribute: " + e);
          }
        }
        out.println(path + " <- " + uri);
      }
    } catch (IOException | InterruptedException e) {
      err.println("Failed to load: " + uri + " -> " + e);
    }
    return path;
  }
}

@@ -1055,78 +1055,415 @@
 til.
-Locale;%0A%0A/*BODY*/%0A/** File transfer. */%0A/*STATIC*/ class Transfer %7B%0A%0A 
+Collection;%0Aimport java.util.Locale;%0Aimport java.util.stream.Collectors;%0A%0A/*BODY*/%0A/** File transfer. */%0A/*STATIC*/ class Transfer %7B%0A%0A  static class Item %7B%0A%0A    static Item of(URI uri, String file) %7B%0A      return new Item(uri, file);%0A    %7D%0A%0A    private final URI uri;%0A    private final String file;%0A%0A    private Item(URI uri, String file) %7B%0A      this.uri = uri;%0A      this.file = file;%0A    %7D%0A  %7D%0A%0A  private
  fin
@@ -1489,16 +1489,24 @@
  err;%0A  
+private 
 final Ht
@@ -1670,24 +1670,32 @@
 ());%0A  %7D%0A%0A  
+private 
 Transfer(Pri
@@ -1823,39 +1823,266 @@
 %0A%0A  
-Path getFile(Path path, URI uri
+void getFiles(Path path, Collection%3CItem%3E items) %7B%0A    Util.treeCreate(path);%0A    items.stream()%0A        .parallel()%0A        .map(item -%3E getFile(item.uri, path.resolve(item.file)))%0A        .collect(Collectors.toSet());%0A  %7D%0A%0A  Path getFile(URI uri, Path path
 ) %7B%0A
@@ -3658,32 +3658,63 @@
     %7D%0A        %7D%0A
+        synchronized (out) %7B%0A  
         out.prin
@@ -3739,16 +3739,26 @@
 + uri);%0A
+        %7D%0A
       %7D%0A
@@ -3864,24 +3864,54 @@
  -%3E %22 + e);%0A
+      e.printStackTrace(err);%0A
     %7D%0A    re

Implement parallel download of files