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