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 static aQute.bnd.osgi.repository.BridgeRepository.addInformationCapability;
022
023import aQute.bnd.osgi.resource.CapabilityBuilder;
024import aQute.bnd.osgi.resource.ResourceBuilder;
025import aQute.maven.api.Archive;
026import aQute.maven.api.Program;
027import aQute.maven.api.Revision;
028import aQute.maven.provider.MavenBackingRepository;
029import aQute.service.reporter.Reporter;
030
031import java.io.File;
032import java.io.IOException;
033import java.util.ArrayList;
034import java.util.Collection;
035import java.util.HashSet;
036import java.util.List;
037import java.util.Map;
038import java.util.Optional;
039import java.util.Set;
040import java.util.concurrent.ConcurrentHashMap;
041import java.util.concurrent.Executor;
042import java.util.function.Function;
043import java.util.stream.Collectors;
044
045import org.apache.maven.model.Dependency;
046import org.apache.maven.model.Model;
047import org.apache.maven.model.building.ModelBuildingException;
048import org.apache.maven.model.resolution.UnresolvableModelException;
049import org.osgi.resource.Capability;
050import org.osgi.resource.Requirement;
051import org.osgi.resource.Resource;
052
053/**
054 * Wraps the artifacts from a maven reposityor as {@link Resource}s.
055 */
056public class MavenResourceRepository extends CompositeMavenRepository {
057
058    /** The namespace used to store the maven dependencies information. */
059    public static final String MAVEN_DEPENDENCIES_NS
060        = "maven.dependencies.info";
061
062    private Function<Revision, Optional<Resource>> resourceSupplier
063        = resource -> Optional.empty();
064    private final Map<Revision, MavenResource> resourceCache
065        = new ConcurrentHashMap<>();
066
067    public MavenResourceRepository(File base, String repoId,
068            List<MavenBackingRepository> releaseRepos,
069            List<MavenBackingRepository> snapshotRepos, Executor executor,
070            Reporter reporter) throws Exception {
071        super(base, repoId, releaseRepos, snapshotRepos, executor, reporter);
072    }
073
074    @Override
075    public void reset() {
076        super.reset();
077        resourceCache.clear();
078    }
079
080    /**
081     * Sets a function that can provide resource information more
082     * efficiently (e.g. from some local persistent cache) than
083     * the remote maven repository.
084     * <P>
085     * Any resource information provided by the function must be
086     * complete, i.e. must hold the information from the "bnd.info"
087     * namespace and from the "maven.dependencies.info" namespace.
088     *
089     * @param resourceSupplier the resource supplier
090     * @return the composite maven repository
091     */
092    public MavenResourceRepository setResourceSupplier(
093            Function<Revision, Optional<Resource>> resourceSupplier) {
094        this.resourceSupplier = resourceSupplier;
095        return this;
096    }
097
098    /**
099     * Creates a {@link MavenResource} for the given revision. 
100     *
101     * @param revision the revision
102     * @param location which URL to use for the binary in the {@link Resource}
103     * @return the resource
104     */
105    public MavenResource resource(BoundRevision revision,
106            BinaryLocation location) {
107        return resourceCache.computeIfAbsent(revision.unbound(),
108            rev -> resourceSupplier.apply(rev)
109                .map(resource -> new MavenResourceImpl(revision, resource))
110                .orElseGet(() -> new MavenResourceImpl(revision, location)));
111    }
112
113    /**
114     * Creates a {@link MavenResource} for the given program and version. 
115     *
116     * @param program the program
117     * @param version the version
118     * @param location which URL to use for the binary in the {@link Resource}
119     * @return the resource
120     * @throws IOException Signals that an I/O exception has occurred.
121     */
122    public Optional<MavenResource> resource(Program program,
123            MavenVersionSpecification version, BinaryLocation location)
124            throws IOException {
125        return find(program, version)
126            .map(revision -> resource(revision, location));
127    }
128
129    /**
130     * Retrieves the dependency information from the provided
131     * resource. Assumes that the resource was created by this
132     * repository, i.e. with capabilities in the
133     * "maven.dependencies.info" name space.
134     *
135     * @param resource the resource
136     * @param dependencies the dependencies
137     */
138    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
139    private static void retrieveDependencies(Resource resource,
140            Collection<Dependency> dependencies) {
141        // Actually, there should be only one such capability per resource.
142        for (Capability capability : resource
143            .getCapabilities(MAVEN_DEPENDENCIES_NS)) {
144            Map<String, Object> depAttrs = capability.getAttributes();
145            depAttrs.values().stream()
146                .flatMap(val -> COORDS_SPLITTER.splitAsStream((String) val))
147                .map(rev -> {
148                    String[] parts = rev.split(":");
149                    Dependency dep = new Dependency();
150                    dep.setGroupId(parts[0]);
151                    dep.setArtifactId(parts[1]);
152                    dep.setVersion(parts[2]);
153                    return dep;
154                }).forEach(dependencies::add);
155        }
156    }
157
158    /**
159     * A maven resource that obtains its information
160     * lazily from a {@link CompositeMavenRepository}.
161     */
162    public class MavenResourceImpl implements MavenResource {
163
164        private final Revision revision;
165        private BoundRevision cachedRevision;
166        private Resource cachedDelegee;
167        private List<Dependency> cachedDependencies;
168        private final BinaryLocation location;
169
170        /**
171         * Instantiates a new maven resource from the given data.
172         *
173         * @param revision the revision
174         * @param resource the delegee
175         */
176        private MavenResourceImpl(Revision revision, BinaryLocation location) {
177            this.revision = revision;
178            this.location = location;
179        }
180
181        /**
182         * Instantiates a new maven resource from the given data.
183         *
184         * @param revision the revision
185         * @param resource the delegee
186         */
187        private MavenResourceImpl(BoundRevision revision,
188                BinaryLocation location) {
189            this.revision = revision.unbound();
190            this.cachedRevision = revision;
191            this.location = location;
192        }
193
194        /**
195         * Instantiates a new maven resource from the given data.
196         *
197         * @param revision the revision
198         * @param resource the delegee
199         */
200        private MavenResourceImpl(Revision revision, Resource resource) {
201            this.revision = revision;
202            this.cachedDelegee = resource;
203            // Doesn't matter, resource won't be created (already there).
204            this.location = BinaryLocation.REMOTE;
205        }
206
207        /**
208         * Instantiates a new maven resource from the given data.
209         *
210         * @param revision the revision
211         * @param resource the delegee
212         */
213        private MavenResourceImpl(BoundRevision revision, Resource resource) {
214            this.revision = revision.unbound();
215            this.cachedRevision = revision;
216            this.cachedDelegee = resource;
217            // Doesn't matter, resource won't be created (already there).
218            this.location = BinaryLocation.REMOTE;
219        }
220
221        @Override
222        public Revision revision() {
223            return revision;
224        }
225
226        @Override
227        public BoundRevision boundRevision() throws MavenResourceException {
228            try {
229                if (cachedRevision == null) {
230                    cachedRevision = find(revision).get();
231                }
232                return cachedRevision;
233            } catch (IOException e) {
234                throw new MavenResourceException(e);
235            }
236        }
237
238        @Override
239        public Resource asResource() throws MavenResourceException {
240            if (cachedDelegee == null) {
241                createResource();
242            }
243            return cachedDelegee;
244        }
245
246        /**
247         * Creates a {@link Resource} representation from the manifest
248         * of the artifact.  
249         *
250         * @throws IOException Signals that an I/O exception has occurred.
251         * @throws ModelBuildingException the model building exception
252         * @throws UnresolvableModelException the unresolvable model exception
253         */
254        @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
255            "PMD.AvoidInstanceofChecksInCatchClause",
256            "PMD.CyclomaticComplexity", "PMD.NcssCount" })
257        private void createResource() throws MavenResourceException {
258            Model model = model(revision);
259            String extension = model.getPackaging();
260            if (extension.equals("bundle")
261                || extension.equals("eclipse-plugin")) {
262                extension = "jar";
263            }
264            Archive archive = revision.archive(extension, "");
265            ResourceBuilder builder = new ResourceBuilder();
266            addInformationCapability(builder, archive.toString(),
267                archive.getRevision().toString(), null);
268            if (extension.equals("jar")) {
269                File binary;
270                try {
271                    binary = get(archive);
272                } catch (IOException e) {
273                    throw new MavenResourceException(e);
274                }
275                try {
276                    if (location == BinaryLocation.LOCAL) {
277                        builder.addFile(binary, binary.toURI());
278                    } else {
279                        builder.addFile(binary,
280                            boundRevision().mavenBackingRepository()
281                                .toURI(archive.remotePath));
282                    }
283                } catch (Exception e) {
284                    // That's what the exceptions thrown here come down to.
285                    throw new IllegalArgumentException(e);
286                }
287            }
288
289            // Add dependency infos
290            if (!dependencies().isEmpty()) {
291                CapabilityBuilder cap
292                    = new CapabilityBuilder(MAVEN_DEPENDENCIES_NS);
293                Set<Dependency> cpDeps = new HashSet<>();
294                Set<Dependency> rtDeps = new HashSet<>();
295                for (Dependency dep : dependencies()) {
296                    if (dep.getScope() == null
297                        || dep.getScope().equalsIgnoreCase("compile")) {
298                        cpDeps.add(dep);
299                    } else if (dep.getScope().equalsIgnoreCase("runtime")) {
300                        rtDeps.add(dep);
301                    }
302                }
303                try {
304                    if (!cpDeps.isEmpty()) {
305                        cap.addAttribute("compile", toVersionList(cpDeps));
306                    }
307                    if (!rtDeps.isEmpty()) {
308                        cap.addAttribute("runtime", toVersionList(rtDeps));
309                    }
310                } catch (Exception e) {
311                    throw new IllegalArgumentException(e);
312                }
313                builder.addCapability(cap);
314            }
315            cachedDelegee = builder.build();
316        }
317
318        private String toVersionList(Collection<Dependency> deps) {
319            StringBuilder depsList = new StringBuilder("");
320            for (Dependency dep : deps) {
321                if (depsList.length() > 0) {
322                    depsList.append(';');
323                }
324                depsList.append(dep.getGroupId());
325                depsList.append(':');
326                depsList.append(dep.getArtifactId());
327                depsList.append(':');
328                depsList.append(dep.getVersion());
329            }
330            return depsList.toString();
331        }
332
333        @Override
334        public List<Capability> getCapabilities(String namespace)
335                throws MavenResourceException {
336            return asResource().getCapabilities(namespace);
337        }
338
339        @Override
340        public List<Requirement> getRequirements(String namespace)
341                throws MavenResourceException {
342            return asResource().getRequirements(namespace);
343        }
344
345        @Override
346        public boolean equals(Object obj) {
347            if (obj == null) {
348                return false;
349            }
350            if (obj instanceof MavenResource) {
351                return revision.equals(((MavenResource) obj).revision());
352            }
353            if (obj instanceof Resource) {
354                try {
355                    return asResource().equals(obj);
356                } catch (MavenResourceException e) {
357                    return false;
358                }
359            }
360            return false;
361        }
362
363        @Override
364        public int hashCode() {
365            return revision.hashCode();
366        }
367
368        @Override
369        public String toString() {
370            return revision.toString();
371        }
372
373        @Override
374        @SuppressWarnings("PMD.ConfusingTernary")
375        public final List<Dependency> dependencies()
376                throws MavenResourceException {
377            if (cachedDependencies == null) {
378                if (cachedDelegee != null) {
379                    cachedDependencies = new ArrayList<>();
380                    retrieveDependencies(cachedDelegee, cachedDependencies);
381                } else {
382                    cachedDependencies = MavenResourceRepository.this
383                        .model(revision()).getDependencies().stream()
384                        .filter(dep -> !dep.getGroupId().contains("$")
385                            && !dep.getArtifactId().contains("$")
386                            && !dep.isOptional()
387                            && (dep.getScope() == null
388                                || dep.getScope().equals("compile")
389                                || dep.getScope().equals("runtime")
390                                || dep.getScope().equals("provided")))
391                        .collect(Collectors.toList());
392                }
393            }
394            return cachedDependencies;
395        }
396
397    }
398}