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.Query;
45  import org.apache.maven.search.api.support.SearchBackendSupport;
46  import org.apache.maven.search.api.transport.Transport;
47  import org.apache.maven.search.backend.smo.SmoSearchBackend;
48  import org.apache.maven.search.backend.smo.SmoSearchBackendFactory;
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      protected 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      protected final String smoUri;
77  
78      protected final Transport transport;
79  
80      protected 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      protected 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     protected String toURI(SearchRequest searchRequest) {
129         HashSet<Field> searchedFields = new HashSet<>();
130         String smoQuery = toSMOQuery(searchedFields, searchRequest.getQuery());
131         smoQuery += paging(searchRequest, searchedFields);
132         smoQuery += extra(searchRequest, searchedFields);
133         return smoUri + "?q=" + smoQuery;
134     }
135 
136     protected String paging(SearchRequest searchRequest, HashSet<Field> searchedFields) {
137         if (SmoSearchBackendFactory.CSC_BACKEND_ID.equals(backendId)) {
138             return cscPaging(searchRequest, searchedFields);
139         } else {
140             return smoPaging(searchRequest, searchedFields);
141         }
142     }
143 
144     protected String smoPaging(SearchRequest searchRequest, HashSet<Field> searchedFields) {
145         return "&start="
146                 + searchRequest.getPaging().getPageSize()
147                         * searchRequest.getPaging().getPageOffset() + "&rows="
148                 + searchRequest.getPaging().getPageSize();
149     }
150 
151     protected String cscPaging(SearchRequest searchRequest, HashSet<Field> searchedFields) {
152         // this is a bug in CSC: it should work same as SMO but this is life
153         return "&start=" + searchRequest.getPaging().getPageOffset() + "&rows="
154                 + searchRequest.getPaging().getPageSize();
155     }
156 
157     protected String extra(SearchRequest searchRequest, HashSet<Field> searchedFields) {
158         String extra = "&wt=json";
159         if (searchedFields.contains(MAVEN.GROUP_ID) && searchedFields.contains(MAVEN.ARTIFACT_ID)) {
160             extra += "&core=gav";
161         }
162         return extra;
163     }
164 
165     protected String fetch(String serviceUri, Map<String, String> headers) throws IOException {
166         try (Transport.Response response = transport.get(serviceUri, headers)) {
167             if (response.getCode() == HttpURLConnection.HTTP_OK) {
168                 return new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
169             } else {
170                 throw new IOException("Unexpected response: " + response);
171             }
172         }
173     }
174 
175     protected String toSMOQuery(HashSet<Field> searchedFields, Query query) {
176         if (query instanceof BooleanQuery.And) {
177             BooleanQuery bq = (BooleanQuery) query;
178             return toSMOQuery(searchedFields, bq.getLeft()) + "%20AND%20" + toSMOQuery(searchedFields, bq.getRight());
179         } else if (query instanceof FieldQuery) {
180             FieldQuery fq = (FieldQuery) query;
181             String smoFieldName = FIELD_TRANSLATION.get(fq.getField());
182             if (smoFieldName != null) {
183                 searchedFields.add(fq.getField());
184                 return smoFieldName + ":" + encodeQueryParameterValue(fq.getValue());
185             } else {
186                 throw new IllegalArgumentException("Unsupported SMO field: " + fq.getField());
187             }
188         }
189         return encodeQueryParameterValue(query.getValue());
190     }
191 
192     protected String encodeQueryParameterValue(String parameterValue) {
193         return URLEncoder.encode(parameterValue, StandardCharsets.UTF_8).replace("+", "%20");
194     }
195 
196     protected int populateFromRaw(JsonObject raw, List<Record> page) {
197         JsonObject response = raw.getAsJsonObject("response");
198         Number numFound = response.get("numFound").getAsNumber();
199 
200         JsonArray docs = response.getAsJsonArray("docs");
201         for (JsonElement doc : docs) {
202             page.add(convert((JsonObject) doc));
203         }
204         return numFound.intValue();
205     }
206 
207     protected Record convert(JsonObject doc) {
208         HashMap<Field, Object> result = new HashMap<>();
209 
210         mayPut(result, MAVEN.GROUP_ID, mayGet("g", doc));
211         mayPut(result, MAVEN.ARTIFACT_ID, mayGet("a", doc));
212         String version = mayGet("v", doc);
213         if (version == null) {
214             version = mayGet("latestVersion", doc);
215         }
216         mayPut(result, MAVEN.VERSION, version);
217         mayPut(result, MAVEN.PACKAGING, mayGet("p", doc));
218         mayPut(result, MAVEN.CLASSIFIER, mayGet("l", doc));
219 
220         // version count
221         Number versionCount = doc.has("versionCount") ? doc.get("versionCount").getAsNumber() : null;
222         if (versionCount != null) {
223             mayPut(result, MAVEN.VERSION_COUNT, versionCount.intValue());
224         }
225         // ec
226         JsonArray ec = doc.getAsJsonArray("ec");
227         if (ec != null) {
228             result.put(MAVEN.HAS_SOURCE, ec.contains(EC_SOURCE_JAR));
229             result.put(MAVEN.HAS_JAVADOC, ec.contains(EC_JAVADOC_JAR));
230             // result.put( MAVEN.HAS_GPG_SIGNATURE, ec.contains( ".jar.asc" ) );
231         }
232 
233         return new Record(
234                 getBackendId(),
235                 getRepositoryId(),
236                 doc.has("id") ? doc.get("id").getAsString() : null,
237                 doc.has("timestamp") ? doc.get("timestamp").getAsLong() : null,
238                 result);
239     }
240 
241     protected static final JsonPrimitive EC_SOURCE_JAR = new JsonPrimitive("-sources.jar");
242 
243     protected static final JsonPrimitive EC_JAVADOC_JAR = new JsonPrimitive("-javadoc.jar");
244 
245     protected static String mayGet(String field, JsonObject object) {
246         return object.has(field) ? object.get(field).getAsString() : null;
247     }
248 
249     protected static void mayPut(Map<Field, Object> result, Field fieldName, Object value) {
250         if (value == null) {
251             return;
252         }
253         if (value instanceof String && ((String) value).trim().isEmpty()) {
254             return;
255         }
256         result.put(fieldName, value);
257     }
258 }