Skip to content

Commit 2a09917

Browse files
jamesagnewtadgh
andauthored
Add partial parsing support (#6810)
* Some parser improvements * Cleanup * Work on tokenization * Call presearch pointcut for conditional URL evaluation * Clean up changelog * Resolve fixmes * Changelogs * Add one more changelog * Test fix * Spotless * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_2_0/6810-call-presearch-interceptor-for-match-url-searches.yaml Co-authored-by: tadgh <[email protected]> * Address review comments * Build fix * Fix android issue * Test fix * Test fixes * Bump core * Test fixes * Bump corelib --------- Co-authored-by: tadgh <[email protected]>
1 parent 34e1206 commit 2a09917

File tree

39 files changed

+520
-75
lines changed

39 files changed

+520
-75
lines changed

hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import ca.uhn.fhir.util.UrlUtil;
5151
import com.google.common.base.Charsets;
5252
import jakarta.annotation.Nullable;
53+
import org.apache.commons.io.IOUtils;
5354
import org.apache.commons.io.output.StringBuilderWriter;
5455
import org.apache.commons.lang3.StringUtils;
5556
import org.apache.commons.lang3.Validate;
@@ -295,6 +296,20 @@ public void encodeToWriter(IBase theElement, Writer theWriter) throws DataFormat
295296
}
296297
}
297298

299+
@Override
300+
public void parseInto(Reader theSource, IBase theTarget) throws IOException {
301+
if (theTarget instanceof IPrimitiveType) {
302+
((IPrimitiveType<?>) theTarget).setValueAsString(IOUtils.toString(theSource));
303+
} else {
304+
doParseIntoComplexStructure(theSource, theTarget);
305+
}
306+
}
307+
308+
protected void doParseIntoComplexStructure(Reader theSource, IBase theTarget) {
309+
throw new InternalErrorException(
310+
Msg.code(2633) + "This " + getEncoding() + " parser does not support parsing non-resource values");
311+
}
312+
298313
protected void encodeResourceToWriter(IBaseResource theResource, Writer theWriter, EncodeContext theEncodeContext)
299314
throws IOException {
300315
Validate.notNull(theResource, "theResource can not be null");

hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/IParser.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
import ca.uhn.fhir.context.ConfigurationException;
2323
import ca.uhn.fhir.context.FhirContext;
2424
import ca.uhn.fhir.context.ParserOptions;
25+
import ca.uhn.fhir.i18n.Msg;
2526
import ca.uhn.fhir.model.api.IResource;
2627
import ca.uhn.fhir.rest.api.EncodingEnum;
28+
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
2729
import ca.uhn.fhir.util.CollectionUtil;
2830
import jakarta.annotation.Nonnull;
2931
import jakarta.annotation.Nullable;
@@ -35,6 +37,7 @@
3537
import java.io.IOException;
3638
import java.io.InputStream;
3739
import java.io.Reader;
40+
import java.io.StringReader;
3841
import java.io.Writer;
3942
import java.util.Collection;
4043
import java.util.List;
@@ -512,4 +515,53 @@ default IParser setEncodeElements(@Nonnull String... theEncodeElements) {
512515
* @see ParserOptions
513516
*/
514517
IParser setDontStripVersionsFromReferencesAtPaths(Collection<String> thePaths);
518+
519+
/**
520+
* Parses an object fragment into the given structure.
521+
*
522+
* @param theSource The source value to parse and use to populate {@literal theTarget}.
523+
* If {@literal theTarget} is an instance of {@link org.hl7.fhir.instance.model.api.IPrimitiveType}
524+
* the value is treated as a simple string value. So for example, when populating
525+
* a {@literal DateTimeTime}, the value should resemble
526+
* {@literal 2020-01-01}, not {@literal <birthDate value="2020-01-01"/>}.
527+
* If {@literal theTarget} is a complex structure, the value should be
528+
* a container element suitable for the parser's encoding. So for example,
529+
* if the target is an {@literal Identifier}, the value would be expected
530+
* to resemble {@literal <identifier><system value="..."/><value value="..."/></identifier>}
531+
* or <code>{"system":"...", "value":"..."}</code>.
532+
* @param theTarget The target structure to populate. Note that this structure is not
533+
* cleared automatically by the parser, so existing values will be
534+
* overwritten only if {@literal theSource} has a value for the
535+
* given element.
536+
* @since 8.2.0
537+
*/
538+
default void parseInto(String theSource, IBase theTarget) {
539+
try {
540+
parseInto(new StringReader(theSource), theTarget);
541+
} catch (IOException e) {
542+
throw new InternalErrorException(
543+
Msg.code(2634) + "Encountered IOException during read from - This should not happen!", e);
544+
}
545+
}
546+
547+
/**
548+
* Parses an object fragment into the given structure.
549+
*
550+
* @param theSource The source value to parse and use to populate {@literal theTarget}.
551+
* If {@literal theTarget} is an instance of {@link org.hl7.fhir.instance.model.api.IPrimitiveType}
552+
* the value is treated as a simple string value. So for example, when populating
553+
* a {@literal DateTimeTime}, the value should resemble
554+
* {@literal 2020-01-01}, not {@literal <birthDate value="2020-01-01"/>}.
555+
* If {@literal theTarget} is a complex structure, the value should be
556+
* a container element suitable for the parser's encoding. So for example,
557+
* if the target is an {@literal Identifier}, the value would be expected
558+
* to resemble {@literal <identifier><system value="..."/><value value="..."/></identifier>}
559+
* or <code>{"system":"...", "value":"..."}</code>.
560+
* @param theTarget The target structure to populate. Note that this structure is not
561+
* cleared automatically by the parser, so existing values will be
562+
* overwritten only if {@literal theSource} has a value for the
563+
* given element.
564+
* @since 8.2.0
565+
*/
566+
void parseInto(Reader theSource, IBase theTarget) throws IOException;
515567
}

hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,20 @@ protected void doEncodeToWriter(IBase theElement, Writer theWriter, EncodeContex
236236
eventWriter.close();
237237
}
238238

239+
@Override
240+
protected void doParseIntoComplexStructure(Reader theSource, IBase theTarget) {
241+
JsonLikeStructure jsonStructure = new JacksonStructure();
242+
jsonStructure.load(theSource);
243+
244+
ParserState<IBase> state =
245+
ParserState.getComplexObjectState(this, getContext(), getContext(), true, theTarget, getErrorHandler());
246+
state.enteringNewElement(null, null);
247+
248+
parseChildren(jsonStructure.getRootObject(), state);
249+
250+
state.endingElement();
251+
}
252+
239253
@Override
240254
public <T extends IBaseResource> T doParseResource(Class<T> theResourceType, Reader theReader) {
241255
JsonLikeStructure jsonStructure = new JacksonStructure();

hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import ca.uhn.fhir.util.FhirTerser;
5353
import ca.uhn.fhir.util.ReflectionUtil;
5454
import ca.uhn.fhir.util.XmlUtil;
55+
import jakarta.annotation.Nullable;
5556
import org.apache.commons.lang3.StringUtils;
5657
import org.apache.commons.lang3.Validate;
5758
import org.apache.commons.lang3.math.NumberUtils;
@@ -72,6 +73,7 @@
7273
import org.hl7.fhir.instance.model.api.IIdType;
7374
import org.hl7.fhir.instance.model.api.IPrimitiveType;
7475

76+
import java.io.Reader;
7577
import java.util.ArrayList;
7678
import java.util.Collections;
7779
import java.util.HashMap;
@@ -203,15 +205,17 @@ public IBase newInstance(RuntimeChildDeclaredExtensionDefinition theDefinition)
203205
}
204206

205207
public ICompositeType newCompositeInstance(
206-
BasePreResourceState thePreResourceState,
208+
@Nullable BasePreResourceState thePreResourceState,
207209
BaseRuntimeChildDefinition theChild,
208210
BaseRuntimeElementCompositeDefinition<?> theCompositeTarget) {
209211
ICompositeType retVal =
210212
(ICompositeType) theCompositeTarget.newInstance(theChild.getInstanceConstructorArguments());
211213
if (retVal instanceof IBaseReference) {
212214
IBaseReference ref = (IBaseReference) retVal;
213215
myGlobalReferences.add(ref);
214-
thePreResourceState.getLocalReferences().add(ref);
216+
if (thePreResourceState != null) {
217+
thePreResourceState.getLocalReferences().add(ref);
218+
}
215219
}
216220
return retVal;
217221
}
@@ -552,6 +556,36 @@ protected IBase getCurrentElement() {
552556
}
553557
}
554558

