001/*
002 * Extra Bnd Repository Plugins
003 * Copyright (C) 2019-2021  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.repository.maven.provider;
020
021import aQute.bnd.build.Workspace;
022import aQute.bnd.http.HttpClient;
023import static aQute.bnd.osgi.Constants.BSN_SOURCE_SUFFIX;
024import aQute.bnd.osgi.repository.BaseRepository;
025import aQute.bnd.osgi.repository.BridgeRepository;
026import aQute.bnd.osgi.repository.BridgeRepository.ResourceInfo;
027import aQute.bnd.service.Plugin;
028import aQute.bnd.service.Refreshable;
029import aQute.bnd.service.Registry;
030import aQute.bnd.service.RegistryPlugin;
031import aQute.bnd.service.RepositoryListenerPlugin;
032import aQute.bnd.service.RepositoryPlugin;
033import aQute.bnd.util.repository.DownloadListenerPromise;
034import aQute.bnd.version.Version;
035import aQute.lib.converter.Converter;
036import aQute.lib.io.IO;
037import aQute.libg.reporter.slf4j.Slf4jReporter;
038import aQute.maven.api.Archive;
039import aQute.service.reporter.Reporter;
040import de.mnl.osgi.bnd.maven.RepositoryUtils;
041import de.mnl.osgi.bnd.repository.maven.idxmvn.IndexedMavenConfiguration;
042import de.mnl.osgi.bnd.repository.maven.idxmvn.IndexedMavenRepository;
043import java.io.File;
044import java.io.InputStream;
045import java.net.MalformedURLException;
046import java.net.URL;
047import java.util.Collection;
048import java.util.List;
049import java.util.Map;
050import java.util.SortedSet;
051import java.util.stream.Collectors;
052import org.osgi.resource.Capability;
053import org.osgi.resource.Requirement;
054import org.osgi.service.repository.Repository;
055import org.osgi.util.promise.Promise;
056
057/**
058 * Maintains an index of a subset of one or more maven repositories
059 * and provides it as an OSGi repository.
060 */
061public class IndexedMavenRepositoryProvider extends BaseRepository
062        implements Repository, Plugin, RegistryPlugin, RepositoryPlugin,
063        Refreshable {
064    private static final String MAVEN_REPO_LOCAL
065        = System.getProperty("maven.repo.local", "~/.m2/repository");
066
067    private boolean initialized;
068    private IndexedMavenConfiguration configuration;
069    private String name = "Indexed Maven";
070    private String location;
071    private Registry registry;
072    private Reporter reporter
073        = new Slf4jReporter(IndexedMavenRepositoryProvider.class);
074    private IndexedMavenRepository osgiRepository;
075    private BridgeRepository bridge;
076    private boolean logIndexing;
077
078    @Override
079    @SuppressWarnings({ "PMD.UseLocaleWithCaseConversions", "restriction" })
080    public void setProperties(Map<String, String> properties) throws Exception {
081        configuration
082            = Converter.cnv(IndexedMavenConfiguration.class, properties);
083        name = configuration.name(name);
084        location = configuration.location(
085            "cnf/" + name.toLowerCase().replace(' ', '-').replace('/', ':'));
086        logIndexing = configuration.logIndexing();
087    }
088
089    @Override
090    public void setRegistry(Registry registry) {
091        this.registry = registry;
092    }
093
094    @Override
095    public void setReporter(Reporter reporter) {
096        this.reporter = reporter;
097    }
098
099    @Override
100    public String getName() {
101        return name;
102    }
103
104    @Override
105    public PutResult put(InputStream stream, PutOptions options)
106            throws Exception {
107        throw new IllegalStateException("Read-only repository");
108    }
109
110    @Override
111    public boolean canWrite() {
112        return false;
113    }
114
115    /**
116     * Performs initialization. Initialization must be delayed because the
117     * precise sequence of injecting dependencies seems to be undefined.
118     * 
119     * @throws MalformedURLException 
120     */
121    @SuppressWarnings({ "PMD.AvoidCatchingGenericException",
122        "PMD.AvoidThrowingRawExceptionTypes" })
123    private void init() {
124        synchronized (this) {
125            if (initialized) {
126                return;
127            }
128            initialized = true;
129            Workspace workspace = registry.getPlugin(Workspace.class);
130            HttpClient client = registry.getPlugin(HttpClient.class);
131            File indexDb = workspace.getFile(getLocation());
132            File localRepo = IO.getFile(configuration.local(MAVEN_REPO_LOCAL));
133            try {
134                Thread.currentThread()
135                    .setContextClassLoader(getClass().getClassLoader());
136                osgiRepository = new IndexedMavenRepository(name,
137                    RepositoryUtils.itemizeList(configuration.releaseUrls())
138                        .map(ru -> stringToUrl(ru))
139                        .collect(Collectors.toList()),
140                    RepositoryUtils.itemizeList(configuration.snapshotUrls())
141                        .map(ru -> stringToUrl(ru))
142                        .collect(Collectors.toList()),
143                    localRepo, indexDb, reporter, client, logIndexing);
144                bridge = new BridgeRepository(osgiRepository);
145            } catch (Exception e) {
146                throw new RuntimeException(e);
147            }
148        }
149    }
150
151    private URL stringToUrl(String url) {
152        try {
153            return new URL(url);
154        } catch (MalformedURLException e) {
155            throw new IllegalArgumentException(e);
156        }
157    }
158
159    @Override
160    public File getRoot() throws Exception {
161        return osgiRepository.location();
162    }
163
164    @Override
165    @SuppressWarnings("PMD.AvoidCatchingGenericException")
166    public boolean refresh() throws Exception {
167        init();
168        if (!osgiRepository.refresh()) {
169            return false;
170        }
171        bridge = new BridgeRepository(osgiRepository);
172        for (RepositoryListenerPlugin listener : registry
173            .getPlugins(RepositoryListenerPlugin.class)) {
174            try {
175                listener.repositoryRefreshed(this);
176            } catch (Exception e) {
177                reporter.exception(e, "Updating listener plugin %s", listener);
178            }
179        }
180        return true;
181    }
182
183    @Override
184    public File get(String bsn, Version version, Map<String, String> properties,
185            DownloadListener... listeners) throws Exception {
186        init();
187        Archive archive;
188        ResourceInfo resource = bridge.getInfo(bsn, version);
189        if (resource == null) {
190            archive = trySources(bsn, version);
191            if (archive == null) {
192                return null;
193            }
194        } else {
195            String from = resource.getInfo().from();
196            archive = Archive.valueOf(from);
197        }
198
199        Promise<File> prmse
200            = osgiRepository.mavenRepository().retrieve(archive);
201
202        if (listeners.length == 0) {
203            return prmse.getValue();
204        }
205        new DownloadListenerPromise(reporter,
206            name + ": get " + bsn + ";" + version, prmse, listeners);
207        return osgiRepository.mavenRepository().toLocalFile(archive);
208    }
209
210    /**
211     * The Eclipse bndtools plugin attempts to retrieve a bundle's sources
212     * by calling {@link #get(String, Version, Map, 
213     * aQute.bnd.service.RepositoryPlugin.DownloadListener...)} with the 
214     * bundle symbol name and ".source" appended as suffix. Check if the 
215     * given bsn matches this pattern and return an archive specification
216     * for the artifact containing the sources using maven conventions.
217     *
218     * @param bsn the bsn
219     * @param version the version
220     * @return the archive
221     * @throws Exception the exception
222     */
223    private Archive trySources(String bsn, Version version) throws Exception {
224        if (!bsn.endsWith(BSN_SOURCE_SUFFIX)) {
225            return null;
226        }
227        String baseBsn
228            = bsn.substring(0, bsn.length() - BSN_SOURCE_SUFFIX.length());
229        ResourceInfo resource = bridge.getInfo(baseBsn, version);
230        if (resource == null) {
231            return null;
232        }
233        String from = resource.getInfo().from();
234        return Archive.valueOf(from)
235            .getOther(Archive.JAR_EXTENSION, Archive.SOURCES_CLASSIFIER);
236    }
237
238    @Override
239    public List<String> list(String pattern) throws Exception {
240        init();
241        return bridge.list(pattern);
242    }
243
244    @Override
245    public SortedSet<Version> versions(String bsn) throws Exception {
246        init();
247        return bridge.versions(bsn);
248    }
249
250    @Override
251    public String getLocation() {
252        return location;
253    }
254
255    @Override
256    public Map<Requirement, Collection<Capability>> findProviders(
257            Collection<? extends Requirement> requirements) {
258        init();
259        return osgiRepository.findProviders(requirements);
260    }
261
262    @Override
263    public String toString() {
264        return name;
265    }
266
267}