View Javadoc
1   /*
2    * Copyright (c) 2002-2017, Mairie de 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.directory.service.search;
35  
36  import fr.paris.lutece.plugins.directory.business.Directory;
37  import fr.paris.lutece.plugins.directory.business.DirectoryFilter;
38  import fr.paris.lutece.plugins.directory.business.DirectoryHome;
39  import fr.paris.lutece.plugins.directory.business.EntryFilter;
40  import fr.paris.lutece.plugins.directory.business.EntryHome;
41  import fr.paris.lutece.plugins.directory.business.IEntry;
42  import fr.paris.lutece.plugins.directory.business.Record;
43  import fr.paris.lutece.plugins.directory.business.RecordField;
44  import fr.paris.lutece.plugins.directory.business.RecordFieldFilter;
45  import fr.paris.lutece.plugins.directory.business.RecordFieldHome;
46  import fr.paris.lutece.plugins.directory.service.DirectoryPlugin;
47  import fr.paris.lutece.plugins.directory.service.record.IRecordService;
48  import fr.paris.lutece.plugins.directory.service.record.RecordService;
49  import fr.paris.lutece.plugins.directory.utils.DirectoryUtils;
50  import fr.paris.lutece.portal.service.content.XPageAppService;
51  import fr.paris.lutece.portal.service.message.SiteMessageException;
52  import fr.paris.lutece.portal.service.plugin.Plugin;
53  import fr.paris.lutece.portal.service.plugin.PluginService;
54  import fr.paris.lutece.portal.service.search.IndexationService;
55  import fr.paris.lutece.portal.service.search.SearchIndexer;
56  import fr.paris.lutece.portal.service.search.SearchItem;
57  import fr.paris.lutece.portal.service.spring.SpringContextService;
58  import fr.paris.lutece.portal.service.util.AppLogService;
59  import fr.paris.lutece.portal.service.util.AppPathService;
60  import fr.paris.lutece.portal.service.util.AppPropertiesService;
61  import fr.paris.lutece.util.url.UrlItem;
62  
63  import org.apache.commons.lang.StringUtils;
64  
65  import org.apache.lucene.document.DateTools;
66  import org.apache.lucene.document.Document;
67  import org.apache.lucene.document.Field;
68  import org.apache.lucene.document.FieldType;
69  import org.apache.lucene.document.StoredField;
70  import org.apache.lucene.document.StringField;
71  import org.apache.lucene.document.TextField;
72  
73  import java.io.IOException;
74  
75  import java.util.ArrayList;
76  import java.util.List;
77  
78  /**
79   * Directory global indexer
80   */
81  public class DirectorySearchIndexer implements SearchIndexer
82  {
83      public static final String INDEXER_NAME = "DirectoryIndexer";
84      public static final String SHORT_NAME = "dry";
85      private static final String DIRECTORY = "directory";
86      private static final String INDEXER_DESCRIPTION = "Indexer service for directories";
87      private static final String INDEXER_VERSION = "1.0.0";
88      private static final String PARAMETER_ID_DIRECTORY_RECORD = "id_directory_record";
89      private static final String PARAMETER_VIEW_DIRECTORY_RECORD = "view_directory_record";
90      private static final String PROPERTY_INDEXER_ENABLE = "directory.globalIndexer.enable";
91      private static final String ROLE_NONE = "none";
92  
93      /**
94       * {@inheritDoc}
95       */
96      @Override
97      public String getName( )
98      {
99          return INDEXER_NAME;
100     }
101 
102     /**
103      * {@inheritDoc}
104      */
105     @Override
106     public String getDescription( )
107     {
108         return INDEXER_DESCRIPTION;
109     }
110 
111     /**
112      * {@inheritDoc}
113      */
114     @Override
115     public String getVersion( )
116     {
117         return INDEXER_VERSION;
118     }
119 
120     /**
121      * {@inheritDoc}
122      */
123     @Override
124     public boolean isEnable( )
125     {
126         String strEnable = AppPropertiesService.getProperty( PROPERTY_INDEXER_ENABLE );
127 
128         return ( strEnable.equalsIgnoreCase( "true" ) );
129     }
130 
131     /**
132      * {@inheritDoc}
133      */
134     @Override
135     public List<String> getListType( )
136     {
137         List<String> listType = new ArrayList<String>( 1 );
138         listType.add( DIRECTORY );
139 
140         return listType;
141     }
142 
143     /**
144      * {@inheritDoc}
145      */
146     @Override
147     public String getSpecificSearchAppUrl( )
148     {
149         UrlItem url = new UrlItem( AppPathService.getPortalUrl( ) );
150         url.addParameter( XPageAppService.PARAM_XPAGE_APP, DIRECTORY );
151 
152         return url.getUrl( );
153     }
154 
155     /**
156      * {@inheritDoc}
157      */
158     @Override
159     public List<Document> getDocuments( String recordId ) throws IOException, InterruptedException, SiteMessageException
160     {
161         Plugin plugin = PluginService.getPlugin( DirectoryPlugin.PLUGIN_NAME );
162 
163         int nIdRecord;
164 
165         try
166         {
167             nIdRecord = Integer.parseInt( recordId );
168         }
169         catch( NumberFormatException ne )
170         {
171             AppLogService.error( recordId + " not parseable to an int", ne );
172 
173             return new ArrayList<Document>( 0 );
174         }
175 
176         IRecordService recordService = SpringContextService.getBean( RecordService.BEAN_SERVICE );
177         Record record = recordService.findByPrimaryKey( nIdRecord, plugin );
178         Directory directory = record.getDirectory( );
179 
180         if ( !record.isEnabled( ) || !directory.isEnabled( ) || !directory.isIndexed( ) )
181         {
182             return new ArrayList<Document>( 0 );
183         }
184 
185         int nIdDirectory = directory.getIdDirectory( );
186 
187         // Parse the entries to gather the ones marked as indexed
188         EntryFilter entryFilter = new EntryFilter( );
189         entryFilter.setIdDirectory( nIdDirectory );
190         entryFilter.setIsIndexed( EntryFilter.FILTER_TRUE );
191 
192         List<IEntry> listIndexedEntry = EntryHome.getEntryList( entryFilter, plugin );
193 
194         entryFilter = new EntryFilter( );
195         entryFilter.setIdDirectory( nIdDirectory );
196         entryFilter.setIsIndexedAsTitle( EntryFilter.FILTER_TRUE );
197 
198         List<IEntry> listIndexedAsTitleEntry = EntryHome.getEntryList( entryFilter, plugin );
199 
200         entryFilter = new EntryFilter( );
201         entryFilter.setIdDirectory( nIdDirectory );
202         entryFilter.setIsIndexedAsSummary( EntryFilter.FILTER_TRUE );
203 
204         List<IEntry> listIndexedAsSummaryEntry = EntryHome.getEntryList( entryFilter, plugin );
205 
206         Document doc = getDocument( record, listIndexedEntry, listIndexedAsTitleEntry, listIndexedAsSummaryEntry, plugin );
207 
208         if ( doc != null )
209         {
210             List<Document> listDocument = new ArrayList<Document>( 1 );
211             listDocument.add( doc );
212 
213             return listDocument;
214         }
215 
216         return new ArrayList<Document>( 0 );
217     }
218 
219     /**
220      * {@inheritDoc}
221      */
222     @Override
223     public void indexDocuments( ) throws IOException, InterruptedException, SiteMessageException
224     {
225         Plugin plugin = PluginService.getPlugin( DirectoryPlugin.PLUGIN_NAME );
226 
227         // Index only the directories that have the attribute is_indexed as true
228         DirectoryFilter dirFilter = new DirectoryFilter( );
229         dirFilter.setIsIndexed( DirectoryFilter.FILTER_TRUE );
230         dirFilter.setIsDisabled( DirectoryFilter.FILTER_TRUE ); // Bad naming: IsDisable( true ) stands for enabled
231 
232         IRecordService recordService = SpringContextService.getBean( RecordService.BEAN_SERVICE );
233 
234         for ( Directory directory : DirectoryHome.getDirectoryList( dirFilter, plugin ) )
235         {
236             int nIdDirectory = directory.getIdDirectory( );
237 
238             // Index only the records that have the attribute is_enable as true
239             RecordFieldFilter recFilter = new RecordFieldFilter( );
240             recFilter.setIdDirectory( nIdDirectory );
241             recFilter.setIsDisabled( RecordFieldFilter.FILTER_TRUE ); // Bad naming: IsDisable( true ) stands for enabled
242 
243             List<Record> listRecord = recordService.getListRecord( recFilter, plugin );
244 
245             // Keep processing this directory only if there are enabled records
246             if ( !listRecord.isEmpty( ) )
247             {
248                 // Parse the entries to gather the ones marked as indexed
249                 EntryFilter entryFilter = new EntryFilter( );
250                 entryFilter.setIdDirectory( nIdDirectory );
251                 entryFilter.setIsIndexed( EntryFilter.FILTER_TRUE );
252 
253                 List<IEntry> listIndexedEntry = EntryHome.getEntryList( entryFilter, plugin );
254 
255                 entryFilter = new EntryFilter( );
256                 entryFilter.setIdDirectory( nIdDirectory );
257                 entryFilter.setIsIndexedAsTitle( EntryFilter.FILTER_TRUE );
258 
259                 List<IEntry> listIndexedAsTitleEntry = EntryHome.getEntryList( entryFilter, plugin );
260 
261                 entryFilter = new EntryFilter( );
262                 entryFilter.setIdDirectory( nIdDirectory );
263                 entryFilter.setIsIndexedAsSummary( EntryFilter.FILTER_TRUE );
264 
265                 List<IEntry> listIndexedAsSummaryEntry = EntryHome.getEntryList( entryFilter, plugin );
266 
267                 for ( Record record : listRecord )
268                 {
269                     Document recordDoc = null;
270 
271                     try
272                     {
273                         recordDoc = getDocument( record, listIndexedEntry, listIndexedAsTitleEntry, listIndexedAsSummaryEntry, plugin );
274                     }
275                     catch( Exception e )
276                     {
277                         String strMessage = "Directory ID : " + directory.getIdDirectory( ) + " - Record ID : " + record.getIdRecord( );
278                         IndexationService.error( this, e, strMessage );
279                     }
280 
281                     if ( recordDoc != null )
282                     {
283                         IndexationService.write( recordDoc );
284                     }
285                 }
286             }
287         }
288     }
289 
290     /**
291      * Builds a document which will be used by Lucene during the indexing of this record
292      * 
293      * @param record
294      *            the record to convert into a document
295      * @param listContentEntry
296      *            the entries in this record that are marked as is_indexed
297      * @param listTitleEntry
298      *            the entries in this record that are marked as is_indexed_as_title
299      * @param listSummaryEntry
300      *            the entries in this record that are marked as is_indexed_as_summary
301      * @param plugin
302      *            the plugin object
303      * @return a lucene document filled with the record data
304      */
305     public Document getDocument( Record record, List<IEntry> listContentEntry, List<IEntry> listTitleEntry, List<IEntry> listSummaryEntry, Plugin plugin )
306     {
307         Document doc = new Document( );
308 
309         FieldType ft = new FieldType( StringField.TYPE_STORED );
310         ft.setOmitNorms( false );
311 
312         FieldType ftNotStored = new FieldType( StringField.TYPE_NOT_STORED );
313         ftNotStored.setOmitNorms( false );
314         ftNotStored.setTokenized( false );
315 
316         boolean bFallback = false;
317 
318         // Fallback if there is no entry marker as indexed_as_title
319         // Uses the first indexed field instead
320         if ( listTitleEntry.isEmpty( ) && !listContentEntry.isEmpty( ) )
321         {
322             listTitleEntry.add( listContentEntry.get( 0 ) );
323             bFallback = true;
324         }
325 
326         String strTitle = getContentToIndex( record, listTitleEntry, plugin );
327 
328         // Fallback if fields were empty
329         // Uses the first indexed field instead
330         if ( StringUtils.isBlank( strTitle ) && !bFallback && !listContentEntry.isEmpty( ) )
331         {
332             listTitleEntry.clear( );
333             listTitleEntry.add( listContentEntry.get( 0 ) );
334             strTitle = getContentToIndex( record, listTitleEntry, plugin );
335         }
336 
337         // No more fallback. Giving up
338         if ( StringUtils.isBlank( strTitle ) )
339         {
340             return null;
341         }
342 
343         doc.add( new Field( SearchItem.FIELD_TITLE, strTitle, ft ) );
344 
345         if ( !listContentEntry.isEmpty( ) )
346         {
347             String strContent = getContentToIndex( record, listContentEntry, plugin );
348 
349             if ( StringUtils.isNotBlank( strContent ) )
350             {
351                 doc.add( new Field( SearchItem.FIELD_CONTENTS, strContent, TextField.TYPE_NOT_STORED ) );
352             }
353         }
354 
355         if ( !listSummaryEntry.isEmpty( ) )
356         {
357             String strSummary = getContentToIndex( record, listSummaryEntry, plugin );
358 
359             if ( StringUtils.isNotBlank( strSummary ) )
360             {
361                 doc.add( new StoredField( SearchItem.FIELD_SUMMARY, strSummary ) );
362             }
363         }
364 
365         String strRoleKey = record.getRoleKey( );
366 
367         if ( StringUtils.isBlank( strRoleKey ) )
368         {
369             strRoleKey = ROLE_NONE;
370         }
371 
372         doc.add( new Field( SearchItem.FIELD_ROLE, strRoleKey, ft ) );
373 
374         String strDate = DateTools.dateToString( record.getDateCreation( ), DateTools.Resolution.DAY );
375         doc.add( new Field( SearchItem.FIELD_DATE, strDate, ft ) );
376 
377         String strDateModification = DateTools.dateToString( record.getDateModification( ), DateTools.Resolution.DAY );
378         doc.add( new Field( SearchItem.FIELD_DATE, strDateModification, ft ) );
379 
380         doc.add( new Field( SearchItem.FIELD_TYPE, DIRECTORY, ft ) );
381 
382         UrlItem url = new UrlItem( AppPathService.getPortalUrl( ) );
383         url.addParameter( XPageAppService.PARAM_XPAGE_APP, DIRECTORY );
384         url.addParameter( PARAMETER_ID_DIRECTORY_RECORD, record.getIdRecord( ) );
385         url.addParameter( PARAMETER_VIEW_DIRECTORY_RECORD, "" );
386         doc.add( new Field( SearchItem.FIELD_URL, url.getUrl( ), ft ) );
387 
388         // Add the uid as a field, so that index can be incrementally maintained.
389         // This field is not stored with question/answer, it is indexed, but it is not
390         // tokenized prior to indexing.
391         String strUID = Integer.toString( record.getIdRecord( ) ) + "_" + SHORT_NAME;
392         doc.add( new Field( SearchItem.FIELD_UID, strUID, ftNotStored ) );
393 
394         return doc;
395     }
396 
397     /**
398      * Concatenates the value of the specified field in this record
399      * 
400      * @param record
401      *            the record to seek
402      * @param listEntry
403      *            the list of field to concatenate
404      * @param plugin
405      *            the plugin object
406      * @return
407      */
408     private String getContentToIndex( Record record, List<IEntry> listEntry, Plugin plugin )
409     {
410         List<Integer> listIdEntry = new ArrayList<Integer>( listEntry.size( ) );
411 
412         for ( IEntry entry : listEntry )
413         {
414             listIdEntry.add( entry.getIdEntry( ) );
415         }
416 
417         StringBuffer sb = new StringBuffer( );
418 
419         List<RecordField> listField = RecordFieldHome.getRecordFieldSpecificList( listIdEntry, record.getIdRecord( ), plugin,
420                 DirectoryUtils.getMapFieldsOfListEntry( listEntry, plugin ) );
421 
422         for ( RecordField field : listField )
423         {
424             sb.append( RecordFieldHome.findByPrimaryKey( field.getIdRecordField( ), plugin ).getValue( ) );
425             sb.append( " " );
426         }
427 
428         return sb.toString( );
429     }
430 }