559+
/**
560+
* This state represents the start of parsing an arbitrary fragment
561+
* of a FHIR resource. In other words, it is the initial state when
562+
* {@link IParser#parseInto(Reader, IBase)} is called.
563+
*/
564+
private class PreElementCompositeState extends BaseState {
565+
566+
private final String myElementName;
567+
private final BaseRuntimeElementCompositeDefinition<?> myDef;
568+
private final IBase myInstance;
569+
570+
PreElementCompositeState(
571+
String theElementName, BaseRuntimeElementCompositeDefinition<?> theDef, IBase theInstance) {
572+
super(null);
573+
myElementName = theElementName;
574+
myDef = theDef;
575+
myInstance = theInstance;
576+
}
577+
578+
@Override
579+
public void enteringNewElement(String theNamespaceUri, String theLocalPart) throws DataFormatException {
580+
push(new ElementCompositeState(null, myElementName, myDef, myInstance));
581+
}
582+
583+
@Override
584+
public void endingElement() {
585+
pop();
586+
}
587+
}
588+
555589
private class ElementCompositeState extends BaseState {
556590

557591
private final BaseRuntimeElementCompositeDefinition<?> myDefinition;
@@ -1769,6 +1803,21 @@ public void doPop() {
17691803
}
17701804
}
17711805

