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.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
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
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
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
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
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 }