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}