001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.dao.index;
021
022import ca.uhn.fhir.jpa.model.entity.BaseResourceIndex;
023import ca.uhn.fhir.jpa.model.entity.ResourceTable;
024import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
025import ca.uhn.fhir.jpa.util.AddRemoveCount;
026import com.google.common.annotations.VisibleForTesting;
027import org.springframework.stereotype.Service;
028
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.HashSet;
032import java.util.List;
033import javax.persistence.EntityManager;
034import javax.persistence.PersistenceContext;
035import javax.persistence.PersistenceContextType;
036
037@Service
038public class DaoSearchParamSynchronizer {
039        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
040        protected EntityManager myEntityManager;
041
042        public AddRemoveCount synchronizeSearchParamsToDatabase(
043                        ResourceIndexedSearchParams theParams,
044                        ResourceTable theEntity,
045                        ResourceIndexedSearchParams existingParams) {
046                AddRemoveCount retVal = new AddRemoveCount();
047
048                synchronize(theEntity, retVal, theParams.myStringParams, existingParams.myStringParams);
049                synchronize(theEntity, retVal, theParams.myTokenParams, existingParams.myTokenParams);
050                synchronize(theEntity, retVal, theParams.myNumberParams, existingParams.myNumberParams);
051                synchronize(theEntity, retVal, theParams.myQuantityParams, existingParams.myQuantityParams);
052                synchronize(theEntity, retVal, theParams.myQuantityNormalizedParams, existingParams.myQuantityNormalizedParams);
053                synchronize(theEntity, retVal, theParams.myDateParams, existingParams.myDateParams);
054                synchronize(theEntity, retVal, theParams.myUriParams, existingParams.myUriParams);
055                synchronize(theEntity, retVal, theParams.myCoordsParams, existingParams.myCoordsParams);
056                synchronize(theEntity, retVal, theParams.myLinks, existingParams.myLinks);
057                synchronize(theEntity, retVal, theParams.myComboTokenNonUnique, existingParams.myComboTokenNonUnique);
058
059                // make sure links are indexed
060                theEntity.setResourceLinks(theParams.myLinks);
061
062                return retVal;
063        }
064
065        @VisibleForTesting
066        public void setEntityManager(EntityManager theEntityManager) {
067                myEntityManager = theEntityManager;
068        }
069
070        private <T extends BaseResourceIndex> void synchronize(
071                        ResourceTable theEntity,
072                        AddRemoveCount theAddRemoveCount,
073                        Collection<T> theNewParams,
074                        Collection<T> theExistingParams) {
075                Collection<T> newParams = theNewParams;
076                for (T next : newParams) {
077                        next.setPartitionId(theEntity.getPartitionId());
078                        next.calculateHashes();
079                }
080
081                /*
082                 * HashCodes may have changed as a result of setting the partition ID, so
083                 * create a new set that will reflect the new hashcodes
084                 */
085                newParams = new HashSet<>(newParams);
086
087                List<T> paramsToRemove = subtract(theExistingParams, newParams);
088                List<T> paramsToAdd = subtract(newParams, theExistingParams);
089                tryToReuseIndexEntities(paramsToRemove, paramsToAdd);
090
091                for (T next : paramsToRemove) {
092                        myEntityManager.remove(next);
093                        theEntity.getParamsQuantity().remove(next);
094                        theEntity.getParamsQuantityNormalized().remove(next);
095                }
096                for (T next : paramsToAdd) {
097                        myEntityManager.merge(next);
098                }
099
100                // TODO:  are there any unintended consequences to fixing this bug?
101                theAddRemoveCount.addToAddCount(paramsToAdd.size());
102                theAddRemoveCount.addToRemoveCount(paramsToRemove.size());
103        }
104
105        /**
106         * The logic here is that often times when we update a resource we are dropping
107         * one index row and adding another. This method tries to reuse rows that would otherwise
108         * have been deleted by updating them with the contents of rows that would have
109         * otherwise been added. In other words, we're trying to replace
110         * "one delete + one insert" with "one update"
111         *
112         * @param theIndexesToRemove The rows that would be removed
113         * @param theIndexesToAdd    The rows that would be added
114         */
115        private <T extends BaseResourceIndex> void tryToReuseIndexEntities(
116                        List<T> theIndexesToRemove, List<T> theIndexesToAdd) {
117                for (int addIndex = 0; addIndex < theIndexesToAdd.size(); addIndex++) {
118
119                        // If there are no more rows to remove, there's nothing we can reuse
120                        if (theIndexesToRemove.isEmpty()) {
121                                break;
122                        }
123
124                        T targetEntity = theIndexesToAdd.get(addIndex);
125                        if (targetEntity.getId() != null) {
126                                continue;
127                        }
128
129                        // Take a row we were going to remove, and repurpose its ID
130                        T entityToReuse = theIndexesToRemove.remove(theIndexesToRemove.size() - 1);
131                        entityToReuse.copyMutableValuesFrom(targetEntity);
132                        theIndexesToAdd.set(addIndex, entityToReuse);
133                }
134        }
135
136        public static <T> List<T> subtract(Collection<T> theSubtractFrom, Collection<T> theToSubtract) {
137                assert theSubtractFrom != theToSubtract || (theSubtractFrom.isEmpty());
138
139                if (theSubtractFrom.isEmpty()) {
140                        return new ArrayList<>();
141                }
142
143                ArrayList<T> retVal = new ArrayList<>();
144                for (T next : theSubtractFrom) {
145                        if (!theToSubtract.contains(next)) {
146                                retVal.add(next);
147                        }
148                }
149                return retVal;
150        }
151}