Startup and Testpage Overlay tweaks (#6578)

* Startup and Testpage Overlay tweaks

* Add changelog

* Account for review comments

* Test fix
This commit is contained in:
James Agnew 2024-12-28 17:08:41 -05:00 committed by GitHub
parent 4feb489735
commit 24abd6bd65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 229 additions and 245 deletions

View File

@ -23,6 +23,11 @@ indent_style = tab
tab_width = 3
indent_size = 3
[*.js]
indent_style = tab
tab_width = 3
indent_size = 3
[*.vm]
indent_style = tab
tab_width = 3

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 6578
title: "The search parameter picker in the Testpage Overlay module has been adjusted to
correct several display issues which appeared after the upgrade from Bootstrap 4 to
Bootstrap 5."

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 6578
title: "When starting up the JPA server under heavy load, the validation support cache
could perform a large number of identical parallel queries. A synchronization guard
has been placed around the cache loader to avoid this."

View File

@ -31,6 +31,8 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.StringUtil;
import ca.uhn.fhir.util.VersionUtil;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
@ -78,7 +80,7 @@ import static ca.uhn.fhir.util.MessageSupplier.msg;
public class GraphQLProviderWithIntrospection extends GraphQLProvider {
private static final Logger ourLog = LoggerFactory.getLogger(GraphQLProviderWithIntrospection.class);
private final GraphQLSchemaGenerator myGenerator;
private final Supplier<GraphQLSchemaGenerator> myGenerator;
private final ISearchParamRegistry mySearchParamRegistry;
private final VersionSpecificWorkerContextWrapper myContext;
private final IDaoRegistry myDaoRegistry;
@ -99,12 +101,16 @@ public class GraphQLProviderWithIntrospection extends GraphQLProvider {
myDaoRegistry = theDaoRegistry;
myContext = VersionSpecificWorkerContextWrapper.newVersionSpecificWorkerContextWrapper(theValidationSupport);
myGenerator = new GraphQLSchemaGenerator(myContext, VersionUtil.getVersion());
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Collections.emptyList().getClass(), (JsonSerializer<Object>)
(src, typeOfSrc, context) -> new JsonArray());
myGson = gsonBuilder.create();
// Lazy-load this because it's expensive, but more importantly because it makes a bunch of
// calls for StructureDefinitions and other such things during startup so we want to be sure
// that everything else is initialized first
myGenerator = Suppliers.memoize(() -> new GraphQLSchemaGenerator(myContext, VersionUtil.getVersion()));
}
@Override
@ -153,37 +159,19 @@ public class GraphQLProviderWithIntrospection extends GraphQLProvider {
Collection<String> theResourceTypes,
EnumSet<GraphQLSchemaGenerator.FHIROperationType> theOperations) {
GraphQLSchemaGenerator generator = myGenerator.get();
final StringBuilder schemaBuilder = new StringBuilder();
try (Writer writer = new StringBuilderWriter(schemaBuilder)) {
// Generate FHIR base types schemas
myGenerator.generateTypes(writer, theOperations);
generator.generateTypes(writer, theOperations);
// Fix up a few things that are missing from the generated schema
writer.append("\ninterface Element {")
.append("\n id: ID")
.append("\n}")
.append("\n");
// writer
// .append("\ninterface Quantity {\n")
// .append("id: String\n")
// .append("extension: [Extension]\n")
// .append("value: decimal _value: ElementBase\n")
// .append("comparator: code _comparator: ElementBase\n")
// .append("unit: String _unit: ElementBase\n")
// .append("system: uri _system: ElementBase\n")
// .append("code: code _code: ElementBase\n")
// .append("\n}")
// .append("\n");
// writer
// .append("\ntype Resource {")
// .append("\n id: [token]" + "\n}")
// .append("\n");
// writer
// .append("\ninput ResourceInput {")
// .append("\n id: [token]" + "\n}")
// .append("\n");
// Generate schemas for the resource types
for (String nextResourceType : theResourceTypes) {
@ -192,7 +180,7 @@ public class GraphQLProviderWithIntrospection extends GraphQLProvider {
.getActiveSearchParams(
nextResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)
.values());
myGenerator.generateResource(writer, sd, parameters, theOperations);
generator.generateResource(writer, sd, parameters, theOperations);
}
// Generate queries
@ -210,8 +198,8 @@ public class GraphQLProviderWithIntrospection extends GraphQLProvider {
.getActiveSearchParams(
nextResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)
.values());
myGenerator.generateListAccessQuery(writer, parameters, nextResourceType);
myGenerator.generateConnectionAccessQuery(writer, parameters, nextResourceType);
generator.generateListAccessQuery(writer, parameters, nextResourceType);
generator.generateConnectionAccessQuery(writer, parameters, nextResourceType);
}
}
writer.append("\n}");

View File

@ -285,6 +285,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
mySearchProperties.setMaxResultsRequested(theMaxResultsToFetch);
}
@Override
public void setDeduplicateInDatabase(boolean theShouldDeduplicateInDB) {
mySearchProperties.setDeduplicateInDatabase(theShouldDeduplicateInDB);
}

