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.maven; 020 021import static aQute.bnd.osgi.repository.BridgeRepository.addInformationCapability; 022import aQute.bnd.osgi.resource.CapabilityBuilder; 023import aQute.bnd.osgi.resource.ResourceBuilder; 024import aQute.bnd.osgi.resource.ResourceUtils; 025import aQute.bnd.version.Version; 026import aQute.maven.api.Archive; 027import aQute.maven.api.Program; 028import aQute.maven.provider.MavenBackingRepository; 029import aQute.service.reporter.Reporter; 030import java.io.File; 031import java.io.IOException; 032import java.util.ArrayList; 033import java.util.Collection; 034import java.util.Collections; 035import java.util.HashMap; 036import java.util.HashSet; 037import java.util.List; 038import java.util.Map; 039import java.util.Optional; 040import java.util.Set; 041import java.util.concurrent.ConcurrentHashMap; 042import java.util.concurrent.Executor; 043import java.util.function.Function; 044import java.util.stream.Collectors; 045import org.apache.maven.model.Dependency; 046import org.apache.maven.model.Model; 047import org.apache.maven.model.building.ModelBuildingException; 048import org.apache.maven.model.resolution.UnresolvableModelException; 049import org.osgi.framework.namespace.IdentityNamespace; 050import org.osgi.resource.Capability; 051import org.osgi.resource.Requirement; 052import org.osgi.resource.Resource; 053 054/** 055 * Wraps the artifacts from a maven repository as {@link Resource}s. 056 */ 057@SuppressWarnings("PMD.UseLocaleWithCaseConversions") 058public class MavenResourceRepository extends CompositeMavenRepository { 059 060 /** The namespace used to store the maven dependencies information. */ 061 public static final String MAVEN_DEPENDENCIES_NS 062 = "maven.dependencies.info"; 063 064 private Function<Archive, Optional<Resource>> resourceSupplier 065 = resource -> Optional.empty(); 066 private final Map<Archive, MavenResource> resourceCache 067 = new ConcurrentHashMap<>(); 068 069 /** 070 * Instantiates a new maven resource repository. 071 * 072 * @param base the base 073 * @param repoId the repo id 074 * @param releaseRepos the release repos 075 * @param snapshotRepos the snapshot repos 076 * @param executor the executor 077 * @param reporter the reporter 078 * @throws Exception the exception 079 */ 080 @SuppressWarnings("PMD.SignatureDeclareThrowsException") 081 public MavenResourceRepository(File base, String repoId, 082 List<MavenBackingRepository> releaseRepos, 083 List<MavenBackingRepository> snapshotRepos, Executor executor, 084 Reporter reporter) throws Exception { 085 super(base, repoId, releaseRepos, snapshotRepos, executor, reporter); 086 } 087 088 @Override 089 public void reset() { 090 super.reset(); 091 resourceCache.clear(); 092 } 093 094 /** 095 * Sets a function that can provide resource information more 096 * efficiently (e.g. from some local persistent cache) than 097 * the remote maven repository. 098 * <P> 099 * Any resource information provided by the function must be 100 * complete, i.e. must hold the information from the "bnd.info" 101 * namespace and from the "maven.dependencies.info" namespace. 102 * 103 * @param resourceSupplier the resource supplier 104 * @return the composite maven repository 105 */ 106 public MavenResourceRepository setResourceSupplier( 107 Function<Archive, Optional<Resource>> resourceSupplier) { 108 this.resourceSupplier = resourceSupplier; 109 return this; 110 } 111 112 /** 113 * Creates a {@link MavenResource} for the given program and version. 114 * 115 * @param program the program 116 * @param version the version 117 * @param extension the extension (or {@code null} for "jar") 118 * @param classifier the classifier (or {@code null} for "") 119 * @param location which URL to use for the binary in the {@link Resource} 120 * @return the resource 121 */ 122 public Optional<MavenResource> resource(Program program, 123 MavenVersionSpecification version, String extension, 124 String classifier, BinaryLocation location) { 125 return find(program, version) 126 .map(revision -> resource(revision.archive(extension, classifier), 127 location)); 128 } 129 130 /** 131 * Creates a {@link MavenResource} for the given archive. 132 * 133 * @param archive the archive 134 * @param location which URL to use for the binary in the {@link Resource} 135 * @return the resource 136 */ 137 public MavenResource resource(BoundArchive archive, 138 BinaryLocation location) { 139 return resourceCache.computeIfAbsent(archive, 140 a -> resourceSupplier.apply(a) 141 .map(resource -> new MavenResourceImpl(archive, resource)) 142 .orElseGet(() -> new MavenResourceImpl(archive, location))); 143 } 144 145 /** 146 * Retrieves the dependency information from the provided 147 * resource. Assumes that the resource was created by this 148 * repository, i.e. with capabilities in the 149 * "maven.dependencies.info" name space. 150 * 151 * @param resource the resource 152 * @param dependencies the dependencies 153 */ 154 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 155 private static void retrieveDependencies(Resource resource, 156 Collection<Dependency> dependencies) { 157 // Actually, there should be only one such capability per resource. 158 for (Capability capability : resource 159 .getCapabilities(MAVEN_DEPENDENCIES_NS)) { 160 Map<String, Object> depAttrs = capability.getAttributes(); 161 depAttrs.values().stream() 162 .flatMap(val -> COORDS_SPLITTER.splitAsStream((String) val)) 163 .map(rev -> { 164 String[] parts = rev.split(":"); 165 Dependency dep = new Dependency(); 166 dep.setGroupId(parts[0]); 167 dep.setArtifactId(parts[1]); 168 dep.setVersion(parts[2]); 169 return dep; 170 }).forEach(dependencies::add); 171 } 172 } 173 174 /** 175 * A maven resource that obtains its information 176 * lazily from a {@link CompositeMavenRepository}. 177 */ 178 @SuppressWarnings("PMD.DataflowAnomalyAnalysis") 179 public class MavenResourceImpl implements MavenResource { 180 181 private final Archive archive; 182 private BoundArchive cachedArchive; 183 private Resource cachedDelegee; 184 private List<Dependency> cachedDependencies; 185 private final BinaryLocation location; 186 187 /** 188 * Instantiates a new maven resource from the given data. 189 * 190 * @param revision the archive 191 * @param location the location 192 */ 193 private MavenResourceImpl(Archive archive, BinaryLocation location) { 194 this.archive = archive; 195 this.location = location; 196 } 197 198 /** 199 * Instantiates a new maven resource from the given data. 200 * 201 * @param revision the archive 202 * @param location the location 203 */ 204 private MavenResourceImpl(BoundArchive archive, 205 BinaryLocation location) { 206 this.archive = archive; 207 this.cachedArchive = archive; 208 this.location = location; 209 } 210 211 /** 212 * Instantiates a new maven resource from the given data. 213 * 214 * @param revision the archive 215 * @param resource the resource information associated with the 216 * archive. 217 */ 218 private MavenResourceImpl(Archive archive, Resource resource) { 219 this.archive = archive; 220 this.cachedDelegee = resource; 221 // Doesn't matter, resource won't be created (already there). 222 this.location = BinaryLocation.REMOTE; 223 } 224 225 /** 226 * Instantiates a new maven resource from the given data. 227 * 228 * @param revision the revision 229 * @param resource the resource information associated with the 230 * archive. 231 */ 232 private MavenResourceImpl(BoundArchive archive, Resource resource) { 233 this.archive = archive; 234 this.cachedArchive = archive; 235 this.cachedDelegee = resource; 236 // Doesn't matter, resource won't be created (already there). 237 this.location = BinaryLocation.REMOTE; 238 } 239 240 @Override 241 public Archive archive() { 242 return archive; 243 } 244 245 @Override 246 public BoundArchive boundArchive() throws MavenResourceException { 247 try { 248 if (cachedArchive == null) { 249 cachedArchive = find(archive).get(); 250 } 251 return cachedArchive; 252 } catch (IOException e) { 253 throw new MavenResourceException(e); 254 } 255 } 256 257 @Override 258 public Resource asResource() throws MavenResourceException { 259 if (cachedDelegee == null) { 260 createResource(); 261 } 262 return cachedDelegee; 263 } 264 265 /** 266 * Creates a {@link Resource} representation from the manifest 267 * of the artifact. 268 * 269 * @throws IOException Signals that an I/O exception has occurred. 270 * @throws ModelBuildingException the model building exception 271 * @throws UnresolvableModelException the unresolvable model exception 272 */ 273 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 274 "PMD.AvoidInstanceofChecksInCatchClause", 275 "PMD.CyclomaticComplexity", "PMD.NcssCount", 276 "PMD.AvoidInstantiatingObjectsInLoops", 277 "PMD.AvoidLiteralsInIfCondition", "PMD.CognitiveComplexity" }) 278 private void createResource() throws MavenResourceException { 279 Model model = model(archive.revision); 280 String extension = model.getPackaging(); 281 if ("bundle".equals(extension) 282 || "eclipse-plugin".equals(extension)) { 283 extension = Archive.JAR_EXTENSION; 284 } 285 ResourceBuilder builder = new ResourceBuilder(); 286 if (extension.equals(Archive.JAR_EXTENSION)) { 287 File binary; 288 try { 289 binary = get(archive); 290 } catch (IOException e) { 291 throw new MavenResourceException(e); 292 } 293 try { 294 if (location == BinaryLocation.LOCAL) { 295 builder.addFile(binary, binary.toURI()); 296 } else { 297 builder.addFile(binary, 298 boundArchive().mavenBackingRepository() 299 .toURI(archive.remotePath)); 300 } 301 } catch (Exception e) { 302 // That's what the exceptions thrown here come down to. 303 throw new MavenResourceException(e); 304 } 305 } 306 List<Capability> caps = builder.getCapabilities(); 307 Map<String, Object> idAttrs = caps.stream() 308 .filter(cap -> IdentityNamespace.IDENTITY_NAMESPACE 309 .equals(cap.getNamespace())) 310 .findFirst().map(Capability::getAttributes) 311 .orElse(Collections.emptyMap()); 312 String bsn = (String) idAttrs.getOrDefault( 313 IdentityNamespace.IDENTITY_NAMESPACE, 314 archive.getWithoutVersion()); 315 Version version = ResourceUtils.toVersion(idAttrs.getOrDefault( 316 IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, 317 archive.revision.version.getOSGiVersion())); 318 addInformationCapability(builder, bsn, version, archive.toString(), 319 null); 320 321 // Add dependency infos 322 if (!dependencies().isEmpty()) { 323 CapabilityBuilder cap 324 = new CapabilityBuilder(MAVEN_DEPENDENCIES_NS); 325 @SuppressWarnings("PMD.UseConcurrentHashMap") 326 Map<String, Set<Dependency>> depsByScope = new HashMap<>(); 327 for (Dependency dep : dependencies()) { 328 String scope = Optional.ofNullable(dep.getScope()) 329 .orElse("compile").toLowerCase(); 330 depsByScope.computeIfAbsent(scope, key -> new HashSet<>()) 331 .add(dep); 332 } 333 try { 334 for (var deps : depsByScope.entrySet()) { 335 cap.addAttribute(deps.getKey(), 336 toVersionList(deps.getValue())); 337 } 338 } catch (Exception e) { 339 throw new IllegalArgumentException(e); 340 } 341 builder.addCapability(cap); 342 } 343 cachedDelegee = builder.build(); 344 } 345 346 private String toVersionList(Collection<Dependency> deps) { 347 StringBuilder depsList = new StringBuilder(""); 348 for (Dependency dep : deps) { 349 if (depsList.length() > 0) { 350 depsList.append(';'); 351 } 352 depsList.append(dep.getGroupId()); 353 depsList.append(':'); 354 depsList.append(dep.getArtifactId()); 355 depsList.append(':'); 356 depsList.append(dep.getVersion()); 357 } 358 return depsList.toString(); 359 } 360 361 @Override 362 public List<Capability> getCapabilities(String namespace) 363 throws MavenResourceException { 364 return asResource().getCapabilities(namespace); 365 } 366 367 @Override 368 public List<Requirement> getRequirements(String namespace) 369 throws MavenResourceException { 370 return asResource().getRequirements(namespace); 371 } 372 373 @Override 374 public boolean equals(Object obj) { 375 if (obj == null) { 376 return false; 377 } 378 if (obj instanceof MavenResource) { 379 return archive.equals(((MavenResource) obj).archive()); 380 } 381 if (obj instanceof Resource) { 382 try { 383 return asResource().equals(obj); 384 } catch (MavenResourceException e) { 385 return false; 386 } 387 } 388 return false; 389 } 390 391 @Override 392 public int hashCode() { 393 return archive.hashCode(); 394 } 395 396 @Override 397 public String toString() { 398 return archive.toString(); 399 } 400 401 /* 402 * (non-Javadoc) 403 * 404 * @see de.mnl.osgi.bnd.maven.MavenResource#dependencies() 405 */ 406 @Override 407 @SuppressWarnings({ "PMD.ConfusingTernary", 408 "PMD.AvoidSynchronizedAtMethodLevel" }) 409 public final synchronized List<Dependency> dependencies() 410 throws MavenResourceException { 411 if (cachedDependencies == null) { 412 if (cachedDelegee != null) { 413 cachedDependencies = new ArrayList<>(); 414 retrieveDependencies(cachedDelegee, cachedDependencies); 415 } else { 416 cachedDependencies = MavenResourceRepository.this 417 .model(archive.revision).getDependencies().stream() 418 .filter(dep -> !dep.getGroupId().contains("$") 419 && !dep.getArtifactId().contains("$") 420 && !dep.isOptional() 421 && (dep.getScope() == null 422 || dep.getScope().equals("compile") 423 || dep.getScope().equals("runtime") 424 || dep.getScope().equals("provided"))) 425 .collect(Collectors.toList()); 426 } 427 } 428 return cachedDependencies; 429 } 430 431 } 432}