blob: c6a1af5258f27458c11f66d7d8043584ef80c306 [file] [log] [blame]
Stuart McCulloch26e7a5a2011-10-17 10:31:43 +00001package aQute.lib.deployer.obr;
2
3import java.io.File;
4import java.io.FileNotFoundException;
5import java.io.IOException;
6import java.io.InputStream;
7import java.net.MalformedURLException;
8import java.net.URL;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.Dictionary;
13import java.util.EnumSet;
14import java.util.HashMap;
15import java.util.Hashtable;
16import java.util.Iterator;
17import java.util.LinkedList;
18import java.util.List;
19import java.util.Map;
20import java.util.Map.Entry;
21import java.util.Properties;
22import java.util.Set;
23import java.util.SortedMap;
24import java.util.StringTokenizer;
25import java.util.TreeMap;
26import java.util.regex.Pattern;
27
28import javax.xml.parsers.ParserConfigurationException;
29import javax.xml.parsers.SAXParser;
30import javax.xml.parsers.SAXParserFactory;
31
32import org.xml.sax.SAXException;
33
34import aQute.bnd.build.ResolverMode;
35import aQute.bnd.service.OBRIndexProvider;
36import aQute.bnd.service.OBRResolutionMode;
37import aQute.bnd.service.Plugin;
38import aQute.bnd.service.Registry;
39import aQute.bnd.service.RegistryPlugin;
40import aQute.bnd.service.RemoteRepositoryPlugin;
41import aQute.bnd.service.ResourceHandle;
42import aQute.bnd.service.ResourceHandle.Location;
43import aQute.lib.filter.Filter;
44import aQute.lib.osgi.Jar;
45import aQute.libg.generics.Create;
46import aQute.libg.reporter.Reporter;
47import aQute.libg.version.Version;
48import aQute.libg.version.VersionRange;
49
50/**
51 * Abstract base class for OBR-based repositories.
52 *
53 * <p>
54 * The repository implementation is read-only by default. To implement a writable
55 * repository, subclasses should override {@link #canWrite()} and {@link #put(Jar)}.
56 *
57 * @author Neil Bartlett
58 *
59 */
60public abstract class AbstractBaseOBR implements RegistryPlugin, Plugin, RemoteRepositoryPlugin, OBRIndexProvider {
61
62 public static final String PROP_NAME = "name";
63 public static final String PROP_RESOLUTION_MODE = "mode";
64 public static final String PROP_RESOLUTION_MODE_ANY = "any";
65
66 public static final String REPOSITORY_FILE_NAME = "repository.xml";
67
68 protected Registry registry;
69 protected Reporter reporter;
70 protected String name = this.getClass().getName();
71 protected Set<OBRResolutionMode> supportedModes = EnumSet.allOf(OBRResolutionMode.class);
72
73 private boolean initialised = false;
74 private final Map<String, SortedMap<Version, Resource>> pkgResourceMap = new HashMap<String, SortedMap<Version, Resource>>();
75 private final Map<String, SortedMap<Version, Resource>> bsnMap = new HashMap<String, SortedMap<Version, Resource>>();
76
77 protected abstract File getCacheDirectory();
78
79 protected void addResourceToIndex(Resource resource) {
80 addBundleSymbolicNameToIndex(resource);
81 addPackagesToIndex(resource);
82 }
83
84 protected synchronized void reset() {
85 initialised = false;
86 }
87
88 /**
89 * Initialise the indexes prior to main initialisation of internal
90 * data structures. This implementation does nothing, but subclasses
91 * may override if they need to perform such initialisation.
92 * @throws Exception
93 */
94 protected void initialiseIndexes() throws Exception {
95 }
96
97 protected final synchronized void init() throws Exception {
98 if (!initialised) {
99 bsnMap.clear();
100 pkgResourceMap.clear();
101
102 initialiseIndexes();
103
104 IResourceListener listener = new IResourceListener() {
105 public boolean processResource(Resource resource) {
106 addResourceToIndex(resource);
107 return true;
108 }
109 };
110 Collection<URL> indexes = getOBRIndexes();
111 for (URL indexLocation : indexes) {
112 try {
113 InputStream stream = indexLocation.openStream();
114 readIndex(indexLocation.toString(), stream, listener);
115 } catch (Exception e) {
116 e.printStackTrace();
117 reporter.error("Unable to read index at URL '%s'.", indexLocation);
118 }
119 }
120
121 initialised = true;
122 }
123 }
124
125 public void setRegistry(Registry registry) {
126 this.registry = registry;
127 }
128
129 public void setProperties(Map<String, String> map) {
130 if (map.containsKey(PROP_NAME))
131 name = map.get(PROP_NAME);
132
133 if (map.containsKey(PROP_RESOLUTION_MODE)) {
134 supportedModes = EnumSet.noneOf(OBRResolutionMode.class);
135 StringTokenizer tokenizer = new StringTokenizer(map.get(PROP_RESOLUTION_MODE), ",");
136 while (tokenizer.hasMoreTokens()) {
137 String token = tokenizer.nextToken().trim();
138 if (PROP_RESOLUTION_MODE_ANY.equalsIgnoreCase(token))
139 supportedModes = EnumSet.allOf(OBRResolutionMode.class);
140 else {
141 try {
142 supportedModes.add(OBRResolutionMode.valueOf(token));
143 } catch (Exception e) {
144 if (reporter != null) reporter.error("Unknown OBR resolution mode: " + token);
145 }
146 }
147 }
148 }
149 }
150
151 public File[] get(String bsn, String range) throws Exception {
152 ResourceHandle[] handles = getHandles(bsn, range);
153
154 return requestAll(handles);
155 }
156
157 protected static File[] requestAll(ResourceHandle[] handles) throws IOException {
158 File[] result = (handles == null) ? new File[0] : new File[handles.length];
159 for (int i = 0; i < result.length; i++) {
160 result[i] = handles[i].request();
161 }
162 return result;
163 }
164
165 protected ResourceHandle[] getHandles(String bsn, String rangeStr) throws Exception {
166 init();
167
168 // If the range is set to "project", we cannot resolve it.
169 if ("project".equals(rangeStr))
170 return null;
171
172
173 SortedMap<Version, Resource> versionMap = bsnMap.get(bsn);
174 if (versionMap == null || versionMap.isEmpty())
175 return null;
176 List<Resource> resources = narrowVersionsByVersionRange(versionMap, rangeStr);
177 List<ResourceHandle> handles = mapResourcesToHandles(resources);
178
179 return (ResourceHandle[]) handles.toArray(new ResourceHandle[handles.size()]);
180 }
181
182 public void setReporter(Reporter reporter) {
183 this.reporter = reporter;
184 }
185
186 public File get(String bsn, String range, Strategy strategy, Map<String, String> properties) throws Exception {
187 ResourceHandle handle = getHandle(bsn, range, strategy, properties);
188 return handle != null ? handle.request() : null;
189 }
190
191 public ResourceHandle getHandle(String bsn, String range, Strategy strategy, Map<String, String> properties) throws Exception {
192 ResourceHandle result;
193 if (bsn != null)
194 result = resolveBundle(bsn, range, strategy);
195 else {
196 String pkgName = properties.get(CapabilityType.PACKAGE.getTypeName());
197
198 String modeName = properties.get(CapabilityType.MODE.getTypeName());
199 ResolverMode mode = (modeName != null) ? ResolverMode.valueOf(modeName) : null;
200
201 if (pkgName != null)
202 result = resolvePackage(pkgName, range, strategy, mode, properties);
203 else
204 throw new IllegalArgumentException("Cannot resolve bundle: neither bsn nor package specified.");
205 }
206 return result;
207 }
208
209 public boolean canWrite() {
210 return false;
211 }
212
213 public File put(Jar jar) throws Exception {
214 throw new UnsupportedOperationException("Read-only repository.");
215 }
216
217 public List<String> list(String regex) throws Exception {
218 init();
219 Pattern pattern = regex != null ? Pattern.compile(regex) : null;
220 List<String> result = new LinkedList<String>();
221
222 for (String bsn : bsnMap.keySet()) {
223 if (pattern == null || pattern.matcher(bsn).matches())
224 result.add(bsn);
225 }
226
227 return result;
228 }
229
230 public List<Version> versions(String bsn) throws Exception {
231 init();
232 SortedMap<Version, Resource> versionMap = bsnMap.get(bsn);
233 List<Version> list;
234 if (versionMap != null) {
235 list = new ArrayList<Version>(versionMap.size());
236 list.addAll(versionMap.keySet());
237 } else {
238 list = Collections.emptyList();
239 }
240 return list;
241 }
242
243 public String getName() {
244 return name;
245 }
246
247 void addBundleSymbolicNameToIndex(Resource resource) {
248 String bsn = resource.getSymbolicName();
249 Version version;
250 String versionStr = resource.getVersion();
251 try {
252 version = new Version(versionStr);
253 } catch (Exception e) {
254 version = new Version("0.0.0");
255 }
256 SortedMap<Version, Resource> versionMap = bsnMap.get(bsn);
257 if (versionMap == null) {
258 versionMap = new TreeMap<Version, Resource>();
259 bsnMap.put(bsn, versionMap);
260 }
261 versionMap.put(version, resource);
262 }
263
264 void addPackagesToIndex(Resource resource) {
265 for (Capability capability : resource.getCapabilities()) {
266 if (CapabilityType.PACKAGE.getTypeName().equals(capability.getName())) {
267 String pkgName = null;
268 String versionStr = null;
269
270 for (Property prop : capability.getProperties()) {
271 if (Property.PACKAGE.equals(prop.getName()))
272 pkgName = prop.getValue();
273 else if (Property.VERSION.equals(prop.getName()))
274 versionStr = prop.getValue();
275 }
276
277 Version version;
278 try {
279 version = new Version(versionStr);
280 } catch (Exception e) {
281 version = new Version("0.0.0");
282 }
283
284 if (pkgName != null) {
285 SortedMap<Version, Resource> versionMap = pkgResourceMap.get(pkgName);
286 if (versionMap == null) {
287 versionMap = new TreeMap<Version, Resource>();
288 pkgResourceMap.put(pkgName, versionMap);
289 }
290 versionMap.put(version, resource);
291 }
292 }
293 }
294 }
295
296 /**
297 * @return Whether to continue parsing other indexes
298 * @throws IOException
299 */
300 boolean readIndex(String baseUrl, InputStream stream, IResourceListener listener) throws ParserConfigurationException, SAXException, IOException {
301 SAXParserFactory parserFactory = SAXParserFactory.newInstance();
302 SAXParser parser = parserFactory.newSAXParser();
303 try {
304 parser.parse(stream, new OBRSAXHandler(baseUrl, listener));
305 return true;
306 } catch (StopParseException e) {
307 return false;
308 } finally {
309 stream.close();
310 }
311 }
312
313 List<Resource> narrowVersionsByFilter(String pkgName, SortedMap<Version, Resource> versionMap, Filter filter) {
314 List<Resource> result = new ArrayList<Resource>(versionMap.size());
315
316 Dictionary<String, String> dict = new Hashtable<String, String>();
317 dict.put("package", pkgName);
318
319 for (Version version : versionMap.keySet()) {
320 dict.put("version", version.toString());
321 if (filter.match(dict))
322 result.add(versionMap.get(version));
323 }
324
325 return result;
326 }
327
328 List<Resource> narrowVersionsByVersionRange(SortedMap<Version, Resource> versionMap, String rangeStr) {
329 List<Resource> result;
330 if ("latest".equals(rangeStr)) {
331 Version highest = versionMap.lastKey();
332 result = Create.list(new Resource[] { versionMap.get(highest) });
333 } else {
334 VersionRange range = rangeStr != null ? new VersionRange(rangeStr) : null;
335
336 // optimisation: skip versions definitely less than the range
337 if (range != null && range.getLow() != null)
338 versionMap = versionMap.tailMap(range.getLow());
339
340 result = new ArrayList<Resource>(versionMap.size());
341 for (Version version : versionMap.keySet()) {
342 if (range == null || range.includes(version))
343 result.add(versionMap.get(version));
344
345 // optimisation: skip versions definitely higher than the range
346 if (range != null && range.isRange() && version.compareTo(range.getHigh()) >= 0)
347 break;
348 }
349 }
350 return result;
351 }
352
353 void filterResourcesByResolverMode(Collection<Resource> resources, ResolverMode mode) {
354 assert mode != null;
355
356 Properties modeCapability = new Properties();
357 modeCapability.setProperty(CapabilityType.MODE.getTypeName(), mode.name());
358
359 for (Iterator<Resource> iter = resources.iterator(); iter.hasNext(); ) {
360 Resource resource = iter.next();
361
362 Require modeRequire = resource.findRequire(CapabilityType.MODE.getTypeName());
363 if (modeRequire == null)
364 continue;
365 else if (modeRequire.getFilter() == null)
366 iter.remove();
367 else {
368 try {
369 Filter filter = new Filter(modeRequire.getFilter());
370 if (!filter.match(modeCapability))
371 iter.remove();
372 } catch (IllegalArgumentException e) {
373 if (reporter != null)
374 reporter.error("Error parsing mode filter requirement on resource %s: %s", resource.getUrl(), modeRequire.getFilter());
375 iter.remove();
376 }
377 }
378 }
379 }
380
381 List<ResourceHandle> mapResourcesToHandles(Collection<Resource> resources) throws Exception {
382 List<ResourceHandle> result = new ArrayList<ResourceHandle>(resources.size());
383
384 for (Resource resource : resources) {
385 ResourceHandle handle = mapResourceToHandle(resource);
386 if (handle != null)
387 result.add(handle);
388 }
389
390 return result;
391 }
392
393 ResourceHandle mapResourceToHandle(Resource resource) throws Exception {
394 ResourceHandle result = null;
395
396 URLResourceHandle handle ;
397 try {
398 handle = new URLResourceHandle(resource.getUrl(), resource.getBaseUrl(), getCacheDirectory());
399 } catch (FileNotFoundException e) {
400 throw new FileNotFoundException("Broken link in repository index: " + e.getMessage());
401 }
402 if (handle.getLocation() == Location.local || getCacheDirectory() != null)
403 result = handle;
404
405 return result;
406 }
407
408 ResourceHandle resolveBundle(String bsn, String rangeStr, Strategy strategy) throws Exception {
409 if (rangeStr == null) rangeStr = "0.0.0";
410
411 if (strategy == Strategy.EXACT) {
412 return findExactMatch(bsn, rangeStr, bsnMap);
413 }
414
415 ResourceHandle[] handles = getHandles(bsn, rangeStr);
416 ResourceHandle selected;
417 if (handles == null || handles.length == 0)
418 selected = null;
419 else {
420 switch(strategy) {
421 case LOWEST:
422 selected = handles[0];
423 break;
424 default:
425 selected = handles[handles.length - 1];
426 }
427 }
428 return selected;
429 }
430
431 ResourceHandle resolvePackage(String pkgName, String rangeStr, Strategy strategy, ResolverMode mode, Map<String, String> props) throws Exception {
432 init();
433 if (rangeStr == null) rangeStr = "0.0.0";
434
435 SortedMap<Version, Resource> versionMap = pkgResourceMap.get(pkgName);
436 if (versionMap == null)
437 return null;
438
439 // Was a filter expression supplied?
440 Filter filter = null;
441 String filterStr = props.get("filter");
442 if (filterStr != null) {
443 filter = new Filter(filterStr);
444 }
445
446 // Narrow the resources by version range string or filter.
447 List<Resource> resources;
448 if (filter != null)
449 resources = narrowVersionsByFilter(pkgName, versionMap, filter);
450 else
451 resources = narrowVersionsByVersionRange(versionMap, rangeStr);
452
453 // Remove resources that are invalid for the current resolution mode
454 if (mode != null)
455 filterResourcesByResolverMode(resources, mode);
456
457 // Select the most suitable one
458 Resource selected;
459 if (resources == null || resources.isEmpty())
460 selected = null;
461 else {
462 switch (strategy) {
463 case LOWEST:
464 selected = resources.get(0);
465 break;
466 default:
467 selected = resources.get(resources.size() - 1);
468 }
469 expandPackageUses(pkgName, selected, props);
470 }
471 return selected != null ? mapResourceToHandle(selected) : null;
472 }
473
474 void expandPackageUses(String pkgName, Resource resource, Map<String, String> props) {
475 List<String> internalUses = new LinkedList<String>();
476 Map<String, Require> externalUses = new HashMap<String, Require>();
477
478 internalUses.add(pkgName);
479
480 Capability capability = resource.findPackageCapability(pkgName);
481 Property usesProp = capability.findProperty(Property.USES);
482 if (usesProp != null) {
483 StringTokenizer tokenizer = new StringTokenizer(usesProp.getValue(), ",");
484 while (tokenizer.hasMoreTokens()) {
485 String usesPkgName = tokenizer.nextToken();
486 Capability usesPkgCap = resource.findPackageCapability(usesPkgName);
487 if (usesPkgCap != null)
488 internalUses.add(usesPkgName);
489 else {
490 Require require = resource.findPackageRequire(usesPkgName);
491 if (require != null)
492 externalUses.put(usesPkgName, require);
493 }
494 }
495 }
496 props.put("packages", listToString(internalUses));
497 props.put("import-uses", formatPackageRequires(externalUses));
498 }
499
500 String listToString(List<?> list) {
501 StringBuilder builder = new StringBuilder();
502
503 int count = 0;
504 for (Object item : list) {
505 if (count++ > 0) builder.append(',');
506 builder.append(item);
507 }
508
509 return builder.toString();
510 }
511
512 String formatPackageRequires(Map<String, Require> externalUses) {
513 StringBuilder builder = new StringBuilder();
514
515 int count = 0;
516 for (Entry<String, Require> entry : externalUses.entrySet()) {
517 String pkgName = entry.getKey();
518 String filter = entry.getValue().getFilter();
519
520 if (count++ > 0)
521 builder.append(',');
522 builder.append(pkgName);
523 builder.append(";filter='");
524 builder.append(filter);
525 builder.append('\'');
526 }
527
528 return builder.toString();
529 }
530
531 ResourceHandle findExactMatch(String identity, String version, Map<String, SortedMap<Version, Resource>> resourceMap) throws Exception {
532 Resource resource;
533 VersionRange range = new VersionRange(version);
534 if (range.isRange())
535 return null;
536
537 SortedMap<Version, Resource> versions = resourceMap.get(identity);
538 resource = versions.get(range.getLow());
539
540 return mapResourceToHandle(resource);
541 }
542
543 /**
544 * Utility function for parsing lists of URLs.
545 *
546 * @param locationsStr
547 * Comma-separated list of URLs
548 * @throws MalformedURLException
549 */
550 protected static List<URL> parseLocations(String locationsStr) throws MalformedURLException {
551 StringTokenizer tok = new StringTokenizer(locationsStr, ",");
552 List<URL> urls = new ArrayList<URL>(tok.countTokens());
553 while (tok.hasMoreTokens()) {
554 String urlStr = tok.nextToken().trim();
555 urls.add(new URL(urlStr));
556 }
557 return urls;
558 }
559
560 public Set<OBRResolutionMode> getSupportedModes() {
561 return supportedModes;
562 }
563
564}