View File

@ -34,6 +34,8 @@ import org.hl7.fhir.r5.model.Enumerations;
import org.hl7.fhir.r5.model.SubscriptionTopic;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import java.util.HashSet;
import java.util.List;
@ -59,6 +61,7 @@ public class SubscriptionTopicLoader extends BaseResourceCacheSynchronizer {
}
@Override
@EventListener(classes = ContextRefreshedEvent.class)
public void registerListener() {
if (!myFhirContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4B)) {
return;

View File

@ -19,6 +19,7 @@
*/
package ca.uhn.fhir.cache;
import ca.uhn.fhir.IHapiBootOrder;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.cache.IResourceChangeEvent;
@ -30,7 +31,6 @@ import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Nonnull;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -40,7 +40,9 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.ContextStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import java.util.Collection;
import java.util.List;
@ -78,21 +80,34 @@ public abstract class BaseResourceCacheSynchronizer implements IResourceChangeLi
myResourceChangeListenerRegistry = theResourceChangeListenerRegistry;
}
@PostConstruct
/**
* This method performs a search in the DB, so use the {@link ContextStartedEvent}
* to ensure that it runs after the database initializer
*/
@EventListener(classes = ContextRefreshedEvent.class)
@Order(IHapiBootOrder.AFTER_SUBSCRIPTION_INITIALIZED)
public void registerListener() {
if (myDaoRegistry.getResourceDaoOrNull(myResourceName) == null) {
ourLog.info("No resource DAO found for resource type {}, not registering listener", myResourceName);
return;
}
mySearchParameterMap = getSearchParameterMap();
mySystemRequestDetails = SystemRequestDetails.forAllPartitions();
IResourceChangeListenerCache resourceCache =
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(
myResourceName, mySearchParameterMap, this, REFRESH_INTERVAL);
myResourceName, provideSearchParameterMap(), this, REFRESH_INTERVAL);
resourceCache.forceRefresh();
}
private SearchParameterMap provideSearchParameterMap() {
SearchParameterMap searchParameterMap = mySearchParameterMap;
if (searchParameterMap == null) {
searchParameterMap = getSearchParameterMap();
mySearchParameterMap = searchParameterMap;
}
return searchParameterMap;
}
@PreDestroy
public void unregisterListener() {
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this);
@ -149,7 +164,7 @@ public abstract class BaseResourceCacheSynchronizer implements IResourceChangeLi
ourLog.debug("Starting sync {}s", myResourceName);
List<IBaseResource> resourceList = (List<IBaseResource>)
getResourceDao().searchForResources(mySearchParameterMap, mySystemRequestDetails);
getResourceDao().searchForResources(provideSearchParameterMap(), mySystemRequestDetails);
return syncResourcesIntoCache(resourceList);
}
}

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.to;
import ca.uhn.fhir.system.HapiSystemProperties;
import ca.uhn.fhir.to.mvc.AnnotationMethodHandlerAdapterConfigurer;
import ca.uhn.fhir.to.util.WebUtil;
import jakarta.annotation.Nonnull;
@ -49,7 +50,7 @@ public class FhirTesterMvcConfig implements WebMvcConfigurer {
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCharacterEncoding("UTF-8");
if (theTesterConfig.getDebugTemplatesMode()) {
if (theTesterConfig.getDebugTemplatesMode() || HapiSystemProperties.isUnitTestModeEnabled()) {
resolver.setCacheable(false);
}

View File

@ -134,69 +134,72 @@
<h4>Includes <small>Also include resources which are referenced by the search results</small></h4>
</div>
<div class="row">
<span th:each="include : ${includes}" class="includeCheckContainer">
<span class="includeCheckCheck">
<input type="checkbox" th:value="${include}" th:id="'inc_' + ${include}"></input>
<div class="col">
<span th:each="include : ${includes}" class="includeCheckContainer">
<span class="includeCheckCheck">
<input type="checkbox" th:value="${include}" th:id="'inc_' + ${include}"></input>
</span>
<span class="includeCheckName" th:text="${include}"/>
</span>
<span class="includeCheckName" th:text="${include}"/>
</span>
</div>
</div>
<!-- Results Sorting -->
<br clear="all"/>
<div class="row">
<h4>Sort Results</h4>
</div>
<div class="row">
<!-- Sort By... -->
<div class='col-sm-6'>
<label>Sort By</label>
<div class="btn-group">
<input type="hidden" id="sort_by" />
<button type="button" class="btn btn-info" id="search_sort_button">Default</button>
<button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown">
<span class="caret"></span>
<span class="visually-hidden">Default Sort</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="javascript:updateSort('');">Default Sort</a></li>
<li class="divider"></li>
<li th:each="nextParam : ${sortParams}"><a th:href="'javascript:updateSort(\'' + ${nextParam} + '\');'" th:text="${nextParam}"></a></li>
</ul>
</div>
<div class='col'>
<div class="d-inline-block">
<div class="input-group">
<label class="input-group-text">Sort By</label>
<input type="hidden" id="sort_by" />
<label class="input-group-text border border-primary text-primary" id="search_sort_button">Default</label>
<button type="button" class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown">
<span class="caret"></span>
<span class="visually-hidden">Default Sort</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="javascript:updateSort('');" class="dropdown-item">Default Sort</a></li>
<li class="divider"></li>
<li th:each="nextParam : ${sortParams}"><a th:href="'javascript:updateSort(\'' + ${nextParam} + '\');'" th:text="${nextParam}" class="dropdown-item"></a></li>
</ul>
</div>
</div>
<label>Direction</label>
<div class="btn-group">
<input type="hidden" id="sort_direction" />
<button type="button" class="btn btn-info" id="search_sort_direction_button">Default</button>
<button type="button" class="btn btn-info dropdown-toggle" data-bs-toggle="dropdown">
<span class="caret"></span>
<span class="visually-hidden">Default Sort</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="javascript:updateSortDirection('');">Default</a></li>
<li class="divider"></li>
<li><a href="javascript:updateSortDirection('asc');">Ascending</a></li>
<li><a href="javascript:updateSortDirection('desc');">Descending</a></li>
</ul>
</div>
<div class="d-inline-block ms-3">
<div class="input-group">
<label class="input-group-text">Direction</label>
<input type="hidden" id="sort_direction" />
<label class="input-group-text border border-primary text-primary" id="search_sort_direction_button">Default</label>
<button type="button" class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown">
<span class="caret"></span>
<span class="visually-hidden">Default Sort</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="javascript:updateSortDirection('');" class="dropdown-item">Default</a></li>
<li class="divider"></li>
<li><a href="javascript:updateSortDirection('asc');" class="dropdown-item">Ascending</a></li>
<li><a href="javascript:updateSortDirection('desc');" class="dropdown-item">Descending</a></li>
</ul>
</div>
</div>
</div>
</div>
<br clear="all"/>
<!-- Other Options -->
<br clear="all"/>
<div class="row">
<h4>Other Options</h4>
<div class="col">
<h4>Other Options</h4>
</div>
</div>
<div class="row">
<div class='col-sm-3'>
<div class="form-group">
<div class='input-group date'>
<div class="input-group-addon">
Limit
</div>
<label class="input-group-text">Limit</label>
<input type="text" class="form-control" id="resource-search-limit" placeholder="max # returned"/>
</div>
</div>
@ -209,15 +212,17 @@
<h4>Reverse Includes <small>Also include resources which reference to the search results</small></h4>
</div>
<div class="row">
<span class="includeCheckCheck">
<input type="checkbox" th:value="'*'" th:id="'revinc_STAR'" />
</span>
<span class="includeCheckName" th:text="'*'"/>
<span th:each="include : ${revincludes}" class="includeCheckContainer">
<span class="includeCheckContainer">
<span class="includeCheckCheck">
<input type="checkbox" th:value="${include}" th:id="'revinc_' + ${include}" />
<input type="checkbox" th:value="'*'" th:id="'revinc_STAR'" />
</span>
<span class="includeCheckName" th:text="'*'"/>
<span th:each="include : ${revincludes}" class="includeCheckContainer">
<span class="includeCheckCheck">
<input type="checkbox" th:value="${include}" th:id="'revinc_' + ${include}" />
</span>
<span class="includeCheckName" th:text="${include}"/>
</span>
<span class="includeCheckName" th:text="${include}"/>
</span>
</div>
</div>

View File

@ -9,46 +9,46 @@
<label class="navBarButtonLabel">Encoding</label>
<div class="btn-group" id="encodingBtnGroup" role="group">
<input type="radio" class="btn-check" name="encoding" id="encode-default" value="" />
<label class="btn btn-info" for="encode-default">(default)</label>
<label class="btn btn-outline-info" for="encode-default">(default)</label>
<input type="radio" class="btn-check" name="encoding" id="encode-xml" value="xml" />
<label class="btn btn-info" for="encode-xml">XML</label>
<label class="btn btn-outline-info" for="encode-xml">XML</label>
<input type="radio" class="btn-check" name="encoding" id="encode-json" value="json" />
<label class="btn btn-info" for="encode-json">JSON</label>
<label class="btn btn-outline-info" for="encode-json">JSON</label>
</div>
<!-- Pretty -->
<br /> <label class="navBarButtonLabel">Pretty</label>
<div role="group" class="btn-group" id="prettyBtnGroup" style="margin-top: 5px;">
<input type="radio" class="btn-check" name="pretty" id="pretty-default" value="" />
<label class="btn btn-info" for="pretty-default">(default)</label>
<label class="btn btn-outline-info" for="pretty-default">(default)</label>
<input type="radio" class="btn-check" name="pretty" id="pretty-true" value="true" />
<label class="btn btn-info" for="pretty-true">On</label>
<label class="btn btn-outline-info" for="pretty-true">On</label>
<input
type="radio" class="btn-check" name="pretty" id="pretty-false" value="false" />
<label class="btn btn-info" for="pretty-false"> Off</label>
<label class="btn btn-outline-info" for="pretty-false"> Off</label>
</div>
<!-- Summary -->
<br /> <label class="navBarButtonLabel">Summary</label>
<div role="group" class="btn-group" id="summaryBtnGroup" style="margin-top: 5px;">
<input type="radio" class="btn-check" name="_summary" id="summary-default" value="" />
<label class="btn btn-info" for="summary-default">(none)</label>
<label class="btn btn-outline-info" for="summary-default">(none)</label>
<input type="radio" class="btn-check" name="_summary" id="summary-true" value="true" />
<label class="btn btn-info" for="summary-true">true</label>
<label class="btn btn-outline-info" for="summary-true">true</label>
<input type="radio" class="btn-check" name="_summary" id="summary-text" value="text" />
<label class="btn btn-info" for="summary-text">text</label>
<label class="btn btn-outline-info" for="summary-text">text</label>
<input type="radio" class="btn-check" name="_summary" id="summary-data" value="data" />
<label class="btn btn-info" for="summary-data">data</label>
<label class="btn btn-outline-info" for="summary-data">data</label>
<input type="radio" class="btn-check" name="_summary" id="summary-count" value="count" />
<label class="btn btn-info" for="summary-count">count</label>
<label class="btn btn-outline-info" for="summary-count">count</label>
</div>
<script type="text/javascript" th:inline="javascript">
@ -126,28 +126,30 @@
<h4>Server</h4>
<ul class="nav nav-sidebar">
<li th:class="${page} == 'home' ? 'active' : ''">
<a href="#" onclick="doAction(this, 'home', null);">Server Home/Actions</a>
<ul class="nav flex-column nav-pills">
<li class="nav-item">
<a href="#" onclick="doAction(this, 'home', null);" class="nav-link" th:classappend="${page} == 'home' ? 'active' : ''">Server Home/Actions</a>
</li>
<li th:if="${supportsHfql}" th:class="${page} == 'hfql' ? 'active' : ''">
<a href="#" id="leftHfql" onclick="doAction(this, 'hfql', null);">HFQL / SQL</a>
<li th:if="${supportsHfql}" class="nav-item">
<a href="#" id="leftHfql" onclick="doAction(this, 'hfql', null);" class="nav-link" th:classappend="${page} == 'hfql' ? 'active' : ''">HFQL / SQL</a>
</li>
</ul>
<h4>Resources</h4>
<ul class="nav nav-sidebar" th:unless="${conf.rest.empty}">
<ul class="nav flex-column nav-pills" th:unless="${conf.rest.empty}">
<th:block th:each="resource, resIterStat : ${conf.rest[0].resource}">
<li th:class="${resourceName} == ${resource.typeElement.valueAsString} ? 'active' : ''">
<li class="nav-item">
<a
th:id="'leftResource' + ${resource.typeElement.valueAsString}"
href="#"
th:data1="${resource.typeElement.valueAsString}"
class="nav-link"
th:classappend="${resourceName} == ${resource.typeElement.valueAsString} ? 'active' : ''"
onclick="doAction(this, 'resource', this.getAttribute('data1'));">
<th:block th:text="${resource.typeElement.valueAsString}" >Patient</th:block>
<span class="badge badge-secondary" th:if="${resourceCounts[resource.typeElement.valueAsString]} != null" th:text="${resourceCounts[resource.typeElement.valueAsString]}"/>
<span class="badge text-bg-secondary" th:if="${resourceCounts[resource.typeElement.valueAsString]} != null" th:text="${resourceCounts[resource.typeElement.valueAsString]}"/>
</a>
</li>
</th:block>

View File

@ -38,6 +38,9 @@ H3 {
H4 {
font-size: 1.1em;
}
.sidebar > H4 {
margin-top: 1.0em;
}
.clientCodeBox {
font-family: monospace;
@ -220,53 +223,11 @@ body .syntaxhighlighter .line {
@media (min-width: 768px) {
.sidebar {
top: 5px;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding: 5px;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
background-color: #f5f5f5;
border-right: 1px solid #eee;
}
}
/* Sidebar navigation */
.nav-sidebar {
margin-right: -21px; /* 20px padding + 1px border */
margin-bottom: 20px;
margin-left: -20px;
}
.nav-sidebar > li {
width: 100%;
}
.nav-sidebar > li > a {
padding-right: 20px;
padding-left: 20px;
padding-top: 5px;
padding-bottom: 5px;
display: block;
}
.nav-sidebar > .active > a {
color: #fff;
background-color: #428bca;
padding-top: 10px;
padding-bottom: 10px;
}
.nav-link.active {
background-color: #007bff !important;
border: none !important;
color: #FFF !important;
font-weight: bold;
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
/*
* Main content

View File

@ -61,7 +61,7 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
$('<input />', { id: 'param.' + theRowNum + '.0', placeholder: 'ResourceType/nnn', type: 'text', 'class': 'form-control' })
)
);
} else if (theSearchParamType == 'token') {
} else if (theSearchParamType === 'token') {
var tokenQualifiers = [];
tokenQualifiers.push({});
@ -115,31 +115,25 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
for (var i = 0; i < tokenQualifiers.length; i++) {
var qualName = tokenQualifiers[i].name;
var nextValue = tokenQualifiers[i].value;
var nextLink = $('<a>' + tokenQualifiers[i].name+'</a>');
tokenQualifierDropdown.append($('<li />').append(nextLink));
var nextLink = $('<a class="dropdown-item">' + tokenQualifiers[i].name+'</a>');
tokenQualifierDropdown.append($('<li/>').append(nextLink));
nextLink.click(clickTokenFunction(nextValue, qualName));
}
$('#search-param-rowopts-' + theContainerRowNum).append(
$('<div />', { 'class':'input-group'}).append(
$('<div />', {'class':'input-group-prepend'}).append(
$('<button />', {'class':'btn btn-default dropdown-toggle input-group-text', 'data-bs-toggle':'dropdown'}).append(
tokenQualifierLabel,
$('<span class="caret" style="margin-left: 5px;"></span>')
),
tokenQualifierDropdown
),
$('<div />', { 'class':'input-group-prepend'} ).append(
$('<div class="input-group-text">System</div>')
),
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.0', placeholder: "(opt)" }),
$('<div />', { 'class':'input-group-prepend'} ).append(
$('<div class="input-group-text">Code</div>')
),
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.1', placeholder: "(opt)" })
)
$('<div />', { 'class':'input-group'}).append(
$('<button />', {'class':'btn btn-outline-primary', 'data-bs-toggle':'dropdown'}).append(
tokenQualifierLabel,
$('<span class="caret" style="margin-left: 5px;"></span>')
),
tokenQualifierDropdown,
$('<label class="input-group-text bg-light">System</label>'),
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.0', placeholder: "(opt)" }),
$('<label class="input-group-text bg-light">Code</label>'),
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.1', placeholder: "(opt)" })
),
);
} else if (theSearchParamType === 'string') {
@ -158,7 +152,7 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
);
var matchesLabel = $('<span>' + qualifiers[0].name + '</span>');
var qualifierDropdown = $('<div />', {'class':'dropdown-menu', role:'menu'});
var qualifierDropdown = $('<ul />', {'class':'dropdown-menu', role:'menu'});
function clickFunction(value, name){
return function(){
@ -176,13 +170,11 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
$('#search-param-rowopts-' + theContainerRowNum).append(
$('<div />', { 'class': 'input-group' }).append(
$('<div />', {'class':'input-group-prepend btn-group'}).append(
$('<button />', {'class':'btn btn-default dropdown-toggle input-group-text', 'data-bs-toggle':'dropdown'}).append(
matchesLabel,
$('<span class="caret" style="margin-left: 5px;"></span>')
),
qualifierDropdown
$('<button />', {'class':'btn btn-outline-primary dropdown-toggle', 'data-bs-toggle':'dropdown'}).append(
matchesLabel,
$('<span class="caret" style="margin-left: 5px;"></span>')
),
qualifierDropdown,
$('<input />', { id: 'param.' + theRowNum + '.0', placeholder: placeholderText, type: 'text', 'class': 'form-control' })
)
);
@ -299,9 +291,8 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
qualifierInput
);
var matchesLabel = $('<span>' + qualifiers[0].name + '</span>');
var qualifierDropdown = $('<div />', {'class': 'dropdown-menu', role: 'menu'});
var qualifierDropdown = $('<ul />', {'class': 'dropdown-menu', role: 'menu'});
function clickFunction(value, name) {
return function () {
@ -310,22 +301,20 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
}
}
for (var i = 0; i < qualifiers.length; i++) {
var nextLink = $('<a>' + qualifiers[i].name + '</a>');
var nextLink = $('<a class="dropdown-item">' + qualifiers[i].name + '</a>');
var qualName = qualifiers[i].name;
var nextValue = qualifiers[i].value;
qualifierDropdown.append($('<li />').append(nextLink));
nextLink.click(clickFunction(nextValue, qualName));
}
$('#search-param-rowopts-' + theContainerRowNum).append(
$('<div />', {'class': 'input-group'}).append(
$('<div />', {'class': 'input-group-prepend btn-group'}).append(
$('<button />', {'class': 'btn btn-default dropdown-toggle input-group-text', 'data-bs-toggle': 'dropdown'}).append(
matchesLabel,
$('<span class="caret" style="margin-left: 5px;"></span>')
),
qualifierDropdown
),
$('#search-param-rowopts-' + theContainerRowNum).append(
$('<div />', {'class': 'input-group'}).append(
$('<button />', {'class': 'btn btn-outline-primary dropdown-toggle', 'data-bs-toggle': 'dropdown'}).append(
matchesLabel,
$('<span class="caret" style="margin-left: 5px;"></span>')
),
qualifierDropdown,
$('<input />', {
id: 'param.' + theRowNum + '.0',
placeholder: placeholderText,
@ -359,37 +348,22 @@ function addSearchControlDate(theSearchParamName, theContainerRowNum, theRowNum,
} else {
input = $('<div />', { 'class':'input-group date', 'data-bs-toggledate-format':'YYYY-MM-DDTHH:mm:ss' });
}
var qualifierDiv = $('<div />', {'class':'input-group-prepend'});
input.append(
qualifierDiv,
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + inputId1 }),
$('<div />', { 'class':'input-group-append input-group-addon'} ).append(
$('<span />', {'class':'input-group-text'}).append(
$('<i />', { 'class':'far fa-calendar-alt'})
)
)
);
input.datetimepicker({
format: "YYYY-MM-DD",
showTodayButton: true
});
// Set up the qualifier dropdown after we've initialized the datepicker, since it
// overrides all addon buttons while it inits..
qualifierDiv.addClass('input-group-btn');
var qualifierTooltip = "Set a qualifier and a date to specify a boundary date. Set two qualifiers and dates to specify a range.";
var qualifierBtn = $('<button />', {type:'button', 'class':'btn btn-default dropdown-toggle input-group-text', 'data-bs-toggle':'dropdown', 'data-bs-toggleplacement':'top', 'title':qualifierTooltip}).text('eq');
// Set up the qualifier dropdown after we've initialized the datepicker, since it
// overrides all addon buttons while it inits..
var qualifierTooltip = "Set a qualifier and a date to specify a boundary date. Set two qualifiers and dates to specify a range.";
var qualifierBtn = $('<button />', {type:'button', 'class':'btn btn-outline-primary dropdown-toggle', 'data-bs-toggle':'dropdown', 'data-bs-toggleplacement':'top', 'title':qualifierTooltip}).text('eq');
qualifierBtn.tooltip({
'selector': '',
'placement': 'top',
'container':'body'
});
var qualifierBtnEq = $('<a>eq</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'eq'); });
var qualifierBtnGt = $('<a>gt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'gt'); });
var qualifierBtnGe = $('<a>ge</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ge'); });
var qualifierBtnLt = $('<a>lt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'lt'); });
var qualifierBtnLe = $('<a>le</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'le'); });
qualifierDiv.append(
var qualifierBtnEq = $('<a class="dropdown-item">eq</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'eq'); });
var qualifierBtnGt = $('<a class="dropdown-item">gt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'gt'); });
var qualifierBtnGe = $('<a class="dropdown-item">ge</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ge'); });
var qualifierBtnLt = $('<a class="dropdown-item">lt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'lt'); });
var qualifierBtnLe = $('<a class="dropdown-item">le</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'le'); });
input.append(
qualifierBtn,
$('<ul class="dropdown-menu" role="menu">').append(
$('<li />').append(qualifierBtnEq),
@ -400,46 +374,50 @@ function addSearchControlDate(theSearchParamName, theContainerRowNum, theRowNum,
)
);
var dateTimePicker = $('<input />', { type:'text', 'class':'form-control', id: 'param.' + inputId1 });
input.append(
dateTimePicker
);
dateTimePicker.datetimepicker({
format: "YYYY-MM-DD",
showTodayButton: true
});
input.append(
$('<span />', {'class':'input-group-text'}).append(
$('<i />', { 'class':'far fa-calendar-alt'})
)
);
$('#search-param-rowopts-' + theContainerRowNum).append(
qualifier,
$('<div />', { }).append(
input
)
input
);
}
function addSearchControlQuantity(theSearchParamName, theContainerRowNum, theRowNum) {
var input = $('<div />', { 'class':'input-group'});
var qualifier = $('<input />', {type:'hidden', id:'param.' + theRowNum + '.0'});
var qualifierDiv = $('<div />', {'class':'input-group-prepend'});
input.append(
qualifierDiv,
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.1', placeholder: "value" }),
$('<div />', { 'class':'input-group-append'} ).append(
$('<span class="input-group-text">System</span>')
),
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.2', placeholder: "(opt)" }),
$('<div />', { 'class':'input-group-append'} ).append(
$('<span class="input-group-text">Code</span>')
),
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.3', placeholder: "(opt)" })
$('#search-param-rowopts-' + theContainerRowNum).append(
qualifier,
input
);
var qualifierTooltip = "You can optionally use a qualifier to specify a range.";
var qualifierBtn = $('<button />', {type:'button', 'class':'btn btn-default dropdown-toggle input-group-text', 'data-bs-toggle':'dropdown', 'data-bs-toggleplacement':'top', 'title':qualifierTooltip}).text('=');
qualifierBtn.tooltip({
'selector': '',
'placement': 'left',
'container':'body'
});
var qualifierBtnEq = $('<a>=</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, '='); });
var qualifierBtnAp = $('<a>ap (Approx)</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ap'); });
var qualifierBtnGt = $('<a>gt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'gt'); });
var qualifierBtnGe = $('<a>ge</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ge'); });
var qualifierBtnLt = $('<a>lt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'lt'); });
var qualifierBtnLe = $('<a>le</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'le'); });
qualifierDiv.append(
var qualifierTooltip = "You can optionally use a qualifier to specify a range.";
var qualifierBtn = $('<button />', {type:'button', 'class':'btn btn-outline-primary dropdown-toggle', 'data-bs-toggle':'dropdown', 'data-bs-toggleplacement':'top', 'title':qualifierTooltip}).text('=');
qualifierBtn.tooltip({
'selector': '',
'placement': 'left',
'container':'body'
});
var qualifierBtnEq = $('<a class="dropdown-item">=</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, '='); });
var qualifierBtnAp = $('<a class="dropdown-item">ap (Approx)</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ap'); });
var qualifierBtnGt = $('<a class="dropdown-item">gt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'gt'); });
var qualifierBtnGe = $('<a class="dropdown-item">ge</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'ge'); });
var qualifierBtnLt = $('<a class="dropdown-item">lt</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'lt'); });
var qualifierBtnLe = $('<a class="dropdown-item">le</a>').click(function() { updateSearchDateQualifier(qualifierBtn, qualifier, 'le'); });
input.append(
qualifierBtn,
$('<ul class="dropdown-menu" role="menu">').append(
$('<li />').append(qualifierBtnEq),
@ -451,12 +429,14 @@ function addSearchControlQuantity(theSearchParamName, theContainerRowNum, theRow
)
);
$('#search-param-rowopts-' + theContainerRowNum).append(
qualifier,
$('<div />', { }).append(
input
)
input.append(
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.1', placeholder: "value" }),
$('<label class="input-group-text">System</label>'),
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.2', placeholder: "(opt)" }),
$('<label class="input-group-text">Code</label>'),
$('<input />', { type:'text', 'class':'form-control', id: 'param.' + theRowNum + '.3', placeholder: "(opt)" })
);
}
function handleSearchParamTypeChange(select, params, theContainerRowNum, theParamRowNum) {

View File

@ -16,6 +16,7 @@ import ca.uhn.fhir.sl.cache.Cache;
import ca.uhn.fhir.sl.cache.CacheFactory;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.Logs;
import ca.uhn.fhir.util.StopWatch;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
@ -955,9 +956,19 @@ public class ValidationSupportChain implements IValidationSupport {
return returnValue;
} else {
retVal = new CacheValue<>(theLoader.get());
myNonExpiringCache.put(theKey, retVal);
putInCache(theKey, retVal);
// Avoid flooding the validation support modules tons of concurrent
// requests for the same thing
synchronized (this) {
retVal = getFromCache(theKey);
if (retVal == null) {
StopWatch sw = new StopWatch();
ourLog.info("Performing initial retrieval for non-expiring cache: {}", theKey);
retVal = new CacheValue<>(theLoader.get());
ourLog.info("Initial retrieval for non-expiring cache {} succeeded in {}", theKey, sw);
myNonExpiringCache.put(theKey, retVal);
putInCache(theKey, retVal);
}
}
}
}