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