001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.internal.impl;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.inject.Singleton;
024
025import java.io.IOException;
026import java.nio.file.Files;
027import java.nio.file.Path;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.Collections;
031import java.util.HashMap;
032import java.util.IdentityHashMap;
033import java.util.List;
034import java.util.ListIterator;
035import java.util.Map;
036
037import org.eclipse.aether.ConfigurationProperties;
038import org.eclipse.aether.RepositoryEvent;
039import org.eclipse.aether.RepositoryEvent.EventType;
040import org.eclipse.aether.RepositorySystemSession;
041import org.eclipse.aether.RequestTrace;
042import org.eclipse.aether.SyncContext;
043import org.eclipse.aether.artifact.Artifact;
044import org.eclipse.aether.impl.Installer;
045import org.eclipse.aether.impl.MetadataGenerator;
046import org.eclipse.aether.impl.MetadataGeneratorFactory;
047import org.eclipse.aether.impl.RepositoryEventDispatcher;
048import org.eclipse.aether.installation.InstallRequest;
049import org.eclipse.aether.installation.InstallResult;
050import org.eclipse.aether.installation.InstallationException;
051import org.eclipse.aether.metadata.MergeableMetadata;
052import org.eclipse.aether.metadata.Metadata;
053import org.eclipse.aether.repository.LocalArtifactRegistration;
054import org.eclipse.aether.repository.LocalMetadataRegistration;
055import org.eclipse.aether.repository.LocalRepositoryManager;
056import org.eclipse.aether.spi.artifact.generator.ArtifactGenerator;
057import org.eclipse.aether.spi.artifact.generator.ArtifactGeneratorFactory;
058import org.eclipse.aether.spi.artifact.transformer.ArtifactTransformer;
059import org.eclipse.aether.spi.io.PathProcessor;
060import org.eclipse.aether.spi.synccontext.SyncContextFactory;
061import org.eclipse.aether.util.ConfigUtils;
062import org.slf4j.Logger;
063import org.slf4j.LoggerFactory;
064
065import static java.util.Objects.requireNonNull;
066
067/**
068 */
069@Singleton
070@Named
071public class DefaultInstaller implements Installer {
072    private final Logger logger = LoggerFactory.getLogger(getClass());
073
074    private final PathProcessor pathProcessor;
075
076    private final RepositoryEventDispatcher repositoryEventDispatcher;
077
078    private final Map<String, ArtifactGeneratorFactory> artifactFactories;
079
080    private final Map<String, MetadataGeneratorFactory> metadataFactories;
081
082    private final Map<String, ArtifactTransformer> artifactTransformers;
083
084    private final SyncContextFactory syncContextFactory;
085
086    @Inject
087    public DefaultInstaller(
088            PathProcessor pathProcessor,
089            RepositoryEventDispatcher repositoryEventDispatcher,
090            Map<String, ArtifactGeneratorFactory> artifactFactories,
091            Map<String, MetadataGeneratorFactory> metadataFactories,
092            Map<String, ArtifactTransformer> artifactTransformers,
093            SyncContextFactory syncContextFactory) {
094        this.pathProcessor = requireNonNull(pathProcessor, "path processor cannot be null");
095        this.repositoryEventDispatcher =
096                requireNonNull(repositoryEventDispatcher, "repository event dispatcher cannot be null");
097        this.artifactFactories = Collections.unmodifiableMap(artifactFactories);
098        this.metadataFactories = Collections.unmodifiableMap(metadataFactories);
099        this.artifactTransformers = Collections.unmodifiableMap(artifactTransformers);
100        this.syncContextFactory = requireNonNull(syncContextFactory, "sync context factory cannot be null");
101    }
102
103    @Override
104    public InstallResult install(RepositorySystemSession session, InstallRequest request) throws InstallationException {
105        requireNonNull(session, "session cannot be null");
106        requireNonNull(request, "request cannot be null");
107        for (ArtifactTransformer transformer : artifactTransformers.values()) {
108            request = transformer.transformInstallArtifacts(session, request);
109        }
110        try (SyncContext syncContext = syncContextFactory.newInstance(session, false)) {
111            return install(syncContext, session, request);
112        }
113    }
114
115    private InstallResult install(SyncContext syncContext, RepositorySystemSession session, InstallRequest request)
116            throws InstallationException {
117        InstallResult result = new InstallResult(request);
118
119        RequestTrace trace = RequestTrace.newChild(request.getTrace(), request);
120
121        List<Artifact> artifacts = new ArrayList<>(request.getArtifacts());
122        List<? extends ArtifactGenerator> artifactGenerators =
123                Utils.getArtifactGenerators(session, artifactFactories, request);
124        try {
125            List<Artifact> generatedArtifacts = new ArrayList<>();
126            for (ArtifactGenerator artifactGenerator : artifactGenerators) {
127                Collection<? extends Artifact> generated = artifactGenerator.generate(generatedArtifacts);
128                for (Artifact generatedArtifact : generated) {
129                    Map<String, String> properties = new HashMap<>(generatedArtifact.getProperties());
130                    properties.put(
131                            ArtifactGeneratorFactory.ARTIFACT_GENERATOR_ID,
132                            requireNonNull(artifactGenerator.generatorId(), "generatorId"));
133                    Artifact ga = generatedArtifact.setProperties(properties);
134                    generatedArtifacts.add(ga);
135                }
136            }
137            artifacts.addAll(generatedArtifacts);
138
139            List<? extends MetadataGenerator> metadataGenerators =
140                    Utils.getMetadataGenerators(session, metadataFactories, request);
141
142            IdentityHashMap<Metadata, Object> processedMetadata = new IdentityHashMap<>();
143
144            List<Metadata> metadatas = Utils.prepareMetadata(metadataGenerators, artifacts);
145
146            syncContext.acquire(artifacts, Utils.combine(request.getMetadata(), metadatas));
147
148            for (Metadata metadata : metadatas) {
149                install(session, trace, metadata);
150                processedMetadata.put(metadata, null);
151                result.addMetadata(metadata);
152            }
153
154            boolean hardLink = ConfigUtils.getBoolean(
155                    session,
156                    ConfigurationProperties.DEFAULT_INSTALLER_HARD_LINK,
157                    ConfigurationProperties.INSTALLER_HARD_LINK);
158
159            Exception linkFailure = null;
160            for (ListIterator<Artifact> iterator = artifacts.listIterator(); iterator.hasNext(); ) {
161                Artifact artifact = iterator.next();
162
163                for (MetadataGenerator generator : metadataGenerators) {
164                    artifact = generator.transformArtifact(artifact);
165                }
166
167                iterator.set(artifact);
168
169                Exception failure = install(session, trace, artifact, hardLink);
170                if (failure != null && linkFailure == null) {
171                    linkFailure = failure;
172                } else if (failure != null) {
173                    linkFailure.addSuppressed(failure);
174                }
175                if (artifact.getProperty(ArtifactGeneratorFactory.ARTIFACT_GENERATOR_ID, null) == null) {
176                    result.addArtifact(artifact);
177                }
178            }
179            if (linkFailure != null) {
180                logger.warn(
181                        "Hard link not supported, falling back to copy for all artifacts: {}",
182                        linkFailure.getMessage(),
183                        linkFailure);
184            }
185
186            metadatas = Utils.finishMetadata(metadataGenerators, artifacts);
187
188            syncContext.acquire(null, metadatas);
189
190            for (Metadata metadata : metadatas) {
191                install(session, trace, metadata);
192                processedMetadata.put(metadata, null);
193                result.addMetadata(metadata);
194            }
195
196            for (Metadata metadata : request.getMetadata()) {
197                if (!processedMetadata.containsKey(metadata)) {
198                    install(session, trace, metadata);
199                    result.addMetadata(metadata);
200                }
201            }
202
203            return result;
204        } finally {
205            for (ArtifactGenerator artifactGenerator : artifactGenerators) {
206                try {
207                    artifactGenerator.close();
208                } catch (Exception e) {
209                    logger.warn("ArtifactGenerator close failure: {}", artifactGenerator.generatorId(), e);
210                }
211            }
212        }
213    }
214
215    /**
216     * Installs a single artifact. Returns the link failure exception if hard-linking was attempted but failed
217     * (caller logs it once after the loop), or {@code null} if the artifact was installed successfully via hard link
218     * or if {@code hardLink} is {@code false}.
219     */
220    private Exception install(RepositorySystemSession session, RequestTrace trace, Artifact artifact, boolean hardLink)
221            throws InstallationException {
222        final LocalRepositoryManager lrm = session.getLocalRepositoryManager();
223        final Path srcPath = artifact.getPath();
224        final Path dstPath = lrm.getAbsolutePathForLocalArtifact(artifact);
225
226        artifactInstalling(session, trace, artifact, dstPath);
227
228        Exception linkFailure = null;
229        Exception exception = null;
230        try {
231            if (dstPath.equals(srcPath)) {
232                throw new IllegalStateException("cannot install " + dstPath + " to same path");
233            }
234
235            if (hardLink) {
236                Files.createDirectories(dstPath.getParent());
237                if (Files.isDirectory(dstPath)) {
238                    throw new IOException("destination is a directory: " + dstPath);
239                }
240                try {
241                    Files.deleteIfExists(dstPath);
242                    Files.createLink(dstPath, srcPath);
243                } catch (UnsupportedOperationException | IllegalArgumentException | IOException e) {
244                    // cross-device link (EXDEV), provider mismatch (e.g. Jimfs), or unsupported FS — fall back to copy
245                    linkFailure = e;
246                    pathProcessor.copyWithTimestamp(srcPath, dstPath);
247                }
248            } else {
249                pathProcessor.copyWithTimestamp(srcPath, dstPath);
250            }
251            lrm.add(session, new LocalArtifactRegistration(artifact));
252        } catch (Exception e) {
253            exception = e;
254            throw new InstallationException("Failed to install artifact " + artifact + ": " + e.getMessage(), e);
255        } finally {
256            artifactInstalled(session, trace, artifact, dstPath, exception);
257        }
258        return linkFailure;
259    }
260
261    private void install(RepositorySystemSession session, RequestTrace trace, Metadata metadata)
262            throws InstallationException {
263        LocalRepositoryManager lrm = session.getLocalRepositoryManager();
264
265        Path dstPath = lrm.getAbsolutePathForLocalMetadata(metadata);
266
267        metadataInstalling(session, trace, metadata, dstPath);
268
269        Exception exception = null;
270        try {
271            if (metadata instanceof MergeableMetadata) {
272                ((MergeableMetadata) metadata).merge(dstPath, dstPath);
273            } else {
274                if (dstPath.equals(metadata.getPath())) {
275                    throw new IllegalStateException("cannot install " + dstPath + " to same path");
276                }
277                pathProcessor.copy(metadata.getPath(), dstPath);
278            }
279
280            lrm.add(session, new LocalMetadataRegistration(metadata));
281        } catch (Exception e) {
282            exception = e;
283            throw new InstallationException("Failed to install metadata " + metadata + ": " + e.getMessage(), e);
284        } finally {
285            metadataInstalled(session, trace, metadata, dstPath, exception);
286        }
287    }
288
289    private void artifactInstalling(
290            RepositorySystemSession session, RequestTrace trace, Artifact artifact, Path dstPath) {
291        RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.ARTIFACT_INSTALLING);
292        event.setTrace(trace);
293        event.setArtifact(artifact);
294        event.setRepository(session.getLocalRepositoryManager().getRepository());
295        event.setPath(dstPath);
296
297        repositoryEventDispatcher.dispatch(event.build());
298    }
299
300    private void artifactInstalled(
301            RepositorySystemSession session, RequestTrace trace, Artifact artifact, Path dstPath, Exception exception) {
302        RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.ARTIFACT_INSTALLED);
303        event.setTrace(trace);
304        event.setArtifact(artifact);
305        event.setRepository(session.getLocalRepositoryManager().getRepository());
306        event.setPath(dstPath);
307        event.setException(exception);
308
309        repositoryEventDispatcher.dispatch(event.build());
310    }
311
312    private void metadataInstalling(
313            RepositorySystemSession session, RequestTrace trace, Metadata metadata, Path dstPath) {
314        RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_INSTALLING);
315        event.setTrace(trace);
316        event.setMetadata(metadata);
317        event.setRepository(session.getLocalRepositoryManager().getRepository());
318        event.setPath(dstPath);
319
320        repositoryEventDispatcher.dispatch(event.build());
321    }
322
323    private void metadataInstalled(
324            RepositorySystemSession session, RequestTrace trace, Metadata metadata, Path dstPath, Exception exception) {
325        RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_INSTALLED);
326        event.setTrace(trace);
327        event.setMetadata(metadata);
328        event.setRepository(session.getLocalRepositoryManager().getRepository());
329        event.setPath(dstPath);
330        event.setException(exception);
331
332        repositoryEventDispatcher.dispatch(event.build());
333    }
334}