001// --------------------------------------------------------------------------------
002// Copyright 2002-2025 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.model.control.index.server.indexer;
018
019import com.echothree.model.control.core.common.EntityAttributeTypes;
020import com.echothree.model.control.core.server.control.AppearanceControl;
021import com.echothree.model.control.core.server.control.ComponentControl;
022import com.echothree.model.control.core.server.control.CoreControl;
023import com.echothree.model.control.core.server.control.EntityAliasControl;
024import com.echothree.model.control.core.server.control.EntityTypeControl;
025import com.echothree.model.control.core.server.control.EventControl;
026import com.echothree.model.control.index.common.IndexConstants;
027import com.echothree.model.control.index.common.IndexFields;
028import com.echothree.model.control.index.common.IndexSubfields;
029import com.echothree.model.control.index.common.exception.IndexIOErrorException;
030import com.echothree.model.control.index.server.analyzer.BasicAnalyzer;
031import com.echothree.model.control.index.server.control.IndexControl;
032import com.echothree.model.control.tag.server.control.TagControl;
033import com.echothree.model.control.workflow.server.control.WorkflowControl;
034import com.echothree.model.data.core.server.entity.EntityAliasType;
035import com.echothree.model.data.core.server.entity.EntityAttribute;
036import com.echothree.model.data.core.server.entity.EntityInstance;
037import com.echothree.model.data.core.server.entity.EntityType;
038import com.echothree.model.data.index.server.entity.Index;
039import com.echothree.model.data.index.server.entity.IndexStatus;
040import com.echothree.model.data.party.server.entity.Language;
041import com.echothree.model.data.tag.server.entity.TagScope;
042import com.echothree.util.common.message.ExecutionErrors;
043import com.echothree.util.common.persistence.BasePK;
044import com.echothree.util.server.control.BaseLogic;
045import com.echothree.util.server.message.ExecutionErrorAccumulator;
046import com.echothree.util.server.persistence.BaseEntity;
047import com.echothree.util.server.persistence.Session;
048import java.io.Closeable;
049import java.io.File;
050import java.io.IOException;
051import java.nio.file.Paths;
052import java.util.List;
053import org.apache.commons.logging.Log;
054import org.apache.commons.logging.LogFactory;
055import org.apache.lucene.analysis.Analyzer;
056import org.apache.lucene.document.Document;
057import org.apache.lucene.document.Field;
058import org.apache.lucene.document.IntPoint;
059import org.apache.lucene.document.LongPoint;
060import org.apache.lucene.index.IndexWriter;
061import org.apache.lucene.index.IndexWriterConfig;
062import org.apache.lucene.index.SerialMergeScheduler;
063import org.apache.lucene.index.Term;
064import org.apache.lucene.store.Directory;
065import org.apache.lucene.store.FSDirectory;
066
067public abstract class BaseIndexer<BE extends BaseEntity>
068        extends BaseLogic
069        implements Closeable {
070
071    protected AppearanceControl appearanceControl = Session.getModelController(AppearanceControl.class);
072    protected CoreControl coreControl = Session.getModelController(CoreControl.class);
073    protected ComponentControl componentControl = Session.getModelController(ComponentControl.class);
074    protected EntityAliasControl entityAliasControl = Session.getModelController(EntityAliasControl.class);
075    protected EntityTypeControl entityTypeControl = Session.getModelController(EntityTypeControl.class);
076    protected EventControl eventControl = Session.getModelController(EventControl.class);
077    protected IndexControl indexControl = Session.getModelController(IndexControl.class);
078    protected TagControl tagControl = Session.getModelController(TagControl.class);
079    protected WorkflowControl workflowControl = Session.getModelController(WorkflowControl.class);
080    protected Log log = LogFactory.getLog(this.getClass());
081
082    protected ExecutionErrorAccumulator eea;
083    protected Index index;
084    
085    protected Language language;
086    protected EntityType entityType;
087    protected IndexStatus indexStatus;
088    protected List<EntityAliasType> entityAliasTypes;
089    protected List<EntityAttribute> entityAttributes;
090    protected List<TagScope> tagScopes;
091    
092    protected IndexWriter indexWriter;
093    
094    protected BaseIndexer(final ExecutionErrorAccumulator eea, final Index index) {
095        this.eea = eea;
096        this.index = index;
097    }
098    
099    public Index getIndex() {
100        return index;
101    }
102    
103    public void open() {
104        if(indexWriter == null) {
105            var indexDetail = index.getLastDetail();
106
107            this.language = indexDetail.getLanguage();
108            this.entityType = indexDetail.getIndexType().getLastDetail().getEntityType();
109            this.indexStatus = indexControl.getIndexStatusForUpdate(index);
110            this.entityAliasTypes = entityAliasControl.getEntityAliasTypesByEntityType(entityType);
111            this.entityAttributes = coreControl.getEntityAttributesByEntityType(entityType);
112            this.tagScopes = tagControl.getTagScopesByEntityType(entityType);
113
114            openIndexWriter(getAnalyzer());
115        }
116    }
117    
118    public EntityType getEntityType() {
119        return entityType;
120    }
121    
122    @Override
123    public void close() {
124        if(indexWriter != null) {
125            closeIndexWriter();
126        }
127    }
128    
129    /** Index an EntityInstance in all of its Workflows. */
130    private void indexWorkflowEntityStatuses(final Document document, final EntityInstance entityInstance) {
131        workflowControl.getWorkflowsByEntityType(entityInstance.getEntityType()).forEach((workflow) -> {
132            var workflowEntityStatuses = workflowControl.getWorkflowEntityStatusesByEntityInstance(workflow, entityInstance);
133
134            if (!workflowEntityStatuses.isEmpty()) {
135                var workflowStepNamesBuilder = new StringBuilder();
136
137                workflowEntityStatuses.forEach((workflowEntityStatus) -> {
138                    if(workflowStepNamesBuilder.length() != 0) {
139                        workflowStepNamesBuilder.append(' ');
140                    }
141
142                    workflowStepNamesBuilder.append(workflowEntityStatus.getWorkflowStep().getLastDetail().getWorkflowStepName());
143                });
144
145                document.add(new Field(workflow.getLastDetail().getWorkflowName(), workflowStepNamesBuilder.toString(), FieldTypes.NOT_STORED_TOKENIZED));
146            }
147        });
148    }
149
150    private void indexEntityTimes(final Document document, final EntityInstance entityInstance) {
151        var entityTime = eventControl.getEntityTime(entityInstance);
152
153        if(entityTime != null) {
154            var createdTime = entityTime.getCreatedTime();
155            var modifiedTime = entityTime.getModifiedTime();
156            var deletedTime = entityTime.getDeletedTime();
157
158            if(createdTime != null) {
159                document.add(new LongPoint(IndexFields.createdTime.name(), createdTime));
160            }
161
162            if(modifiedTime != null) {
163                document.add(new LongPoint(IndexFields.modifiedTime.name(), modifiedTime));
164            }
165
166            if(deletedTime != null) {
167                document.add(new LongPoint(IndexFields.deletedTime.name(), deletedTime));
168            }
169        }
170    }
171
172    private void indexEntityAliases(final Document document, final EntityInstance entityInstance) {
173        var entityAliases = entityAliasControl.getEntityAliasesByEntityInstance(entityInstance);
174
175        for(var entityAlias : entityAliases) {
176            var fieldName = entityAlias.getEntityAliasType().getLastDetail().getEntityAliasTypeName();
177            var alias = entityAlias.getAlias();
178
179            document.add(new Field(fieldName, alias, FieldTypes.NOT_STORED_NOT_TOKENIZED));
180        }
181
182    }
183
184    private void indexEntityAttributes(final Document document, final EntityInstance entityInstance) {
185        entityAttributes.forEach((entityAttribute) -> {
186            var entityAttributeDetail = entityAttribute.getLastDetail();
187            var fieldName = entityAttributeDetail.getEntityAttributeName();
188            var entityAttributeTypeName = entityAttributeDetail.getEntityAttributeType().getEntityAttributeTypeName();
189
190            if (entityAttributeTypeName.equals(EntityAttributeTypes.BOOLEAN.name())) {
191                var entityBooleanAttribute = coreControl.getEntityBooleanAttribute(entityAttribute, entityInstance);
192
193                if(entityBooleanAttribute != null) {
194                    var booleanAttribute = entityBooleanAttribute.getBooleanAttribute();
195                    if(IndexerDebugFlags.LogBaseIndexing) {
196                        log.info("--- fieldName =\"" + fieldName + ", \"booleanAttribute = \"" + booleanAttribute + "\"");
197                    }
198                    document.add(new Field(fieldName, booleanAttribute.toString(), FieldTypes.NOT_STORED_NOT_TOKENIZED));
199                }
200            } else if (entityAttributeTypeName.equals(EntityAttributeTypes.NAME.name())) {
201                var entityNameAttribute = coreControl.getEntityNameAttribute(entityAttribute, entityInstance);
202
203                if(entityNameAttribute != null) {
204                    var nameAttribute = entityNameAttribute.getNameAttribute();
205                    if(IndexerDebugFlags.LogBaseIndexing) {
206                        log.info("--- fieldName =\"" + fieldName + ", \"nameAttribute = \"" + nameAttribute + "\"");
207                    }
208                    document.add(new Field(fieldName, nameAttribute, FieldTypes.NOT_STORED_NOT_TOKENIZED));
209                }
210            } else if (entityAttributeTypeName.equals(EntityAttributeTypes.INTEGER.name())) {
211                var entityIntegerAttribute = coreControl.getEntityIntegerAttribute(entityAttribute, entityInstance);
212                
213                if(entityIntegerAttribute != null) {
214                    var integerAttribute = entityIntegerAttribute.getIntegerAttribute();
215                    if(IndexerDebugFlags.LogBaseIndexing) {
216                        log.info("--- fieldName =\"" + fieldName + ",\" integerAttribute = \"" + integerAttribute + "\"");
217                    }
218                    document.add(new IntPoint(fieldName, integerAttribute));
219                }
220            } else if (entityAttributeTypeName.equals(EntityAttributeTypes.LONG.name())) {
221                var entityLongAttribute = coreControl.getEntityLongAttribute(entityAttribute, entityInstance);
222                
223                if(entityLongAttribute != null) {
224                    var longAttribute = entityLongAttribute.getLongAttribute();
225                    if(IndexerDebugFlags.LogBaseIndexing) {
226                        log.info("--- fieldName =\"" + fieldName + ",\" longAttribute = \"" + longAttribute + "\"");
227                    }
228                    document.add(new LongPoint(fieldName, longAttribute));
229                }
230            } else if (language != null && entityAttributeTypeName.equals(EntityAttributeTypes.STRING.name())) {
231                var entityStringAttribute = coreControl.getEntityStringAttribute(entityAttribute, entityInstance, language);
232                
233                if(entityStringAttribute != null) {
234                    var stringAttribute = entityStringAttribute.getStringAttribute();
235                    if(IndexerDebugFlags.LogBaseIndexing) {
236                        log.info("--- fieldName = \"" + fieldName + ",\" stringAttribute = \"" + stringAttribute + "\"");
237                    }
238                    document.add(new Field(fieldName, stringAttribute, FieldTypes.NOT_STORED_TOKENIZED));
239                }
240            } else if (entityAttributeTypeName.equals(EntityAttributeTypes.GEOPOINT.name())) {
241                var entityGeoPointAttribute = coreControl.getEntityGeoPointAttribute(entityAttribute, entityInstance);
242                
243                if(entityGeoPointAttribute != null) {
244                    var latitude = entityGeoPointAttribute.getLatitude();
245                    var longitude = entityGeoPointAttribute.getLongitude();
246                    var elevation = entityGeoPointAttribute.getElevation();
247                    var altitude = entityGeoPointAttribute.getAltitude();
248                    
249                    if(IndexerDebugFlags.LogBaseIndexing) {
250                        log.info("--- fieldName = \"" + fieldName + ",\" latitude = \"" + latitude + ",\" longitude = \"" + longitude + ",\" elevation = \"" + elevation + ",\" altitude = \"" + altitude + "\"");
251                    }
252                    
253                    document.add(new IntPoint(fieldName + IndexConstants.INDEX_SUBFIELD_SEPARATOR + IndexSubfields.latitude.name(), latitude));
254                    document.add(new IntPoint(fieldName + IndexConstants.INDEX_SUBFIELD_SEPARATOR + IndexSubfields.longitude.name(), longitude));
255                    
256                    if(elevation != null) {
257                        document.add(new LongPoint(fieldName + IndexConstants.INDEX_SUBFIELD_SEPARATOR + IndexSubfields.elevation.name(), elevation));
258                    }
259                    
260                    if(altitude != null) {
261                        document.add(new LongPoint(fieldName + IndexConstants.INDEX_SUBFIELD_SEPARATOR + IndexSubfields.altitude.name(), altitude));
262                    }
263                }
264            } else if (language != null && entityAttributeTypeName.equals(EntityAttributeTypes.CLOB.name())) {
265                // TODO: MIME type should be taken into account
266                var entityClobAttribute = coreControl.getEntityClobAttribute(entityAttribute, entityInstance, language);
267                
268                if(entityClobAttribute != null) {
269                    var clobAttribute = entityClobAttribute.getClobAttribute();
270                    if(IndexerDebugFlags.LogBaseIndexing) {
271                        log.info("--- fieldName =\"" + fieldName + ",\" clobAttribute = \"" + clobAttribute + "\"");
272                    }
273                    document.add(new Field(fieldName, clobAttribute, FieldTypes.NOT_STORED_TOKENIZED));
274                }
275            } else if (entityAttributeTypeName.equals(EntityAttributeTypes.DATE.name())) {
276                var entityDateAttribute = coreControl.getEntityDateAttribute(entityAttribute, entityInstance);
277                
278                if(entityDateAttribute != null) {
279                    var dateAttribute = entityDateAttribute.getDateAttribute();
280                    if(IndexerDebugFlags.LogBaseIndexing) {
281                        log.info("--- fieldName =\"" + fieldName + ",\" dateAttribute = \"" + dateAttribute + "\"");
282                    }
283                    document.add(new IntPoint(fieldName, dateAttribute));
284                }
285            } else if (entityAttributeTypeName.equals(EntityAttributeTypes.TIME.name())) {
286                var entityTimeAttribute = coreControl.getEntityTimeAttribute(entityAttribute, entityInstance);
287                
288                if(entityTimeAttribute != null) {
289                    var timeAttribute = entityTimeAttribute.getTimeAttribute();
290                    if(IndexerDebugFlags.LogBaseIndexing) {
291                        log.info("--- fieldName =\"" + fieldName + ",\" dateAttribute = \"" + timeAttribute + "\"");
292                    }
293                    document.add(new LongPoint(fieldName, timeAttribute));
294                }
295            } else if (entityAttributeTypeName.equals(EntityAttributeTypes.LISTITEM.name())) {
296                var entityListItemAttribute = coreControl.getEntityListItemAttribute(entityAttribute, entityInstance);
297                
298                if(entityListItemAttribute != null) {
299                    var entityListItemName = entityListItemAttribute.getEntityListItem().getLastDetail().getEntityListItemName();
300                    if(IndexerDebugFlags.LogBaseIndexing) {
301                        log.info("--- fieldName =\"" + fieldName + ",\" entityListItemName = \"" + entityListItemName + "\"");
302                    }
303                    document.add(new Field(fieldName, entityListItemName, FieldTypes.NOT_STORED_TOKENIZED));
304                }
305            } else if (entityAttributeTypeName.equals(EntityAttributeTypes.MULTIPLELISTITEM.name())) {
306                var entityMultipleListItemAttributes = coreControl.getEntityMultipleListItemAttributes(entityAttribute, entityInstance);
307                if (entityMultipleListItemAttributes != null && !entityMultipleListItemAttributes.isEmpty()) {
308                    var entityListItemNamesBuilder = new StringBuilder();
309                    entityMultipleListItemAttributes.forEach((entityMultipleListItemAttribute) -> {
310                        if(entityListItemNamesBuilder.length() != 0) {
311                            entityListItemNamesBuilder.append(' ');
312                        }
313                        
314                        entityListItemNamesBuilder.append(entityMultipleListItemAttribute.getEntityListItem().getLastDetail().getEntityListItemName());
315                    });
316                    var entityListItemNames = entityListItemNamesBuilder.toString();
317                    if(IndexerDebugFlags.LogBaseIndexing) {
318                        log.info("--- fieldName =\"" + fieldName + ",\" entityListItemNames = \"" + entityListItemNames + "\"");
319                    }
320                    document.add(new Field(fieldName, entityListItemNames, FieldTypes.NOT_STORED_TOKENIZED));
321                }
322            }
323        });
324    }
325
326    private void indexEntityTags(final Document document, final EntityInstance entityInstance) {
327        var entityTags = tagControl.getEntityTagsByTaggedEntityInstance(entityInstance);
328
329        entityTags.stream().map((entityTag) -> entityTag.getTag().getLastDetail()).forEach((tagDetail) -> {
330            var tagScopeName = tagDetail.getTagScope().getLastDetail().getTagScopeName();
331            var tagName = tagDetail.getTagName();
332
333            document.add(new Field(tagScopeName, tagName, FieldTypes.NOT_STORED_TOKENIZED));
334        });
335    }
336
337    private void indexEntityAppearance(final Document document, final EntityInstance entityInstance) {
338        var entityAppearance = appearanceControl.getEntityAppearance(entityInstance);
339
340        if(entityAppearance != null) {
341            var entityAppearanceName = entityAppearance.getAppearance().getLastDetail().getAppearanceName();
342
343            document.add(new Field(IndexFields.appearance.name(), entityAppearanceName, FieldTypes.NOT_STORED_TOKENIZED));
344        }
345    }
346
347    protected void addEntityInstanceToDocument(final Document document, final EntityInstance entityInstance, final BasePK basePK) {
348        document.add(new Field(IndexFields.entityRef.name(), basePK.getEntityRef(), FieldTypes.STORED_NOT_TOKENIZED));
349        document.add(new Field(IndexFields.entityInstanceId.name(), entityInstance.getPrimaryKey().getEntityId().toString(), FieldTypes.STORED_NOT_TOKENIZED));
350    }
351
352    protected void addEntityInstanceFieldsToDocument(final Document document, final EntityInstance entityInstance) {
353        indexWorkflowEntityStatuses(document, entityInstance);
354        indexEntityTimes(document, entityInstance);
355        indexEntityAliases(document, entityInstance);
356        indexEntityAttributes(document, entityInstance);
357        indexEntityTags(document, entityInstance);
358        indexEntityAppearance(document, entityInstance);
359    }
360
361    protected Document newDocumentWithEntityInstanceFields(final EntityInstance entityInstance, final BasePK basePK) {
362        final var document = new Document();
363
364        addEntityInstanceToDocument(document, entityInstance, basePK);
365        addEntityInstanceFieldsToDocument(document, entityInstance);
366
367        return document;
368    }
369
370    /**
371     * Create a Lucene IndexWriter.
372     */
373    protected void openIndexWriter(final Analyzer analyzer) {
374        if(IndexerDebugFlags.LogBaseIndexing) {
375            log.info(">>> getIndexWriter");
376        }
377
378        var createIndex = indexStatus.getCreatedTime() != null ? false : true;
379        
380        if(createIndex) {
381            checkIndexDirectory(eea, indexStatus);
382        }
383
384        // indexCreatedTime is rechecked because if checkIndexDirectory was called,
385        // it will be set to the time the directory was created, vs. remaining null.
386        if(!hasExecutionErrors(eea)) {
387            try {
388                Directory fsDir = FSDirectory.open(Paths.get(index.getLastDetail().getDirectory()));
389                var indexWriterConfig = new IndexWriterConfig(analyzer);
390
391                indexWriterConfig.setMergeScheduler(new SerialMergeScheduler());
392                indexWriterConfig.setOpenMode(createIndex ? IndexWriterConfig.OpenMode.CREATE : IndexWriterConfig.OpenMode.APPEND);
393
394                indexWriter = new IndexWriter(fsDir, indexWriterConfig);
395            } catch (IOException ioe1) {
396                // indexWriter will remain null, signaling that index was not able to be opened
397                if(IndexerDebugFlags.LogBaseIndexing) {
398                    log.info("--- new IndexWriter failed");
399                }
400                
401                try {
402                    if(indexWriter != null) {
403                        indexWriter.close();
404                    }
405                } catch (IOException ioe2) {
406                    // ioe2 discarded.
407                } finally {
408                    indexWriter = null;
409                }
410                
411                handleExecutionError(IndexIOErrorException.class, eea, ExecutionErrors.IndexIOError.name(), ioe1.getMessage());
412            }
413        }
414        
415        if(IndexerDebugFlags.LogBaseIndexing) {
416            log.info("<<< getIndexWriter");
417        }
418    }
419    
420    protected void closeIndexWriter() {
421        if(IndexerDebugFlags.LogBaseIndexing) {
422            log.info(">>> closeIndexWriter");
423        }
424        
425        try {
426            indexWriter.commit();
427            indexWriter.close();
428        } catch(IOException ioe) {
429            // unrecoverable error
430            // TODO: Index should be marked as possibly invalid
431            if(IndexerDebugFlags.LogBaseIndexing) {
432                log.info("--- indexWriter.close failed");
433            }
434
435            handleExecutionError(IndexIOErrorException.class, eea, ExecutionErrors.IndexIOError.name(), ioe.getMessage());
436        }
437
438        if(IndexerDebugFlags.LogBaseIndexing) {
439            log.info("<<< closeIndexWriter");
440        }
441    }
442    
443    private void checkIndexDirectory(final ExecutionErrorAccumulator eea, final IndexStatus indexStatus) {
444        if(IndexerDebugFlags.LogBaseIndexing) {
445            log.info("--- checkIndexDirectory, index = " + index);
446        }
447        var indexCreatedTime = indexStatus.getCreatedTime();
448        
449        if(indexCreatedTime == null) {
450            indexCreatedTime = createIndexDirectory(eea);
451            
452            if(!hasExecutionErrors(eea)) {
453                indexStatus.setCreatedTime(indexCreatedTime);
454            }
455        }
456    }
457    
458    private Long createIndexDirectory(final ExecutionErrorAccumulator eea) {
459        if(IndexerDebugFlags.LogBaseIndexing) {
460            log.info(">>> createIndexDirectory, index = " + index);
461        }
462        Long indexCreatedTime = null;
463        var strDirectory = index.getLastDetail().getDirectory();
464        var directory = new File(strDirectory);
465        
466        if(directory.exists()) {
467            if(directory.isDirectory()) {
468                var files = directory.listFiles();
469                
470                if(files == null) {
471                    handleExecutionError(IndexIOErrorException.class, eea, ExecutionErrors.IndexIOError.name(), "listFiles failed for " + strDirectory);
472                } else {
473                    var numFiles = files.length;
474
475                    for(var i = 0; i < numFiles ; i++) {
476                        var indivFile = files[i];
477
478                        if(indivFile.isFile()) {
479                            if(!indivFile.delete()) {
480                                handleExecutionError(IndexIOErrorException.class, eea, ExecutionErrors.IndexIOError.name(), "delete failed for " + indivFile.getPath());
481                            }
482                        }
483                    }
484                }
485            } else {
486                handleExecutionError(IndexIOErrorException.class, eea, ExecutionErrors.IndexIOError.name(), "directory isn't a directory:  " + directory.getPath());
487            }
488        } else {
489            if(!directory.mkdirs()) {
490                handleExecutionError(IndexIOErrorException.class, eea, ExecutionErrors.IndexIOError.name(), "mkdirs failed for " + directory.getPath());
491            }
492        }
493        
494        if(!hasExecutionErrors(eea)) {
495            indexCreatedTime = System.currentTimeMillis();
496        }
497        
498        if(IndexerDebugFlags.LogBaseIndexing) {
499            log.info("<<< createIndexDirectory, indexCreatedTime = " + indexCreatedTime);
500        }
501        return indexCreatedTime;
502    }
503    
504    public void forceReindex() {
505        indexStatus.setCreatedTime(null);
506    }
507    
508    protected Analyzer getAnalyzer() {
509        return new BasicAnalyzer(eea, language, entityType, entityAliasTypes, entityAttributes, tagScopes);
510    }
511
512    protected abstract BE getEntity(final EntityInstance entityInstance);
513    
514    protected abstract Document convertToDocument(final EntityInstance entityInstance, final BE item);
515    
516    protected void addEntityToIndex(final EntityInstance entityInstance, final BE baseEntity)
517        throws IOException {
518        var document = convertToDocument(entityInstance, baseEntity);
519        
520        if(document != null) {
521            indexWriter.addDocument(document);
522        }
523    }
524    
525    protected void removeEntityFromIndex(final BE baseEntity)
526        throws IOException {
527        indexWriter.deleteDocuments(new Term(IndexFields.entityRef.name(), baseEntity.getPrimaryKey().getEntityRef()));
528    }
529
530    public void updateIndex(final EntityInstance entityInstance) {
531        var baseEntity = getEntity(entityInstance);
532        
533        if(baseEntity != null) {
534            var entityTime = eventControl.getEntityTime(entityInstance);
535            
536            if(entityTime != null) {
537                var modifiedTime = entityTime.getModifiedTime();
538                var deletedTime = entityTime.getDeletedTime();
539                
540                try {
541                    if(modifiedTime != null || deletedTime != null) {
542                        removeEntityFromIndex(baseEntity);
543                    }
544
545                    if(deletedTime == null) {
546                        addEntityToIndex(entityInstance, baseEntity);
547                    }
548                } catch(IOException ioe) {
549                    handleExecutionError(IndexIOErrorException.class, eea, ExecutionErrors.IndexIOError.name(), ioe.getMessage());
550                }
551            }
552        }
553    }
554
555}