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