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.smo.internal;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.net.HttpURLConnection;
24  import java.net.URLEncoder;
25  import java.nio.charset.StandardCharsets;
26  import java.util.ArrayList;
27  import java.util.HashMap;
28  import java.util.HashSet;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Properties;
32  
33  import com.google.gson.JsonArray;
34  import com.google.gson.JsonElement;
35  import com.google.gson.JsonObject;
36  import com.google.gson.JsonParser;
37  import com.google.gson.JsonPrimitive;
38  import org.apache.maven.search.api.MAVEN;
39  import org.apache.maven.search.api.Record;
40  import org.apache.maven.search.api.SearchRequest;
41  import org.apache.maven.search.api.request.BooleanQuery;
42  import org.apache.maven.search.api.request.Field;
43  import org.apache.maven.search.api.request.FieldQuery;
44  import org.apache.maven.search.api.request.Paging;
45  import org.apache.maven.search.api.request.Query;
46  import org.apache.maven.search.api.support.SearchBackendSupport;
47  import org.apache.maven.search.api.transport.Transport;
48  import org.apache.maven.search.backend.smo.SmoSearchBackend;
49  import org.apache.maven.search.backend.smo.SmoSearchResponse;
50  
51  import static java.util.Objects.requireNonNull;
52  
53  public class SmoSearchBackendImpl extends SearchBackendSupport implements SmoSearchBackend {
54      private static final Map<Field, String> FIELD_TRANSLATION;
55  
56      static {
57          FIELD_TRANSLATION = Map.of(
58                  MAVEN.GROUP_ID,
59                  "g",
60                  MAVEN.ARTIFACT_ID,
61                  "a",
62                  MAVEN.VERSION,
63                  "v",
64                  MAVEN.CLASSIFIER,
65                  "l",
66                  MAVEN.PACKAGING,
67                  "p",
68                  MAVEN.CLASS_NAME,
69                  "c",
70                  MAVEN.FQ_CLASS_NAME,
71                  "fc",
72                  MAVEN.SHA1,
73                  "1");
74      }
75  
76      private final String smoUri;
77  
78      private final Transport transport;
79  
80      private final Map<String, String> commonHeaders;
81  
82      /**
83       * Creates a customized instance of SMO backend, like an in-house instances of SMO or different IDs.
84       */
85      public SmoSearchBackendImpl(String backendId, String repositoryId, String smoUri, Transport transport) {
86          super(backendId, repositoryId);
87          this.smoUri = requireNonNull(smoUri);
88          this.transport = requireNonNull(transport);
89  
90          this.commonHeaders = new HashMap<>();
91          this.commonHeaders.put(
92                  "User-Agent",
93                  "Apache-Maven-Search-SMO/" + discoverVersion() + " "
94                          + transport.getClass().getSimpleName());
95          this.commonHeaders.put("Accept", "application/json");
96      }
97  
98      private String discoverVersion() {
99          Properties properties = new Properties();
100         InputStream inputStream = getClass()
101                 .getClassLoader()
102                 .getResourceAsStream("org/apache/maven/search/backend/smo/internal/smo-version.properties");
103         if (inputStream != null) {
104             try (InputStream is = inputStream) {
105                 properties.load(is);
106             } catch (IOException e) {
107                 // fall through
108             }
109         }
110         return properties.getProperty("version", "unknown");
111     }
112 
113     @Override
114     public String getSmoUri() {
115         return smoUri;
116     }
117 
118     @Override
119     public SmoSearchResponse search(SearchRequest searchRequest) throws IOException {
120         String searchUri = toURI(searchRequest);
121         String payload = fetch(searchUri, commonHeaders);
122         JsonObject raw = JsonParser.parseString(payload).getAsJsonObject();
123         List<Record> page = new ArrayList<>(searchRequest.getPaging().getPageSize());
124         int totalHits = populateFromRaw(raw, page);
125         return new SmoSearchResponseImpl(searchRequest, totalHits, page, searchUri, payload);
126     }
127 
128     private String toURI(SearchRequest searchRequest) {
129         Paging paging = searchRequest.getPaging();
130         HashSet<Field> searchedFields = new HashSet<>();
131         String smoQuery = toSMOQuery(searchedFields, searchRequest.getQuery());
132         smoQuery += "&start=" + paging.getPageSize() * paging.getPageOffset();
133         smoQuery += "&rows=" + paging.getPageSize();
134         smoQuery += "&wt=json";
135         if (searchedFields.contains(MAVEN.GROUP_ID) && searchedFields.contains(MAVEN.ARTIFACT_ID)) {
136             smoQuery += "&core=gav";
137         }
138         return smoUri + "?q=" + smoQuery;
139     }
140 
141     private String fetch(String serviceUri, Map<String, String> headers) throws IOException {
142         try (Transport.Response response = transport.get(serviceUri, headers)) {
143             if (response.getCode() == HttpURLConnection.HTTP_OK) {
144                 return new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
145             } else {
146                 throw new IOException("Unexpected response: " + response);
147             }
148         }
149     }
150 
151     private String toSMOQuery(HashSet<Field> searchedFields, Query query) {
152         if (query instanceof BooleanQuery.And) {
153             BooleanQuery bq = (BooleanQuery) query;
154             return toSMOQuery(searchedFields, bq.getLeft()) + "%20AND%20" + toSMOQuery(searchedFields, bq.getRight());
155         } else if (query instanceof FieldQuery) {
156             FieldQuery fq = (FieldQuery) query;
157             String smoFieldName = FIELD_TRANSLATION.get(fq.getField());
158             if (smoFieldName != null) {
159                 searchedFields.add(fq.getField());
160                 return smoFieldName + ":" + encodeQueryParameterValue(fq.getValue());
161             } else {
162                 throw new IllegalArgumentException("Unsupported SMO field: " + fq.getField());
163             }
164         }
165         return encodeQueryParameterValue(query.getValue());
166     }
167 
168     private String encodeQueryParameterValue(String parameterValue) {
169         return URLEncoder.encode(parameterValue, StandardCharsets.UTF_8).replace("+", "%20");
170     }
171 
172     private int populateFromRaw(JsonObject raw, List<Record> page) {
173         JsonObject response = raw.getAsJsonObject("response");
174         Number numFound = response.get("numFound").getAsNumber();
175 
176         JsonArray docs = response.getAsJsonArray("docs");
177         for (JsonElement doc : docs) {
178             page.add(convert((JsonObject) doc));
179         }
180         return numFound.intValue();
181     }
182 
183     private Record convert(JsonObject doc) {
184         HashMap<Field, Object> result = new HashMap<>();
185 
186         mayPut(result, MAVEN.GROUP_ID, mayGet("g", doc));
187         mayPut(result, MAVEN.ARTIFACT_ID, mayGet("a", doc));
188         String version = mayGet("v", doc);
189         if (version == null) {
190             version = mayGet("latestVersion", doc);
191         }
192         mayPut(result, MAVEN.VERSION, version);
193         mayPut(result, MAVEN.PACKAGING, mayGet("p", doc));
194         mayPut(result, MAVEN.CLASSIFIER, mayGet("l", doc));
195 
196         // version count
197         Number versionCount = doc.has("versionCount") ? doc.get("versionCount").getAsNumber() : null;
198         if (versionCount != null) {
199             mayPut(result, MAVEN.VERSION_COUNT, versionCount.intValue());
200         }
201         // ec
202         JsonArray ec = doc.getAsJsonArray("ec");
203         if (ec != null) {
204             result.put(MAVEN.HAS_SOURCE, ec.contains(EC_SOURCE_JAR));
205             result.put(MAVEN.HAS_JAVADOC, ec.contains(EC_JAVADOC_JAR));
206             // result.put( MAVEN.HAS_GPG_SIGNATURE, ec.contains( ".jar.asc" ) );
207         }
208 
209         return new Record(
210                 getBackendId(),
211                 getRepositoryId(),
212                 doc.has("id") ? doc.get("id").getAsString() : null,
213                 doc.has("timestamp") ? doc.get("timestamp").getAsLong() : null,
214                 result);
215     }
216 
217     private static final JsonPrimitive EC_SOURCE_JAR = new JsonPrimitive("-sources.jar");
218 
219     private static final JsonPrimitive EC_JAVADOC_JAR = new JsonPrimitive("-javadoc.jar");
220 
221     private static String mayGet(String field, JsonObject object) {
222         return object.has(field) ? object.get(field).getAsString() : null;
223     }
224 
225     private static void mayPut(Map<Field, Object> result, Field fieldName, Object value) {
226         if (value == null) {
227             return;
228         }
229         if (value instanceof String && ((String) value).trim().isEmpty()) {
230             return;
231         }
232         result.put(fieldName, value);
233     }
234 }