1806+
static ParserState<IBase> getComplexObjectState(
1807+
IParser theParser,
1808+
FhirContext theContext,
1809+
FhirContext theFhirContext,
1810+
boolean theJsonMode,
1811+
IBase theTarget,
1812+
IParserErrorHandler theErrorHandler) {
1813+
ParserState<IBase> retVal = new ParserState<>(theParser, theContext, theJsonMode, theErrorHandler);
1814+
1815+
BaseRuntimeElementCompositeDefinition<?> def =
1816+
(BaseRuntimeElementCompositeDefinition<?>) theFhirContext.getElementDefinition(theTarget.getClass());
1817+
retVal.push(retVal.new PreElementCompositeState(def.getName(), def, theTarget));
1818+
return retVal;
1819+
}
1820+
17721821
/**
17731822
* @param theResourceType May be null
17741823
*/
@@ -1779,7 +1828,7 @@ static <T extends IBaseResource> ParserState<T> getPreResourceInstance(
17791828
boolean theJsonMode,
17801829
IParserErrorHandler theErrorHandler)
17811830
throws DataFormatException {
1782-
ParserState<T> retVal = new ParserState<T>(theParser, theContext, theJsonMode, theErrorHandler);
1831+
ParserState<T> retVal = new ParserState<>(theParser, theContext, theJsonMode, theErrorHandler);
17831832
if (theResourceType == null) {
17841833
if (theContext.getVersion().getVersion().isRi()) {
17851834
retVal.push(retVal.new PreResourceStateHl7Org(theResourceType));

hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
import org.hl7.fhir.instance.model.api.IIdType;
5959
import org.hl7.fhir.instance.model.api.IPrimitiveType;
6060

61-
import java.io.IOException;
6261
import java.io.Reader;
6362
import java.io.Writer;
6463
import java.util.ArrayList;
@@ -144,7 +143,7 @@ public void doEncodeResourceToWriter(IBaseResource theResource, Writer theWriter
144143

145144
@Override
146145
protected void doEncodeToWriter(IBase theElement, Writer theWriter, EncodeContext theEncodeContext)
147-
throws IOException, DataFormatException {
146+
throws DataFormatException {
148147
XMLStreamWriter eventWriter;
149148
try {
150149
eventWriter = createXmlWriter(theWriter);
@@ -165,6 +164,16 @@ protected void doEncodeToWriter(IBase theElement, Writer theWriter, EncodeContex
165164
}
166165
}
167166

167+
@Override
168+
protected void doParseIntoComplexStructure(Reader theSource, IBase theTarget) {
169+
XMLEventReader streamReader = createStreamReader(theSource);
170+
171+
ParserState<IBase> state = ParserState.getComplexObjectState(
172+
this, getContext(), getContext(), false, theTarget, getErrorHandler());
173+
174+
doXmlLoop(streamReader, state);
175+
}
176+
168177
@Override
169178
public <T extends IBaseResource> T doParseResource(Class<T> theResourceType, Reader theReader) {
170179
XMLEventReader streamReader = createStreamReader(theReader);
@@ -265,7 +274,7 @@ private <T> T doXmlLoop(XMLEventReader streamReader, ParserState<T> parserState)
265274
}
266275
return parserState.getObject();
267276
} catch (XMLStreamException e) {
268-
throw new DataFormatException(Msg.code(1852) + e);
277+
throw new DataFormatException(Msg.code(1852) + "Failed to parse XML content: " + e.getMessage());
269278
}
270279
}
271280

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* #%L
3-
* HAPI FHIR JPA Server
3+
* HAPI FHIR - Core Library
44
* %%
55
* Copyright (C) 2014 - 2025 Smile CDR, Inc.
66
* %%
@@ -17,9 +17,7 @@
1717
* limitations under the License.
1818
* #L%
1919
*/
20-
package ca.uhn.fhir.jpa.util;
21-
22-
import ca.uhn.fhir.util.CoverageIgnore;
20+
package ca.uhn.fhir.util;
2321

2422
public class LogicUtil {
2523

hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ResourceUtil.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
public class ResourceUtil {
3333

3434
private static final String ENCODING = "ENCODING_TYPE";
35-
private static final String RAW_ = "RAW_%s";
35+
private static final String RAW_ = "RAW_";
3636

3737
private ResourceUtil() {}
3838

@@ -77,6 +77,6 @@ public static String getRawStringFromResourceOrNull(@Nonnull IBaseResource theRe
7777
}
7878

7979
private static String getRawUserDataKey(EncodingEnum theEncodingEnum) {
80-
return String.format(RAW_, theEncodingEnum.name());
80+
return RAW_ + theEncodingEnum.name();
8181
}
8282
}

hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/8_0_0/6469-reduce-memory-overhead-for-searches.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,5 @@ type: perf
33
issue: 6469
44
title: "Searching for a large number of resources can use a lot of
55
memory, due to the nature of deduplication of results in memory.
6-
We will instead push this responsibility to the db to save
7-
reduce this overhead.
6+
We will instead push this responsibility to the db to reduce this overhead.
87
"
File renamed without changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
type: add
3+
issue: 6810
4+
title: "The Pointcut `STORAGE_PRESEARCH_REGISTERED` will now also be called before the
5+
internal FHIR searches being performed in order to resolve conditional create/update/etc
6+
URLs when processing FHIR transactions. Previously this pointcut was not called for
7+
these specific searches."

0 commit comments

Comments
 (0)