View Javadoc
1   /*
2    * Copyright (c) 2002-2024, City of Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.plugins.identitystore.service.indexer.elastic.client;
35  
36  import com.fasterxml.jackson.databind.DeserializationFeature;
37  import com.fasterxml.jackson.databind.ObjectMapper;
38  import fr.paris.lutece.plugins.identitystore.service.indexer.elastic.search.model.inner.response.Response;
39  import fr.paris.lutece.plugins.identitystore.service.indexer.elastic.search.model.inner.response.Responses;
40  import fr.paris.lutece.portal.service.util.AppPropertiesService;
41  import org.apache.commons.lang3.StringUtils;
42  import org.apache.hc.client5.http.ClientProtocolException;
43  import org.apache.hc.client5.http.classic.methods.HttpDelete;
44  import org.apache.hc.client5.http.classic.methods.HttpGet;
45  import org.apache.hc.client5.http.classic.methods.HttpHead;
46  import org.apache.hc.client5.http.classic.methods.HttpPost;
47  import org.apache.hc.client5.http.classic.methods.HttpPut;
48  import org.apache.hc.client5.http.config.RequestConfig;
49  import org.apache.hc.client5.http.impl.classic.AbstractHttpClientResponseHandler;
50  import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
51  import org.apache.hc.client5.http.impl.classic.HttpClients;
52  import org.apache.hc.core5.http.ContentType;
53  import org.apache.hc.core5.http.Header;
54  import org.apache.hc.core5.http.HttpEntity;
55  import org.apache.hc.core5.http.HttpHeaders;
56  import org.apache.hc.core5.http.HttpResponse;
57  import org.apache.hc.core5.http.ParseException;
58  import org.apache.hc.core5.http.io.entity.EntityUtils;
59  import org.apache.hc.core5.http.io.entity.StringEntity;
60  import org.apache.hc.core5.http.message.BasicHeader;
61  import org.apache.hc.core5.util.Timeout;
62  
63  import java.io.IOException;
64  import java.nio.charset.StandardCharsets;
65  import java.util.ArrayList;
66  import java.util.Base64;
67  import java.util.List;
68  import java.util.concurrent.atomic.AtomicBoolean;
69  
70  /**
71   * The Class ElasticConnexion.
72   */
73  public final class ElasticConnexion
74  {
75      private static final long RESPONSE_TIMEOUT = AppPropertiesService.getPropertyInt( "identitystore.elastic.client.response.timeout", 30 );
76      private static final long CONNECT_TIMEOUT = AppPropertiesService.getPropertyInt( "identitystore.elastic.client.connect.timeout", 30 );
77      private static final ObjectMapper _mapper = new ObjectMapper( ).disable( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES );
78      private static final int ERROR_CODE_START = 300;
79      private final AbstractHttpClientResponseHandler<String> _simpleResponseHandler = buildSimpleResponseHandler( );
80      private final AbstractHttpClientResponseHandler<Response> _searchResponseHandler = buildSearchResponseHandler( );
81      private final AbstractHttpClientResponseHandler<Responses> _mSearchResponseHandler = buildSearchesResponseHandler( );
82      private String _userLogin;
83      private String _userPassword;
84  
85      /**
86       * Basic Authentification constructor
87       *
88       * @param userLogin
89       *            Login
90       * @param userPassword
91       *            Password
92       */
93      public ElasticConnexion( final String userLogin, final String userPassword )
94      {
95          _userLogin = userLogin;
96          _userPassword = userPassword;
97      }
98  
99      /**
100      * Constructor
101      */
102     public ElasticConnexion( )
103     {
104     }
105 
106     /**
107      * Send a GET request to Elastic Search server
108      *
109      * @param strURI
110      *            The URI
111      * @return The response
112      */
113     public String GET( final String strURI ) throws ElasticConnexionException
114     {
115         try ( final CloseableHttpClient _httpClient = this.buildHttpClient( ) )
116         {
117             return _httpClient.execute( new HttpGet( strURI ), _simpleResponseHandler );
118         }
119         catch( final IOException e )
120         {
121             throw new ElasticConnexionException( "An error occurred during GET call to Elastic Search: ", e );
122         }
123     }
124 
125     /**
126      * Send a HEAD request to Elasticsearch server
127      *
128      * @param strURI
129      *            The URI
130      * @return The response
131      */
132     public String HEAD( final String strURI ) throws ElasticConnexionException
133     {
134         try ( final CloseableHttpClient _httpClient = this.buildHttpClient( ) )
135         {
136             return _httpClient.execute( new HttpHead( strURI ), _simpleResponseHandler );
137         }
138         catch( final IOException e )
139         {
140             throw new ElasticConnexionException( "An error occurred during HEAD call to Elastic Search: ", e );
141         }
142     }
143 
144     /**
145      * Send a PUT request to Elastic Search server
146      *
147      * @param strURI
148      *            the uri
149      * @param strJSON
150      *            the json
151      * @return the string
152      */
153     public void PUT( final String strURI, final String strJSON ) throws ElasticConnexionException
154     {
155         try ( final CloseableHttpClient _httpClient = this.buildHttpClient( ) )
156         {
157             final HttpPut request = new HttpPut( strURI );
158             request.setEntity( new StringEntity( strJSON, ContentType.APPLICATION_JSON, null, false ) );
159             final Integer code = _httpClient.execute( request, HttpResponse::getCode );
160             if ( code >= ERROR_CODE_START )
161             {
162                 throw new ElasticConnexionException( "An error occurred during PUT call to Elastic Search with status code: " + code );
163             }
164         }
165         catch( final IOException e )
166         {
167             throw new ElasticConnexionException( "An error occurred during PUT call to Elastic Search: ", e );
168         }
169     }
170 
171     /**
172      * Send a POST request to Elastic Search server
173      *
174      * @param strURI
175      *            the uri
176      * @param strJSON
177      *            the json
178      * @return the string
179      */
180     public void POST( final String strURI, final String strJSON ) throws ElasticConnexionException
181     {
182         try ( final CloseableHttpClient _httpClient = this.buildHttpClient( ) )
183         {
184             final HttpPost request = new HttpPost( strURI );
185             request.setEntity( new StringEntity( strJSON, ContentType.APPLICATION_JSON, null, false ) );
186             final Integer code = _httpClient.execute( request, HttpResponse::getCode );
187             if ( code >= ERROR_CODE_START )
188             {
189                 throw new ElasticConnexionException( "An error occurred during POST call to Elastic Search with status code: " + code );
190             }
191         }
192         catch( final IOException e )
193         {
194             throw new ElasticConnexionException( "An error occurred during POST call to Elastic Search: ", e );
195         }
196     }
197 
198     /**
199      * Send a POST request to Elastic Search server
200      *
201      * @param strURI
202      *            the uri
203      * @param strJSON
204      *            the json
205      * @return the string
206      */
207     public Response SEARCH( final String strURI, final String strJSON ) throws ElasticConnexionException
208     {
209         try ( final CloseableHttpClient _httpClient = this.buildHttpClient( ) )
210         {
211             final HttpGet request = new HttpGet( strURI );
212             request.setEntity( new StringEntity( strJSON, ContentType.APPLICATION_JSON, null, false ) );
213             final Response execute = _httpClient.execute( request, _searchResponseHandler );
214             if ( execute.getStatus( ) != null && execute.getStatus( ) >= ERROR_CODE_START )
215             {
216                 throw new ElasticConnexionException( "An error occurred during SEARCH call to Elastic Search with status code: " + execute.getStatus( ) );
217             }
218             return execute;
219         }
220         catch( final IOException e )
221         {
222             throw new ElasticConnexionException( "An error occurred during SEARCH call to Elastic Search: ", e );
223         }
224     }
225 
226     /**
227      * Send a POST request to Elastic Search server
228      *
229      * @param strURI
230      *            the uri
231      * @param strJSON
232      *            the json
233      * @return the string
234      */
235     public Responses MSEARCH( final String strURI, final String strJSON ) throws ElasticConnexionException
236     {
237         try ( final CloseableHttpClient _httpClient = this.buildHttpClient( ) )
238         {
239             final HttpGet request = new HttpGet( strURI );
240             request.setEntity( new StringEntity( strJSON, ContentType.APPLICATION_JSON, null, false ) );
241             final Responses execute = _httpClient.execute( request, _mSearchResponseHandler );
242             final AtomicBoolean failOccurred = new AtomicBoolean( false );
243             execute.getResponses( ).removeIf( response -> {
244                 final boolean failed = response.getStatus( ) >= ERROR_CODE_START;
245                 if ( failed )
246                 {
247                     failOccurred.set( true );
248                 }
249                 return failed;
250             } );
251             if ( failOccurred.get( ) )
252             {
253                 throw new ElasticConnexionException( "An error occurred during MSEARCH call to Elastic Search. Could not get responses" );
254             }
255             return execute;
256         }
257         catch( final IOException e )
258         {
259             throw new ElasticConnexionException( "An error occurred during MSEARCH call to Elastic Search: ", e );
260         }
261     }
262 
263     /**
264      * Send a DELETE request to Elastic Search server
265      *
266      * @param strURI
267      *            the uri
268      * @return the string
269      */
270     public void DELETE( final String strURI ) throws ElasticConnexionException
271     {
272         try ( final CloseableHttpClient _httpClient = this.buildHttpClient( ) )
273         {
274             final HttpDelete request = new HttpDelete( strURI );
275             final Integer code = _httpClient.execute( request, HttpResponse::getCode );
276             if ( code >= ERROR_CODE_START )
277             {
278                 throw new ElasticConnexionException( "An error occurred during DELETE call to Elastic Search with status code: " + code );
279             }
280         }
281         catch( final IOException e )
282         {
283             throw new ElasticConnexionException( "An error occurred during DELETE call to Elastic Search: ", e );
284         }
285     }
286 
287     private static AbstractHttpClientResponseHandler<String> buildSimpleResponseHandler( )
288     {
289         return new AbstractHttpClientResponseHandler<String>( )
290         {
291             @Override
292             public String handleEntity( HttpEntity httpEntity ) throws IOException
293             {
294 
295                 try
296                 {
297                     final String response = EntityUtils.toString( httpEntity );
298                     EntityUtils.consume( httpEntity );
299                     return response;
300                 }
301                 catch( ParseException var3 )
302                 {
303                     throw new ClientProtocolException( var3 );
304                 }
305             }
306         };
307     }
308 
309     private static AbstractHttpClientResponseHandler<Response> buildSearchResponseHandler( )
310     {
311         return new AbstractHttpClientResponseHandler<Response>( )
312         {
313             @Override
314             public Response handleEntity( HttpEntity httpEntity ) throws IOException
315             {
316                 final Response response = _mapper.readValue( httpEntity.getContent( ), Response.class );
317                 EntityUtils.consume( httpEntity );
318                 return response;
319             }
320         };
321     }
322 
323     private static AbstractHttpClientResponseHandler<Responses> buildSearchesResponseHandler( )
324     {
325         return new AbstractHttpClientResponseHandler<Responses>( )
326         {
327             @Override
328             public Responses handleEntity( HttpEntity httpEntity ) throws IOException
329             {
330                 final Responses response = _mapper.readValue( httpEntity.getContent( ), Responses.class );
331                 EntityUtils.consume( httpEntity );
332                 return response;
333             }
334         };
335     }
336 
337     private CloseableHttpClient buildHttpClient( )
338     {
339         final RequestConfig requestConfig = RequestConfig.custom( ).setResponseTimeout( Timeout.ofSeconds( RESPONSE_TIMEOUT ) )
340                 .setConnectTimeout( Timeout.ofSeconds( CONNECT_TIMEOUT ) ).setConnectionRequestTimeout( Timeout.ofSeconds( CONNECT_TIMEOUT ) ).build( );
341         return HttpClients.custom( ).setDefaultHeaders( this.buildDefaultHeaders( ) ).setDefaultRequestConfig( requestConfig ).build( );
342     }
343 
344     private List<Header> buildDefaultHeaders( )
345     {
346         final List<Header> defaultHeaders = new ArrayList<>( );
347         if ( StringUtils.isNoneEmpty( _userLogin, _userPassword ) )
348         {
349             final String auth = _userLogin + ":" + _userPassword;
350             final byte [ ] encodedAuth = Base64.getEncoder( ).encode( auth.getBytes( StandardCharsets.ISO_8859_1 ) );
351             final String authHeader = "Basic " + new String( encodedAuth );
352             defaultHeaders.add( new BasicHeader( HttpHeaders.AUTHORIZATION, authHeader ) );
353         }
354         return defaultHeaders;
355     }
356 }