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