001// --------------------------------------------------------------------------------
002// Copyright 2002-2026 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.control.user.index.server.command;
018
019import com.echothree.control.user.index.common.form.UpdateIndexesForm;
020import com.echothree.control.user.index.common.result.IndexResultFactory;
021import com.echothree.model.control.contact.server.indexer.ContactMechanismIndexer;
022import com.echothree.model.control.content.server.indexer.ContentCatalogIndexer;
023import com.echothree.model.control.content.server.indexer.ContentCatalogItemIndexer;
024import com.echothree.model.control.content.server.indexer.ContentCategoryIndexer;
025import com.echothree.model.control.core.server.indexer.ComponentVendorIndexer;
026import com.echothree.model.control.core.server.indexer.EntityAliasTypeIndexer;
027import com.echothree.model.control.core.server.indexer.EntityAttributeGroupIndexer;
028import com.echothree.model.control.core.server.indexer.EntityAttributeIndexer;
029import com.echothree.model.control.core.server.indexer.EntityListItemIndexer;
030import com.echothree.model.control.core.server.indexer.EntityTypeIndexer;
031import com.echothree.model.control.customer.server.indexer.CustomerIndexer;
032import com.echothree.model.control.employee.server.indexer.EmployeeIndexer;
033import com.echothree.model.control.forum.server.indexer.ForumMessageIndexer;
034import com.echothree.model.control.index.common.IndexTypes;
035import com.echothree.model.control.index.server.control.IndexControl;
036import com.echothree.model.control.index.server.indexer.BaseIndexer;
037import com.echothree.model.control.item.server.indexer.HarmonizedTariffScheduleCodeIndexer;
038import com.echothree.model.control.item.server.indexer.ItemIndexer;
039import com.echothree.model.control.offer.server.indexer.OfferIndexer;
040import com.echothree.model.control.offer.server.indexer.UseIndexer;
041import com.echothree.model.control.offer.server.indexer.UseTypeIndexer;
042import com.echothree.model.control.party.common.PartyTypes;
043import com.echothree.model.control.queue.common.QueueTypes;
044import com.echothree.model.control.queue.server.control.QueueControl;
045import com.echothree.model.control.queue.server.logic.QueueTypeLogic;
046import com.echothree.model.control.search.server.logic.SearchLogic;
047import com.echothree.model.control.security.server.indexer.SecurityRoleGroupIndexer;
048import com.echothree.model.control.security.server.indexer.SecurityRoleIndexer;
049import com.echothree.model.control.shipping.server.indexer.ShippingMethodIndexer;
050import com.echothree.model.control.vendor.server.indexer.VendorIndexer;
051import com.echothree.model.control.warehouse.server.indexer.WarehouseIndexer;
052import com.echothree.model.data.core.server.entity.EntityInstance;
053import com.echothree.model.data.core.server.entity.EntityType;
054import com.echothree.model.data.queue.common.QueuedEntityConstants;
055import com.echothree.model.data.queue.server.entity.QueueType;
056import com.echothree.model.data.queue.server.entity.QueuedEntity;
057import com.echothree.util.common.command.BaseResult;
058import com.echothree.util.common.transfer.Limit;
059import com.echothree.util.server.control.BaseSimpleCommand;
060import com.echothree.util.server.control.CommandSecurityDefinition;
061import com.echothree.util.server.control.PartyTypeDefinition;
062import com.echothree.util.server.persistence.PersistenceUtils;
063import com.echothree.util.server.persistence.Session;
064import com.echothree.util.server.persistence.ThreadSession;
065import static java.lang.Math.toIntExact;
066import java.util.ArrayList;
067import java.util.HashMap;
068import java.util.List;
069import java.util.Map;
070import java.util.Objects;
071import javax.enterprise.context.Dependent;
072import javax.enterprise.inject.spi.CDI;
073
074@Dependent
075public class UpdateIndexesCommand
076        extends BaseSimpleCommand<UpdateIndexesForm> {
077    
078    private final static CommandSecurityDefinition COMMAND_SECURITY_DEFINITION;
079    
080    static {
081        COMMAND_SECURITY_DEFINITION = new CommandSecurityDefinition(List.of(
082                new PartyTypeDefinition(PartyTypes.UTILITY.name(), null))
083        );
084    }
085    
086    /** Creates a new instance of UpdateIndexesCommand */
087    public UpdateIndexesCommand() {
088        super(COMMAND_SECURITY_DEFINITION, null, false);
089    }
090    
091    private static final int QUEUED_ENTITY_COUNT = 10;
092    private static final long MAXIMUM_MILLISECONDS = 40 * 1000; // 40 seconds, allows time to close indexes
093    
094    private void setLimits() {
095        var limits = new HashMap<String, Limit>(1);
096        
097        limits.put(QueuedEntityConstants.ENTITY_TYPE_NAME, new Limit(Integer.toString(QUEUED_ENTITY_COUNT), null));
098        session.setLimits(limits);
099    }
100    
101    private Map<EntityInstance, List<QueuedEntity>> getQueuedEntities(final QueueType queueType) {
102        var queueControl = Session.getModelController(QueueControl.class);
103        var queuedEntityMap = new HashMap<EntityInstance, List<QueuedEntity>>(QUEUED_ENTITY_COUNT);
104        var queuedEntities = queueControl.getQueuedEntitiesByQueueType(queueType);
105        
106        queuedEntities.stream().map(QueuedEntity::getEntityInstance).filter(
107                (entityInstance) -> !queuedEntityMap.containsKey(entityInstance)).forEach((entityInstance) -> {
108            var duplicateQueuedEntities = queueControl.getQueuedEntities(queueType, entityInstance);
109            
110            queuedEntityMap.put(entityInstance, duplicateQueuedEntities);
111        });
112        
113        return queuedEntityMap;
114    }
115    
116    private void setupIndexers(final IndexControl indexControl, final Map<EntityType, List<BaseIndexer<?>>> indexersMap, final EntityType entityType) {
117        var indexTypes = indexControl.getIndexTypesByEntityType(entityType);
118        var size = 0L;
119
120        size = indexTypes.stream().map(indexControl::countIndexesByIndexType).reduce(size, Long::sum);
121
122        var indexers = new ArrayList<BaseIndexer<?>>(toIntExact(size));
123
124        indexTypes.forEach((indexType) -> {
125            var indexes = indexControl.getIndexesByIndexType(indexType);
126            var indexTypeName = indexType.getLastDetail().getIndexTypeName();
127
128            indexes.stream().map((index) -> {
129                BaseIndexer<?> baseIndexer = null;
130
131                if(indexTypeName.equals(IndexTypes.CUSTOMER.name())) {
132                    baseIndexer = CDI.current().select(CustomerIndexer.class).get().setup(this, index);
133                } else if(indexTypeName.equals(IndexTypes.EMPLOYEE.name())) {
134                    baseIndexer = CDI.current().select(EmployeeIndexer.class).get().setup(this, index);
135                } else if(indexTypeName.equals(IndexTypes.VENDOR.name())) {
136                    baseIndexer = CDI.current().select(VendorIndexer.class).get().setup(this, index);
137                } else if(indexTypeName.equals(IndexTypes.ITEM.name())) {
138                    baseIndexer = CDI.current().select(ItemIndexer.class).get().setup(this, index);
139                } else if(indexTypeName.equals(IndexTypes.FORUM_MESSAGE.name())) {
140                    baseIndexer = CDI.current().select(ForumMessageIndexer.class).get().setup(this, index);
141                } else if(indexTypeName.equals(IndexTypes.COMPONENT_VENDOR.name())) {
142                    baseIndexer = CDI.current().select(ComponentVendorIndexer.class).get().setup(this, index);
143                } else if(indexTypeName.equals(IndexTypes.ENTITY_TYPE.name())) {
144                    baseIndexer = CDI.current().select(EntityTypeIndexer.class).get().setup(this, index);
145                } else if(indexTypeName.equals(IndexTypes.ENTITY_ALIAS_TYPE.name())) {
146                    baseIndexer = CDI.current().select(EntityAliasTypeIndexer.class).get().setup(this, index);
147                } else if(indexTypeName.equals(IndexTypes.ENTITY_ATTRIBUTE_GROUP.name())) {
148                    baseIndexer = CDI.current().select(EntityAttributeGroupIndexer.class).get().setup(this, index);
149                } else if(indexTypeName.equals(IndexTypes.ENTITY_ATTRIBUTE.name())) {
150                    baseIndexer = CDI.current().select(EntityAttributeIndexer.class).get().setup(this, index);
151                } else if(indexTypeName.equals(IndexTypes.ENTITY_LIST_ITEM.name())) {
152                    baseIndexer = CDI.current().select(EntityListItemIndexer.class).get().setup(this, index);
153                } else if(indexTypeName.equals(IndexTypes.CONTENT_CATALOG.name())) {
154                    baseIndexer = CDI.current().select(ContentCatalogIndexer.class).get().setup(this, index);
155                } else if(indexTypeName.equals(IndexTypes.CONTENT_CATALOG_ITEM.name())) {
156                    baseIndexer = CDI.current().select(ContentCatalogItemIndexer.class).get().setup(this, index);
157                } else if(indexTypeName.equals(IndexTypes.CONTENT_CATEGORY.name())) {
158                    baseIndexer = CDI.current().select(ContentCategoryIndexer.class).get().setup(this, index);
159                } else if(indexTypeName.equals(IndexTypes.SECURITY_ROLE_GROUP.name())) {
160                    baseIndexer = CDI.current().select(SecurityRoleGroupIndexer.class).get().setup(this, index);
161                } else if(indexTypeName.equals(IndexTypes.SECURITY_ROLE.name())) {
162                    baseIndexer = CDI.current().select(SecurityRoleIndexer.class).get().setup(this, index);
163                } else if(indexTypeName.equals(IndexTypes.HARMONIZED_TARIFF_SCHEDULE_CODE.name())) {
164                    baseIndexer = CDI.current().select(HarmonizedTariffScheduleCodeIndexer.class).get().setup(this, index);
165                } else if(indexTypeName.equals(IndexTypes.CONTACT_MECHANISM.name())) {
166                    baseIndexer = CDI.current().select(ContactMechanismIndexer.class).get().setup(this, index);
167                } else if(indexTypeName.equals(IndexTypes.OFFER.name())) {
168                    baseIndexer = CDI.current().select(OfferIndexer.class).get().setup(this, index);
169                } else if(indexTypeName.equals(IndexTypes.USE.name())) {
170                    baseIndexer = CDI.current().select(UseIndexer.class).get().setup(this, index);
171                } else if(indexTypeName.equals(IndexTypes.USE_TYPE.name())) {
172                    baseIndexer = CDI.current().select(UseTypeIndexer.class).get().setup(this, index);
173                } else if(indexTypeName.equals(IndexTypes.SHIPPING_METHOD.name())) {
174                    baseIndexer = CDI.current().select(ShippingMethodIndexer.class).get().setup(this, index);
175                } else if(indexTypeName.equals(IndexTypes.WAREHOUSE.name())) {
176                    baseIndexer = CDI.current().select(WarehouseIndexer.class).get().setup(this, index);
177                }
178
179                return baseIndexer;
180            }).filter(Objects::nonNull).peek(BaseIndexer::open).forEach(indexers::add);
181        });
182
183        indexersMap.put(entityType, indexers);
184    }
185    
186    private void indexQueuedEntity(final QueueControl queueControl, final Map<EntityType, List<BaseIndexer<?>>> indexersMap,
187            final Map.Entry<EntityInstance, List<QueuedEntity>> queuedEntityEntry) {
188        var entityInstance = queuedEntityEntry.getKey();
189        var entityType = entityInstance.getEntityType();
190        var baseIndexers = indexersMap.get(entityType);
191
192        log.info("indexing {}", PersistenceUtils.getInstance().getBasePKFromEntityInstance(entityInstance).toString());
193
194        for(var baseIndexer : baseIndexers) {
195            baseIndexer.updateIndex(entityInstance);
196
197            if(hasExecutionErrors()) {
198                break;
199            }
200        }
201
202        if(!hasExecutionErrors()) {
203            queuedEntityEntry.getValue().forEach(queueControl::removeQueuedEntity);
204        }
205    }
206    
207    private void closeIndexers(final QueueControl queueControl, final QueueType queueType, final Map<EntityType, List<BaseIndexer<?>>> indexersMap) {
208        indexersMap.forEach((key, value) -> value.stream().peek((baseIndexer) -> {
209            if(queueControl.countQueuedEntitiesByEntityType(queueType, baseIndexer.getEntityType()) == 0) {
210                SearchLogic.getInstance().invalidateCachedSearchesByIndex(baseIndexer.getIndex());
211            }
212        }).forEach(BaseIndexer::close));
213    }
214    
215    private void verifyIndexersAreSetup(final IndexControl indexControl, final Map<EntityType, List<BaseIndexer<?>>> indexersMap,
216            final Map<EntityInstance, List<QueuedEntity>> queuedEntityMap) {
217        for(var queuedEntityEntry : queuedEntityMap.entrySet()) {
218            var entityType = queuedEntityEntry.getKey().getEntityType();
219            
220            if(!indexersMap.containsKey(entityType)) {
221                setupIndexers(indexControl, indexersMap, entityType);
222            }
223            
224            if(hasExecutionErrors()) {
225                break;
226            }
227        }
228    }
229
230    private void indexQueuedEntities(final QueueControl queueControl, final Map<EntityType, List<BaseIndexer<?>>> indexersMap,
231            final Map<EntityInstance, List<QueuedEntity>> queuedEntityMap) {
232        try {
233            ThreadSession.pushSessionEntityCache();
234
235            for(var queuedEntityEntry : queuedEntityMap.entrySet()) {
236                indexQueuedEntity(queueControl, indexersMap, queuedEntityEntry);
237
238                if(hasExecutionErrors()) {
239                    break;
240                }
241            }
242        } finally {
243            ThreadSession.popSessionEntityCache();
244        }
245    }
246    
247    @Override
248    protected BaseResult execute() {
249        var result = IndexResultFactory.getUpdateIndexesResult();
250        var queueType = QueueTypeLogic.getInstance().getQueueTypeByName(this, QueueTypes.INDEXING.name());
251        var indexingComplete = false; // Indexing is only complete when we can absolutely verify it as being complete.
252        
253        if(!hasExecutionErrors()) {
254            var queueControl = Session.getModelController(QueueControl.class);
255            
256            indexingComplete = queueControl.countQueuedEntitiesByQueueType(queueType) == 0;
257            
258            // If there isn't anything in the queue, skip over all of this.
259            if(!indexingComplete) {
260                var indexControl = Session.getModelController(IndexControl.class);
261                var indexersMap = new HashMap<EntityType, List<BaseIndexer<?>>>(toIntExact(indexControl.countIndexes()));
262
263                try {
264                    var exitTime = session.getStartTime() + MAXIMUM_MILLISECONDS;
265
266                    setLimits();
267
268                    while(System.currentTimeMillis() < exitTime) {
269                        var queuedEntityMap = getQueuedEntities(queueType);
270
271                        // If there are no more to index, break out of here.
272                        if(queuedEntityMap.isEmpty()) {
273                            break;
274                        }
275                        
276                        // Make sure we have the indexers available for each EntityType we've found.
277                        verifyIndexersAreSetup(indexControl, indexersMap, queuedEntityMap);
278
279                        if(!hasExecutionErrors()) {
280                            indexQueuedEntities(queueControl, indexersMap, queuedEntityMap);
281                        }
282
283                        if(hasExecutionErrors()) {
284                            break;
285                        }
286                    }
287                } finally {
288                    closeIndexers(queueControl, queueType, indexersMap);
289                }
290
291                // Either the QueuedEntities have run out, or the time expired. Check to see which it is, and
292                // set indexingComplete to indicate if the QueuedEntities have run out.
293                indexingComplete = queueControl.countQueuedEntitiesByQueueType(queueType) == 0;
294            }
295        }    
296        
297        result.setIndexingComplete(indexingComplete);
298        
299        return result;
300    }
301
302}