001/**
002 * Copyright (C) 2006-2024 Talend Inc. - www.talend.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.talend.sdk.component.tools.exec;
017
018import static java.util.Locale.ROOT;
019import static java.util.stream.Collectors.joining;
020
021import java.io.BufferedInputStream;
022import java.io.BufferedOutputStream;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.FileNotFoundException;
026import java.io.FileOutputStream;
027import java.io.FileWriter;
028import java.io.IOException;
029import java.io.InputStream;
030import java.io.OutputStream;
031import java.io.UnsupportedEncodingException;
032import java.io.Writer;
033import java.net.HttpURLConnection;
034import java.net.URL;
035import java.net.URLDecoder;
036import java.nio.file.Files;
037import java.nio.file.StandardCopyOption;
038import java.time.LocalDateTime;
039import java.time.format.DateTimeFormatter;
040import java.util.ArrayList;
041import java.util.Base64;
042import java.util.Enumeration;
043import java.util.List;
044import java.util.Objects;
045import java.util.Properties;
046import java.util.UUID;
047import java.util.concurrent.CountDownLatch;
048import java.util.concurrent.ExecutorService;
049import java.util.concurrent.Executors;
050import java.util.concurrent.TimeUnit;
051import java.util.jar.JarEntry;
052import java.util.jar.JarFile;
053import java.util.jar.JarInputStream;
054import java.util.stream.Collectors;
055import java.util.stream.Stream;
056
057import javax.xml.parsers.DocumentBuilder;
058import javax.xml.parsers.DocumentBuilderFactory;
059import javax.xml.parsers.ParserConfigurationException;
060
061import org.w3c.dom.Document;
062import org.w3c.dom.Node;
063import org.w3c.dom.NodeList;
064import org.xml.sax.SAXException;
065
066// IMPORTANT: this class MUST not have ANY dependency, not even slf4j!
067public class CarMain {
068
069    public static final String COMPONENT_JAVA_COORDINATES = "component.java.coordinates";
070
071    public static final String COMPONENT_COORDINATES = "component_coordinates";
072
073    public static final String COMPONENT_SERVER_EXTENSIONS = "component.server.extensions";
074
075    public static final String UTF_8_ENC = "UTF-8";
076
077    private CarMain() {
078        // no-op
079    }
080
081    public static void main(final String[] args) {
082        if (args == null || args.length < 2) {
083            help();
084            return;
085        }
086        if (Stream.of(args).anyMatch(Objects::isNull)) {
087            throw new IllegalArgumentException("No argument can be null");
088        }
089        boolean forceOverwrite = false;
090        for (String arg : args) {
091            if ("-f".equals(arg)) {
092                forceOverwrite = true;
093                break;
094            }
095        }
096
097        switch (args[0].toLowerCase(ROOT)) {
098        case "studio-deploy":
099            final String studioPath;
100            if (args.length == 2) {
101                studioPath = args[1];
102            } else {
103                studioPath = getArgument("--location", args);
104            }
105            if (studioPath == null || studioPath.isEmpty()) {
106                System.err.println("Path to studio is not set. Use '--location <location>' to set it.");
107                help();
108                break;
109            }
110            deployInStudio(studioPath, forceOverwrite);
111            break;
112        case "maven-deploy":
113            final String mavenPath;
114            if (args.length == 2) {
115                mavenPath = args[1];
116            } else {
117                mavenPath = getArgument("--location", args);
118            }
119            if (mavenPath == null || mavenPath.isEmpty()) {
120                System.err.println("Path to maven repository is not set. Use '--location <location>' to set it.");
121                help();
122                break;
123            }
124            deployInM2(mavenPath, forceOverwrite);
125            break;
126        case "deploy-to-nexus":
127            final String url = getArgument("--url", args);
128            final String repo = getArgument("--repo", args);
129            final String user = getArgument("--user", args);
130            final String pass = getArgument("--pass", args);
131            final String threads = getArgument("--threads", args);
132            final int threadsNum;
133            if (threads == null) {
134                threadsNum = Runtime.getRuntime().availableProcessors();
135            } else {
136                threadsNum = Integer.parseInt(threads);
137            }
138            final String dir = getArgument("--dir", args);
139            if (url == null || url.isEmpty()) {
140                System.err.println("Nexus url is not set. Use '--url <url>' to set it");
141                help();
142                break;
143            }
144            if (repo == null || repo.isEmpty()) {
145                System.err.println("Nexus repo is not set. Use '--repo <repository>' to set it");
146                help();
147                break;
148            }
149            deployToNexus(url, repo, user, pass, threadsNum, dir);
150            break;
151        default:
152            help();
153            throw new IllegalArgumentException("Unknown command '" + args[0] + "'");
154        }
155    }
156
157    private static String getArgument(final String argumentPrefix, final String... args) {
158        for (int i = 1; i < args.length - 1; i++) {
159            if (args[i].equals(argumentPrefix)) {
160                return args[i + 1];
161            }
162        }
163        return null;
164    }
165
166    private static void deployInM2(final String m2, final boolean forceOverwrite) {
167        final File m2File = new File(m2);
168        if (!m2File.exists()) {
169            throw new IllegalArgumentException(m2 + " doesn't exist");
170        }
171        final String component = installJars(m2File, forceOverwrite).getProperty(COMPONENT_COORDINATES);
172        System.out
173                .println("Installed " + jarLocation(CarMain.class).getName() + " in " + m2 + ", "
174                        + "you can now register '" + component + "' component in your application.");
175    }
176
177    private static void deployInStudio(final String studioLocation, final boolean forceOverwrite) {
178        System.out.println(String.format("Connector is being deployed to %s.", studioLocation));
179        final File root = new File(studioLocation);
180        if (!root.isDirectory()) {
181            throw new IllegalArgumentException(studioLocation + " is not a valid directory");
182        }
183
184        final File config = new File(studioLocation, "configuration/config.ini");
185        if (!config.exists()) {
186            throw new IllegalArgumentException("No " + config + " found, is your studio location right?");
187        }
188
189        final Properties configuration = readProperties(config);
190        final File m2Root;
191        String m2RepoPath = System.getProperty("talend.studio.m2.repo", null);
192        if (m2RepoPath != null) {
193            m2Root = new File(m2RepoPath);
194        } else {
195            final String repositoryType = configuration.getProperty("maven.repository");
196            if ("global".equals(repositoryType)) {
197                // grab local maven setup, we use talend env first to override dev one then dev env setup
198                // and finally this main system props as a more natural config but less likely set on a dev machine
199                m2Root = Stream
200                        .of("TALEND_STUDIO_MAVEN_HOME", "MAVEN_HOME", "M2_HOME",
201                                "talend.component.server.maven.repository",
202                                "talend.studio.m2")
203                        .map(it -> it.contains(".") ? System.getProperty(it) : System.getenv(it))
204                        .filter(Objects::nonNull)
205                        .findFirst()
206                        .map(mvnHome -> {
207                            final File globalSettings = new File(mvnHome, "conf/settings.xml");
208                            if (globalSettings.exists()) {
209                                try {
210                                    final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
211                                    factory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true);
212                                    factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
213                                    final DocumentBuilder builder = factory.newDocumentBuilder();
214                                    final Document document = builder.parse(globalSettings);
215                                    final NodeList localRepository = document.getElementsByTagName("localRepository");
216                                    if (localRepository.getLength() == 1) {
217                                        final Node node = localRepository.item(0);
218                                        if (node != null) {
219                                            final String repoPath = node.getTextContent();
220                                            if (repoPath != null) {
221                                                return new File(repoPath);
222                                            }
223                                        }
224                                    }
225                                } catch (final ParserConfigurationException | SAXException | IOException e) {
226                                    throw new IllegalStateException(e);
227                                }
228                            }
229                            return null;
230                        })
231                        .orElseGet(() -> new File(System.getProperty("user.home"), ".m2/repository/"));
232            } else {
233                m2Root = new File(studioLocation, "configuration/.m2/repository/");
234            }
235        }
236        if (!m2Root.isDirectory()) {
237            throw new IllegalArgumentException(m2Root + " does not exist, did you specify a valid m2 studio property?");
238        }
239
240        // install jars
241        final Properties carProperties = installJars(m2Root, forceOverwrite);
242        final String mainGav = carProperties.getProperty(COMPONENT_COORDINATES);
243        final String type = carProperties.getProperty("type", "connector");
244
245        // register the component/extension
246        final String key =
247                "extension".equalsIgnoreCase(type) ? COMPONENT_SERVER_EXTENSIONS : COMPONENT_JAVA_COORDINATES;
248        final String components = configuration.getProperty(key);
249        try {
250            final List<String> configLines = Files.readAllLines(config.toPath());
251            if (components == null) {
252                final String original = configLines.stream().collect(joining("\n"));
253                try (final Writer writer = new FileWriter(config)) {
254                    writer.write(original + "\n" + key + " = " + mainGav);
255                }
256            } else {
257                // backup
258                final File backup = new File(config.getParentFile(), "backup/" + config.getName() + "_"
259                        + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-mm-dd_HH-mm-ss")));
260                backup.getParentFile().mkdirs();
261                try (final OutputStream to = new BufferedOutputStream(new FileOutputStream(backup))) {
262                    Files.copy(config.toPath(), to);
263                }
264
265                boolean skip = false;
266                try (final Writer writer = new FileWriter(config)) {
267                    for (final String line : configLines) {
268                        if (line.trim().startsWith(key)) {
269                            skip = true;
270                            continue;
271                        } else if (skip && line.trim().contains("=")) {
272                            skip = false;
273                        } else if (skip) {
274                            continue;
275                        }
276                        writer.write(line + "\n");
277                    }
278                    final String toFilter = Stream
279                            .of(mainGav.contains(":") ? mainGav.split(":") : mainGav.split("/"))
280                            .limit(2)
281                            .collect(Collectors.joining(":", "", ":"));
282                    writer
283                            .write(key + " = " + Stream
284                                    .concat(Stream.of(mainGav),
285                                            Stream
286                                                    .of(components.trim().split(","))
287                                                    .map(String::trim)
288                                                    .filter(it -> !it.isEmpty())
289                                                    .filter(it -> !it.startsWith(toFilter)))
290                                    .collect(joining(",")));
291                }
292                System.out.println(type + " registered.");
293            }
294        } catch (final IOException ioe) {
295            throw new IllegalStateException(ioe);
296        }
297        System.out.println(type + " deployed successfully.");
298    }
299
300    private static Properties installJars(final File m2Root, final boolean forceOverwrite) {
301        String mainGav = null;
302        final Properties properties = new Properties();
303        try (final JarInputStream jar =
304                new JarInputStream(new BufferedInputStream(new FileInputStream(jarLocation(CarMain.class))))) {
305            JarEntry entry;
306            while ((entry = jar.getNextJarEntry()) != null) {
307                if (entry.isDirectory()) {
308                    continue;
309                }
310                if (entry.getName().startsWith("MAVEN-INF/repository/")) {
311                    final String path = entry.getName().substring("MAVEN-INF/repository/".length());
312                    final File output = new File(m2Root, path);
313                    if (!output.getCanonicalPath().startsWith(m2Root.getCanonicalPath())) {
314                        throw new IOException("The output file is not contained in the destination directory");
315                    }
316                    if (!output.exists() || forceOverwrite) {
317                        output.getParentFile().mkdirs();
318                        Files.copy(jar, output.toPath(), StandardCopyOption.REPLACE_EXISTING);
319                    }
320                } else if ("TALEND-INF/metadata.properties".equals(entry.getName())) {
321                    // mainGav
322                    properties.load(jar);
323                    mainGav = properties.getProperty(COMPONENT_COORDINATES);
324                }
325            }
326        } catch (final IOException e) {
327            throw new IllegalArgumentException(e);
328        }
329        if (mainGav == null || mainGav.trim().isEmpty()) {
330            throw new IllegalArgumentException("Didn't find the component coordinates");
331        }
332        System.out.println(String.format("Connector %s and dependencies jars installed to %s.", mainGav, m2Root));
333        return properties;
334    }
335
336    private static Properties readProperties(final File config) {
337        final Properties configuration = new Properties();
338        try (final InputStream stream = new FileInputStream(config)) {
339            configuration.load(stream);
340        } catch (final IOException e) {
341            throw new IllegalArgumentException(e);
342        }
343        return configuration;
344    }
345
346    private static void help() {
347        System.err.println("Usage:\n\njava -jar " + jarLocation(CarMain.class).getName() + " [command] [arguments]");
348        System.err.println("commands:");
349        System.err.println("   studio-deploy");
350        System.err.println("      arguments:");
351        System.err.println("         --location <location>: path to studio");
352        System.err.println("         or");
353        System.err.println("         <location>: path to studio");
354        System.err.println("         -f : force overwrite existing jars");
355        System.err.println();
356        System.err.println("   maven-deploy");
357        System.err.println("      arguments:");
358        System.err.println("         --location <location>: path to .m2 repository");
359        System.err.println("         or");
360        System.err.println("         <location>: path to .m2 repository");
361        System.err.println("         -f : force overwrite existing jars");
362        System.err.println();
363        System.err.println("   deploy-to-nexus");
364        System.err.println("      arguments:");
365        System.err.println("         --url <nexusUrl>: nexus server url");
366        System.err.println("         --repo <repositoryName>: nexus repository name to upload dependencies to");
367        System.err.println("         --user <username>: username to connect to nexus(optional)");
368        System.err.println("         --pass <password>: password to connect to nexus(optional)");
369        System.err
370                .println(
371                        "         --threads <parallelThreads>: threads number to use during upload to nexus(optional)");
372        System.err.println("         --dir <tempDirectory>: temporary directory to use during upload(optional)");
373    }
374
375    private static File jarLocation(final Class clazz) {
376        try {
377            final String classFileName = clazz.getName().replace(".", "/") + ".class";
378            final ClassLoader loader = clazz.getClassLoader();
379            return jarFromResource(loader, classFileName);
380        } catch (final RuntimeException e) {
381            throw e;
382        } catch (final Exception e) {
383            throw new IllegalStateException(e);
384        }
385    }
386
387    private static File jarFromResource(final ClassLoader loader, final String resourceName) {
388        try {
389            final URL url = loader.getResource(resourceName);
390            if (url == null) {
391                throw new IllegalStateException("didn't find " + resourceName);
392            }
393            if ("jar".equals(url.getProtocol())) {
394                final String spec = url.getFile();
395                final int separator = spec.indexOf('!');
396                return new File(
397                        URLDecoder.decode(new URL(spec.substring(0, separator)).getFile(), UTF_8_ENC));
398            } else if ("file".equals(url.getProtocol())) {
399                return toFile(resourceName, url);
400            } else {
401                throw new IllegalArgumentException("Unsupported URL scheme: " + url.toExternalForm());
402            }
403        } catch (final RuntimeException e) {
404            throw e;
405        } catch (final Exception e) {
406            throw new IllegalStateException(e);
407        }
408    }
409
410    private static File toFile(final String classFileName, final URL url) throws UnsupportedEncodingException {
411        final String path = url.getFile();
412        return new File(
413                URLDecoder.decode(path.substring(0, path.length() - classFileName.length()), UTF_8_ENC));
414    }
415
416    private static void deployToNexus(final String serverUrl, final String repositoryName, final String username,
417            final String password, final int parallelThreads, final String tempDirLocation) {
418        String mainGav = null;
419        final List<JarEntry> entriesToProcess = new ArrayList<>();
420        try (JarFile jar = new JarFile(jarLocation(CarMain.class))) {
421            Enumeration<JarEntry> entries = jar.entries();
422            JarEntry entry;
423            while (entries.hasMoreElements()) {
424                entry = entries.nextElement();
425                if (entry.isDirectory()) {
426                    continue;
427                }
428                if (entry.getName().startsWith("MAVEN-INF/repository/")) {
429                    entriesToProcess.add(entry);
430                } else if ("TALEND-INF/metadata.properties".equals(entry.getName())) {
431                    // mainGav
432                    final Properties properties = new Properties();
433                    properties.load(jar.getInputStream(entry));
434                    mainGav = properties.getProperty(COMPONENT_COORDINATES);
435                }
436            }
437            if (mainGav == null || mainGav.trim().isEmpty()) {
438                throw new IllegalArgumentException("Didn't find the component coordinates");
439            }
440            uploadEntries(serverUrl, repositoryName, username, password, entriesToProcess, jar, parallelThreads,
441                    tempDirLocation);
442        } catch (final IOException e) {
443            throw new IllegalArgumentException(e);
444        }
445        System.out
446                .println("Installed " + jarLocation(CarMain.class).getName() + " on " + serverUrl + ", "
447                        + "you can now register '" + mainGav + "' component in your application.");
448    }
449
450    private static void uploadEntries(final String serverUrl, final String repositoryName, final String username,
451            final String password, final List<JarEntry> entriesToProcess, final JarFile jar, final int parallelThreads,
452            final String tempDirLocation) {
453        if (entriesToProcess.isEmpty()) {
454            return;
455        }
456        final File tempDirectory;
457        if (tempDirLocation == null || tempDirLocation.isEmpty()) {
458            System.out.println("No temporary directory is set. Creating a new one...");
459            try {
460                tempDirectory = Files.createTempDirectory("car-deploy-to-nexus").toFile();
461            } catch (final IOException e1) {
462                String message = "Could not create temp directory: " + e1.getMessage();
463                throw new UnsupportedOperationException(message, e1);
464            }
465        } else {
466            tempDirectory = new File(tempDirLocation, "car-deploy-to-nexus-" + UUID.randomUUID().toString());
467            tempDirectory.mkdirs();
468            if (!tempDirectory.exists() || !(tempDirectory.canWrite() && tempDirectory.canRead())) {
469                throw new IllegalArgumentException("Cannot access temporary directory " + tempDirLocation);
470            }
471        }
472        System.out.println(tempDirectory.getPath() + " will be used as temporary directory.");
473        final String basicAuth = getAuthHeader(username, password);
474        final String nexusVersion = getNexusVersion(serverUrl, username, password, basicAuth);
475        final ExecutorService executor =
476                Executors.newFixedThreadPool(Math.min(entriesToProcess.size(), parallelThreads));
477        try {
478            final CountDownLatch latch = new CountDownLatch(entriesToProcess.size());
479            for (final JarEntry entry : entriesToProcess) {
480                final String path = entry.getName().substring("MAVEN-INF/repository/".length());
481                executor.execute(() -> {
482                    try {
483                        if (!artifactExists(nexusVersion, serverUrl, basicAuth, repositoryName, path)) {
484                            final File extracted = extractJar(tempDirectory, jar, entry);
485                            sendJar(nexusVersion, serverUrl, basicAuth, repositoryName, path, extracted);
486                            sendPom(nexusVersion, serverUrl, basicAuth, repositoryName, path, extracted);
487                        }
488                    } catch (final Exception e) {
489                        System.err.println("A problem occured while uploading artifact: " + e.getMessage());
490                    } finally {
491                        latch.countDown();
492                    }
493                });
494            }
495            try {
496                latch.await();
497            } catch (InterruptedException e) {
498                System.err.println("Exception caught while awaiting for latch: " + e.getMessage());
499            }
500        } finally {
501            executor.shutdown();
502            try {
503                executor.awaitTermination(1, TimeUnit.DAYS);
504            } catch (final InterruptedException e) {
505                System.err.println("Interrupted while awaiting for executor to shutdown.");
506                Thread.currentThread().interrupt();
507            }
508            try {
509                System.out.println("Removing " + tempDirectory.getPath());
510                if (tempDirectory.exists()) {
511                    removeTempDirectoryRecursively(tempDirectory);
512                }
513            } catch (Exception e) {
514                System.err.println("Couldn't remove " + tempDirectory.getPath() + ": " + e.getMessage());
515            }
516        }
517    }
518
519    private static void removeTempDirectoryRecursively(final File file) {
520        if (file.exists() && file.isFile()) {
521            file.delete();
522        } else if (file.isDirectory()) {
523            final File[] files = file.listFiles();
524            if (files != null) {
525                for (final File child : files) {
526                    removeTempDirectoryRecursively(child);
527                }
528            }
529            file.delete();
530        }
531    }
532
533    private static File extractJar(final File destDirectory, final JarFile jar, final JarEntry entry)
534            throws IOException {
535        File extracted;
536        try (final InputStream is = jar.getInputStream(entry)) {
537            final String fileName = entry.getName().substring(entry.getName().lastIndexOf("/") + 1);
538            extracted = File.createTempFile("temp-", fileName, destDirectory);
539            Files.copy(is, extracted.toPath(), StandardCopyOption.REPLACE_EXISTING);
540        }
541        return extracted;
542    }
543
544    private static String getAuthHeader(final String username, final String password) {
545        if (username == null || username.isEmpty()) {
546            return null;
547        }
548        return "Basic " + Base64
549                .getEncoder()
550                .encodeToString((username + (password == null || password.isEmpty() ? "" : ":" + password)).getBytes());
551    }
552
553    private static boolean artifactExists(final String nexusVersion, final String serverUrl, final String basicAuth,
554            final String repositoryName, final String path) throws IOException {
555        HttpURLConnection conn = null;
556        try {
557            final URL url = new URL(getNexusUploadUrl(nexusVersion, serverUrl, repositoryName, path));
558            conn = HttpURLConnection.class.cast(url.openConnection());
559            conn.setDoOutput(true);
560            conn.setRequestMethod("GET");
561            if (basicAuth != null) {
562                conn.setRequestProperty("Authorization", basicAuth);
563            }
564            conn.connect();
565            if (conn.getResponseCode() == 404) {
566                return false;
567            } else if (conn.getResponseCode() == 401) {
568                throw new IllegalArgumentException("Authentication failed!");
569            } else if (conn.getResponseCode() == 400) {
570                System.out.println("Ignoring " + path + ", it is likely not deployed on the right repository.");
571            } else {
572                System.out.println("Artifact " + path + " already exists on " + serverUrl + ". Skipping.");
573            }
574        } finally {
575            if (conn != null) {
576                conn.disconnect();
577            }
578        }
579        return true;
580    }
581
582    private static void sendPom(final String nexusVersion, final String serverUrl, final String basicAuth,
583            final String repositoryName, final String path, final File jarFile) throws IOException {
584        final String pomPath = getPomPathFromPath(path);
585        System.out.println("Path of pom file resolved: " + pomPath);
586        try (final JarFile jar = new JarFile(jarFile)) {
587            JarEntry entry = jar.getJarEntry(pomPath);
588            if (entry == null) {
589                throw new FileNotFoundException("Could not find " + pomPath + " inside " + jar.getName());
590            }
591            try (final InputStream jarIs = jar.getInputStream(entry)) {
592                final String serverPomPath = path.substring(0, path.lastIndexOf(".")) + ".pom";
593                sendData(nexusVersion, serverUrl, basicAuth, repositoryName, serverPomPath, jarIs);
594            }
595        }
596    }
597
598    private static String getPomPathFromPath(final String path) {
599        final String parentPath = path.substring(0, path.lastIndexOf("/"));
600        final String version = parentPath.substring(parentPath.lastIndexOf("/") + 1);
601        final String fileName = path.substring(path.lastIndexOf("/") + 1);
602        final int versionIndex = fileName.indexOf(version);
603        final String artifactName;
604        if (versionIndex > 0) {
605            artifactName = fileName.substring(0, versionIndex - 1);
606        } else if (fileName.endsWith(".jar")) {
607            artifactName = fileName.substring(0, fileName.length() - 4);
608        } else {
609            artifactName = fileName;
610        }
611        String group = parentPath.substring(0, parentPath.lastIndexOf(artifactName));
612        if (group.startsWith("/")) {
613            group = group.substring(1);
614        }
615        if (group.endsWith("/")) {
616            group = group.substring(0, group.length() - 1);
617        }
618        group = group.replace("/", ".");
619        return "META-INF/maven/" + group + "/" + artifactName + "/pom.xml";
620    }
621
622    private static void sendJar(final String nexusVersion, final String serverUrl, final String basicAuth,
623            final String repositoryName, final String path, final File jarFile) throws IOException {
624        try (InputStream is = new FileInputStream(jarFile)) {
625            sendData(nexusVersion, serverUrl, basicAuth, repositoryName, path, is);
626        }
627    }
628
629    private static void sendData(final String nexusVersion, final String serverUrl, final String basicAuth,
630            final String repositoryName, final String path, final InputStream is) throws IOException {
631        System.out.println("Uploading " + path + " to " + serverUrl);
632        HttpURLConnection conn = null;
633        try {
634            URL url = new URL(getNexusUploadUrl(nexusVersion, serverUrl, repositoryName, path));
635            conn = (HttpURLConnection) url.openConnection();
636            conn.setDoOutput(true);
637            conn.setRequestMethod(getRequestMethod(nexusVersion));
638            if (basicAuth != null) {
639                conn.setRequestProperty("Authorization", basicAuth);
640            }
641            conn.setRequestProperty("Content-Type", "multipart/form-data");
642            conn.setRequestProperty("Accept", "*/*");
643            conn.connect();
644            try (OutputStream out = conn.getOutputStream()) {
645                byte[] buffer = new byte[1024];
646                int bytesRead = -1;
647                while ((bytesRead = is.read(buffer)) != -1) {
648                    out.write(buffer, 0, bytesRead);
649                }
650                out.flush();
651                if (conn.getResponseCode() != 201) {
652                    throw new IOException(conn.getResponseCode() + " - " + conn.getResponseMessage());
653                }
654            }
655            System.out.println(path + " Uploaded");
656        } finally {
657            if (conn != null) {
658                conn.disconnect();
659            }
660        }
661    }
662
663    // note: maybe move to restapi. see
664    // https://support.sonatype.com/hc/en-us/articles/115006744008-How-can-I-programmatically-upload-files-into-Nexus-3-
665    private static String getNexusUploadUrl(final String nexusVersion, final String serverUrl,
666            final String repositoryName, final String path) {
667        if (nexusVersion.equals("V2")) {
668            return getUploadUrl(serverUrl, "content/repositories", repositoryName, path);
669        } else if (nexusVersion.startsWith("V3")) {
670            return getUploadUrl(serverUrl, "repository", repositoryName, path);
671        }
672        throw new IllegalArgumentException("Unknown Nexus version: " + nexusVersion);
673    }
674
675    private static String getUploadUrl(final String serverUrl, final String repositoriesLocation, final String repo,
676            final String path) {
677        return serverUrl + (serverUrl.endsWith("/") ? "" : "/") + repositoriesLocation + "/" + repo + "/" + path;
678    }
679
680    private static String getNexusVersion(final String serverUrl, final String username, final String password,
681            final String auth) {
682        System.out.println("Checking " + serverUrl + " API version.");
683        final String version;
684        if (isV2(serverUrl, username, password, auth)) {
685            version = "V2";
686        } else if (isStableV3(serverUrl, username, password, auth)) {
687            version = "V3";
688        } else if (isBetaV3(serverUrl, username, password, auth)) {
689            version = "V3Beta";
690        } else {
691            throw new UnsupportedOperationException(
692                    "Provided url doesn't respond neither to Nexus 2 nor to Nexus 3 endpoints.");
693        }
694        System.out.println("Nexus API version is recognized as " + version);
695        return version;
696    }
697
698    private static boolean isV2(final String serverUrl, final String username, final String password,
699            final String auth) {
700        System.out.println("Checking for V2 version...");
701        HttpURLConnection conn = null;
702        try {
703            conn = prepareGet(serverUrl, username, password, "service/local/status", "*/*", auth);
704            if (conn.getResponseCode() >= 200 && conn.getResponseCode() <= 299) {
705                try (InputStream is = conn.getInputStream()) {
706                    final byte[] b = new byte[1024];
707                    final StringBuilder out = new StringBuilder();
708                    int read;
709                    while ((read = is.read(b, 0, b.length)) > 0) {
710                        out.append(new String(b, 0, read));
711                    }
712                    if (out.toString().contains("\"apiVersion\":\"2.")) {
713                        System.out.println("version is v2");
714                        return true;
715                    }
716                }
717            }
718        } catch (final IOException e) {
719            // no-op
720        } finally {
721            if (conn != null) {
722                conn.disconnect();
723            }
724        }
725        return false;
726    }
727
728    private static boolean isBetaV3(final String serverUrl, final String username, final String password,
729            final String auth) {
730        System.out.println("Checking for V3Beta version...");
731        return get(serverUrl, username, password, "service/rest/beta/repositories", auth);
732    }
733
734    private static boolean isStableV3(final String serverUrl, final String username, final String password,
735            final String auth) {
736        System.out.println("Checking for V3 version...");
737        return get(serverUrl, username, password, "service/rest/v1/repositories", auth);
738    }
739
740    private static boolean get(final String serverUrl, final String username, final String password, final String path,
741            final String auth) {
742        boolean passed = false;
743        HttpURLConnection conn = null;
744        try {
745            conn = prepareGet(serverUrl, username, password, path, "application/json", auth);
746            if (conn.getResponseCode() == 200) {
747                passed = true;
748            }
749        } catch (final IOException e) {
750            // no-op
751        } finally {
752            if (conn != null) {
753                conn.disconnect();
754            }
755        }
756        return passed;
757    }
758
759    private static HttpURLConnection prepareGet(final String serverUrl, final String username, final String password,
760            final String path, final String accept, final String auth) throws IOException {
761        final URL url = new URL(serverUrl + (serverUrl.endsWith("/") ? "" : "/") + path);
762        System.out.println("Sending GET request to " + url.getPath());
763        final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
764        conn.setDoInput(true);
765        final String userpass = username + ":" + password;
766        final String basicAuth = "Basic " + Base64.getEncoder().encodeToString(userpass.getBytes());
767        conn.setRequestMethod("GET");
768        if (auth != null) {
769            conn.setRequestProperty("Authorization", auth);
770        }
771        conn.setRequestProperty("Accept", accept);
772        conn.connect();
773        return conn;
774    }
775
776    private static String getRequestMethod(final String nexusVersion) {
777        if ("V2".equals(nexusVersion)) {
778            return "POST";
779        } else if (nexusVersion.startsWith("V3")) {
780            return "PUT";
781        }
782        throw new IllegalArgumentException("Unknown Nexus version: " + nexusVersion);
783    }
784}