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 aQute.bnd.service.RepositoryPlugin; 022import aQute.maven.api.Archive; 023import aQute.maven.api.IMavenRepo; 024import aQute.maven.api.Program; 025import aQute.maven.api.Revision; 026import aQute.maven.provider.MavenBackingRepository; 027import aQute.maven.provider.MavenRepository; 028import aQute.maven.provider.MetadataParser; 029import aQute.maven.provider.MetadataParser.RevisionMetadata; 030import aQute.service.reporter.Reporter; 031import static de.mnl.osgi.bnd.maven.RepositoryUtils.rethrow; 032import static de.mnl.osgi.bnd.maven.RepositoryUtils.unthrow; 033import java.io.Closeable; 034import java.io.File; 035import java.io.IOException; 036import java.lang.reflect.InvocationTargetException; 037import java.lang.reflect.UndeclaredThrowableException; 038import java.util.ArrayList; 039import java.util.Comparator; 040import java.util.List; 041import java.util.Map; 042import java.util.Optional; 043import java.util.concurrent.ConcurrentHashMap; 044import java.util.concurrent.Executor; 045import java.util.regex.Pattern; 046import java.util.stream.Collectors; 047import java.util.stream.Stream; 048import org.apache.maven.model.Model; 049import org.apache.maven.model.building.DefaultModelBuilder; 050import org.apache.maven.model.building.DefaultModelBuildingRequest; 051import org.apache.maven.model.building.DefaultModelProcessor; 052import org.apache.maven.model.building.ModelBuilder; 053import org.apache.maven.model.building.ModelBuildingException; 054import org.apache.maven.model.building.ModelBuildingRequest; 055import org.apache.maven.model.building.ModelBuildingResult; 056import org.apache.maven.model.building.ModelSource; 057import org.apache.maven.model.composition.DefaultDependencyManagementImporter; 058import org.apache.maven.model.inheritance.DefaultInheritanceAssembler; 059import org.apache.maven.model.interpolation.StringSearchModelInterpolator; 060import org.apache.maven.model.io.DefaultModelReader; 061import org.apache.maven.model.locator.DefaultModelLocator; 062import org.apache.maven.model.management.DefaultDependencyManagementInjector; 063import org.apache.maven.model.management.DefaultPluginManagementInjector; 064import org.apache.maven.model.normalization.DefaultModelNormalizer; 065import org.apache.maven.model.path.DefaultModelPathTranslator; 066import org.apache.maven.model.path.DefaultModelUrlNormalizer; 067import org.apache.maven.model.path.DefaultPathTranslator; 068import org.apache.maven.model.path.DefaultUrlNormalizer; 069import org.apache.maven.model.profile.DefaultProfileInjector; 070import org.apache.maven.model.profile.DefaultProfileSelector; 071import org.apache.maven.model.resolution.UnresolvableModelException; 072import org.apache.maven.model.superpom.DefaultSuperPomProvider; 073import org.apache.maven.model.validation.DefaultModelValidator; 074import org.osgi.util.promise.Promise; 075 076/** 077 * Provides a composite {@link IMavenRepo} view on several 078 * {@link MavenBackingRepository} instances. 079 * The class replaces {@link MavenRepository} which lacks some 080 * required functionality. (Besides, this class has a more 081 * appropriate name.) 082 * <P> 083 * The information about artifacts is provided as a maven 084 * {@link Model}. It is evaluated using the maven libraries and 085 * should therefore be consistent with the model information 086 * used in other maven repository based tools. 087 */ 088@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "deprecation", 089 "PMD.ExcessiveImports" }) 090public class CompositeMavenRepository implements Closeable { 091 092 public static final Pattern COORDS_SPLITTER = Pattern.compile("\\s*;\\s*"); 093 private final MavenRepository bndMavenRepo; 094 private final Reporter reporter; 095 private final Map<Program, List<BoundRevision>> programCache 096 = new ConcurrentHashMap<>(); 097 private final Map<Revision, Model> modelCache 098 = new ConcurrentHashMap<>(); 099 private final BndModelResolver modelResolver; 100 private final ModelBuilder modelBuilder; 101 102 /** 103 * Use local or remote URL in index. 104 */ 105 public enum BinaryLocation { 106 LOCAL, REMOTE 107 } 108 109 /** 110 * Instantiates a new composite maven repository. 111 * 112 * @param base the base 113 * @param repoId the repository id 114 * @param releaseRepos the backing release repositories 115 * @param snapshotRepos the backing snapshot repositories 116 * @param executor an executor 117 * @param reporter the reporter 118 * @throws Exception the exception 119 */ 120 @SuppressWarnings({ "PMD.SignatureDeclareThrowsException", 121 "PMD.AvoidDuplicateLiterals", "PMD.NcssCount" }) 122 public CompositeMavenRepository(File base, String repoId, 123 List<MavenBackingRepository> releaseRepos, 124 List<MavenBackingRepository> snapshotRepos, Executor executor, 125 Reporter reporter) 126 throws Exception { 127 bndMavenRepo = new MavenRepository(base, repoId, releaseRepos, 128 snapshotRepos, executor, reporter); 129 this.reporter = reporter; 130 modelResolver = new BndModelResolver(bndMavenRepo, reporter); 131 132 // Create the maven model builder. This code is ridiculous, 133 // but using maven's CDI pulls in an even more ridiculous 134 // number of libraries. To get an idea why, read 135 // https://lairdnelson.wordpress.com/2017/03/06/maven-and-the-project-formerly-known-as-aether/ 136 DefaultModelBuilder builder = new DefaultModelBuilder(); 137 builder.setProfileSelector(new DefaultProfileSelector()); 138 DefaultModelProcessor processor = new DefaultModelProcessor(); 139 processor.setModelLocator(new DefaultModelLocator()); 140 processor.setModelReader(new DefaultModelReader()); 141 builder.setModelProcessor(processor); 142 builder.setModelValidator(new DefaultModelValidator()); 143 DefaultSuperPomProvider pomProvider = new DefaultSuperPomProvider(); 144 pomProvider.setModelProcessor(processor); 145 DefaultSuperPomProvider superPomProvider 146 = new DefaultSuperPomProvider(); 147 superPomProvider.setModelProcessor(processor); 148 builder.setSuperPomProvider(superPomProvider); 149 builder.setModelNormalizer(new DefaultModelNormalizer()); 150 builder.setInheritanceAssembler(new DefaultInheritanceAssembler()); 151 StringSearchModelInterpolator modelInterpolator 152 = new StringSearchModelInterpolator(); 153 DefaultPathTranslator pathTranslator = new DefaultPathTranslator(); 154 modelInterpolator.setPathTranslator(pathTranslator); 155 DefaultUrlNormalizer urlNormalizer = new DefaultUrlNormalizer(); 156 modelInterpolator.setUrlNormalizer(urlNormalizer); 157 builder.setModelInterpolator(modelInterpolator); 158 DefaultModelUrlNormalizer modelUrlNormalizer 159 = new DefaultModelUrlNormalizer(); 160 modelUrlNormalizer.setUrlNormalizer(urlNormalizer); 161 builder.setModelUrlNormalizer(modelUrlNormalizer); 162 DefaultModelPathTranslator modelPathTranslator 163 = new DefaultModelPathTranslator(); 164 modelPathTranslator.setPathTranslator(pathTranslator); 165 builder.setModelPathTranslator(modelPathTranslator); 166 builder 167 .setPluginManagementInjector(new DefaultPluginManagementInjector()); 168 builder.setDependencyManagementInjector( 169 new DefaultDependencyManagementInjector()); 170 builder.setDependencyManagementImporter( 171 new DefaultDependencyManagementImporter()); 172 builder.setProfileInjector(new DefaultProfileInjector()); 173 modelBuilder = builder; 174 } 175 176 /** 177 * Gets the name of this repository. 178 * 179 * @return the name 180 */ 181 public String name() { 182 return bndMavenRepo.getName(); 183 } 184 185 /** 186 * Reset any cached information. 187 */ 188 public void reset() { 189 programCache.clear(); 190 modelCache.clear(); 191 } 192 193 @Override 194 public void close() throws IOException { 195 bndMavenRepo.close(); 196 } 197 198 /** 199 * Returns all backing repositories. 200 * 201 * @return the list of all repositories 202 */ 203 public List<MavenBackingRepository> backing() { 204 List<MavenBackingRepository> result 205 = new ArrayList<>(bndMavenRepo.getReleaseRepositories()); 206 result.addAll(bndMavenRepo.getSnapshotRepositories()); 207 return result; 208 } 209 210 /** 211 * Returns all repositories as a stream. 212 * 213 * @return the repositories as stream 214 */ 215 public Stream<MavenBackingRepository> backingAsStream() { 216 return Stream.concat(bndMavenRepo.getReleaseRepositories().stream(), 217 bndMavenRepo.getSnapshotRepositories().stream()).distinct(); 218 } 219 220 /** 221 * Retrieves the file from a remote repository into the repositories 222 * local cache directory if it doesn't exist yet. 223 * 224 * @param archive The archive to fetch 225 * @return the file or null if not found 226 * @throws IOException Signals that an I/O exception has occurred. 227 */ 228 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 229 "PMD.AvoidInstanceofChecksInCatchClause", 230 "PMD.AvoidDuplicateLiterals" }) 231 public Promise<File> retrieve(Archive archive) throws IOException { 232 try { 233 return bndMavenRepo.get(archive); 234 } catch (Exception e) { 235 if (e instanceof InvocationTargetException 236 && ((InvocationTargetException) e) 237 .getTargetException() instanceof IOException) { 238 // Should be the only possible exception here 239 throw (IOException) ((InvocationTargetException) e) 240 .getTargetException(); 241 } 242 throw new UndeclaredThrowableException(e); 243 } 244 } 245 246 /** 247 * Get the file object for the archive. The file does not have to exist. 248 * The use case for this is to have the {@link File} already while 249 * waiting for the {@link Promise} returned by {@link #retrieve(Archive)} 250 * to complete. 251 * <P> 252 * This is required for the implementation of {@link RepositoryPlugin#get}. 253 * Besides this use case, it should probably not be used. 254 * 255 * @param archive the archive to find the file for 256 * @return the File or null if not found 257 */ 258 public File toLocalFile(Archive archive) { 259 return bndMavenRepo.toLocalFile(archive); 260 } 261 262 /** 263 * Gets the file from the local cache directory, retrieving it 264 * first if it doesn't exist yet. 265 * 266 * @param archive The archive to fetch 267 * @return the file or null if not found 268 * @throws IOException Signals that an I/O exception has occurred. 269 */ 270 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 271 "PMD.AvoidInstanceofChecksInCatchClause", 272 "PMD.AvoidDuplicateLiterals" }) 273 public File get(Archive archive) throws IOException { 274 try { 275 return bndMavenRepo.get(archive).getValue(); 276 } catch (Exception e) { 277 if (e instanceof InvocationTargetException 278 && ((InvocationTargetException) e) 279 .getTargetException() instanceof IOException) { 280 // Should be the only possible exception here 281 throw (IOException) ((InvocationTargetException) e) 282 .getTargetException(); 283 } 284 throw new UndeclaredThrowableException(e); 285 } 286 } 287 288 /** 289 * Wrapper for {@link MavenBackingRepository#getRevisions(Program, List)} 290 * that returns the result instead of accumulating it and maps the 291 * (too general) exception. 292 * 293 * @param mbr the backing repository 294 * @param program the program 295 * @return the revisions 296 * @throws IOException Signals that an I/O exception has occurred. 297 */ 298 @SuppressWarnings({ "PMD.LinguisticNaming", "PMD.AvoidRethrowingException", 299 "PMD.AvoidCatchingGenericException", 300 "PMD.AvoidThrowingRawExceptionTypes", "PMD.AvoidDuplicateLiterals" }) 301 private List<Revision> revisionsFrom(MavenBackingRepository mbr, 302 Program program) { 303 List<Revision> result = new ArrayList<>(); 304 try { 305 mbr.getRevisions(program, result); 306 } catch (IOException e) { 307 reporter.exception(e, 308 "Failed to get list of revisions of %s from %s: %s", 309 program, mbr, e.getMessage()); 310 return result; 311 } catch (Exception e) { 312 throw new RuntimeException( 313 "Cannot evaluate revisions for " + program + " from " + mbr, e); 314 } 315 return result; 316 } 317 318 /** 319 * Get the bound revisions of the given program. 320 * 321 * @param program the program 322 * @return the list 323 */ 324 public Stream<BoundRevision> findRevisions(Program program) { 325 return programCache.computeIfAbsent(program, 326 prg -> backingAsStream() 327 .flatMap(mbr -> revisionsFrom(mbr, program).stream() 328 .map(revision -> new BoundRevision(mbr, revision))) 329 .collect(Collectors.toList())) 330 .stream(); 331 } 332 333 /** 334 * Converts a {@link Revision} to a {@link BoundRevision}. 335 * 336 * @param revision the revision 337 * @return the bound revision 338 */ 339 @SuppressWarnings("PMD.SignatureDeclareThrowsException") 340 public Optional<BoundRevision> find(Revision revision) { 341 return findRevisions(revision.program) 342 .filter(rev -> rev.unbound().equals(revision)).findFirst(); 343 } 344 345 /** 346 * Converts an {@link Archive} to a {@link BoundArchive}. 347 * 348 * @param archive the archive 349 * @return the bound archive 350 * @throws IOException Signals that an I/O exception has occurred. 351 */ 352 @SuppressWarnings("PMD.SignatureDeclareThrowsException") 353 public Optional<BoundArchive> find(Archive archive) 354 throws IOException { 355 return rethrow(IOException.class, 356 () -> find(archive.revision).flatMap(rev -> unthrow(() -> Optional 357 .ofNullable( 358 resolve(rev, archive.extension, archive.classifier))))); 359 } 360 361 /** 362 * Converts a {@link Program} and a version, which 363 * may be a range, to a {@link BoundRevision}. 364 * 365 * @param program the program 366 * @param version the version 367 * @return the bound revision 368 */ 369 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 370 public Optional<BoundRevision> find(Program program, 371 MavenVersionSpecification version) { 372 if (version instanceof MavenVersion) { 373 return find(((MavenVersion) version).of(program)); 374 } 375 MavenVersionRange range = (MavenVersionRange) version; 376 return findRevisions(program) 377 .filter(rev -> range.includes(rev.version())) 378 .max(Comparator.naturalOrder()); 379 } 380 381 /** 382 * Get a model of the specified revision. Dependency versions 383 * remain unresolved, i.e. when specified as a range, the range 384 * is preserved. 385 * 386 * @param revision the archive 387 * @return the dependencies 388 * @throws MavenResourceException the maven resource exception 389 */ 390 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 391 "PMD.AvoidInstanceofChecksInCatchClause", "PMD.PreserveStackTrace", 392 "PMD.AvoidRethrowingException" }) 393 public Model model(Revision revision) throws MavenResourceException { 394 return rethrow(MavenResourceException.class, 395 () -> modelCache.computeIfAbsent( 396 revision, key -> unthrow(() -> readModel(key)))); 397 } 398 399 private Model readModel(Revision revision) throws MavenResourceException { 400 DefaultModelBuildingRequest request = new DefaultModelBuildingRequest(); 401 try { 402 ModelSource modelSource = modelResolver.resolveModel(revision.group, 403 revision.artifact, revision.version.toString()); 404 request.setModelResolver(modelResolver) 405 .setModelSource(modelSource) 406 .setValidationLevel( 407 ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL) 408 .setTwoPhaseBuilding(false); 409 ModelBuildingResult result = modelBuilder.build(request); 410 return result.getEffectiveModel(); 411 } catch (UnresolvableModelException | ModelBuildingException e) { 412 throw new MavenResourceException(e); 413 } 414 } 415 416 /** 417 * Gets the resolved archive. "Resolving" an archive means finding 418 * the binaries with the specified extension and classifier belonging 419 * to the given version. While this can be done with straight forward 420 * name mapping for releases, snapshots have a timestamp that has to 421 * be looked up in the backing repository. 422 * 423 * @param revision the revision 424 * @param extension the extension 425 * @param classifier the classifier 426 * @return the resolved archive 427 * @throws IOException Signals that an I/O exception has occurred. 428 */ 429 @SuppressWarnings({ "PMD.AvoidCatchingGenericException", 430 "PMD.AvoidThrowingRawExceptionTypes", "PMD.AvoidRethrowingException" }) 431 public BoundArchive resolve(BoundRevision revision, 432 String extension, String classifier) throws IOException { 433 if (!revision.isSnapshot()) { 434 return revision.archive(extension, classifier); 435 } 436 try { 437 MavenVersion version = MavenVersion.from(revision 438 .mavenBackingRepository().getVersion(revision.unbound())); 439 return revision.archive(version, extension, classifier); 440 } catch (IOException e) { 441 throw e; 442 } catch (Exception e) { 443 throw new RuntimeException(e); 444 } 445 } 446 447 /** 448 * Refresh a snapshot. 449 * 450 * @param archive the archive 451 */ 452 @SuppressWarnings("PMD.AvoidCatchingGenericException") 453 protected void refreshSnapshot(BoundArchive archive) { 454 File metaFile = bndMavenRepo.toLocalFile( 455 archive.getRevision() 456 .metadata(archive.mavenBackingRepository().getId())); 457 RevisionMetadata metaData; 458 try { 459 metaData = MetadataParser.parseRevisionMetadata(metaFile); 460 } catch (Exception e) { 461 reporter.exception(e, "Problem accessing %s.", archive); 462 return; 463 } 464 File archiveFile = bndMavenRepo.toLocalFile(archive); 465 if (archiveFile.lastModified() < metaData.lastUpdated) { 466 archiveFile.delete(); 467 } 468 File pomFile = bndMavenRepo.toLocalFile(archive.getPomArchive()); 469 if (pomFile.lastModified() < metaData.lastUpdated) { 470 pomFile.delete(); 471 } 472 } 473 474}