001/*
002 * Extra Bnd Repository Plugins
003 * Copyright (C) 2017,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 
013 * License for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License 
016 * along with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package de.mnl.osgi.bnd.repository.maven.nexussearch;
020
021import aQute.bnd.osgi.Processor;
022import static aQute.bnd.osgi.repository.BridgeRepository.addInformationCapability;
023import aQute.bnd.osgi.repository.ResourcesRepository;
024import aQute.bnd.osgi.repository.XMLResourceGenerator;
025import aQute.bnd.osgi.repository.XMLResourceParser;
026import aQute.bnd.osgi.resource.ResourceBuilder;
027import aQute.bnd.version.MavenVersionRange;
028import aQute.maven.api.Archive;
029import aQute.maven.api.IPom;
030import aQute.maven.api.IPom.Dependency;
031import aQute.maven.api.MavenScope;
032import aQute.maven.api.Program;
033import aQute.maven.api.Revision;
034import aQute.maven.provider.MavenBackingRepository;
035import aQute.maven.provider.MavenRepository;
036import aQute.maven.provider.MetadataParser;
037import aQute.maven.provider.MetadataParser.RevisionMetadata;
038
039import java.io.File;
040
041import java.util.Collection;
042import java.util.Collections;
043import java.util.Comparator;
044import java.util.HashSet;
045import java.util.Iterator;
046import java.util.LinkedHashMap;
047import java.util.List;
048import java.util.Map;
049import java.util.Set;
050import java.util.concurrent.Callable;
051import java.util.concurrent.ConcurrentHashMap;
052
053import org.apache.maven.artifact.versioning.ComparableVersion;
054import org.osgi.resource.Resource;
055import org.osgi.service.repository.Repository;
056import org.slf4j.Logger;
057import org.slf4j.LoggerFactory;
058
059/**
060 * Provide an OSGi repository (a collection of {@link Resource}s, see 
061 * {@link Repository}), filled with the results from recursively resolving
062 * an initial set of artifacts.
063 * <P>
064 * The resources in this repository are maintained in a local 
065 * maven repository.
066 */
067public abstract class LocalMavenBackedOsgiRepository
068        extends ResourcesRepository {
069
070    private static final Logger LOG = LoggerFactory.getLogger(
071        LocalMavenBackedOsgiRepository.class);
072    private final String name;
073    private final File obrIndexFile;
074    private final Set<Revision> toBeProcessed = new HashSet<>();
075    private final Set<Revision> processing = new HashSet<>();
076    private final Set<Revision> processed = new HashSet<>();
077
078    /**
079     * Create a new instance that uses the provided information/resources 
080     * to perform its work.
081     * 
082     * @param name the name
083     * @param obrIndexFile the persistent representation of this 
084     *     repository's content
085     * @throws Exception if a problem occurs
086     */
087    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
088    public LocalMavenBackedOsgiRepository(String name, File obrIndexFile)
089            throws Exception {
090        this.name = name;
091        this.obrIndexFile = obrIndexFile;
092
093        if (!location().exists() || !location().isFile()) {
094            refresh();
095        } else {
096            try (XMLResourceParser parser = new XMLResourceParser(location())) {
097                List<Resource> resources = parser.parse();
098                addAll(resources);
099            }
100        }
101    }
102
103    /**
104     * Return the name of this repository.
105     * 
106     * @return the name;
107     */
108    public String name() {
109        return name;
110    }
111
112    /**
113     * Return the representation of this repository in the local file system.
114     * 
115     * @return the location
116     */
117    public final File location() {
118        return obrIndexFile;
119    }
120
121    /**
122     * Refresh this repository's content.
123     * 
124     * @return true if refreshed, false if not refreshed possibly due to error
125     * @throws Exception if a problem occurs
126     */
127    public abstract boolean refresh() throws Exception;
128
129    /**
130     * Refresh this repository's content.
131     *
132     * @param mavenRepository the maven repository
133     * @param startArtifacts the collection of artifacts to start with
134     * @return true if refreshed, false if not refreshed possibly due to error
135     * @throws Exception if a problem occurs
136     */
137    public boolean refresh(MavenRepository mavenRepository,
138            Collection<? extends Revision> startArtifacts) throws Exception {
139        set(new HashSet<>()); // Clears this repository
140        toBeProcessed.addAll(startArtifacts);
141        // Repository information is obtained from both querying the
142        // repositories
143        // (provides information about existing repositories) and from executing
144        // the query (provides information about actually used repositories).
145        Set<Resource> collectedResources
146            = Collections.newSetFromMap(new ConcurrentHashMap<>());
147        synchronized (this) {
148            while (true) {
149                if (toBeProcessed.isEmpty() && processing.isEmpty()) {
150                    break;
151                }
152                if (!toBeProcessed.isEmpty() && processing.size() < 4) {
153                    Revision rev = toBeProcessed.iterator().next();
154                    toBeProcessed.remove(rev);
155                    processing.add(rev);
156                    Processor.getScheduledExecutor().submit(
157                        new RevisionProcessor(mavenRepository,
158                            collectedResources, rev));
159                }
160                wait();
161            }
162        }
163        processed.clear();
164        // Set this repository's content to the results...
165        addAll(collectedResources);
166        // ... and persist the content.
167        XMLResourceGenerator generator = new XMLResourceGenerator();
168        generator.resources(getResources());
169        generator.name(name());
170        generator.save(obrIndexFile);
171        return true;
172    }
173
174    /**
175     * A callable (allows it to throw an exception) that processes a single
176     * Revision.
177     */
178    private class RevisionProcessor implements Callable<Void> {
179        private final MavenRepository mavenRepository;
180        private final Revision revision;
181        private final Set<Resource> collectedResources;
182
183        public RevisionProcessor(MavenRepository mavenRepository,
184                Set<Resource> collectedResources, Revision revision) {
185            this.mavenRepository = mavenRepository;
186            this.collectedResources = collectedResources;
187            this.revision = revision;
188        }
189
190        /**
191         * Get this revision's dependencies from the POM and enqueue them as
192         * to be processed (unless processed already) and create an entry
193         * for the resource in this repository.
194         */
195        @Override
196        public Void call() throws Exception {
197            try {
198                // Get and add this revision's OSGi information (refreshes
199                // snapshots)
200                Archive archive
201                    = mavenRepository.getResolvedArchive(revision, "jar", "");
202                if (archive != null) {
203                    if (archive.isSnapshot()) {
204                        for (MavenBackingRepository mbr : mavenRepository
205                            .getSnapshotRepositories()) {
206                            if (mbr.getVersion(archive.getRevision()) != null) {
207                                // Found backing repository
208                                File metaFile = mavenRepository.toLocalFile(
209                                    revision.metadata(mbr.getId()));
210                                RevisionMetadata metaData
211                                    = MetadataParser.parseRevisionMetadata(
212                                        metaFile);
213                                File archiveFile
214                                    = mavenRepository.toLocalFile(archive);
215                                if (archiveFile
216                                    .lastModified() < metaData.lastUpdated) {
217                                    archiveFile.delete();
218                                }
219                                File pomFile = mavenRepository
220                                    .toLocalFile(archive.getPomArchive());
221                                if (pomFile
222                                    .lastModified() < metaData.lastUpdated) {
223                                    pomFile.delete();
224                                }
225                                break;
226                            }
227                        }
228                    }
229                    // Get POM for dependencies
230                    IPom pom = mavenRepository.getPom(archive.getRevision());
231                    if (pom != null) {
232                        // Get pom and add all dependencies as to be processed.
233                        addDependencies(pom);
234                    }
235                    Resource resource = parseResource(archive);
236                    if (resource != null) {
237                        collectedResources.add(resource);
238                    }
239                }
240                return null;
241            } finally {
242                // We're done witht his revision.
243                synchronized (LocalMavenBackedOsgiRepository.this) {
244                    processing.remove(revision);
245                    processed.add(revision);
246                    LocalMavenBackedOsgiRepository.this.notifyAll();
247                }
248            }
249        }
250
251        private void addDependencies(IPom pom) {
252            try {
253                Map<Program, Dependency> deps = new LinkedHashMap<>();
254                deps.putAll(pom.getDependencies(MavenScope.compile, false));
255                deps.putAll(pom.getDependencies(MavenScope.runtime, false));
256                synchronized (LocalMavenBackedOsgiRepository.this) {
257                    for (Map.Entry<Program, Dependency> entry : deps
258                        .entrySet()) {
259                        bindToVersion(entry.getValue());
260                        try {
261                            Revision rev = entry.getValue().getRevision();
262                            if (!toBeProcessed.contains(rev)
263                                && !processing.contains(rev)
264                                && !processed.contains(rev)) {
265                                toBeProcessed.add(rev);
266                                LOG.debug("Added as dependency {}", rev);
267                            }
268                            LocalMavenBackedOsgiRepository.this.notifyAll();
269                        } catch (Exception e) {
270                            LOG.warn("Unbindable dependency {}",
271                                entry.getValue().toString());
272                            continue;
273                        }
274                    }
275                }
276            } catch (Exception e) {
277                LOG.error("Failed to get POM of " + revision + ".", e);
278            }
279        }
280
281        private void bindToVersion(Dependency dependency) throws Exception {
282            if (MavenVersionRange.isRange(dependency.version)) {
283
284                MavenVersionRange range
285                    = new MavenVersionRange(dependency.version);
286                List<Revision> revisions
287                    = mavenRepository.getRevisions(dependency.program);
288
289                for (Iterator<Revision> it = revisions.iterator();
290                        it.hasNext();) {
291                    Revision rev = it.next();
292                    if (!range.includes(rev.version)) {
293                        it.remove();
294                    }
295                }
296
297                if (!revisions.isEmpty()) {
298                    Collections.sort(revisions, new MavenRevisionComparator());
299                    Revision highest = revisions.get(revisions.size() - 1);
300                    dependency.version = highest.version.toString();
301                }
302            }
303        }
304
305        private Resource parseResource(Archive archive) throws Exception {
306            ResourceBuilder rb = new ResourceBuilder();
307            try {
308                File binary = mavenRepository.get(archive).getValue();
309                rb.addFile(binary, binary.toURI());
310                addInformationCapability(rb, archive.toString(),
311                    archive.getRevision().toString(), null);
312            } catch (Exception e) {
313                return null;
314            }
315            return rb.build();
316        }
317
318    }
319
320    public class MavenRevisionComparator implements Comparator<Revision> {
321        @Override
322        public int compare(Revision rev1, Revision rev2) {
323            int res = rev1.program.compareTo(rev2.program);
324            if (res != 0) {
325                return res;
326            }
327
328            ComparableVersion rev1ver
329                = new ComparableVersion(rev1.version.toString());
330            ComparableVersion rev2ver
331                = new ComparableVersion(rev2.version.toString());
332            return rev1ver.compareTo(rev2ver);
333        }
334    }
335
336}