001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 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;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
024import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
025import ca.uhn.fhir.jpa.model.entity.ResourceTable;
026import ca.uhn.fhir.parser.IParser;
027import com.google.common.hash.HashCode;
028import com.google.common.hash.HashFunction;
029import com.google.common.hash.Hashing;
030import jakarta.annotation.Nonnull;
031import jakarta.annotation.Nullable;
032import org.apache.commons.lang3.StringUtils;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034
035import java.nio.charset.StandardCharsets;
036import java.util.Arrays;
037import java.util.List;
038
039/**
040 * Responsible for various resource history-centric and {@link FhirContext} aware operations called by
041 * {@link BaseHapiFhirDao} or {@link BaseHapiFhirResourceDao} that require knowledge of whether an Oracle database is
042 * being used.
043 */
044public class ResourceHistoryCalculator {
045        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceHistoryCalculator.class);
046        private static final HashFunction SHA_256 = Hashing.sha256();
047
048        private final FhirContext myFhirContext;
049        private final boolean myIsOracleDialect;
050
051        public ResourceHistoryCalculator(FhirContext theFhirContext, boolean theIsOracleDialect) {
052                myFhirContext = theFhirContext;
053                myIsOracleDialect = theIsOracleDialect;
054        }
055
056        ResourceHistoryState calculateResourceHistoryState(
057                        IBaseResource theResource, ResourceEncodingEnum theEncoding, List<String> theExcludeElements) {
058                final String encodedResource = encodeResource(theResource, theEncoding, theExcludeElements);
059                final byte[] resourceBinary;
060                final String resourceText;
061                final ResourceEncodingEnum encoding;
062                final HashCode hashCode;
063
064                if (myIsOracleDialect) {
065                        resourceText = null;
066                        resourceBinary = getResourceBinary(theEncoding, encodedResource);
067                        encoding = theEncoding;
068                        hashCode = SHA_256.hashBytes(resourceBinary);
069                } else {
070                        resourceText = encodedResource;
071                        resourceBinary = null;
072                        encoding = ResourceEncodingEnum.JSON;
073                        hashCode = SHA_256.hashUnencodedChars(encodedResource);
074                }
075
076                return new ResourceHistoryState(resourceText, resourceBinary, encoding, hashCode);
077        }
078
079        boolean conditionallyAlterHistoryEntity(
080                        ResourceTable theEntity, ResourceHistoryTable theHistoryEntity, String theResourceText) {
081                if (!myIsOracleDialect) {
082                        ourLog.debug(
083                                        "Storing text of resource {} version {} as inline VARCHAR",
084                                        theEntity.getResourceId(),
085                                        theHistoryEntity.getVersion());
086                        theHistoryEntity.setResourceTextVc(theResourceText);
087                        theHistoryEntity.setResource(null);
088                        theHistoryEntity.setEncoding(ResourceEncodingEnum.JSON);
089                        return true;
090                }
091
092                return false;
093        }
094
095        boolean isResourceHistoryChanged(
096                        ResourceHistoryTable theCurrentHistoryVersion,
097                        @Nullable byte[] theResourceBinary,
098                        @Nullable String resourceText) {
099                if (myIsOracleDialect) {
100                        return !Arrays.equals(theCurrentHistoryVersion.getResource(), theResourceBinary);
101                }
102
103                return !StringUtils.equals(theCurrentHistoryVersion.getResourceTextVc(), resourceText);
104        }
105
106        String encodeResource(
107                        IBaseResource theResource, ResourceEncodingEnum theEncoding, List<String> theExcludeElements) {
108                final IParser parser = theEncoding.newParser(myFhirContext);
109                parser.setDontEncodeElements(theExcludeElements);
110                return parser.encodeResourceToString(theResource);
111        }
112
113        /**
114         * helper for returning the encoded byte array of the input resource string based on the theEncoding.
115         *
116         * @param theEncoding        the theEncoding to used
117         * @param theEncodedResource the resource to encode
118         * @return byte array of the resource
119         */
120        @Nonnull
121        static byte[] getResourceBinary(ResourceEncodingEnum theEncoding, String theEncodedResource) {
122                switch (theEncoding) {
123                        case JSON:
124                                return theEncodedResource.getBytes(StandardCharsets.UTF_8);
125                        case JSONC:
126                                return GZipUtil.compress(theEncodedResource);
127                        default:
128                                return new byte[0];
129                }
130        }
131
132        void populateEncodedResource(
133                        EncodedResource theEncodedResource,
134                        String theEncodedResourceString,
135                        @Nullable byte[] theResourceBinary,
136                        ResourceEncodingEnum theEncoding) {
137                if (myIsOracleDialect) {
138                        populateEncodedResourceInner(theEncodedResource, null, theResourceBinary, theEncoding);
139                } else {
140                        populateEncodedResourceInner(theEncodedResource, theEncodedResourceString, null, ResourceEncodingEnum.JSON);
141                }
142        }
143
144        private void populateEncodedResourceInner(
145                        EncodedResource encodedResource,
146                        String encodedResourceString,
147                        byte[] theResourceBinary,
148                        ResourceEncodingEnum theEncoding) {
149                encodedResource.setResourceText(encodedResourceString);
150                encodedResource.setResourceBinary(theResourceBinary);
151                encodedResource.setEncoding(theEncoding);
152        }
153}