View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.search.backend.remoterepository.internal;
20  
21  import java.io.BufferedReader;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.InputStreamReader;
25  import java.nio.charset.StandardCharsets;
26  import java.util.ArrayList;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Objects;
30  import java.util.Properties;
31  
32  import org.apache.maven.search.Record;
33  import org.apache.maven.search.SearchRequest;
34  import org.apache.maven.search.backend.remoterepository.Context;
35  import org.apache.maven.search.backend.remoterepository.RecordFactory;
36  import org.apache.maven.search.backend.remoterepository.RemoteRepositorySearchBackend;
37  import org.apache.maven.search.backend.remoterepository.RemoteRepositorySearchResponse;
38  import org.apache.maven.search.backend.remoterepository.RemoteRepositorySearchTransport;
39  import org.apache.maven.search.backend.remoterepository.ResponseExtractor;
40  import org.apache.maven.search.support.SearchBackendSupport;
41  import org.jsoup.Jsoup;
42  import org.jsoup.nodes.Document;
43  import org.jsoup.parser.Parser;
44  
45  import static java.util.Objects.requireNonNull;
46  
47  /**
48   * Implementation of {@link RemoteRepositorySearchBackend} that is tested against Maven Central.
49   * All the methods are "loosely encapsulated" (are protected) to enable easy override of any
50   * required aspect of this implementation, to suit it against different remote repositories
51   * (HTML parsing) if needed.
52   */
53  public class RemoteRepositorySearchBackendImpl extends SearchBackendSupport implements RemoteRepositorySearchBackend {
54      private final String baseUri;
55  
56      private final RemoteRepositorySearchTransport transport;
57  
58      private final ResponseExtractor responseExtractor;
59  
60      private final Map<String, String> commonHeaders;
61  
62      protected enum State {
63          G,
64          GA,
65          GAV,
66          GAVCE,
67          GAVCE1
68      }
69  
70      /**
71       * Creates a customized instance of SMO backend, like an in-house instances of SMO or different IDs.
72       */
73      public RemoteRepositorySearchBackendImpl(
74              String backendId,
75              String repositoryId,
76              String baseUri,
77              RemoteRepositorySearchTransport transport,
78              ResponseExtractor responseExtractor) {
79          super(backendId, repositoryId);
80          this.baseUri = requireNonNull(baseUri);
81          this.transport = requireNonNull(transport);
82          this.responseExtractor = requireNonNull(responseExtractor);
83  
84          this.commonHeaders = Map.of(
85                  "User-Agent",
86                  "Apache-Maven-Search-RR/" + discoverVersion() + " "
87                          + transport.getClass().getSimpleName());
88      }
89  
90      private String discoverVersion() {
91          Properties properties = new Properties();
92          InputStream inputStream = getClass()
93                  .getClassLoader()
94                  .getResourceAsStream(
95                          "org/apache/maven/search/backend/smo/internal/remoterepository-version.properties");
96          if (inputStream != null) {
97              try (InputStream is = inputStream) {
98                  properties.load(is);
99              } catch (IOException e) {
100                 // fall through
101             }
102         }
103         return properties.getProperty("version", "unknown");
104     }
105 
106     @Override
107     public String getBaseUri() {
108         return baseUri;
109     }
110 
111     @Override
112     public RemoteRepositorySearchResponse search(SearchRequest searchRequest) throws IOException {
113         Context context = new Context(searchRequest);
114         String uri = baseUri;
115         State state = null;
116         if (context.getGroupId() != null) {
117             uri += context.getGroupId().replace('.', '/') + "/";
118             state = State.G;
119             if (context.getArtifactId() != null) {
120                 uri += context.getArtifactId() + "/";
121                 state = State.GA;
122                 if (context.getVersion() == null) {
123                     uri += "maven-metadata.xml";
124                 } else {
125                     uri += context.getVersion() + "/";
126                     state = State.GAV;
127                     if (context.getFileExtension() != null) {
128                         // we go for actually specified artifact
129                         uri += context.getArtifactId() + "-" + context.getVersion();
130                         if (context.getClassifier() != null) {
131                             uri += "-" + context.getClassifier();
132                         }
133                         uri += "." + context.getFileExtension();
134                         state = State.GAVCE;
135                         if (context.getSha1() != null) {
136                             state = State.GAVCE1;
137                         }
138                     }
139                 }
140             }
141         }
142         if (state == null) {
143             throw new IllegalArgumentException("Unsupported Query: " + searchRequest.getQuery());
144         }
145 
146         int totalHits = 0;
147         List<Record> page = new ArrayList<>(searchRequest.getPaging().getPageSize());
148         RecordFactory recordFactory = new RecordFactory(this);
149         Document document = null;
150         if (state.ordinal() < State.GAVCE.ordinal()) {
151             Parser parser = state == State.GA ? Parser.xmlParser() : Parser.htmlParser();
152             try (RemoteRepositorySearchTransport.Response response = transport.get(uri, commonHeaders)) {
153                 if (response.getCode() == 200) {
154                     document = Jsoup.parse(response.getBody(), StandardCharsets.UTF_8.name(), uri, parser);
155                 }
156             }
157 
158             if (document == null) {
159                 throw new IOException("Unexpected response from: " + uri);
160             }
161 
162             switch (state) {
163                 case G:
164                     totalHits = responseExtractor.populateG(context, document, recordFactory, page);
165                     break;
166                 case GA:
167                     totalHits = responseExtractor.populateGA(context, document, recordFactory, page);
168                     break;
169                 case GAV:
170                     totalHits = responseExtractor.populateGAV(context, document, recordFactory, page);
171                     break;
172                 default:
173                     throw new IllegalStateException("State" + state); // checkstyle
174             }
175         } else {
176             try (RemoteRepositorySearchTransport.Response response = transport.head(uri, commonHeaders)) {
177                 if (response.getCode() == 200) {
178                     boolean matches = context.getSha1() == null;
179                     if (context.getSha1() != null) {
180                         try (RemoteRepositorySearchTransport.Response sha1Response =
181                                 transport.get(uri + ".sha1", commonHeaders)) {
182                             if (response.getCode() == 200) {
183                                 try (InputStream body = sha1Response.getBody()) {
184                                     String remoteSha1 = readChecksum(body);
185                                     matches = Objects.equals(context.getSha1(), remoteSha1);
186                                 }
187                             }
188                         }
189                     }
190                     if (matches) {
191                         page.add(recordFactory.create(
192                                 context.getGroupId(),
193                                 context.getArtifactId(),
194                                 context.getVersion(),
195                                 context.getClassifier(),
196                                 context.getFileExtension()));
197                         totalHits = 1;
198                     }
199                 }
200             }
201         }
202         return new RemoteRepositorySearchResponseImpl(searchRequest, totalHits, page, uri, document);
203     }
204 
205     private static String readChecksum(InputStream inputStream) throws IOException {
206         String checksum = "";
207         try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8), 512)) {
208             while (true) {
209                 String line = br.readLine();
210                 if (line == null) {
211                     break;
212                 }
213                 line = line.trim();
214                 if (line.length() > 0) {
215                     checksum = line;
216                     break;
217                 }
218             }
219         }
220 
221         if (checksum.matches(".+= [0-9A-Fa-f]+")) {
222             int lastSpacePos = checksum.lastIndexOf(' ');
223             checksum = checksum.substring(lastSpacePos + 1);
224         } else {
225             int spacePos = checksum.indexOf(' ');
226 
227             if (spacePos != -1) {
228                 checksum = checksum.substring(0, spacePos);
229             }
230         }
231 
232         return checksum;
233     }
234 }