1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package fr.paris.lutece.plugins.appointment.modules.management.service.indexer;
35
36 import java.io.IOException;
37 import java.sql.Timestamp;
38 import java.util.ArrayList;
39 import java.util.Collection;
40 import java.util.Collections;
41 import java.util.HashSet;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Set;
45 import java.util.concurrent.atomic.AtomicBoolean;
46 import java.util.function.Function;
47 import java.util.stream.Collectors;
48
49 import javax.inject.Inject;
50
51 import org.apache.commons.collections.CollectionUtils;
52 import org.apache.lucene.document.Document;
53 import org.apache.lucene.document.Field;
54 import org.apache.lucene.document.FieldType;
55 import org.apache.lucene.document.IntPoint;
56 import org.apache.lucene.document.LongPoint;
57 import org.apache.lucene.document.NumericDocValuesField;
58 import org.apache.lucene.document.SortedDocValuesField;
59 import org.apache.lucene.document.StoredField;
60 import org.apache.lucene.document.StringField;
61 import org.apache.lucene.index.IndexWriter;
62 import org.apache.lucene.search.Query;
63 import org.apache.lucene.util.BytesRef;
64 import org.springframework.beans.factory.annotation.Autowired;
65
66 import fr.paris.lutece.plugins.appointment.business.appointment.Appointment;
67 import fr.paris.lutece.plugins.appointment.business.appointment.AppointmentHome;
68 import fr.paris.lutece.plugins.appointment.business.form.Form;
69 import fr.paris.lutece.plugins.appointment.business.form.FormHome;
70 import fr.paris.lutece.plugins.appointment.business.user.User;
71 import fr.paris.lutece.plugins.appointment.business.user.UserHome;
72 import fr.paris.lutece.plugins.appointment.modules.management.business.search.AppointmentSearchItem;
73 import fr.paris.lutece.plugins.appointment.service.AppointmentService;
74 import fr.paris.lutece.plugins.appointment.web.dto.AppointmentDTO;
75 import fr.paris.lutece.plugins.appointment.web.dto.AppointmentFilterDTO;
76 import fr.paris.lutece.plugins.workflowcore.business.state.State;
77 import fr.paris.lutece.plugins.workflowcore.service.state.StateService;
78 import fr.paris.lutece.portal.business.indexeraction.IndexerAction;
79 import fr.paris.lutece.portal.business.indexeraction.IndexerActionFilter;
80 import fr.paris.lutece.portal.business.indexeraction.IndexerActionHome;
81 import fr.paris.lutece.portal.service.message.SiteMessageException;
82 import fr.paris.lutece.portal.service.search.IndexationService;
83 import fr.paris.lutece.portal.service.search.SearchItem;
84 import fr.paris.lutece.portal.service.util.AppLogService;
85 import fr.paris.lutece.portal.service.util.AppPropertiesService;
86
87
88
89
90 public class LuteceAppointmentSearchIndexer implements IAppointmentSearchIndexer
91 {
92
93 private static final String APPOINTMENTS = "appointments";
94 private static final String SUFIX_UID_APPOINTMENTS = "_appointment";
95 private static final String INDEXER_NAME = "AppointmentIndexer";
96 private static final String INDEXER_DESCRIPTION = "Indexer service for appointment";
97 private static final String INDEXER_VERSION = "1.0.0";
98 private static final String PROPERTY_INDEXER_ENABLE = "appointment-management.globalIndexer.enable";
99 private static final int TAILLE_LOT = AppPropertiesService.getPropertyInt( "appointment-management.index.writer.commit.size", 100 );
100
101 @Inject
102 private LuceneAppointmentIndexFactory _luceneAppointmentIndexFactory;
103 private IndexWriter _indexWriter;
104
105 @Autowired( required = false )
106 private StateService _stateService;
107
108 private static AtomicBoolean _bIndexIsRunning = new AtomicBoolean( false );
109 private static AtomicBoolean _bIndexToLunch = new AtomicBoolean( false );
110
111 private static final Object LOCK = new Object( );
112
113 public LuteceAppointmentSearchIndexer( )
114 {
115 IndexationService.registerIndexer( this );
116 }
117
118 @Override
119 public String getName( )
120 {
121 return INDEXER_NAME;
122 }
123
124 @Override
125 public String getDescription( )
126 {
127 return INDEXER_DESCRIPTION;
128 }
129
130 @Override
131 public String getVersion( )
132 {
133 return INDEXER_VERSION;
134 }
135
136 @Override
137 public boolean isEnable( )
138 {
139 return AppPropertiesService.getPropertyBoolean( PROPERTY_INDEXER_ENABLE, false );
140 }
141
142 @Override
143 public List<String> getListType( )
144 {
145 return Collections.singletonList( APPOINTMENTS );
146 }
147
148 @Override
149 public String getSpecificSearchAppUrl( )
150 {
151 return "";
152 }
153
154 @Override
155 public void indexDocuments( ) throws IOException, InterruptedException, SiteMessageException
156 {
157 List<Integer> listAppointmentId = AppointmentHome.selectAllAppointmentId( );
158
159 deleteIndex( );
160 _bIndexToLunch.set( true );
161
162 if ( _bIndexIsRunning.compareAndSet( false, true ) )
163 {
164 new Thread( new IndexerRunnable( listAppointmentId ) ).start( );
165 }
166 }
167
168 @Override
169 public void indexDocument( int nIdAppointment, int idTask )
170 {
171 IndexerAction action = new IndexerAction( );
172 action.setIdDocument( String.valueOf( nIdAppointment ) );
173 action.setIdTask( idTask );
174 action.setIndexerName( INDEXER_NAME );
175 IndexerActionHome.create( action );
176
177 _bIndexToLunch.set( true );
178 if ( _bIndexIsRunning.compareAndSet( false, true ) )
179 {
180 new Thread( new IndexerRunnable( ) ).start( );
181 }
182 }
183
184 @Override
185 public List<Document> getDocuments( String strIdDocument ) throws IOException, InterruptedException, SiteMessageException
186 {
187 int nIdAppointment;
188
189 try
190 {
191 nIdAppointment = Integer.parseInt( strIdDocument );
192 }
193 catch( NumberFormatException ne )
194 {
195 AppLogService.error( strIdDocument + " not parseable to an int", ne );
196 return new ArrayList<>( 0 );
197 }
198
199 AppointmentDTO appointment = AppointmentService.buildAppointmentDTOFromIdAppointment( nIdAppointment );
200 Form form = FormHome.findByPrimaryKey( appointment.getIdForm( ) );
201
202 State appointmentState = null;
203 if ( _stateService != null )
204 {
205 appointmentState = _stateService.findByResource( appointment.getIdAppointment( ), Appointment.APPOINTMENT_RESOURCE_TYPE, form.getIdWorkflow( ) );
206 }
207
208 Document doc = getDocument( appointment, appointmentState, form.getIdCategory( ) );
209
210 List<Document> listDocument = new ArrayList<>( 1 );
211 listDocument.add( doc );
212 return listDocument;
213 }
214
215
216
217
218
219
220
221
222
223
224 private Document getDocument( AppointmentDTO appointmentDTO, State appointmentState, int idCategory )
225 {
226
227 FieldType ftNotStored = new FieldType( StringField.TYPE_NOT_STORED );
228 ftNotStored.setOmitNorms( false );
229 ftNotStored.setTokenized( false );
230
231 Document doc = new Document( );
232
233 int nIdAppointment = appointmentDTO.getIdAppointment( );
234
235
236 doc.add( new Field( SearchItem.FIELD_UID, nIdAppointment + SUFIX_UID_APPOINTMENTS , ftNotStored ) );
237
238
239 doc.add( new IntPoint( AppointmentSearchItem.FIELD_ID_APPOINTMENT, nIdAppointment ) );
240 doc.add( new NumericDocValuesField( AppointmentSearchItem.FIELD_ID_APPOINTMENT, nIdAppointment ) );
241 doc.add( new StoredField( AppointmentSearchItem.FIELD_ID_APPOINTMENT, nIdAppointment ) );
242
243
244 doc.add( new IntPoint( AppointmentSearchItem.FIELD_ID_FORM, appointmentDTO.getIdForm( ) ) );
245 doc.add( new NumericDocValuesField( AppointmentSearchItem.FIELD_ID_FORM, appointmentDTO.getIdForm( ) ) );
246 doc.add( new StoredField( AppointmentSearchItem.FIELD_ID_FORM, appointmentDTO.getIdForm( ) ) );
247
248
249 doc.add( new StringField( AppointmentSearchItem.FIELD_FIRST_NAME, appointmentDTO.getFirstName( ), Field.Store.YES ) );
250 doc.add( new SortedDocValuesField( AppointmentSearchItem.FIELD_FIRST_NAME, new BytesRef( appointmentDTO.getFirstName( ) ) ) );
251
252 doc.add( new StringField( AppointmentSearchItem.FIELD_FIRST_NAME_SEARCH, appointmentDTO.getFirstName( ).toLowerCase( ), Field.Store.YES ) );
253 doc.add( new SortedDocValuesField( AppointmentSearchItem.FIELD_FIRST_NAME_SEARCH, new BytesRef( appointmentDTO.getFirstName( ).toLowerCase( ) ) ) );
254
255
256 doc.add( new StringField( AppointmentSearchItem.FIELD_LAST_NAME, appointmentDTO.getLastName( ), Field.Store.YES ) );
257 doc.add( new SortedDocValuesField( AppointmentSearchItem.FIELD_LAST_NAME, new BytesRef( appointmentDTO.getLastName( ) ) ) );
258
259 doc.add( new StringField( AppointmentSearchItem.FIELD_LAST_NAME_SEARCH, appointmentDTO.getLastName( ).toLowerCase( ), Field.Store.YES ) );
260 doc.add( new SortedDocValuesField( AppointmentSearchItem.FIELD_LAST_NAME_SEARCH, new BytesRef( appointmentDTO.getLastName( ).toLowerCase( ) ) ) );
261
262
263 doc.add( new StringField( AppointmentSearchItem.FIELD_MAIL, appointmentDTO.getEmail( ), Field.Store.YES ) );
264 doc.add( new SortedDocValuesField( AppointmentSearchItem.FIELD_MAIL, new BytesRef( appointmentDTO.getEmail( ) ) ) );
265
266 doc.add( new StringField( AppointmentSearchItem.FIELD_MAIL_SEARCH, appointmentDTO.getEmail( ).toLowerCase( ), Field.Store.YES ) );
267 doc.add( new SortedDocValuesField( AppointmentSearchItem.FIELD_MAIL_SEARCH, new BytesRef( appointmentDTO.getEmail( ).toLowerCase( ) ) ) );
268
269
270
271 String phoneNumber = appointmentDTO.getPhoneNumber( ) != null ? appointmentDTO.getPhoneNumber( ) : "";
272 doc.add( new StringField( AppointmentSearchItem.FIELD_PHONE_NUMBER, phoneNumber, Field.Store.YES ) );
273 doc.add( new SortedDocValuesField( AppointmentSearchItem.FIELD_PHONE_NUMBER, new BytesRef( phoneNumber ) ) );
274
275
276 Long longStartDate = Timestamp.valueOf( appointmentDTO.getStartingDateTime( ) ).getTime( );
277 doc.add( new LongPoint( AppointmentSearchItem.FIELD_START_DATE, longStartDate ) );
278 doc.add( new NumericDocValuesField( AppointmentSearchItem.FIELD_START_DATE, longStartDate ) );
279 doc.add( new StoredField( AppointmentSearchItem.FIELD_START_DATE, longStartDate ) );
280
281
282 Long longEndDate = Timestamp.valueOf( appointmentDTO.getEndingDateTime( ) ).getTime( );
283 doc.add( new LongPoint( AppointmentSearchItem.FIELD_END_DATE, longEndDate ) );
284 doc.add( new NumericDocValuesField( AppointmentSearchItem.FIELD_END_DATE, longEndDate ) );
285 doc.add( new StoredField( AppointmentSearchItem.FIELD_END_DATE, longEndDate ) );
286
287
288 String admin = appointmentDTO.getAdminUser( );
289 doc.add( new StringField( AppointmentSearchItem.FIELD_ADMIN, admin, Field.Store.YES ) );
290 doc.add( new SortedDocValuesField( AppointmentSearchItem.FIELD_ADMIN, new BytesRef( admin ) ) );
291
292
293 String cancelled = String.valueOf( appointmentDTO.getIsCancelled( ) );
294 doc.add( new StringField( AppointmentSearchItem.FIELD_CANCELLED, cancelled, Field.Store.YES ) );
295 doc.add( new SortedDocValuesField( AppointmentSearchItem.FIELD_CANCELLED, new BytesRef( cancelled ) ) );
296
297
298 if ( appointmentState != null )
299 {
300
301 int nIdWorkflowState = appointmentState.getId( );
302 doc.add( new IntPoint( AppointmentSearchItem.FIELD_ID_WORKFLOW_STATE, nIdWorkflowState ) );
303 doc.add( new NumericDocValuesField( AppointmentSearchItem.FIELD_ID_WORKFLOW_STATE, nIdWorkflowState ) );
304 doc.add( new StoredField( AppointmentSearchItem.FIELD_ID_WORKFLOW_STATE, nIdWorkflowState ) );
305 }
306
307
308 doc.add( new IntPoint( AppointmentSearchItem.FIELD_NB_SEATS, appointmentDTO.getNbBookedSeats( ) ) );
309 doc.add( new NumericDocValuesField( AppointmentSearchItem.FIELD_NB_SEATS, appointmentDTO.getNbBookedSeats( ) ) );
310 doc.add( new StoredField( AppointmentSearchItem.FIELD_NB_SEATS, appointmentDTO.getNbBookedSeats( ) ) );
311
312
313 Long longAppointmentTaken = Timestamp.valueOf( appointmentDTO.getDateAppointmentTaken( ) ).getTime( );
314 doc.add( new LongPoint( AppointmentSearchItem.FIELD_DATE_APPOINTMENT_TAKEN, longAppointmentTaken ) );
315 doc.add( new NumericDocValuesField( AppointmentSearchItem.FIELD_DATE_APPOINTMENT_TAKEN, longAppointmentTaken ) );
316 doc.add( new StoredField( AppointmentSearchItem.FIELD_DATE_APPOINTMENT_TAKEN, longAppointmentTaken ) );
317
318
319 doc.add( new IntPoint( AppointmentSearchItem.FIELD_ID_CATEGORY, idCategory ) );
320 doc.add( new NumericDocValuesField( AppointmentSearchItem.FIELD_ID_CATEGORY, idCategory ) );
321 doc.add( new StoredField( AppointmentSearchItem.FIELD_ID_CATEGORY, idCategory ) );
322 return doc;
323 }
324
325 private void deleteIndex( )
326 {
327 if ( _indexWriter == null || !_indexWriter.isOpen( ) )
328 {
329 initIndexing( true );
330 }
331 try
332 {
333 _indexWriter.deleteAll( );
334 }
335 catch( IOException e )
336 {
337 AppLogService.error( "Unable to delete all docs in index ", e );
338 }
339 finally
340 {
341 endIndexing( );
342 }
343 }
344
345
346
347
348
349
350 private void initIndexing( boolean bCreate )
351 {
352 _indexWriter = _luceneAppointmentIndexFactory.getIndexWriter( bCreate );
353 }
354
355
356
357
358 private void endIndexing( )
359 {
360 if ( _indexWriter != null )
361 {
362 try
363 {
364 _indexWriter.commit( );
365 }
366 catch( IOException e )
367 {
368 AppLogService.error( "Unable to close index writer ", e );
369 }
370 }
371 }
372
373 private class IndexerRunnable implements Runnable
374 {
375 private final List<Integer> _idList;
376
377 public IndexerRunnable( List<Integer> idList )
378 {
379 _idList = new ArrayList<>( idList );
380 }
381
382 public IndexerRunnable( )
383 {
384 _idList = new ArrayList<>( );
385 }
386
387 @Override
388 public void run( )
389 {
390 try
391 {
392 processIdList( _idList );
393 while ( _bIndexToLunch.compareAndSet( true, false ) )
394 {
395 processIndexing( );
396 }
397 }
398 catch( Exception e )
399 {
400 AppLogService.error( e.getMessage( ), e );
401 Thread.currentThread( ).interrupt( );
402 }
403 finally
404 {
405 _bIndexIsRunning.set( false );
406 }
407 }
408
409 private void processIndexing( )
410 {
411 synchronized( LOCK )
412 {
413 initIndexing( false );
414
415 Set<Integer> listIdsToAdd = new HashSet<>( );
416 Set<Integer> listIdsToDelete = new HashSet<>( );
417
418
419 IndexerActionFilter filter = new IndexerActionFilter( );
420 filter.setIdTask( IndexerAction.TASK_DELETE );
421 for ( IndexerAction action : IndexerActionHome.getList( filter ) )
422 {
423 listIdsToDelete.add( Integer.valueOf( action.getIdDocument( ) ) );
424 IndexerActionHome.remove( action.getIdAction( ) );
425 }
426
427
428 filter.setIdTask( IndexerAction.TASK_MODIFY );
429 for ( IndexerAction action : IndexerActionHome.getList( filter ) )
430 {
431 listIdsToDelete.add( Integer.valueOf( action.getIdDocument( ) ) );
432 listIdsToAdd.add( Integer.valueOf( action.getIdDocument( ) ) );
433 IndexerActionHome.remove( action.getIdAction( ) );
434 }
435
436
437 filter.setIdTask( IndexerAction.TASK_CREATE );
438 for ( IndexerAction action : IndexerActionHome.getList( filter ) )
439 {
440 listIdsToAdd.add( Integer.valueOf( action.getIdDocument( ) ) );
441 IndexerActionHome.remove( action.getIdAction( ) );
442 }
443
444 List<Query> queryList = new ArrayList<>( TAILLE_LOT );
445 for ( Integer nIdAppointment : listIdsToDelete )
446 {
447 queryList.add( IntPoint.newExactQuery( AppointmentSearchItem.FIELD_ID_APPOINTMENT, nIdAppointment ) );
448 if ( queryList.size( ) == TAILLE_LOT )
449 {
450 deleteDocument( queryList );
451 queryList.clear( );
452 }
453 }
454 deleteDocument( queryList );
455 queryList.clear( );
456
457 processIdList( listIdsToAdd );
458
459 endIndexing( );
460 }
461 }
462
463 private void processIdList( Collection<Integer> idList )
464 {
465 List<Integer> partialIdList = new ArrayList<>( TAILLE_LOT );
466 for ( Integer nIdAppointment : idList )
467 {
468 partialIdList.add( nIdAppointment );
469 if ( partialIdList.size( ) == TAILLE_LOT )
470 {
471 AppointmentFilterDTO filter = new AppointmentFilterDTO( );
472 filter.setListIdAppointment( partialIdList );
473
474 List<AppointmentDTO> appointmentList = AppointmentService.findListAppointmentsDTOByFilter( filter );
475 indexAppointmentList( appointmentList );
476 partialIdList.clear( );
477 appointmentList.clear( );
478 }
479 }
480 if ( CollectionUtils.isNotEmpty( partialIdList ) )
481 {
482 AppointmentFilterDTO filter = new AppointmentFilterDTO( );
483 filter.setListIdAppointment( partialIdList );
484
485 List<AppointmentDTO> appointmentList = AppointmentService.findListAppointmentsDTOByFilter( filter );
486 indexAppointmentList( appointmentList );
487 partialIdList.clear( );
488 appointmentList.clear( );
489 }
490 }
491
492
493
494
495 private void indexAppointmentList( List<AppointmentDTO> listAppointment )
496 {
497 if ( _indexWriter == null || !_indexWriter.isOpen( ) )
498 {
499 initIndexing( true );
500 }
501
502 Map<Integer, Form> mapForms = FormHome.findAllForms( ).stream( ).collect( Collectors.toMap( Form::getIdForm, Function.identity( ) ) );
503 List<Document> documentList = new ArrayList<>( );
504
505 for ( AppointmentDTO appointment : listAppointment )
506 {
507 if ( appointment.getIdUser( ) > 0 )
508 {
509 User user = UserHome.findByPrimaryKey( appointment.getIdUser( ) );
510 appointment.setUser( user );
511 }
512
513 int formId = appointment.getSlot( ).get( 0 ).getIdForm( );
514 Form form = mapForms.get( formId );
515
516 State appointmentState = null;
517 if ( _stateService != null )
518 {
519 appointmentState = _stateService.findByResource( appointment.getIdAppointment( ), Appointment.APPOINTMENT_RESOURCE_TYPE,
520 form.getIdWorkflow( ) );
521 }
522 Document doc = null;
523 try
524 {
525 doc = getDocument( appointment, appointmentState, form.getIdCategory( ) );
526 }
527 catch( Exception e )
528 {
529 AppLogService.error( "Unable to index appointment with id " + appointment.getIdAppointment( ), e );
530 }
531
532 if ( doc != null )
533 {
534 documentList.add( doc );
535 }
536 }
537 addDocuments( documentList );
538 endIndexing( );
539 }
540
541 private void addDocuments( List<Document> documentList )
542 {
543 try
544 {
545 _indexWriter.addDocuments( documentList );
546 }
547 catch( IOException e )
548 {
549 AppLogService.error( "Unable to index documents", e );
550 }
551 documentList.clear( );
552 }
553
554 private void deleteDocument( List<Query> luceneQueryList )
555 {
556 try
557 {
558 _indexWriter.deleteDocuments( luceneQueryList.toArray( new Query [ luceneQueryList.size( )] ) );
559 }
560 catch( IOException e )
561 {
562 AppLogService.error( "Unable to delete document ", e );
563 }
564 }
565 }
566 }