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}