diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java index 711eb746f67..60aebe8b2c4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java @@ -101,10 +101,12 @@ public class FhirContext { private static final Map ourStaticContexts = Collections.synchronizedMap(new EnumMap<>(FhirVersionEnum.class)); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirContext.class); private final IFhirVersion myVersion; + private final Map> myDefaultTypeForProfile = new HashMap<>(); + private final Set myPerformanceOptions = new HashSet<>(); + private final Collection> myResourceTypesToScan; private AddProfileTagEnum myAddProfileTagWhenEncoding = AddProfileTagEnum.ONLY_FOR_CUSTOM; private volatile Map, BaseRuntimeElementDefinition> myClassToElementDefinition = Collections.emptyMap(); private ArrayList> myCustomTypes; - private final Map> myDefaultTypeForProfile = new HashMap<>(); private volatile Map myIdToResourceDefinition = Collections.emptyMap(); private volatile boolean myInitialized; private volatile boolean myInitializing = false; @@ -115,13 +117,14 @@ public class FhirContext { private volatile INarrativeGenerator myNarrativeGenerator; private volatile IParserErrorHandler myParserErrorHandler = new LenientErrorHandler(); private ParserOptions myParserOptions = new ParserOptions(); - private final Set myPerformanceOptions = new HashSet<>(); - private final Collection> myResourceTypesToScan; private volatile IRestfulClientFactory myRestfulClientFactory; private volatile RuntimeChildUndeclaredExtensionDefinition myRuntimeChildUndeclaredExtensionDefinition; private IValidationSupport myValidationSupport; private Map>> myVersionToNameToResourceType = Collections.emptyMap(); private volatile Set myResourceNames; + private volatile Boolean myFormatXmlSupported; + private volatile Boolean myFormatJsonSupported; + private volatile Boolean myFormatRdfSupported; /** * @deprecated It is recommended that you use one of the static initializer methods instead @@ -673,6 +676,51 @@ public class FhirContext { return !myDefaultTypeForProfile.isEmpty(); } + /** + * @return Returns true if the XML serialization format is supported, based on the + * available libraries on the classpath. + * + * @since 5.4.0 + */ + public boolean isFormatXmlSupported() { + Boolean retVal = myFormatXmlSupported; + if (retVal == null) { + retVal = tryToInitParser(() -> newXmlParser()); + myFormatXmlSupported = retVal; + } + return retVal; + } + + /** + * @return Returns true if the JSON serialization format is supported, based on the + * available libraries on the classpath. + * + * @since 5.4.0 + */ + public boolean isFormatJsonSupported() { + Boolean retVal = myFormatJsonSupported; + if (retVal == null) { + retVal = tryToInitParser(() -> newJsonParser()); + myFormatJsonSupported = retVal; + } + return retVal; + } + + /** + * @return Returns true if the RDF serialization format is supported, based on the + * available libraries on the classpath. + * + * @since 5.4.0 + */ + public boolean isFormatRdfSupported() { + Boolean retVal = myFormatRdfSupported; + if (retVal == null) { + retVal = tryToInitParser(() -> newRDFParser()); + myFormatRdfSupported = retVal; + } + return retVal; + } + public IVersionSpecificBundleFactory newBundleFactory() { return myVersion.newBundleFactory(this); } @@ -735,7 +783,6 @@ public class FhirContext { * Performance Note: This method is cheap to call, and may be called once for every message being processed * without incurring any performance penalty *

- * */ public IParser newRDFParser() { return new RDFParser(this, myParserErrorHandler, Lang.TURTLE); @@ -989,6 +1036,24 @@ public class FhirContext { return "FhirContext[" + myVersion.getVersion().name() + "]"; } + // TODO KHS add the other primitive types + public IPrimitiveType getPrimitiveBoolean(Boolean theValue) { + IPrimitiveType retval = (IPrimitiveType) getElementDefinition("boolean").newInstance(); + retval.setValue(theValue); + return retval; + } + + private static boolean tryToInitParser(Runnable run) { + boolean retVal; + try { + run.run(); + retVal = true; + } catch (Exception | NoClassDefFoundError e) { + retVal = false; + } + return retVal; + } + /** * Creates and returns a new FhirContext with version {@link FhirVersionEnum#DSTU2 DSTU2} */ @@ -1066,11 +1131,4 @@ public class FhirContext { } return retVal; } - - // TODO KHS add the other primitive types - public IPrimitiveType getPrimitiveBoolean(Boolean theValue) { - IPrimitiveType retval = (IPrimitiveType) getElementDefinition("boolean").newInstance(); - retval.setValue(theValue); - return retval; - } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java index 45f4f2833e5..e21040f888d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/executor/BaseInterceptorService.java @@ -142,16 +142,16 @@ public abstract class BaseInterceptorService impleme @VisibleForTesting public void unregisterAllInterceptors() { synchronized (myRegistryMutex) { - myAnonymousInvokers.clear(); - myGlobalInvokers.clear(); - myInterceptors.clear(); + unregisterInterceptors(myAnonymousInvokers.values()); + unregisterInterceptors(myGlobalInvokers.values()); + unregisterInterceptors(myInterceptors); } } @Override public void unregisterInterceptors(@Nullable Collection theInterceptors) { if (theInterceptors != null) { - theInterceptors.forEach(t -> unregisterInterceptor(t)); + new ArrayList<>(theInterceptors).forEach(t -> unregisterInterceptor(t)); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/CacheControlDirective.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/CacheControlDirective.java index bab5fbade0d..08dcf86d89d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/CacheControlDirective.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/CacheControlDirective.java @@ -125,4 +125,11 @@ public class CacheControlDirective { return this; } + + /** + * Convenience factory method for a no-cache directivel + */ + public static CacheControlDirective noCache() { + return new CacheControlDirective().setNoCache(true); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index a7604ec3a02..32701bf0419 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -85,7 +85,8 @@ public class Constants { public static final String FORMAT_HTML = "html"; public static final String FORMAT_JSON = "json"; public static final String FORMAT_XML = "xml"; - public static final String FORMAT_TURTLE = "text/turtle"; + public static final String CT_RDF_TURTLE_LEGACY = "text/turtle"; + public static final String FORMAT_TURTLE = "ttl"; /** @@ -94,6 +95,7 @@ public class Constants { public static final Set FORMATS_HTML; public static final String FORMATS_HTML_JSON = "html/json"; public static final String FORMATS_HTML_XML = "html/xml"; + public static final String FORMATS_HTML_TTL = "html/turtle"; public static final String HEADER_ACCEPT = "Accept"; public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding"; public static final String HEADER_ACCEPT_VALUE_JSON_NON_LEGACY = CT_FHIR_JSON_NEW + ";q=1.0, " + CT_FHIR_JSON + ";q=0.9"; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/EncodingEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/EncodingEnum.java index 905e6b0b7ed..6196202a1a7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/EncodingEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/EncodingEnum.java @@ -46,7 +46,7 @@ public enum EncodingEnum { } }, - RDF(Constants.CT_RDF_TURTLE, Constants.CT_RDF_TURTLE, Constants.FORMAT_TURTLE) { + RDF(Constants.CT_RDF_TURTLE_LEGACY, Constants.CT_RDF_TURTLE, Constants.FORMAT_TURTLE) { @Override public IParser newParser(FhirContext theContext) { return theContext.newRDFParser(); @@ -114,6 +114,7 @@ public enum EncodingEnum { ourContentTypeToEncoding.put(JSON_PLAIN_STRING, JSON); ourContentTypeToEncoding.put(XML_PLAIN_STRING, XML); ourContentTypeToEncoding.put(RDF_PLAIN_STRING, RDF); + ourContentTypeToEncoding.put(Constants.FORMAT_TURTLE, RDF); ourContentTypeToEncodingLegacy = Collections.unmodifiableMap(ourContentTypeToEncodingLegacy); diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2521-add-revincludes-to-capabilitystatement.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2521-add-revincludes-to-capabilitystatement.yaml new file mode 100644 index 00000000000..7db2dda5eee --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2521-add-revincludes-to-capabilitystatement.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 2521 +title: "The automatically generated CapabilityStatement for R4+ will now incude the list of supported + revinclude values." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2521-add-ttl-to-capabilitystatement.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2521-add-ttl-to-capabilitystatement.yaml new file mode 100644 index 00000000000..8fb164dbb69 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2521-add-ttl-to-capabilitystatement.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 2521 +title: "The server generated CapabilityStatement now reflects whether RDF/Turtle is supported by + the server. In addition, the ResponseHighlightingInterceptor will now provide some TTL support." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BaseBulkItemReader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BaseBulkItemReader.java index 89bd4a345f5..9ab5e56a75d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BaseBulkItemReader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BaseBulkItemReader.java @@ -67,10 +67,10 @@ public abstract class BaseBulkItemReader implements ItemReader formats = cs + .getFormat() + .stream() + .map(t -> t.getCode()) + .collect(Collectors.toList()); + assertThat(formats.toString(), formats, containsInAnyOrder( + "application/fhir+xml", + "application/fhir+json", + "json", + "xml", + "html/xml", + "html/json")); + } + + @Test + public void testCustomSearchParamsReflectedInIncludesAndRevIncludes_TargetSpecified() { + SearchParameter fooSp = new SearchParameter(); + fooSp.addBase("Observation"); + fooSp.setCode("foo"); + fooSp.setUrl("http://acme.com/foo"); + fooSp.setType(Enumerations.SearchParamType.REFERENCE); + fooSp.setTitle("FOO SP"); + fooSp.setDescription("This is a search param!"); + fooSp.setExpression("Observation.subject"); + fooSp.addTarget("Patient"); + fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL); + fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE); + mySearchParameterDao.create(fooSp); + mySearchParamRegistry.forceRefresh(); + + ourCapabilityStatementProvider.setRestResourceRevIncludesEnabled(true); + + CapabilityStatement cs = myClient + .capabilities() + .ofType(CapabilityStatement.class) + .cacheControl(CacheControlDirective.noCache()) + .execute(); + + List includes = findIncludes(cs, "Patient"); + assertThat(includes.toString(), includes, contains("*", "Patient:general-practitioner", "Patient:link", "Patient:organization")); + + includes = findIncludes(cs, "Observation"); + assertThat(includes.toString(), includes, contains("*", "Observation:based-on", "Observation:derived-from", "Observation:device", "Observation:encounter", "Observation:focus", "Observation:foo", "Observation:has-member", "Observation:part-of", "Observation:patient", "Observation:performer", "Observation:specimen", "Observation:subject")); + + List revIncludes = findRevIncludes(cs, "Patient"); + assertThat(revIncludes.toString(), revIncludes, hasItems( + "Account:patient", // Standard SP reference + "Observation:foo", // Standard SP reference with no explicit target + "Provenance:entity" // Reference in custom SP + )); + assertThat(revIncludes.toString(), revIncludes, not(hasItem( + "CarePlan:based-on" // Standard SP reference with non-matching target + ))); + + } + + @Nonnull + private List findIncludes(CapabilityStatement theCapabilityStatement, String theResourceName) { + return theCapabilityStatement + .getRest() + .stream() + .flatMap(t -> t.getResource().stream()) + .filter(t -> t.getType().equals(theResourceName)) + .flatMap(t -> t.getSearchInclude().stream()) + .map(t -> t.getValue()) + .collect(Collectors.toList()); + } + + @Nonnull + private List findRevIncludes(CapabilityStatement theCapabilityStatement, String theResourceName) { + return theCapabilityStatement + .getRest() + .stream() + .flatMap(t -> t.getResource().stream()) + .filter(t -> t.getType().equals(theResourceName)) + .flatMap(t -> t.getSearchRevInclude().stream()) + .map(t -> t.getValue()) + .collect(Collectors.toList()); + } + @Test public void testRegisteredProfilesReflected_StoredInServer() throws IOException { StructureDefinition sd = loadResourceFromClasspath(StructureDefinition.class, "/r4/StructureDefinition-kfdrc-patient.json"); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index ce08b3f01b5..7dafdd74610 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -46,6 +46,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import javax.annotation.Nonnull; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.ArrayList; @@ -94,6 +95,7 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC } } + @Nonnull @Override public Map getActiveSearchParams(String theResourceName) { requiresActiveSearchParams(); @@ -122,7 +124,7 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceC params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT); IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params); - int size = allSearchParamsBp.size(); + int size = allSearchParamsBp.sizeOrThrowNpe(); ourLog.trace("Loaded {} search params from the DB", size); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index f488c3e7ec1..51439570c7f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -322,14 +322,32 @@ public class RestfulServer extends HttpServlet implements IRestfulServert.getMethodBindings().stream()) + .forEach(t->t.close()); + myGlobalBinding + .getMethodBindings() + .forEach(t->t.close()); + myServerBinding + .getMethodBindings() + .forEach(t->t.close()); + } /** diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java index f97c5834263..c7bc35e1811 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerConfiguration.java @@ -31,12 +31,14 @@ import ca.uhn.fhir.rest.server.method.SearchParameter; import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; import ca.uhn.fhir.util.VersionUtil; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -278,7 +280,7 @@ public class RestfulServerConfiguration implements ISearchParamRetriever { } public Map>> collectMethodBindings() { - Map>> resourceToMethods = new TreeMap>>(); + Map>> resourceToMethods = new TreeMap<>(); for (ResourceBinding next : getResourceBindings()) { String resourceName = next.getResourceName(); for (BaseMethodBinding nextMethodBinding : next.getMethodBindings()) { @@ -365,14 +367,15 @@ public class RestfulServerConfiguration implements ISearchParamRetriever { } @Override - public Map getActiveSearchParams(String theResourceName) { + public Map getActiveSearchParams(@Nonnull String theResourceName) { + Validate.notBlank(theResourceName, "theResourceName must not be null or blank"); Map retVal = new LinkedHashMap<>(); collectMethodBindings() .getOrDefault(theResourceName, Collections.emptyList()) .stream() - .filter(t -> t.getResourceName().equals(theResourceName)) + .filter(t -> theResourceName.equals(t.getResourceName())) .filter(t -> t instanceof SearchMethodBinding) .map(t -> (SearchMethodBinding) t) .filter(t -> t.getQueryName() == null) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java index a9fb72e69b0..fef13edab8f 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java @@ -18,23 +18,35 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding; +import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.UrlUtil; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.text.StringEscapeUtils; import org.hl7.fhir.instance.model.api.IBaseBinary; +import org.hl7.fhir.instance.model.api.IBaseConformance; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; -import java.util.*; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; -import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.apache.commons.lang3.StringUtils.trim; /* * #%L @@ -71,10 +83,10 @@ public class ResponseHighlighterInterceptor { */ public static final String PARAM_RAW = "_raw"; public static final String PARAM_RAW_TRUE = "true"; - public static final String PARAM_TRUE = "true"; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlighterInterceptor.class); private static final String[] PARAM_FORMAT_VALUE_JSON = new String[]{Constants.FORMAT_JSON}; private static final String[] PARAM_FORMAT_VALUE_XML = new String[]{Constants.FORMAT_XML}; + private static final String[] PARAM_FORMAT_VALUE_TTL = new String[]{Constants.FORMAT_TURTLE}; private boolean myShowRequestHeaders = false; private boolean myShowResponseHeaders = true; @@ -128,6 +140,9 @@ public class ResponseHighlighterInterceptor { boolean inValue = false; boolean inQuote = false; boolean inTag = false; + boolean inTurtleDirective = false; + boolean startingLineNext = true; + boolean startingLine = false; int lineCount = 1; for (int i = 0; i < str.length(); i++) { @@ -140,13 +155,23 @@ public class ResponseHighlighterInterceptor { char nextChar6 = (i + 5) < str.length() ? str.charAt(i + 5) : ' '; if (nextChar == '\n') { + if (inTurtleDirective) { + theTarget.append(""); + inTurtleDirective = false; + } lineCount++; theTarget.append("
"); + startingLineNext = true; continue; + } else if (startingLineNext) { + startingLineNext = false; + startingLine = true; + } else { + startingLine = false; } if (theEncodingEnum == EncodingEnum.JSON) { @@ -194,7 +219,45 @@ public class ResponseHighlighterInterceptor { } } + } else if (theEncodingEnum == EncodingEnum.RDF) { + + if (inQuote) { + theTarget.append(nextChar); + if (prevChar != '\\' && nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') { + theTarget.append("quot;"); + i += 5; + inQuote = false; + } else if (nextChar == '\\' && nextChar2 == '"') { + theTarget.append("quot;"); + i += 5; + inQuote = false; + } + } else if (startingLine && nextChar == '@') { + inTurtleDirective = true; + theTarget.append(""); + theTarget.append(nextChar); + } else if (startingLine) { + inTurtleDirective = true; + theTarget.append(""); + theTarget.append(nextChar); + } else if (nextChar == '[' || nextChar == ']' || nextChar == ';' || nextChar == ':') { + theTarget.append(""); + theTarget.append(nextChar); + theTarget.append(""); + } else { + if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') { + theTarget.append("""); + inQuote = true; + i += 5; + } else { + theTarget.append(nextChar); + } + } + } else { + + // Ok it's XML + if (inQuote) { theTarget.append(nextChar); if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') { @@ -345,6 +408,26 @@ public class ResponseHighlighterInterceptor { return false; } + @Hook(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED) + public void capabilityStatementGenerated(RequestDetails theRequestDetails, IBaseConformance theCapabilityStatement) { + FhirTerser terser = theRequestDetails.getFhirContext().newTerser(); + + Set formats = terser.getValues(theCapabilityStatement, "format", IPrimitiveType.class) + .stream() + .map(t -> t.getValueAsString()) + .collect(Collectors.toSet()); + addFormatConditionally(theCapabilityStatement, terser, formats, Constants.CT_FHIR_JSON_NEW, Constants.FORMATS_HTML_JSON); + addFormatConditionally(theCapabilityStatement, terser, formats, Constants.CT_FHIR_XML_NEW, Constants.FORMATS_HTML_XML); + addFormatConditionally(theCapabilityStatement, terser, formats, Constants.CT_RDF_TURTLE, Constants.FORMATS_HTML_TTL); + } + + private void addFormatConditionally(IBaseConformance theCapabilityStatement, FhirTerser terser, Set formats, String wanted, String toAdd) { + if (formats.contains(wanted)) { + terser.addElement(theCapabilityStatement, "format", toAdd); + } + } + + private boolean handleOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse, String theGraphqlResponse, IBaseResource theResourceResponse) { /* * Request for _raw @@ -373,6 +456,9 @@ public class ResponseHighlighterInterceptor { } else if (Constants.FORMATS_HTML_JSON.equals(formatParam)) { force = true; theRequestDetails.addParameter(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_JSON); + } else if (Constants.FORMATS_HTML_TTL.equals(formatParam)) { + force = true; + theRequestDetails.addParameter(Constants.PARAM_FORMAT, PARAM_FORMAT_VALUE_TTL); } else { return true; } @@ -542,8 +628,6 @@ public class ResponseHighlighterInterceptor { outputBuffer.append(" position: relative;\n"); outputBuffer.append("}"); outputBuffer.append(".responseBodyTableFirstColumn {"); -// outputBuffer.append(" position: absolute;\n"); -// outputBuffer.append(" width: 70px;\n"); outputBuffer.append("}"); outputBuffer.append(".responseBodyTableSecondColumn {"); outputBuffer.append(" position: absolute;\n"); @@ -589,24 +673,47 @@ public class ResponseHighlighterInterceptor { outputBuffer.append("This result is being rendered in HTML for easy viewing. "); outputBuffer.append("You may access this content as "); - outputBuffer.append("Raw JSON or "); + if (theRequestDetails.getFhirContext().isFormatJsonSupported()) { + outputBuffer.append("Raw JSON or "); + } - outputBuffer.append("Raw XML, "); + if (theRequestDetails.getFhirContext().isFormatXmlSupported()) { + outputBuffer.append("Raw XML or "); + } - outputBuffer.append(" or view this content in "); + if (theRequestDetails.getFhirContext().isFormatRdfSupported()) { + outputBuffer.append("Raw Turtle or "); + } - outputBuffer.append("HTML JSON "); + outputBuffer.append("view this content in "); - outputBuffer.append("or "); - outputBuffer.append("HTML XML."); + if (theRequestDetails.getFhirContext().isFormatJsonSupported()) { + outputBuffer.append("HTML JSON "); + } + + if (theRequestDetails.getFhirContext().isFormatXmlSupported()) { + outputBuffer.append("or "); + outputBuffer.append("HTML XML "); + } + + if (theRequestDetails.getFhirContext().isFormatRdfSupported()) { + outputBuffer.append("or "); + outputBuffer.append("HTML Turtle "); + } + + outputBuffer.append("."); } Date startTime = (Date) theServletRequest.getAttribute(RestfulServer.REQUEST_START_TIME); @@ -680,7 +787,7 @@ public class ResponseHighlighterInterceptor { outputBuffer.append("\n"); InputStream jsStream = ResponseHighlighterInterceptor.class.getResourceAsStream("ResponseHighlighter.js"); - String jsStr = jsStream != null ? IOUtils.toString(jsStream, "UTF-8") : "console.log('ResponseHighlighterInterceptor: javascript theResource not found')"; + String jsStr = jsStream != null ? IOUtils.toString(jsStream, StandardCharsets.UTF_8) : "console.log('ResponseHighlighterInterceptor: javascript theResource not found')"; jsStr = jsStr.replace("FHIR_BASE", theRequestDetails.getServerBaseForRequest()); outputBuffer.append("