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