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.toList;
020
021import java.io.BufferedOutputStream;
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.FileOutputStream;
025import java.io.FileReader;
026import java.io.FileWriter;
027import java.io.IOException;
028import java.io.InputStream;
029import java.io.OutputStream;
030import java.io.Reader;
031import java.io.Writer;
032import java.nio.file.Files;
033import java.nio.file.StandardOpenOption;
034import java.time.LocalDateTime;
035import java.time.format.DateTimeFormatter;
036import java.util.List;
037import java.util.Map;
038import java.util.Properties;
039import java.util.stream.Stream;
040
041import org.talend.sdk.component.dependencies.maven.Artifact;
042import org.talend.sdk.component.dependencies.maven.MvnCoordinateToFileConverter;
043
044public class StudioInstaller implements Runnable {
045
046    private final String mainGav;
047
048    private final File studioHome;
049
050    private final Map<String, File> artifacts;
051
052    private final Log log;
053
054    private final boolean enforceDeployment;
055
056    private final File m2;
057
058    public StudioInstaller(final String mainGav, final File studioHome, final Map<String, File> artifacts,
059            final Object log, final boolean enforceDeployment, final File m2) {
060        this.mainGav = mainGav;
061        this.studioHome = studioHome;
062        this.artifacts = artifacts;
063        this.m2 = m2;
064        this.enforceDeployment = enforceDeployment;
065        try {
066            this.log = Log.class.isInstance(log) ? Log.class.cast(log) : new ReflectiveLog(log);
067        } catch (final NoSuchMethodException e) {
068            throw new IllegalStateException(e);
069        }
070    }
071
072    @Override
073    public void run() {
074        log.info("Installing development version of " + mainGav + " in " + studioHome);
075
076        final List<String> artifacts = this.artifacts.values().stream().map(File::getName).collect(toList());
077        final String mvnMeta[] = mainGav.split(":");
078        final String artifact = mvnMeta[1];
079        final String version = mvnMeta[2];
080
081        // 0. check if component can be installed.
082        final File configIni = new File(studioHome, "configuration/config.ini");
083        final Properties config = new Properties();
084        if (configIni.exists()) {
085            try (final InputStream is = new FileInputStream(configIni)) {
086                config.load(is);
087            } catch (final IOException e) {
088                throw new IllegalStateException(e);
089            }
090        }
091        String registry = config
092                .getProperty("component.java.registry",
093                        config.getProperty("talend.component.server.component.registry"));
094        if (!enforceDeployment && registry != null && new File(registry).exists()) {
095            final Properties components = new Properties();
096            try (final Reader reader = new FileReader(registry)) {
097                components.load(reader);
098                if (components.containsKey(artifact)) {
099                    final String installedVersion = components.getProperty(artifact).split(":")[2];
100                    if (!version.equals(installedVersion)) {
101                        throw new IllegalStateException("Can't deploy this component. A different version '"
102                                + installedVersion
103                                + "' is already installed.\nYou can enforce the deployment by using -Dtalend.component.enforceDeployment=true");
104                    }
105                }
106            } catch (final IOException e) {
107                log.error("Can't load registered component from the studio " + e.getMessage());
108            }
109        }
110
111        // 1. remove staled libs from the cache (configuration/org.eclipse.osgi)
112        final File osgiCache = new File(studioHome, "configuration/org.eclipse.osgi");
113        if (osgiCache.isDirectory()) {
114            ofNullable(osgiCache.listFiles(child -> {
115                try {
116                    return child.isDirectory() && Integer.parseInt(child.getName()) > 0;
117                } catch (final NumberFormatException nfe) {
118                    return false;
119                }
120            }))
121                    .map(Stream::of)
122                    .orElseGet(Stream::empty)
123                    .map(id -> new File(id, ".cp"))
124                    .filter(File::exists)
125                    .flatMap(cp -> ofNullable(cp.listFiles((dir, name) -> name.endsWith(".jar")))
126                            .map(Stream::of)
127                            .orElseGet(Stream::empty))
128                    .filter(jar -> artifacts.contains(jar.getName()))
129                    .forEach(this::tryDelete);
130        }
131
132        // 2. install the runtime dependency tree (scope compile+runtime) in the studio m2 repo
133        final String repoType = config.getProperty("maven.repository");
134        if (!"global".equals(repoType)) {
135            final MvnCoordinateToFileConverter converter = new MvnCoordinateToFileConverter();
136            this.artifacts.forEach((gav, file) -> {
137                final Artifact dependency = converter.toArtifact(gav);
138                final File target = m2 == null || !m2.exists()
139                        ? new File(studioHome, "configuration/.m2/repository/" + dependency.toPath())
140                        : new File(m2, dependency.toPath());
141                try {
142                    if (target.exists() && !dependency.getVersion().endsWith("-SNAPSHOT")) {
143                        log.info(gav + " already exists, skipping");
144                        return;
145                    }
146                    copy(file, target);
147                    log.info("Installed " + gav + " at " + target.getAbsolutePath());
148                } catch (final IOException e) {
149                    throw new IllegalStateException(e);
150                }
151            });
152        } else {
153            log
154                    .info("Studio " + studioHome
155                            + " configured to use global maven repository, skipping artifact installation");
156        }
157
158        // 3. register component adding them into the registry
159        if (registry == null) {
160            final File registryLocation = new File(configIni.getParentFile(), "components-registration.properties");
161            registryLocation.getParentFile().mkdirs();
162            registry = registryLocation.getAbsolutePath().replace('\\', '/');
163
164            config.setProperty("component.java.registry", registry);
165            try {
166                final File backup = new File(configIni.getParentFile(), "backup/" + configIni.getName() + "_"
167                        + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-mm-dd_HH-mm-ss")));
168                log.info("Saving configuration in " + backup);
169                copy(configIni, backup);
170            } catch (final IOException e) {
171                throw new IllegalStateException(e);
172            }
173            try (final Writer writer = new FileWriter(configIni)) {
174                config
175                        .store(writer, "File rewritten by " + getClass().getName()
176                                + " utility to add component.java.registry entry");
177                log.info("Updated " + configIni + " to add the component registry entry");
178            } catch (final IOException e) {
179                throw new IllegalStateException(e);
180            }
181            try {
182                Files.write(registryLocation.toPath(), new byte[0], StandardOpenOption.CREATE_NEW);
183            } catch (final IOException e) {
184                throw new IllegalStateException(e);
185            }
186        }
187        final Properties components = new Properties();
188        try (final Reader reader = new FileReader(registry)) {
189            components.load(reader);
190        } catch (final IOException e) {
191            throw new IllegalStateException(e);
192        }
193
194        if (!components.containsKey(artifact) || enforceDeployment) {
195            components.setProperty(artifact, mainGav);
196            try (final Writer writer = new FileWriter(registry)) {
197                components.store(writer, "File rewritten to add " + mainGav);
198                log.info("Updated " + registry + " with '" + mainGav + "'");
199            } catch (final IOException e) {
200                throw new IllegalStateException(e);
201            }
202        }
203    }
204
205    private void copy(final File source, final File target) throws IOException {
206        mkdirP(target.getParentFile());
207        try (final OutputStream to = new BufferedOutputStream(new FileOutputStream(target))) {
208            Files.copy(source.toPath(), to);// this copy don't lock on win
209        }
210    }
211
212    private void mkdirP(final File dir) {
213        if (!dir.exists() && !dir.mkdirs()) {
214            throw new IllegalStateException("Can't create " + dir);
215        }
216    }
217
218    private void tryDelete(final File jar) {
219        if (!jar.delete()) {
220            log.error("Can't delete " + jar.getAbsolutePath());
221        } else {
222            log.info("Deleting " + jar.getAbsolutePath());
223        }
224    }
225
226}