1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
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
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
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
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
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 }