001/** 002 * Copyright (C) 2006-2025 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}