View Javadoc
1   package org.apache.maven.search.backend.smo.internal;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.IOException;
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  
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.MAVEN;
39  import org.apache.maven.search.Record;
40  import org.apache.maven.search.SearchRequest;
41  import org.apache.maven.search.backend.smo.SmoSearchBackend;
42  import org.apache.maven.search.backend.smo.SmoSearchResponse;
43  import org.apache.maven.search.request.BooleanQuery;
44  import org.apache.maven.search.request.Field;
45  import org.apache.maven.search.request.FieldQuery;
46  import org.apache.maven.search.request.Paging;
47  import org.apache.maven.search.request.Query;
48  import org.apache.maven.search.support.SearchBackendSupport;
49  
50  import static java.util.Objects.requireNonNull;
51  
52  public class SmoSearchBackendImpl extends SearchBackendSupport implements SmoSearchBackend
53  {
54      public static final String DEFAULT_BACKEND_ID = "central-smo";
55  
56      public static final String DEFAULT_REPOSITORY_ID = "central";
57  
58      public static final String DEFAULT_SMO_URI = "https://search.maven.org/solrsearch/select";
59  
60      private static final Map<Field, String> FIELD_TRANSLATION;
61  
62      static
63      {
64          HashMap<Field, String> map = new HashMap<>();
65          map.put( MAVEN.GROUP_ID, "g" );
66          map.put( MAVEN.ARTIFACT_ID, "a" );
67          map.put( MAVEN.VERSION, "v" );
68          map.put( MAVEN.CLASSIFIER, "l" );
69          map.put( MAVEN.PACKAGING, "p" );
70          map.put( MAVEN.CLASS_NAME, "c" );
71          map.put( MAVEN.FQ_CLASS_NAME, "fc" );
72          map.put( MAVEN.SHA1, "1" );
73          FIELD_TRANSLATION = Collections.unmodifiableMap( map );
74      }
75  
76      private final String smoUri;
77  
78      private final SmoSearchTransportSupport transportSupport;
79  
80      /**
81       * Creates a "default" instance of SMO backend against {@link #DEFAULT_SMO_URI}.
82       */
83      public SmoSearchBackendImpl()
84      {
85          this( DEFAULT_BACKEND_ID, DEFAULT_REPOSITORY_ID, DEFAULT_SMO_URI, new UrlConnectionSmoSearchTransport() );
86      }
87  
88      /**
89       * Creates a customized instance of SMO backend, like an in-house instances of SMO or different IDs.
90       */
91      public SmoSearchBackendImpl( String backendId, String repositoryId, String smoUri,
92                                   SmoSearchTransportSupport transportSupport )
93      {
94          super( backendId, repositoryId );
95          this.smoUri = requireNonNull( smoUri );
96          this.transportSupport = requireNonNull( transportSupport );
97      }
98  
99      @Override
100     public String getSmoUri()
101     {
102         return smoUri;
103     }
104 
105     @Override
106     public SmoSearchResponse search( SearchRequest searchRequest ) throws IOException
107     {
108         String searchUri = toURI( searchRequest );
109         String payload = transportSupport.fetch( searchRequest, searchUri );
110         JsonObject raw = JsonParser.parseString( payload ).getAsJsonObject();
111         List<Record> page = new ArrayList<>( searchRequest.getPaging().getPageSize() );
112         int totalHits = populateFromRaw( raw, page );
113         return new SmoSearchResponseImpl( searchRequest, totalHits, page, searchUri, payload );
114     }
115 
116     private String toURI( SearchRequest searchRequest ) throws UnsupportedEncodingException
117     {
118         Paging paging = searchRequest.getPaging();
119         HashSet<Field> searchedFields = new HashSet<>();
120         String smoQuery = toSMOQuery( searchedFields, searchRequest.getQuery() );
121         smoQuery += "&start=" + paging.getPageSize() * paging.getPageOffset();
122         smoQuery += "&rows=" + paging.getPageSize();
123         smoQuery += "&wt=json";
124         if ( searchedFields.contains( MAVEN.GROUP_ID ) && searchedFields.contains( MAVEN.ARTIFACT_ID ) )
125         {
126             smoQuery += "&core=gav";
127         }
128         return smoUri + "?q=" + smoQuery;
129     }
130 
131     private String toSMOQuery( HashSet<Field> searchedFields, Query query ) throws UnsupportedEncodingException
132     {
133         if ( query instanceof BooleanQuery.And )
134         {
135             BooleanQuery bq = (BooleanQuery) query;
136             return toSMOQuery( searchedFields, bq.getLeft() ) + "%20AND%20"
137                     + toSMOQuery( searchedFields, bq.getRight() );
138         }
139         else if ( query instanceof FieldQuery )
140         {
141             FieldQuery fq = (FieldQuery) query;
142             String smoFieldName = FIELD_TRANSLATION.get( fq.getField() );
143             if ( smoFieldName != null )
144             {
145                 searchedFields.add( fq.getField() );
146                 return smoFieldName + ":" + encodeQueryParameterValue( fq.getValue() );
147             }
148             else
149             {
150                 throw new IllegalArgumentException( "Unsupported SMO field: " + fq.getField() );
151             }
152         }
153         return encodeQueryParameterValue( query.getValue() );
154     }
155 
156     private String encodeQueryParameterValue( String parameterValue ) throws UnsupportedEncodingException
157     {
158         return URLEncoder.encode( parameterValue, StandardCharsets.UTF_8.name() )
159                 .replace( "+", "%20" );
160     }
161 
162     private int populateFromRaw( JsonObject raw, List<Record> page )
163     {
164         JsonObject response = raw.getAsJsonObject( "response" );
165         Number numFound = response.get( "numFound" ).getAsNumber();
166 
167         JsonArray docs = response.getAsJsonArray( "docs" );
168         for ( JsonElement doc : docs )
169         {
170             page.add( convert( (JsonObject) doc ) );
171         }
172         return numFound.intValue();
173     }
174 
175     private Record convert( JsonObject doc )
176     {
177         HashMap<Field, Object> result = new HashMap<>();
178 
179         mayPut( result, MAVEN.GROUP_ID, mayGet( "g", doc ) );
180         mayPut( result, MAVEN.ARTIFACT_ID, mayGet( "a", doc ) );
181         String version = mayGet( "v", doc );
182         if ( version == null )
183         {
184             version = mayGet( "latestVersion", doc );
185         }
186         mayPut( result, MAVEN.VERSION, version );
187         mayPut( result, MAVEN.PACKAGING, mayGet( "p", doc ) );
188         mayPut( result, MAVEN.CLASSIFIER, mayGet( "l", doc ) );
189 
190         // version count
191         Number versionCount = doc.has( "versionCount" ) ? doc.get( "versionCount" ).getAsNumber() : null;
192         if ( versionCount != null )
193         {
194             mayPut( result, MAVEN.VERSION_COUNT, versionCount.intValue() );
195         }
196         // ec
197         JsonArray ec = doc.getAsJsonArray( "ec" );
198         if ( ec != null )
199         {
200             result.put( MAVEN.HAS_SOURCE, ec.contains( EC_SOURCE_JAR ) );
201             result.put( MAVEN.HAS_JAVADOC, ec.contains( EC_JAVADOC_JAR ) );
202             // result.put( MAVEN.HAS_GPG_SIGNATURE, ec.contains( ".jar.asc" ) );
203         }
204 
205         return new Record(
206                 getBackendId(),
207                 getRepositoryId(),
208                 doc.has( "id" ) ? doc.get( "id" ).getAsString() : null,
209                 doc.has( "timestamp" ) ? doc.get( "timestamp" ).getAsLong() : null,
210                 result
211         );
212     }
213 
214     private static final JsonPrimitive EC_SOURCE_JAR = new JsonPrimitive( "-sources.jar" );
215 
216     private static final JsonPrimitive EC_JAVADOC_JAR = new JsonPrimitive( "-javadoc.jar" );
217 
218     private static String mayGet( String field, JsonObject object )
219     {
220         return object.has( field ) ? object.get( field ).getAsString() : null;
221     }
222 
223     private static void mayPut( Map<Field, Object> result, Field fieldName, Object value )
224     {
225         if ( value == null )
226         {
227             return;
228         }
229         if ( value instanceof String && ( (String) value ).trim().isEmpty() )
230         {
231             return;
232         }
233         result.put( fieldName, value );
234     }
235 }