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