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.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       * Creates a customized instance of SMO backend, like an in-house instances of SMO or different IDs.
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                 // fall through
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         // version count
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         // ec
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             // result.put( MAVEN.HAS_GPG_SIGNATURE, ec.contains( ".jar.asc" ) );
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 }