001/* 002 * Bnd Nexus Search Plugin 003 * Copyright (C) 2017 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.nexussearch; 020 021import aQute.maven.api.Program; 022import aQute.maven.api.Revision; 023import java.io.InputStream; 024import java.net.MalformedURLException; 025import java.net.URI; 026import java.net.URL; 027import java.util.Collections; 028import java.util.HashSet; 029import java.util.Iterator; 030import java.util.Map; 031import java.util.Set; 032import java.util.concurrent.ConcurrentHashMap; 033import javax.xml.stream.XMLEventReader; 034import javax.xml.stream.XMLInputFactory; 035import javax.xml.stream.XMLStreamException; 036import javax.xml.stream.events.StartElement; 037import javax.xml.stream.events.XMLEvent; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040 041/** 042 * Parses the XML document returned by a Nexus server in response to a 043 * <code>lucene/search</code> request. 044 */ 045public class NexusSearchNGResponseParser { 046 private static final Logger logger = LoggerFactory.getLogger( 047 NexusSearchNGResponseParser.class); 048 private Set<Revision> artifacts 049 = Collections.newSetFromMap(new ConcurrentHashMap<>()); 050 private Map<String, RepoInfo> repoInfos = new ConcurrentHashMap<>(); 051 052 /** 053 * Returns the reported snapshot repositories. 054 * 055 * @return the result 056 * @throws MalformedURLException if the URL is malformed 057 */ 058 public Set<URL> snapshotRepositories() throws MalformedURLException { 059 Set<URL> result = new HashSet<>(); 060 Iterator<RepoInfo> itr = repoInfos.values().stream().filter( 061 ri -> ri.referenced && ri.repoPolicy == RepoPolicy.Snapshot) 062 .iterator(); 063 while (itr.hasNext()) { 064 result.add(itr.next().contentResourceUri.toURL()); 065 } 066 return result; 067 } 068 069 /** 070 * Returns the reported release repositories. 071 * 072 * @return the result 073 * @throws MalformedURLException if the URL is malformed 074 */ 075 public Set<URL> releaseRepositories() throws MalformedURLException { 076 Set<URL> result = new HashSet<>(); 077 Iterator<RepoInfo> itr = repoInfos.values().stream().filter( 078 ri -> ri.referenced && ri.repoPolicy == RepoPolicy.Release) 079 .iterator(); 080 while (itr.hasNext()) { 081 result.add(itr.next().contentResourceUri.toURL()); 082 } 083 return result; 084 } 085 086 /** 087 * Returns the reported artifacts. 088 * 089 * @return the result 090 */ 091 public Set<Revision> artifacts() { 092 return Collections.unmodifiableSet(artifacts); 093 } 094 095 /** 096 * Parse the result return from the Nexus server. The outcome will be reflected in 097 * the attributes. 098 * 099 * @param in the stream with result data 100 * @return the parse result 101 * @throws Exception if a problem occurs 102 */ 103 public ParseResult parse(InputStream in) throws Exception { 104 XMLEventReader eventReader = XMLInputFactory.newInstance() 105 .createXMLEventReader(in); 106 ParseResult result = new ParseResult(); 107 while (eventReader.hasNext()) { 108 XMLEvent event = eventReader.nextEvent(); 109 if (event.isStartElement()) { 110 StartElement startElement = event.asStartElement(); 111 switch (startElement.getName().getLocalPart()) { 112 case "totalCount": 113 result.totalCount = Integer 114 .parseInt(parseCharacters(eventReader)); 115 break; 116 case "from": 117 result.from = Integer 118 .parseInt(parseCharacters(eventReader)); 119 break; 120 case "count": 121 result.count = Integer 122 .parseInt(parseCharacters(eventReader)); 123 break; 124 case "tooManyResults": 125 result.tooManyResults = Boolean 126 .parseBoolean(parseCharacters(eventReader)); 127 break; 128 // Repositories (item by item) 129 case "repositories-item": 130 parseRepositoryData(eventReader); 131 break; 132 // Repository definitions 133 case "org.sonatype.nexus.rest.model.NexusNGRepositoryDetail": 134 parseRepositoryDetail(eventReader); 135 break; 136 // Artifacts 137 case "artifact": 138 parseArtifact(eventReader); 139 result.artifactsInResult += 1; 140 break; 141 } 142 } 143 } 144 return result; 145 } 146 147 /** 148 * Parse a repository detail section. The results are added to attributes 149 * {@link #snapshotRepositories} and {@link #releaseRepositories} 150 * 151 * @param eventReader 152 * @throws Exception 153 */ 154 private void parseRepositoryData(XMLEventReader eventReader) 155 throws Exception { 156 RepoInfo repoInfo = new RepoInfo(); 157 boolean skip = true; 158 while (eventReader.hasNext()) { 159 XMLEvent event = eventReader.nextEvent(); 160 if (event.isEndElement() && event.asEndElement().getName() 161 .getLocalPart().equals("repositories-item")) { 162 if (!skip && repoInfo.repoPolicy != RepoPolicy.Unknown) { 163 repoInfos.put(repoInfo.id, repoInfo); 164 } 165 break; 166 } 167 if (event.isStartElement()) { 168 switch (event.asStartElement().getName().getLocalPart()) { 169 case "id": 170 repoInfo.id = parseCharacters(eventReader); 171 break; 172 case "contentResourceURI": 173 repoInfo.contentResourceUri 174 = new URI(parseCharacters(eventReader)); 175 break; 176 case "format": 177 if (parseCharacters(eventReader).equals("maven2")) { 178 skip = false; 179 } 180 break; 181 case "repoPolicy": 182 switch (parseCharacters(eventReader)) { 183 case "SNAPSHOT": 184 repoInfo.repoPolicy = RepoPolicy.Snapshot; 185 break; 186 case "RELEASE": 187 repoInfo.repoPolicy = RepoPolicy.Release; 188 break; 189 } 190 break; 191 } 192 } 193 } 194 } 195 196 /** 197 * Parse a repository detail section. The results are added to attributes 198 * {@link #snapshotRepositories} and {@link #releaseRepositories} 199 * 200 * @param eventReader 201 * @throws Exception 202 */ 203 private void parseRepositoryDetail(XMLEventReader eventReader) 204 throws Exception { 205 boolean skip = false; 206 while (eventReader.hasNext()) { 207 XMLEvent event = eventReader.nextEvent(); 208 if (event.isEndElement() && event.asEndElement().getName() 209 .getLocalPart().equals( 210 "org.sonatype.nexus.rest.model.NexusNGRepositoryDetail")) { 211 break; 212 } 213 } 214 if (skip) { 215 return; 216 } 217 } 218 219 /** 220 * Parse an artifact description and return the result as a 221 * set of {@link Revision}s. 222 * 223 * @param eventReader the input 224 * @return the result 225 * @throws XMLStreamException 226 */ 227 private void parseArtifact(XMLEventReader eventReader) 228 throws XMLStreamException { 229 String groupId = null; 230 String artifactId = null; 231 String version = null; 232 boolean foundClassifierInArtifactLink = false; 233 boolean foundBinJar = false; 234 235 while (eventReader.hasNext()) { 236 XMLEvent event = eventReader.nextEvent(); 237 if (event.isEndElement()) { 238 switch (event.asEndElement().getName().getLocalPart()) { 239 case "artifact": 240 if (foundBinJar) { 241 artifacts.add(Program.valueOf(groupId, artifactId) 242 .version(version)); 243 } 244 return; 245 case "artifactLink": 246 if (!foundClassifierInArtifactLink) { 247 foundBinJar = true; 248 } 249 break; 250 } 251 } 252 if (event.isStartElement()) { 253 switch (event.asStartElement().getName().getLocalPart()) { 254 case "repositoryId": 255 String repositoryId = parseCharacters(eventReader); 256 RepoInfo info = repoInfos.get(repositoryId); 257 if (info == null) { 258 logger.warn("Inconsistent search result: reference to " 259 + "non-existant repository with id " + repositoryId 260 + "."); 261 break; 262 } 263 info.referenced = true; 264 break; 265 case "groupId": 266 groupId = parseCharacters(eventReader); 267 break; 268 case "artifactId": 269 artifactId = parseCharacters(eventReader); 270 break; 271 case "version": 272 version = parseCharacters(eventReader); 273 break; 274 case "artifactLink": 275 foundClassifierInArtifactLink = false; 276 break; 277 case "classifier": 278 foundClassifierInArtifactLink = true; 279 break; 280 } 281 } 282 } 283 } 284 285 private String parseCharacters(XMLEventReader eventReader) 286 throws XMLStreamException { 287 StringBuilder sb = new StringBuilder(); 288 while (eventReader.peek().isCharacters()) { 289 XMLEvent event = eventReader.nextEvent(); 290 sb.append(event.asCharacters().getData()); 291 } 292 return sb.toString(); 293 } 294 295 private enum RepoPolicy { 296 Unknown, Snapshot, Release 297 } 298 299 private class RepoInfo { 300 public String id; 301 public RepoPolicy repoPolicy = RepoPolicy.Unknown; 302 public URI contentResourceUri; 303 boolean referenced = false; 304 } 305 306 public class ParseResult { 307 public int totalCount; 308 public int from; 309 public int count; 310 public boolean tooManyResults; 311 public int artifactsInResult; 312 } 313}