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