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