View Javadoc
1   /*
2    * Copyright (c) 2002-2022, 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.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   * Appointment global indexer
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      * Builds a document which will be used by Lucene during the indexing of this record
217      * 
218      * @param appointmentDTO
219      *            the appointment object
220      * @param form
221      *            the form
222      * @return a lucene document filled with the record data
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         // make a new, empty document
231         Document doc = new Document( );
232 
233         int nIdAppointment = appointmentDTO.getIdAppointment( );
234 
235         // --- document identifier
236         doc.add( new Field( SearchItem.FIELD_UID, nIdAppointment + SUFIX_UID_APPOINTMENTS , ftNotStored ) );
237 
238         // --- form response identifier
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         // --- id form
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         // --- First name
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         // --- Last name
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         // --- Mail
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         // --- Phone Number
270         // A User's phone number can be null in DB, so we make sure this variable is always initialized with a value
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         // --- Starting date appointment
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         // --- Ending date appointment
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         // --- Admin user
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         // --- Status
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         // --- State
298         if ( appointmentState != null )
299         {
300             // --- id form response workflow state
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         // --- Nb Seats
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         // --- Date appointment Taken
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         // -- Category
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      * Init the indexing action
347      * 
348      * @param bCreate
349      */
350     private void initIndexing( boolean bCreate )
351     {
352         _indexWriter = _luceneAppointmentIndexFactory.getIndexWriter( bCreate );
353     }
354 
355     /**
356      * End the indexing action
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                 // Delete all record which must be delete
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                 // Update all record which must be update
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                 // Update all record which must be update
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          * {@inheritDoc}
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 }