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