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}