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