@@ -0,0 +1,3339 @@
+/*%0A * Bach - Java Shell Builder%0A * Copyright (C) 2019 Christian Stein%0A *%0A * Licensed under the Apache License, Version 2.0 (the %22License%22);%0A * you may not use this file except in compliance with the License.%0A * You may obtain a copy of the License at%0A *%0A *     https://www.apache.org/licenses/LICENSE-2.0%0A *%0A * Unless required by applicable law or agreed to in writing, software%0A * distributed under the License is distributed on an %22AS IS%22 BASIS,%0A * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.%0A * See the License for the specific language governing permissions and%0A * limitations under the License.%0A */%0A%0Apackage de.sormuras.bach;%0A%0Aimport static java.lang.System.Logger.Level.DEBUG;%0Aimport static java.lang.System.Logger.Level.TRACE;%0A%0Aimport java.net.URI;%0Aimport java.net.URLConnection;%0Aimport java.nio.file.Files;%0Aimport java.nio.file.Path;%0Aimport java.nio.file.attribute.FileTime;%0A%0A/*BODY*/%0A/** Downloader. */%0Apublic /*STATIC*/ class Downloader %7B%0A%0A  /** Extract last path element from the supplied uri. */%0A  static String extractFileName(URI uri) %7B%0A    var path = uri.getPath(); // strip query and fragment elements%0A    return path.substring(path.lastIndexOf('/') + 1);%0A  %7D%0A%0A  /** Extract target file name either from 'Content-Disposition' header or. */%0A  static String extractFileName(URLConnection connection) throws Exception %7B%0A    var contentDisposition = connection.getHeaderField(%22Content-Disposition%22);%0A    if (contentDisposition != null && contentDisposition.indexOf('=') %3E 0) %7B%0A      return contentDisposition.split(%22=%22)%5B1%5D.replaceAll(%22%5C%22%22, %22%22);%0A    %7D%0A    return extractFileName(connection.getURL().toURI());%0A  %7D%0A%0A  final Run run;%0A  final Path destination;%0A%0A  Downloader(Run run, Path destination) %7B%0A    this.run = run;%0A    this.destination = destination;%0A  %7D%0A%0A  /** Download a file denoted by the specified uri. */%0A  Path download(URI uri) throws Exception %7B%0A    var fileName = extractFileName(uri);%0A    var target = Files.createDirectories(destination).resolve(fileName);%0A    var url = uri.toURL(); // fails for non-absolute uri%0A    if (Boolean.getBoolean(%22offline%22)) %7B // TODO run.offline%0A      if (Files.exists(target)) %7B%0A        return target;%0A      %7D%0A      throw new IllegalStateException(%22Target is missing and being offline: %22 + target);%0A    %7D%0A    return download(url.openConnection());%0A  %7D%0A%0A  /** Download a file using the given URL connection. */%0A  Path download(URLConnection connection) throws Exception %7B%0A    var target = destination.resolve(extractFileName(connection));%0A    var millis = connection.getLastModified();%0A    var lastModified = FileTime.fromMillis(millis == 0 ? System.currentTimeMillis() : millis);%0A    if (Files.exists(target)) %7B%0A      var fileModified = Files.getLastModifiedTime(target);%0A      if (fileModified.equals(lastModified)) %7B%0A        run.log(TRACE, %22Timestamp match: %25s, %25d bytes.%22, target.getFileName(), Files.size(target));%0A        return target;%0A      %7D%0A    %7D%0A    try (var sourceStream = connection.getInputStream()) %7B%0A      try (var targetStream = Files.newOutputStream(target)) %7B%0A        run.log(DEBUG, %22Transferring...%22);%0A        sourceStream.transferTo(targetStream);%0A      %7D%0A      Files.setLastModifiedTime(target, lastModified);%0A    %7D%0A    run.log(DEBUG, %22Downloaded %25s, %25d bytes.%22, target.getFileName(), Files.size(target));%0A    return target;%0A  %7D%0A%7D%0A

Add Downloader class