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}