001/* 002 * Extra Bnd Repository Plugins 003 * Copyright (C) 2017,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 013 * License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package de.mnl.osgi.bnd.repository.maven.nexussearch; 020 021import aQute.bnd.osgi.Processor; 022import static aQute.bnd.osgi.repository.BridgeRepository.addInformationCapability; 023import aQute.bnd.osgi.repository.ResourcesRepository; 024import aQute.bnd.osgi.repository.XMLResourceGenerator; 025import aQute.bnd.osgi.repository.XMLResourceParser; 026import aQute.bnd.osgi.resource.ResourceBuilder; 027import aQute.bnd.version.MavenVersionRange; 028import aQute.maven.api.Archive; 029import aQute.maven.api.IPom; 030import aQute.maven.api.IPom.Dependency; 031import aQute.maven.api.MavenScope; 032import aQute.maven.api.Program; 033import aQute.maven.api.Revision; 034import aQute.maven.provider.MavenBackingRepository; 035import aQute.maven.provider.MavenRepository; 036import aQute.maven.provider.MetadataParser; 037import aQute.maven.provider.MetadataParser.RevisionMetadata; 038import java.io.File; 039import java.util.Collection; 040import java.util.Collections; 041import java.util.Comparator; 042import java.util.HashSet; 043import java.util.Iterator; 044import java.util.LinkedHashMap; 045import java.util.List; 046import java.util.Map; 047import java.util.Set; 048import java.util.concurrent.Callable; 049import java.util.concurrent.ConcurrentHashMap; 050import org.apache.maven.artifact.versioning.ComparableVersion; 051import org.osgi.resource.Resource; 052import org.osgi.service.repository.Repository; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055 056/** 057 * Provide an OSGi repository (a collection of {@link Resource}s, see 058 * {@link Repository}), filled with the results from recursively resolving 059 * an initial set of artifacts. 060 * <P> 061 * The resources in this repository are maintained in a local 062 * maven repository. 063 */ 064public abstract class LocalMavenBackedOsgiRepository 065 extends ResourcesRepository { 066 067 private static final Logger LOG = LoggerFactory.getLogger( 068 LocalMavenBackedOsgiRepository.class); 069 private final String name; 070 private final File obrIndexFile; 071 private final Set<Revision> toBeProcessed = new HashSet<>(); 072 private final Set<Revision> processing = new HashSet<>(); 073 private final Set<Revision> processed = new HashSet<>(); 074 075 /** 076 * Create a new instance that uses the provided information/resources 077 * to perform its work. 078 * 079 * @param name the name 080 * @param obrIndexFile the persistent representation of this 081 * repository's content 082 * @throws Exception if a problem occurs 083 */ 084 @SuppressWarnings("PMD.SignatureDeclareThrowsException") 085 public LocalMavenBackedOsgiRepository(String name, File obrIndexFile) 086 throws Exception { 087 this.name = name; 088 this.obrIndexFile = obrIndexFile; 089 090 if (!location().exists() || !location().isFile()) { 091 refresh(); 092 } else { 093 try (XMLResourceParser parser = new XMLResourceParser(location())) { 094 List<Resource> resources = parser.parse(); 095 addAll(resources); 096 } 097 } 098 } 099 100 /** 101 * Return the name of this repository. 102 * 103 * @return the name; 104 */ 105 public String name() { 106 return name; 107 } 108 109 /** 110 * Return the representation of this repository in the local file system. 111 * 112 * @return the location 113 */ 114 public final File location() { 115 return obrIndexFile; 116 } 117 118 /** 119 * Refresh this repository's content. 120 * 121 * @return true if refreshed, false if not refreshed possibly due to error 122 * @throws Exception if a problem occurs 123 */ 124 public abstract boolean refresh() throws Exception; 125 126 /** 127 * Refresh this repository's content. 128 * 129 * @param mavenRepository the maven repository 130 * @param startArtifacts the collection of artifacts to start with 131 * @return true if refreshed, false if not refreshed possibly due to error 132 * @throws Exception if a problem occurs 133 */ 134 public boolean refresh(MavenRepository mavenRepository, 135 Collection<? extends Revision> startArtifacts) throws Exception { 136 set(new HashSet<>()); // Clears this repository 137 toBeProcessed.addAll(startArtifacts); 138 // Repository information is obtained from both querying the 139 // repositories 140 // (provides information about existing repositories) and from executing 141 // the query (provides information about actually used repositories). 142 Set<Resource> collectedResources 143 = Collections.newSetFromMap(new ConcurrentHashMap<>()); 144 synchronized (this) { 145 while (true) { 146 if (toBeProcessed.isEmpty() && processing.isEmpty()) { 147 break; 148 } 149 if (!toBeProcessed.isEmpty() && processing.size() < 4) { 150 Revision rev = toBeProcessed.iterator().next(); 151 toBeProcessed.remove(rev); 152 processing.add(rev); 153 Processor.getScheduledExecutor().submit( 154 new RevisionProcessor(mavenRepository, 155 collectedResources, rev)); 156 } 157 wait(); 158 } 159 } 160 processed.clear(); 161 // Set this repository's content to the results... 162 addAll(collectedResources); 163 // ... and persist the content. 164 XMLResourceGenerator generator = new XMLResourceGenerator(); 165 generator.resources(getResources()); 166 generator.name(name()); 167 generator.save(obrIndexFile); 168 return true; 169 } 170 171 /** 172 * A callable (allows it to throw an exception) that processes a single 173 * Revision. 174 */ 175 private class RevisionProcessor implements Callable<Void> { 176 private final MavenRepository mavenRepository; 177 private final Revision revision; 178 private final Set<Resource> collectedResources; 179 180 public RevisionProcessor(MavenRepository mavenRepository, 181 Set<Resource> collectedResources, Revision revision) { 182 this.mavenRepository = mavenRepository; 183 this.collectedResources = collectedResources; 184 this.revision = revision; 185 } 186 187 /** 188 * Get this revision's dependencies from the POM and enqueue them as 189 * to be processed (unless processed already) and create an entry 190 * for the resource in this repository. 191 */ 192 @Override 193 public Void call() throws Exception { 194 try { 195 // Get and add this revision's OSGi information (refreshes 196 // snapshots) 197 Archive archive = mavenRepository.getResolvedArchive(revision, 198 Archive.JAR_EXTENSION, ""); 199 if (archive != null) { 200 if (archive.isSnapshot()) { 201 for (MavenBackingRepository mbr : mavenRepository 202 .getSnapshotRepositories()) { 203 if (mbr.getVersion(archive.getRevision()) != null) { 204 // Found backing repository 205 File metaFile = mavenRepository.toLocalFile( 206 revision.metadata(mbr.getId())); 207 RevisionMetadata metaData 208 = MetadataParser.parseRevisionMetadata( 209 metaFile); 210 File archiveFile 211 = mavenRepository.toLocalFile(archive); 212 if (archiveFile 213 .lastModified() < metaData.lastUpdated) { 214 archiveFile.delete(); 215 } 216 File pomFile = mavenRepository 217 .toLocalFile(archive.getPomArchive()); 218 if (pomFile 219 .lastModified() < metaData.lastUpdated) { 220 pomFile.delete(); 221 } 222 break; 223 } 224 } 225 } 226 // Get POM for dependencies 227 IPom pom = mavenRepository.getPom(archive.getRevision()); 228 if (pom != null) { 229 // Get pom and add all dependencies as to be processed. 230 addDependencies(pom); 231 } 232 Resource resource = parseResource(archive); 233 if (resource != null) { 234 collectedResources.add(resource); 235 } 236 } 237 return null; 238 } finally { 239 // We're done witht his revision. 240 synchronized (LocalMavenBackedOsgiRepository.this) { 241 processing.remove(revision); 242 processed.add(revision); 243 LocalMavenBackedOsgiRepository.this.notifyAll(); 244 } 245 } 246 } 247 248 private void addDependencies(IPom pom) { 249 try { 250 Map<Program, Dependency> deps = new LinkedHashMap<>(); 251 deps.putAll(pom.getDependencies(MavenScope.compile, false)); 252 deps.putAll(pom.getDependencies(MavenScope.runtime, false)); 253 synchronized (LocalMavenBackedOsgiRepository.this) { 254 for (Map.Entry<Program, Dependency> entry : deps 255 .entrySet()) { 256 bindToVersion(entry.getValue()); 257 try { 258 Revision rev = entry.getValue().getRevision(); 259 if (!toBeProcessed.contains(rev) 260 && !processing.contains(rev) 261 && !processed.contains(rev)) { 262 toBeProcessed.add(rev); 263 LOG.debug("Added as dependency {}", rev); 264 } 265 LocalMavenBackedOsgiRepository.this.notifyAll(); 266 } catch (Exception e) { 267 LOG.warn("Unbindable dependency {}", 268 entry.getValue().toString()); 269 continue; 270 } 271 } 272 } 273 } catch (Exception e) { 274 LOG.error("Failed to get POM of " + revision + ".", e); 275 } 276 } 277 278 private void bindToVersion(Dependency dependency) throws Exception { 279 if (MavenVersionRange.isRange(dependency.version)) { 280 281 MavenVersionRange range 282 = new MavenVersionRange(dependency.version); 283 List<Revision> revisions 284 = mavenRepository.getRevisions(dependency.program); 285 286 for (Iterator<Revision> it = revisions.iterator(); 287 it.hasNext();) { 288 Revision rev = it.next(); 289 if (!range.includes(rev.version)) { 290 it.remove(); 291 } 292 } 293 294 if (!revisions.isEmpty()) { 295 Collections.sort(revisions, new MavenRevisionComparator()); 296 Revision highest = revisions.get(revisions.size() - 1); 297 dependency.version = highest.version.toString(); 298 } 299 } 300 } 301 302 private Resource parseResource(Archive archive) throws Exception { 303 ResourceBuilder rb = new ResourceBuilder(); 304 try { 305 File binary = mavenRepository.get(archive).getValue(); 306 rb.addFile(binary, binary.toURI()); 307 addInformationCapability(rb, archive.toString(), 308 archive.getRevision().toString(), null); 309 } catch (Exception e) { 310 return null; 311 } 312 return rb.build(); 313 } 314 315 } 316 317 public class MavenRevisionComparator implements Comparator<Revision> { 318 @Override 319 public int compare(Revision rev1, Revision rev2) { 320 int res = rev1.program.compareTo(rev2.program); 321 if (res != 0) { 322 return res; 323 } 324 325 ComparableVersion rev1ver 326 = new ComparableVersion(rev1.version.toString()); 327 ComparableVersion rev2ver 328 = new ComparableVersion(rev2.version.toString()); 329 return rev1ver.compareTo(rev2ver); 330 } 331 } 332 333}