001/*
002 * Extra Bnd Repository Plugins
003 * Copyright (C) 2019  Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package de.mnl.osgi.bnd.maven;
020
021import aQute.bnd.service.RepositoryPlugin;
022import aQute.maven.api.Archive;
023import aQute.maven.api.IMavenRepo;
024import aQute.maven.api.Program;
025import aQute.maven.api.Revision;
026import aQute.maven.provider.MavenBackingRepository;
027import aQute.maven.provider.MavenRepository;
028import aQute.maven.provider.MetadataParser;
029import aQute.maven.provider.MetadataParser.RevisionMetadata;
030import aQute.service.reporter.Reporter;
031
032import static de.mnl.osgi.bnd.maven.RepositoryUtils.rethrow;
033import static de.mnl.osgi.bnd.maven.RepositoryUtils.unthrow;
034
035import java.io.Closeable;
036import java.io.File;
037import java.io.IOException;
038import java.lang.reflect.InvocationTargetException;
039import java.lang.reflect.UndeclaredThrowableException;
040import java.util.ArrayList;
041import java.util.Comparator;
042import java.util.List;
043import java.util.Map;
044import java.util.Optional;
045import java.util.concurrent.ConcurrentHashMap;
046import java.util.concurrent.Executor;
047import java.util.regex.Pattern;
048import java.util.stream.Collectors;
049import java.util.stream.Stream;
050
051import org.apache.maven.model.Model;
052import org.apache.maven.model.building.DefaultModelBuilder;
053import org.apache.maven.model.building.DefaultModelBuildingRequest;
054import org.apache.maven.model.building.DefaultModelProcessor;
055import org.apache.maven.model.building.ModelBuilder;
056import org.apache.maven.model.building.ModelBuildingException;
057import org.apache.maven.model.building.ModelBuildingRequest;
058import org.apache.maven.model.building.ModelBuildingResult;
059import org.apache.maven.model.building.ModelSource;
060import org.apache.maven.model.composition.DefaultDependencyManagementImporter;
061import org.apache.maven.model.inheritance.DefaultInheritanceAssembler;
062import org.apache.maven.model.interpolation.StringSearchModelInterpolator;
063import org.apache.maven.model.io.DefaultModelReader;
064import org.apache.maven.model.locator.DefaultModelLocator;
065import org.apache.maven.model.management.DefaultDependencyManagementInjector;
066import org.apache.maven.model.management.DefaultPluginManagementInjector;
067import org.apache.maven.model.normalization.DefaultModelNormalizer;
068import org.apache.maven.model.path.DefaultModelPathTranslator;
069import org.apache.maven.model.path.DefaultModelUrlNormalizer;
070import org.apache.maven.model.path.DefaultPathTranslator;
071import org.apache.maven.model.path.DefaultUrlNormalizer;
072import org.apache.maven.model.profile.DefaultProfileInjector;
073import org.apache.maven.model.profile.DefaultProfileSelector;
074import org.apache.maven.model.resolution.UnresolvableModelException;
075import org.apache.maven.model.superpom.DefaultSuperPomProvider;
076import org.apache.maven.model.validation.DefaultModelValidator;
077import org.osgi.util.promise.Promise;
078
079/**
080 * Provides a composite {@link IMavenRepo} view on several 
081 * {@link MavenBackingRepository} instances.
082 * The class replaces {@link MavenRepository} which lacks some
083 * required functionality. (Besides, this class has a more
084 * appropriate name.)
085 * <P>
086 * The information about artifacts is provided as a maven
087 * {@link Model}. It is evaluated using the maven libraries and
088 * should therefore be consistent with the model information
089 * used in other maven repository based tools.
090 */
091@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "deprecation" })
092public class CompositeMavenRepository implements Closeable {
093
094    public static final Pattern COORDS_SPLITTER = Pattern.compile("\\s*;\\s*");
095    private final MavenRepository bndMavenRepo;
096    private final Reporter reporter;
097    private final Map<Program, List<BoundRevision>> programCache
098        = new ConcurrentHashMap<>();
099    private final Map<Revision, Model> modelCache
100        = new ConcurrentHashMap<>();
101    private final BndModelResolver modelResolver;
102    private final ModelBuilder modelBuilder;
103
104    /**
105     * Use local or remote URL in index.
106     */
107    public enum BinaryLocation {
108        LOCAL, REMOTE
109    }
110
111    /**
112     * Instantiates a new composite maven repository.
113     *
114     * @param base the base
115     * @param repoId the repository id
116     * @param releaseRepos the backing release repositories
117     * @param snapshotRepos the backing snapshot repositories
118     * @param executor an executor
119     * @param reporter the reporter
120     * @throws Exception the exception
121     */
122    @SuppressWarnings({ "PMD.SignatureDeclareThrowsException",
123        "PMD.AvoidDuplicateLiterals" })
124    public CompositeMavenRepository(File base, String repoId,
125            List<MavenBackingRepository> releaseRepos,
126            List<MavenBackingRepository> snapshotRepos, Executor executor,
127            Reporter reporter)
128            throws Exception {
129        bndMavenRepo = new MavenRepository(base, repoId, releaseRepos,
130            snapshotRepos, executor, reporter);
131        this.reporter = reporter;
132        modelResolver = new BndModelResolver(bndMavenRepo, reporter);
133
134        // Create the maven model builder. This code is ridiculous,
135        // but using maven's CDI pulls in an even more ridiculous
136        // number of libraries. To get an idea why, read
137        // https://lairdnelson.wordpress.com/2017/03/06/maven-and-the-project-formerly-known-as-aether/
138        DefaultModelBuilder builder = new DefaultModelBuilder();
139        builder.setProfileSelector(new DefaultProfileSelector());
140        DefaultModelProcessor processor = new DefaultModelProcessor();
141        processor.setModelLocator(new DefaultModelLocator());
142        processor.setModelReader(new DefaultModelReader());
143        builder.setModelProcessor(processor);
144        builder.setModelValidator(new DefaultModelValidator());
145        DefaultSuperPomProvider pomProvider = new DefaultSuperPomProvider();
146        pomProvider.setModelProcessor(processor);
147        DefaultSuperPomProvider superPomProvider
148            = new DefaultSuperPomProvider();
149        superPomProvider.setModelProcessor(processor);
150        builder.setSuperPomProvider(superPomProvider);
151        builder.setModelNormalizer(new DefaultModelNormalizer());
152        builder.setInheritanceAssembler(new DefaultInheritanceAssembler());
153        StringSearchModelInterpolator modelInterpolator
154            = new StringSearchModelInterpolator();
155        DefaultPathTranslator pathTranslator = new DefaultPathTranslator();
156        modelInterpolator.setPathTranslator(pathTranslator);
157        DefaultUrlNormalizer urlNormalizer = new DefaultUrlNormalizer();
158        modelInterpolator.setUrlNormalizer(urlNormalizer);
159        builder.setModelInterpolator(modelInterpolator);
160        DefaultModelUrlNormalizer modelUrlNormalizer
161            = new DefaultModelUrlNormalizer();
162        modelUrlNormalizer.setUrlNormalizer(urlNormalizer);
163        builder.setModelUrlNormalizer(modelUrlNormalizer);
164        DefaultModelPathTranslator modelPathTranslator
165            = new DefaultModelPathTranslator();
166        modelPathTranslator.setPathTranslator(pathTranslator);
167        builder.setModelPathTranslator(modelPathTranslator);
168        builder
169            .setPluginManagementInjector(new DefaultPluginManagementInjector());
170        builder.setDependencyManagementInjector(
171            new DefaultDependencyManagementInjector());
172        builder.setDependencyManagementImporter(
173            new DefaultDependencyManagementImporter());
174        builder.setProfileInjector(new DefaultProfileInjector());
175        modelBuilder = builder;
176    }
177
178    /**
179     * Gets the name of this repository.
180     *
181     * @return the name
182     */
183    public String name() {
184        return bndMavenRepo.getName();
185    }
186
187    /**
188     * Reset any cached information.
189     */
190    public void reset() {
191        programCache.clear();
192        modelCache.clear();
193    }
194
195    @Override
196    public void close() throws IOException {
197        bndMavenRepo.close();
198    }
199
200    /**
201    * Returns all backing repositories.
202    *
203    * @return the list of all repositories
204    */
205    public List<MavenBackingRepository> backing() {
206        List<MavenBackingRepository> result
207            = new ArrayList<>(bndMavenRepo.getReleaseRepositories());
208        result.addAll(bndMavenRepo.getSnapshotRepositories());
209        return result;
210    }
211
212    /**
213     * Returns all repositories as a stream.
214     *
215     * @return the repositories as stream
216     */
217    public Stream<MavenBackingRepository> backingAsStream() {
218        return Stream.concat(bndMavenRepo.getReleaseRepositories().stream(),
219            bndMavenRepo.getSnapshotRepositories().stream()).distinct();
220    }
221
222    /**
223     * Retrieves the file from a remote repository into the repositories 
224     * local cache directory if it doesn't exist yet.
225     *
226     * @param archive The archive to fetch
227     * @return the file or null if not found
228     * @throws IOException Signals that an I/O exception has occurred.
229     */
230    @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
231        "PMD.AvoidInstanceofChecksInCatchClause",
232        "PMD.AvoidDuplicateLiterals" })
233    public Promise<File> retrieve(Archive archive) throws IOException {
234        try {
235            return bndMavenRepo.get(archive);
236        } catch (Exception e) {
237            if (e instanceof InvocationTargetException
238                && ((InvocationTargetException) e)
239                    .getTargetException() instanceof IOException) {
240                // Should be the only possible exception here
241                throw (IOException) ((InvocationTargetException) e)
242                    .getTargetException();
243            }
244            throw new UndeclaredThrowableException(e);
245        }
246    }
247
248    /**
249     * Get the file object for the archive. The file does not have to exist.
250     * The use case for this is to have the {@link File} already while
251     * waiting for the {@link Promise} returned by {@link #retrieve(Archive)}
252     * to complete.
253     * <P>
254     * This is required for the implementation of {@link RepositoryPlugin#get}.
255     * Besides this use case, it should probably not be used.
256     * 
257     * @param archive the archive to find the file for
258     * @return the File or null if not found
259     */
260    public File toLocalFile(Archive archive) {
261        return bndMavenRepo.toLocalFile(archive);
262    }
263
264    /**
265     * Gets the file from the local cache directory, retrieving it
266     * first if it doesn't exist yet.
267     *
268     * @param archive The archive to fetch
269     * @return the file or null if not found
270     * @throws IOException Signals that an I/O exception has occurred.
271     */
272    @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
273        "PMD.AvoidInstanceofChecksInCatchClause",
274        "PMD.AvoidDuplicateLiterals" })
275    public File get(Archive archive) throws IOException {
276        try {
277            return bndMavenRepo.get(archive).getValue();
278        } catch (Exception e) {
279            if (e instanceof InvocationTargetException
280                && ((InvocationTargetException) e)
281                    .getTargetException() instanceof IOException) {
282                // Should be the only possible exception here
283                throw (IOException) ((InvocationTargetException) e)
284                    .getTargetException();
285            }
286            throw new UndeclaredThrowableException(e);
287        }
288    }
289
290    /**
291     * Wrapper for {@link MavenBackingRepository#getRevisions(Program, List)}
292     * that returns the result instead of accumulating it and maps the
293     * (too general) exception.
294     *
295     * @param mbr the backing repository
296     * @param program the program
297     * @return the revisions
298     * @throws IOException Signals that an I/O exception has occurred.
299     */
300    @SuppressWarnings({ "PMD.LinguisticNaming", "PMD.AvoidRethrowingException",
301        "PMD.AvoidCatchingGenericException",
302        "PMD.AvoidThrowingRawExceptionTypes", "PMD.AvoidDuplicateLiterals" })
303    private static List<Revision> revisionsFrom(MavenBackingRepository mbr,
304            Program program) throws IOException {
305        List<Revision> result = new ArrayList<>();
306        try {
307            mbr.getRevisions(program, result);
308        } catch (IOException e) {
309            // Should be the only reason.
310            throw e;
311        } catch (Exception e) {
312            throw new RuntimeException(e);
313        }
314        return result;
315    }
316
317    /**
318     * Get the bound revisions of the given program.
319     *
320     * @param program the program
321     * @return the list
322     * @throws IOException Signals that an I/O exception has occurred.
323     */
324    public Stream<BoundRevision> findRevisions(Program program)
325            throws IOException {
326        return rethrow(IOException.class,
327            () -> programCache.computeIfAbsent(program,
328                prg -> unthrow(() -> backingAsStream())
329                    .flatMap(mbr -> unthrow(
330                        () -> revisionsFrom(mbr, program).stream()
331                            .map(revision -> new BoundRevision(mbr, revision))))
332                    .collect(Collectors.toList()))).stream();
333    }
334
335    /**
336     * Converts a {@link Revision} to a {@link BoundRevision}.
337     *
338     * @param revision the revision
339     * @return the bound revision
340     * @throws IOException Signals that an I/O exception has occurred.
341     */
342    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
343    public Optional<BoundRevision> find(Revision revision)
344            throws IOException {
345        return findRevisions(revision.program)
346            .filter(rev -> rev.unbound().equals(revision)).findFirst();
347    }
348
349    /**
350     * Converts a {@link Program} and a version, which
351     * may be a range, to a {@link BoundRevision}.
352     *
353     * @param program the program
354     * @param version the version
355     * @return the bound revision
356     * @throws IOException Signals that an I/O exception has occurred.
357     */
358    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
359    public Optional<BoundRevision> find(Program program,
360            MavenVersionSpecification version) throws IOException {
361        if (version instanceof MavenVersion) {
362            return find(((MavenVersion) version).of(program));
363        }
364        MavenVersionRange range = (MavenVersionRange) version;
365        return findRevisions(program)
366            .filter(rev -> range.includes(rev.version()))
367            .max(Comparator.naturalOrder());
368    }
369
370    /**
371     * Get a model of the specified revision. Dependency versions
372     * remain unresolved, i.e. when specified as a range, the range
373     * is preserved.
374     *
375     * @param revision the archive
376     * @return the dependencies
377     * @throws MavenResourceException the maven resource exception
378     */
379    @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
380        "PMD.AvoidInstanceofChecksInCatchClause", "PMD.PreserveStackTrace",
381        "PMD.AvoidRethrowingException" })
382    public Model model(Revision revision) throws MavenResourceException {
383        return rethrow(MavenResourceException.class,
384            () -> modelCache.computeIfAbsent(
385                revision, key -> unthrow(() -> readModel(key))));
386    }
387
388    private Model readModel(Revision revision) throws MavenResourceException {
389        DefaultModelBuildingRequest request = new DefaultModelBuildingRequest();
390        try {
391            ModelSource modelSource = modelResolver.resolveModel(revision.group,
392                revision.artifact, revision.version.toString());
393            request.setModelResolver(modelResolver)
394                .setModelSource(modelSource)
395                .setValidationLevel(
396                    ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL)
397                .setTwoPhaseBuilding(false);
398            ModelBuildingResult result = modelBuilder.build(request);
399            return result.getEffectiveModel();
400        } catch (UnresolvableModelException | ModelBuildingException e) {
401            throw new MavenResourceException(e);
402        }
403    }
404
405    /**
406     * Gets the resolved archive. "Resolving" an archive means finding
407     * the binaries with the specified extension and classifier belonging
408     * to the given version. While this can be done with straight forward
409     * name mapping for releases, snapshots have a timestamp that has to
410     * be looked up in the backing repository.
411     *
412     * @param revision the revision
413     * @param extension the extension
414     * @param classifier the classifier
415     * @return the resolved archive
416     * @throws IOException Signals that an I/O exception has occurred.
417     */
418    @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
419        "PMD.AvoidThrowingRawExceptionTypes", "PMD.AvoidRethrowingException" })
420    public BoundArchive resolve(BoundRevision revision,
421            String extension, String classifier) throws IOException {
422        if (!revision.isSnapshot()) {
423            return revision.archive(extension, classifier);
424        }
425        try {
426            MavenVersion version = MavenVersion.from(revision
427                .mavenBackingRepository().getVersion(revision.unbound()));
428            return revision.archive(version, extension, classifier);
429        } catch (IOException e) {
430            throw e;
431        } catch (Exception e) {
432            throw new RuntimeException(e);
433        }
434    }
435
436    @SuppressWarnings("PMD.AvoidCatchingGenericException")
437    protected void refreshSnapshot(BoundArchive archive) {
438        File metaFile = bndMavenRepo.toLocalFile(
439            archive.getRevision()
440                .metadata(archive.mavenBackingRepository().getId()));
441        RevisionMetadata metaData;
442        try {
443            metaData = MetadataParser.parseRevisionMetadata(metaFile);
444        } catch (Exception e) {
445            reporter.exception(e, "Problem accessing %s.", archive);
446            return;
447        }
448        File archiveFile = bndMavenRepo.toLocalFile(archive);
449        if (archiveFile.lastModified() < metaData.lastUpdated) {
450            archiveFile.delete();
451        }
452        File pomFile = bndMavenRepo.toLocalFile(archive.getPomArchive());
453        if (pomFile.lastModified() < metaData.lastUpdated) {
454            pomFile.delete();
455        }
456    }
457
458}