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;
017
018import static java.util.Optional.ofNullable;
019import static java.util.stream.Collectors.joining;
020
021import java.io.BufferedOutputStream;
022import java.io.File;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.nio.file.Files;
027import java.time.LocalDateTime;
028import java.time.ZoneId;
029import java.time.format.DateTimeFormatter;
030import java.util.Collection;
031import java.util.HashSet;
032import java.util.Map;
033import java.util.Properties;
034import java.util.jar.JarEntry;
035import java.util.jar.JarOutputStream;
036import java.util.jar.Manifest;
037import java.util.stream.IntStream;
038import java.util.stream.Stream;
039
040import org.talend.sdk.component.dependencies.maven.MvnCoordinateToFileConverter;
041import org.talend.sdk.component.tools.exec.CarMain;
042import org.talend.sdk.component.tools.exec.Versions;
043
044import lombok.Data;
045
046/**
047 * Defines how to bundle a standard component archive.
048 * Currently the layout is the following one:
049 * - TALEND-INF/lib/*.jar.
050 * - TALEND-INF/metadata.properties.
051 */
052public class CarBundler implements Runnable {
053
054    private final Configuration configuration;
055
056    private final Log log;
057
058    public CarBundler(final Configuration configuration, final Object log) {
059        this.configuration = configuration;
060        try {
061            this.log = Log.class.isInstance(log) ? Log.class.cast(log) : new ReflectiveLog(log);
062        } catch (final NoSuchMethodException e) {
063            throw new IllegalArgumentException(e);
064        }
065    }
066
067    @Override
068    public void run() {
069        final String date = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now(ZoneId.of("UTC")));
070        final Properties metadata = new Properties();
071        metadata.put("date", date);
072        metadata.put("version", ofNullable(configuration.version).orElse("NC"));
073        metadata.put("CarBundlerVersion", Versions.KIT_VERSION);
074        metadata
075                .put("component_coordinates", ofNullable(configuration.mainGav)
076                        .orElseThrow(() -> new IllegalArgumentException("No component coordinates specified")));
077        metadata.put("type", ofNullable(configuration.type).orElse("connector"));
078        if (configuration.getCustomMetadata() != null) {
079            configuration.getCustomMetadata().forEach(metadata::setProperty);
080        }
081
082        configuration.getOutput().getParentFile().mkdirs();
083
084        final Manifest manifest = new Manifest();
085        manifest.getMainAttributes().putValue("Main-Class", CarMain.class.getName());
086        manifest.getMainAttributes().putValue("Created-By", "Talend Component Kit Tooling");
087        manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
088        manifest.getMainAttributes().putValue("Build-Date", date);
089
090        try (final JarOutputStream zos = new JarOutputStream(
091                new BufferedOutputStream(new FileOutputStream(configuration.getOutput())), manifest)) {
092            final Collection<String> created = new HashSet<>();
093
094            // folders
095            Stream.of("TALEND-INF/", "META-INF/", "MAVEN-INF/", "MAVEN-INF/repository/").forEach(folder -> {
096                try {
097                    zos.putNextEntry(new JarEntry(folder));
098                    zos.closeEntry();
099                } catch (final IOException e) {
100                    throw new IllegalStateException(e);
101                }
102                created.add(folder.substring(0, folder.length() - 1));
103            });
104
105            // libs
106            final MvnCoordinateToFileConverter converter = new MvnCoordinateToFileConverter();
107            configuration.getArtifacts().forEach((gav, file) -> {
108                final String path = "MAVEN-INF/repository/" + converter.toArtifact(gav).toPath();
109                try {
110                    createFolders(zos, created, path.split("/"));
111
112                    zos.putNextEntry(new JarEntry(path));
113                    Files.copy(file.toPath(), zos);
114                    zos.closeEntry();
115                } catch (final IOException ioe) {
116                    throw new IllegalStateException(ioe);
117                }
118            });
119
120            // meta
121            zos.putNextEntry(new JarEntry("TALEND-INF/metadata.properties"));
122            metadata.store(zos, "Generated metadata by Talend Component Kit Car Bundle");
123            zos.closeEntry();
124
125            // executable
126            final String main = CarMain.class.getName().replace('.', '/') + ".class";
127            createFolders(zos, created, main.split("/"));
128            zos.putNextEntry(new JarEntry(main));
129            try (final InputStream stream = CarBundler.class.getClassLoader().getResourceAsStream(main)) {
130                byte[] buffer = new byte[1024];
131                int read;
132                while ((read = stream.read(buffer)) >= 0) {
133                    zos.write(buffer, 0, read);
134                }
135            }
136            zos.closeEntry();
137        } catch (final IOException e) {
138            throw new IllegalStateException(e.getMessage(), e);
139        }
140
141        log.info("Created " + configuration.getOutput());
142    }
143
144    private void createFolders(final JarOutputStream zos, final Collection<String> created, final String[] parts) {
145        IntStream
146                .range(0, parts.length - 1)
147                .mapToObj(i -> Stream.of(parts).limit(i + 1).collect(joining("/")))
148                .filter(p -> !created.contains(p))
149                .peek(folder -> {
150                    try {
151                        zos.putNextEntry(new JarEntry(folder + '/'));
152                        zos.closeEntry();
153                    } catch (final IOException e) {
154                        throw new IllegalStateException(e);
155                    }
156                })
157                .forEach(created::add);
158    }
159
160    @Data
161    public static class Configuration {
162
163        private String mainGav;
164
165        private String version;
166
167        private Map<String, File> artifacts;
168
169        private Map<String, String> customMetadata;
170
171        private File output;
172
173        private String type;
174    }
175}