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.net.URI;
026import java.net.URISyntaxException;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collections;
030import java.util.List;
031import java.util.Set;
032import java.util.stream.Collectors;
033
034import org.eclipse.aether.RepositorySystemSession;
035import org.eclipse.aether.artifact.Artifact;
036import org.eclipse.aether.metadata.Metadata;
037import org.eclipse.aether.repository.RemoteRepository;
038import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
039import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactorySelector;
040import org.eclipse.aether.spi.connector.layout.RepositoryLayout;
041import org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory;
042import org.eclipse.aether.transfer.NoRepositoryLayoutException;
043import org.eclipse.aether.util.ConfigUtils;
044
045import static java.util.Objects.requireNonNull;
046
047/**
048 * Provides a Maven-2 repository layout for repositories with content type {@code "default"}.
049 */
050@Singleton
051@Named(Maven2RepositoryLayoutFactory.NAME)
052public final class Maven2RepositoryLayoutFactory implements RepositoryLayoutFactory {
053    public static final String NAME = "maven2";
054
055    public static final String CONFIG_PROP_CHECKSUMS_ALGORITHMS = "aether.checksums.algorithms";
056
057    private static final String DEFAULT_CHECKSUMS_ALGORITHMS = "SHA-1,MD5";
058
059    public static final String CONFIG_PROP_OMIT_CHECKSUMS_FOR_EXTENSIONS =
060            "aether.checksums.omitChecksumsForExtensions";
061
062    private static final String DEFAULT_OMIT_CHECKSUMS_FOR_EXTENSIONS = ".asc,.sigstore";
063
064    private float priority;
065
066    private final ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector;
067
068    public float getPriority() {
069        return priority;
070    }
071
072    @Inject
073    public Maven2RepositoryLayoutFactory(ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector) {
074        this.checksumAlgorithmFactorySelector = requireNonNull(checksumAlgorithmFactorySelector);
075    }
076
077    /**
078     * Sets the priority of this component.
079     *
080     * @param priority The priority.
081     * @return This component for chaining, never {@code null}.
082     */
083    public Maven2RepositoryLayoutFactory setPriority(float priority) {
084        this.priority = priority;
085        return this;
086    }
087
088    public RepositoryLayout newInstance(RepositorySystemSession session, RemoteRepository repository)
089            throws NoRepositoryLayoutException {
090        requireNonNull(session, "session cannot be null");
091        requireNonNull(repository, "repository cannot be null");
092        if (!"default".equals(repository.getContentType())) {
093            throw new NoRepositoryLayoutException(repository);
094        }
095
096        List<ChecksumAlgorithmFactory> checksumsAlgorithms = checksumAlgorithmFactorySelector.selectList(
097                ConfigUtils.parseCommaSeparatedUniqueNames(ConfigUtils.getString(
098                        session,
099                        DEFAULT_CHECKSUMS_ALGORITHMS,
100                        CONFIG_PROP_CHECKSUMS_ALGORITHMS + "." + repository.getId(),
101                        CONFIG_PROP_CHECKSUMS_ALGORITHMS)));
102
103        // ensure uniqueness of (potentially user set) extension list
104        Set<String> omitChecksumsForExtensions = Arrays.stream(ConfigUtils.getString(
105                                session,
106                                DEFAULT_OMIT_CHECKSUMS_FOR_EXTENSIONS,
107                                CONFIG_PROP_OMIT_CHECKSUMS_FOR_EXTENSIONS)
108                        .split(","))
109                .filter(s -> s != null && !s.trim().isEmpty())
110                .collect(Collectors.toSet());
111
112        // validation: enforce that all strings in this set are having leading dot
113        if (omitChecksumsForExtensions.stream().anyMatch(s -> !s.startsWith("."))) {
114            throw new IllegalArgumentException(String.format(
115                    "The configuration %s contains illegal values: %s (all entries must start with '.' (dot))",
116                    CONFIG_PROP_OMIT_CHECKSUMS_FOR_EXTENSIONS, omitChecksumsForExtensions));
117        }
118
119        return new Maven2RepositoryLayout(
120                checksumAlgorithmFactorySelector, checksumsAlgorithms, omitChecksumsForExtensions);
121    }
122
123    private static class Maven2RepositoryLayout implements RepositoryLayout {
124        private final ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector;
125
126        private final List<ChecksumAlgorithmFactory> configuredChecksumAlgorithms;
127
128        private final Set<String> extensionsWithoutChecksums;
129
130        private Maven2RepositoryLayout(
131                ChecksumAlgorithmFactorySelector checksumAlgorithmFactorySelector,
132                List<ChecksumAlgorithmFactory> configuredChecksumAlgorithms,
133                Set<String> extensionsWithoutChecksums) {
134            this.checksumAlgorithmFactorySelector = requireNonNull(checksumAlgorithmFactorySelector);
135            this.configuredChecksumAlgorithms = Collections.unmodifiableList(configuredChecksumAlgorithms);
136            this.extensionsWithoutChecksums = requireNonNull(extensionsWithoutChecksums);
137        }
138
139        private URI toUri(String path) {
140            try {
141                return new URI(null, null, path, null);
142            } catch (URISyntaxException e) {
143                throw new IllegalStateException(e);
144            }
145        }
146
147        @Override
148        public List<ChecksumAlgorithmFactory> getChecksumAlgorithmFactories() {
149            return configuredChecksumAlgorithms;
150        }
151
152        @Override
153        public boolean hasChecksums(Artifact artifact) {
154            String artifactExtension = artifact.getExtension(); // ie. pom.asc
155            for (String extensionWithoutChecksums : extensionsWithoutChecksums) {
156                if (artifactExtension.endsWith(extensionWithoutChecksums)) {
157                    return false;
158                }
159            }
160            return true;
161        }
162
163        @Override
164        public URI getLocation(Artifact artifact, boolean upload) {
165            StringBuilder path = new StringBuilder(128);
166
167            path.append(artifact.getGroupId().replace('.', '/')).append('/');
168
169            path.append(artifact.getArtifactId()).append('/');
170
171            path.append(artifact.getBaseVersion()).append('/');
172
173            path.append(artifact.getArtifactId()).append('-').append(artifact.getVersion());
174
175            if (!artifact.getClassifier().isEmpty()) {
176                path.append('-').append(artifact.getClassifier());
177            }
178
179            if (!artifact.getExtension().isEmpty()) {
180                path.append('.').append(artifact.getExtension());
181            }
182
183            return toUri(path.toString());
184        }
185
186        @Override
187        public URI getLocation(Metadata metadata, boolean upload) {
188            StringBuilder path = new StringBuilder(128);
189
190            if (!metadata.getGroupId().isEmpty()) {
191                path.append(metadata.getGroupId().replace('.', '/')).append('/');
192
193                if (!metadata.getArtifactId().isEmpty()) {
194                    path.append(metadata.getArtifactId()).append('/');
195
196                    if (!metadata.getVersion().isEmpty()) {
197                        path.append(metadata.getVersion()).append('/');
198                    }
199                }
200            }
201
202            path.append(metadata.getType());
203
204            return toUri(path.toString());
205        }
206
207        @Override
208        public List<ChecksumLocation> getChecksumLocations(Artifact artifact, boolean upload, URI location) {
209            if (!hasChecksums(artifact) || isChecksum(artifact.getExtension())) {
210                return Collections.emptyList();
211            }
212            return getChecksumLocations(location);
213        }
214
215        @Override
216        public List<ChecksumLocation> getChecksumLocations(Metadata metadata, boolean upload, URI location) {
217            return getChecksumLocations(location);
218        }
219
220        private List<ChecksumLocation> getChecksumLocations(URI location) {
221            List<ChecksumLocation> checksumLocations = new ArrayList<>(configuredChecksumAlgorithms.size());
222            for (ChecksumAlgorithmFactory checksumAlgorithmFactory : configuredChecksumAlgorithms) {
223                checksumLocations.add(ChecksumLocation.forLocation(location, checksumAlgorithmFactory));
224            }
225            return checksumLocations;
226        }
227
228        private boolean isChecksum(String extension) {
229            return checksumAlgorithmFactorySelector.isChecksumExtension(extension);
230        }
231    }
232}