diff --git a/dsf-fhir/dsf-fhir-rest-adapter/pom.xml b/dsf-fhir/dsf-fhir-rest-adapter/pom.xml index bf9bd91a6..5f3025bb3 100755 --- a/dsf-fhir/dsf-fhir-rest-adapter/pom.xml +++ b/dsf-fhir/dsf-fhir-rest-adapter/pom.xml @@ -14,7 +14,7 @@ dev.dsf dsf-common-auth - + ca.uhn.hapi.fhir hapi-fhir-structures-r4 diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/HtmlGenerator.java b/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/HtmlGenerator.java deleted file mode 100644 index 65e34aa7b..000000000 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/HtmlGenerator.java +++ /dev/null @@ -1,25 +0,0 @@ -package dev.dsf.fhir.adapter; - -import java.io.IOException; -import java.io.OutputStreamWriter; - -import org.hl7.fhir.r4.model.BaseResource; - -public interface HtmlGenerator -{ - /** - * @return the resource type supported by this generator - */ - Class getResourceType(); - - /** - * @param basePath - * the applications base base, e.g. /fhir/ - * @param resource - * the resource, not null - * @param out - * the outputStreamWriter, not null - * @throws IOException - */ - void writeHtml(String basePath, R resource, OutputStreamWriter out) throws IOException; -} diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/HtmlFhirAdapter.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/HtmlFhirAdapter.java similarity index 79% rename from dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/HtmlFhirAdapter.java rename to dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/HtmlFhirAdapter.java index c61087df1..bcc90bb71 100644 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/HtmlFhirAdapter.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/HtmlFhirAdapter.java @@ -13,10 +13,10 @@ import java.security.Principal; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -30,7 +30,6 @@ import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; -import org.hl7.fhir.r4.model.BaseResource; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Resource; @@ -52,7 +51,7 @@ @Provider @Produces(MediaType.TEXT_HTML) -public class HtmlFhirAdapter extends AbstractAdapter implements MessageBodyWriter +public class HtmlFhirAdapter extends AbstractAdapter implements MessageBodyWriter { private static final String RESOURCE_NAMES = "Account|ActivityDefinition|AdverseEvent|AllergyIntolerance|Appointment|AppointmentResponse|AuditEvent|Basic|Binary" + "|BiologicallyDerivedProduct|BodyStructure|Bundle|CapabilityStatement|CarePlan|CareTeam|CatalogEntry|ChargeItem|ChargeItemDefinition|Claim|ClaimResponse" @@ -89,7 +88,7 @@ public class HtmlFhirAdapter extends AbstractAdapter implements MessageBodyWrite private final FhirContext fhirContext; private final Supplier serverBaseProvider; - private final Map, HtmlGenerator> htmlGeneratorsByType; + private final Map, List>> htmlGeneratorsByType; @Context private volatile UriInfo uriInfo; @@ -105,7 +104,7 @@ public HtmlFhirAdapter(FhirContext fhirContext, Supplier serverBaseProvi if (htmlGenerators != null) htmlGeneratorsByType = htmlGenerators.stream() - .collect(Collectors.toMap(HtmlGenerator::getResourceType, Function.identity())); + .collect(Collectors.groupingBy(HtmlGenerator::getResourceType)); else htmlGeneratorsByType = Collections.emptyMap(); } @@ -126,17 +125,17 @@ protected IParser getParser(MediaType mediaType, Supplier parser) @Override public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { - return type != null && BaseResource.class.isAssignableFrom(type); + return type != null && Resource.class.isAssignableFrom(type); } @Override - public void writeTo(BaseResource resource, Class type, Type genericType, Annotation[] annotations, + public void writeTo(Resource resource, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { final String basePath = uriInfo.getBaseUri().getRawPath(); - - OutputStreamWriter out = new OutputStreamWriter(entityStream); + final boolean htmlEnabled = isHtmlEnabled(type, basePath, resource); + final OutputStreamWriter out = new OutputStreamWriter(entityStream); out.write(""" @@ -156,11 +155,10 @@ public void writeTo(BaseResource resource, Class type, Type genericType, Anno """.replace("${basePath}", basePath)); - out.write("DSF" + (uriInfo.getPath() == null || uriInfo.getPath().isEmpty() ? "" : ": ") - + uriInfo.getPath() + "\n"); + out.write("" + getTitle(uriInfo) + "\n"); out.write("\n"); - out.write("\n"); + out.write("\n"); out.write("
\n"); @@ -193,6 +191,14 @@ public void writeTo(BaseResource resource, Class type, Type genericType, Anno Show Help + + Enable Light Mode + + + + Enable Dark Mode + + @@ -244,7 +250,7 @@ public void writeTo(BaseResource resource, Class type, Type genericType, Anno
"""); - if (isHtmlEnabled(type)) + if (htmlEnabled) out.write("\n"); out.write(""" @@ -256,16 +262,26 @@ public void writeTo(BaseResource resource, Class type, Type genericType, Anno writeXml(mediaType, basePath, resource, out); writeJson(mediaType, basePath, resource, out); - if (isHtmlEnabled(type)) + if (htmlEnabled) writeHtml(type, basePath, resource, out); out.write(""); out.flush(); } - private String getUrlHeading(BaseResource resource) throws MalformedURLException + private String getTitle(UriInfo uri) + { + if (uri == null || uri.getPath() == null || uriInfo.getPath().isBlank()) + return "DSF"; + else if (uriInfo.getPath().endsWith("/")) + return "DSF: " + uriInfo.getPath().substring(0, uriInfo.getPath().length() - 1); + else + return "DSF: " + uriInfo.getPath(); + } + + private String getUrlHeading(Resource resource) throws MalformedURLException { - URI uri = getResourceUrl(resource).map(this::toURI).orElse(uriInfo.getRequestUri()); + URI uri = getResourceUri(resource); String[] pathSegments = uri.getPath().split("/"); String u = serverBaseProvider.get(); @@ -294,6 +310,12 @@ else if (uriInfo.getQueryParameters().containsKey("_summary")) return heading.toString(); } + + private URI getResourceUri(Resource resource) throws MalformedURLException + { + return getResourceUrlString(resource).map(this::toURI).orElse(uriInfo.getRequestUri()); + } + private URI toURI(String str) { try @@ -306,7 +328,7 @@ private URI toURI(String str) } } - private Optional getResourceUrl(BaseResource resource) throws MalformedURLException + private Optional getResourceUrlString(Resource resource) throws MalformedURLException { if (resource instanceof Resource && resource.getIdElement().hasIdPart()) { @@ -325,7 +347,7 @@ else if (resource instanceof Bundle && !resource.getIdElement().hasIdPart()) return Optional.empty(); } - private void writeXml(MediaType mediaType, String basePath, BaseResource resource, OutputStreamWriter out) + private void writeXml(MediaType mediaType, String basePath, Resource resource, OutputStreamWriter out) throws IOException { IParser parser = getParser(mediaType, fhirContext::newXmlParser); @@ -389,7 +411,7 @@ private String simplifyXml(String xml) } } - private void writeJson(MediaType mediaType, String basePath, BaseResource resource, OutputStreamWriter out) + private void writeJson(MediaType mediaType, String basePath, Resource resource, OutputStreamWriter out) throws IOException { IParser parser = getParser(mediaType, fhirContext::newJsonParser); @@ -419,23 +441,33 @@ private void writeJson(MediaType mediaType, String basePath, BaseResource resour } @SuppressWarnings("unchecked") - private void writeHtml(Class resourceType, String basePath, BaseResource resource, OutputStreamWriter out) + private void writeHtml(Class resourceType, String basePath, Resource resource, OutputStreamWriter out) throws IOException { out.write("
\n"); - HtmlGenerator generator = (HtmlGenerator) htmlGeneratorsByType.get(resourceType); - generator.writeHtml(basePath, resource, out); + URI resourceUri = getResourceUri(resource); + + HtmlGenerator generator = (HtmlGenerator) htmlGeneratorsByType.get(resourceType).stream() + .filter(g -> g.isResourceSupported(basePath, resourceUri, resource)).findFirst().get(); + generator.writeHtml(basePath, resourceUri, resource, out); out.write("
\n"); } - private boolean isHtmlEnabled(Class resourceType) + private boolean isHtmlEnabled(Class resourceType, String basePath, Resource resource) + throws MalformedURLException { - return htmlGeneratorsByType.containsKey(resourceType); + URI resourceUri = getResourceUri(resource); + + if (htmlGeneratorsByType.containsKey(resourceType)) + return uriInfo != null && htmlGeneratorsByType.get(resourceType).stream() + .anyMatch(g -> g.isResourceSupported(basePath, resourceUri, resource)); + else + return false; } - private String adaptFormInputsIfTask(BaseResource resource) + private String adaptFormInputsIfTask(Resource resource) { if (resource instanceof Task task) return Task.TaskStatus.DRAFT.equals(task.getStatus()) ? "adaptTaskFormInputs();" : ""; @@ -443,7 +475,7 @@ private String adaptFormInputsIfTask(BaseResource resource) return ""; } - private Optional getResourceName(BaseResource resource, String uuid) + private Optional getResourceName(Resource resource, String uuid) { if (resource instanceof Bundle) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/HtmlGenerator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/HtmlGenerator.java new file mode 100644 index 000000000..f27b2ca44 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/HtmlGenerator.java @@ -0,0 +1,39 @@ +package dev.dsf.fhir.adapter; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.URI; + +import org.hl7.fhir.r4.model.Resource; + +public interface HtmlGenerator +{ + /** + * @return the resource type supported by this generator + */ + Class getResourceType(); + + /** + * @param basePath + * the applications base base, e.g. /fhir/ + * @param resourceUri + * not null + * @param resource + * the resource, not null + * @param out + * the outputStreamWriter, not null + * @throws IOException + */ + void writeHtml(String basePath, URI resourceUri, R resource, OutputStreamWriter out) throws IOException; + + /** + * @param basePath + * the applications base base, e.g. /fhir/ + * @param resourceUri + * not null + * @param resource + * not null + * @return true if this HtmlGenerator supports the given resource for the given uri + */ + boolean isResourceSupported(String basePath, URI resourceUri, Resource resource); +} diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/InputHtmlGenerator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/InputHtmlGenerator.java similarity index 100% rename from dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/InputHtmlGenerator.java rename to dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/InputHtmlGenerator.java diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/QuestionnaireResponseHtmlGenerator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/QuestionnaireResponseHtmlGenerator.java similarity index 50% rename from dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/QuestionnaireResponseHtmlGenerator.java rename to dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/QuestionnaireResponseHtmlGenerator.java index c4de31090..4e819336d 100644 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/QuestionnaireResponseHtmlGenerator.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/QuestionnaireResponseHtmlGenerator.java @@ -2,11 +2,13 @@ import java.io.IOException; import java.io.OutputStreamWriter; +import java.net.URI; import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; public class QuestionnaireResponseHtmlGenerator extends InputHtmlGenerator @@ -22,54 +24,57 @@ public Class getResourceType() } @Override - public void writeHtml(String basePath, QuestionnaireResponse questionnaireResponse, OutputStreamWriter out) - throws IOException + public boolean isResourceSupported(String basePath, URI resourceUri, Resource resource) { - boolean completed = QuestionnaireResponse.QuestionnaireResponseStatus.COMPLETED + return resource != null && resource instanceof QuestionnaireResponse; + } + + @Override + public void writeHtml(String basePath, URI resourceUri, QuestionnaireResponse questionnaireResponse, + OutputStreamWriter out) throws IOException + { + final boolean completed = QuestionnaireResponse.QuestionnaireResponseStatus.COMPLETED .equals(questionnaireResponse.getStatus()); out.write("
"); - out.write("
\n"); - out.write("
\n"); + out.write("
\n"); out.write("
"); out.write(""); out.write("Info\n"); - out.write(""); + out.write( + ""); out.write(""); out.write("
\n"); - - String urlVersion = questionnaireResponse.getQuestionnaire(); - String[] urlVersionSplit = urlVersion.split("\\|"); - String href = basePath + "Questionnaire?url=" + urlVersionSplit[0] + "&version=" + urlVersionSplit[1]; - out.write("
"); - out.write("

\n"); - out.write("This QuestionnaireResponse answers the Questionnaire:
" - + urlVersion + ""); - out.write("

\n"); out.write("
    \n"); - out.write("
  • State: " + questionnaireResponse.getStatus().getDisplay() + "
  • \n"); - out.write("
  • Process instance-id: " + getProcessInstanceId(questionnaireResponse) + "
  • \n"); - - String lastUpdated = DATE_TIME_DISPLAY_FORMAT.format(questionnaireResponse.getMeta().getLastUpdated()); - if (completed) - { - out.write("
  • Completion date: " + lastUpdated + "
  • \n"); - } - else - { - out.write("
  • Creation date: " + lastUpdated + "
  • \n"); - } - + out.write("
  • ID / Version: " + + (questionnaireResponse.getIdElement() == null ? "" : questionnaireResponse.getIdElement().getIdPart()) + + " / " + (questionnaireResponse.getIdElement() == null ? "" + : questionnaireResponse.getIdElement().getVersionIdPart()) + + "
  • \n"); + out.write("
  • Last Updated: " + + (questionnaireResponse.getMeta().getLastUpdated() == null ? "" + : DATE_TIME_DISPLAY_FORMAT.format(questionnaireResponse.getMeta().getLastUpdated())) + + "
  • \n"); + out.write("
  • Status: " + + (questionnaireResponse.getStatus() == null ? "" : questionnaireResponse.getStatus().toCode()) + + "
  • \n"); + out.write("
  • Questionnaire: " + (questionnaireResponse.getQuestionnaire() == null ? "" + : questionnaireResponse.getQuestionnaire().replaceAll("\\|", " | ")) + + "
  • \n"); + out.write("
  • Business-Key: " + getProcessInstanceId(questionnaireResponse) + "
  • \n"); out.write("
\n"); out.write("
\n"); out.write("
\n"); - out.write("
\n"); + out.write("
\n"); Map elemenIndexMap = new HashMap<>(); for (QuestionnaireResponse.QuestionnaireResponseItemComponent item : questionnaireResponse.getItem()) @@ -89,39 +94,6 @@ public void writeHtml(String basePath, QuestionnaireResponse questionnaireRespon out.write("\n"); } - private String getColorClass(QuestionnaireResponse.QuestionnaireResponseStatus status, String elementType) - { - switch (status) - { - case INPROGRESS: - if (ELEMENT_TYPE_ROW.equals(elementType)) - return "info-color-progress"; - else if (ELEMENT_TYPE_LINK.equals(elementType)) - return "info-link-progress"; - else if (ELEMENT_TYPE_PATH.equals(elementType)) - return "info-path-progress"; - case COMPLETED: - if (ELEMENT_TYPE_ROW.equals(elementType)) - return "info-color-completed"; - else if (ELEMENT_TYPE_LINK.equals(elementType)) - return "info-link-completed"; - else if (ELEMENT_TYPE_PATH.equals(elementType)) - return "info-path-completed"; - case STOPPED: - case ENTEREDINERROR: - if (ELEMENT_TYPE_ROW.equals(elementType)) - return "info-color-stopped-failed"; - else if (ELEMENT_TYPE_LINK.equals(elementType)) - return "info-link-stopped-failed"; - else if (ELEMENT_TYPE_PATH.equals(elementType)) - return "info-path-stopped-failed"; - case AMENDED: - case NULL: - default: - return ""; - } - } - private String getProcessInstanceId(QuestionnaireResponse questionnaireResponse) { return questionnaireResponse.getItem().stream() diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/SearchBundleHtmlGenerator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/SearchBundleHtmlGenerator.java new file mode 100644 index 000000000..81b4528f1 --- /dev/null +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/SearchBundleHtmlGenerator.java @@ -0,0 +1,258 @@ +package dev.dsf.fhir.adapter; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.URI; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.glassfish.jersey.uri.UriComponent; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.BundleLinkComponent; +import org.hl7.fhir.r4.model.Bundle.SearchEntryMode; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.OperationOutcome.OperationOutcomeIssueComponent; +import org.hl7.fhir.r4.model.QuestionnaireResponse; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Task.ParameterComponent; + +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.PathSegment; + +public class SearchBundleHtmlGenerator extends InputHtmlGenerator implements HtmlGenerator +{ + private static final SimpleDateFormat DATE_TIME_DISPLAY_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss"); + private static final String INSTANTIATES_CANONICAL_PATTERN_STRING = "(?http[s]{0,1}://(?(?:(?:[a-zA-Z0-9]{1,63}|[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])\\.)+(?:[a-zA-Z0-9]{1,63}))" + + "/bpe/Process/(?[a-zA-Z0-9-]+))\\|(?\\d+\\.\\d+)$"; + private static final Pattern INSTANTIATES_CANONICAL_PATTERN = Pattern + .compile(INSTANTIATES_CANONICAL_PATTERN_STRING); + private static final String CODE_SYSTEM_BPMN_MESSAGE = "http://dsf.dev/fhir/CodeSystem/bpmn-message"; + private static final String CODE_SYSTEM_BPMN_MESSAGE_MESSAGE_NAME = "message-name"; + private static final String CODE_SYSTEM_BPMN_MESSAGE_BUSINESS_KEY = "business-key"; + + private final int defaultPageCount; + + public SearchBundleHtmlGenerator(int defaultPageCount) + { + this.defaultPageCount = defaultPageCount; + } + + @Override + public Class getResourceType() + { + return Bundle.class; + } + + @Override + public boolean isResourceSupported(String basePath, URI resourceUri, Resource resource) + { + List segments = UriComponent.decodePath(resourceUri, false); + + return resource != null && resource instanceof Bundle && segments.size() == 2 + && basePath.equals("/" + segments.get(0).getPath() + "/") && switch (segments.get(1).getPath()) + { + case "Task", "QuestionnaireResponse" -> true; + default -> false; + }; + } + + @Override + public void writeHtml(String basePath, URI resourceUri, Bundle resource, OutputStreamWriter out) throws IOException + { + out.write("
"); + out.write("
"); + out.write("
"); + + Optional first = resource.getLink().stream().filter(l -> "first".equals(l.getRelation())).findFirst() + .map(BundleLinkComponent::getUrl); + + if (first.isPresent()) + out.write(""); + out.write(""); + if (first.isPresent()) + out.write(""); + + Optional previous = resource.getLink().stream().filter(l -> "previous".equals(l.getRelation())) + .findFirst().map(BundleLinkComponent::getUrl); + if (previous.isPresent()) + out.write(""); + out.write(""); + if (previous.isPresent()) + out.write(""); + + out.write(""); + if (resource.getEntry().size() > 0) + { + int page = getPage(resourceUri); + int count = getCount(resourceUri); + int max = (int) Math.ceil((double) resource.getTotal() / count); + int firstResource = ((page - 1) * count) + 1; + int lastResource = ((page - 1) * count) + resource.getEntry().size(); + out.write("Resources " + firstResource + " - " + lastResource + " / " + + resource.getTotal() + "Page " + page + " / " + max + ""); + } + out.write(""); + + Optional next = resource.getLink().stream().filter(l -> "next".equals(l.getRelation())).findFirst() + .map(BundleLinkComponent::getUrl); + if (next.isPresent()) + out.write(""); + out.write(""); + if (next.isPresent()) + out.write(""); + + Optional last = resource.getLink().stream().filter(l -> "last".equals(l.getRelation())).findFirst() + .map(BundleLinkComponent::getUrl); + if (last.isPresent()) + out.write(""); + out.write(""); + if (last.isPresent()) + out.write(""); + + out.write("
"); + + out.write(""); + out.write(getHeader(resourceUri)); + out.write( + resource.getEntry().stream() + .filter(e -> e.hasResource() && e.hasSearch() && e.getSearch().hasMode() + && SearchEntryMode.MATCH.equals(e.getSearch().getMode())) + .map(BundleEntryComponent::getResource) + .map(r -> "" + + getRow(r) + "\n") + .collect(Collectors.joining())); + out.write("
"); + + long includeResources = resource.getEntry().stream().filter( + e -> e.hasResource() && e.hasSearch() && SearchEntryMode.INCLUDE.equals(e.getSearch().getMode())) + .count(); + if (includeResources > 0) + out.write("

" + includeResources + " include " + + (includeResources == 1 ? "resource" : "resources") + " hidden.

"); + + List diagnostics = resource.getEntry().stream().filter(BundleEntryComponent::hasResource) + .map(BundleEntryComponent::getResource).filter(r -> r instanceof OperationOutcome) + .map(r -> (OperationOutcome) r).map(OperationOutcome::getIssue).flatMap(List::stream) + .filter(OperationOutcomeIssueComponent::hasSeverity) + .filter(OperationOutcomeIssueComponent::hasDiagnostics) + .map(i -> i.getSeverity().getDisplay() + ": " + i.getDiagnostics()).toList(); + for (String diag : diagnostics) + out.write("

" + diag.replaceAll("&", "&") + .replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">") + "

"); + + out.write("
"); + } + + private int getPage(URI uri) + { + MultivaluedMap params = UriComponent.decodeQuery(uri, false); + String p = params.getFirst("_page"); + if (p != null && !p.isBlank() && p.matches("-{0,1}[0-9]+")) + return Integer.parseInt(p); + else + return 1; + } + + private int getCount(URI uri) + { + MultivaluedMap params = UriComponent.decodeQuery(uri, false); + String p = params.getFirst("_count"); + if (p != null && !p.isBlank() && p.matches("-{0,1}[0-9]+")) + return Integer.parseInt(p); + else + return defaultPageCount; + } + + private String getHeader(URI uri) + { + List segments = UriComponent.decodePath(uri, false); + return switch (segments.get(1).getPath()) + { + case "Task" -> getTaskHeader(); + case "QuestionnaireResponse" -> getQuestionnaireResponseHeader(); + default -> throw new IllegalArgumentException("Unexpected resource path: " + segments.get(1).getPath()); + }; + } + + private String getTaskHeader() + { + return "IDStatusProcessMessage-NameRequesterBusiness-KeyLast Updated"; + } + + private String getQuestionnaireResponseHeader() + { + return "IDStatusQuestionnaireBusiness-KeyLast Updated"; + } + + private String getRow(Resource resource) + { + if (resource instanceof Task) + return getTaskRow((Task) resource); + else if (resource instanceof QuestionnaireResponse) + return getQuestionnaireResponseRow((QuestionnaireResponse) resource); + else + throw new IllegalArgumentException("Unexpected resource type: " + resource.getResourceType().name()); + } + + private String getTaskRow(Task resource) + { + String domain = "", processName = "", processVersion = ""; + if (resource.getInstantiatesCanonical() != null && !resource.getInstantiatesCanonical().isBlank()) + { + Matcher matcher = INSTANTIATES_CANONICAL_PATTERN.matcher(resource.getInstantiatesCanonical()); + if (matcher.matches()) + { + domain = matcher.group("domain"); + processName = matcher.group("processName"); + processVersion = matcher.group("processVersion"); + } + } + + String businessKey = resource.getInput().stream() + .filter(isStringParam(CODE_SYSTEM_BPMN_MESSAGE, CODE_SYSTEM_BPMN_MESSAGE_BUSINESS_KEY)).findFirst() + .map(c -> ((StringType) c.getValue()).getValue()).orElse(""); + String messageName = resource.getInput().stream() + .filter(isStringParam(CODE_SYSTEM_BPMN_MESSAGE, CODE_SYSTEM_BPMN_MESSAGE_MESSAGE_NAME)).findFirst() + .map(c -> ((StringType) c.getValue()).getValue()).orElse(""); + + return "" + + resource.getIdElement().getIdPart() + "" + resource.getStatus().toCode() + "" + + domain + " | " + processName + " | " + processVersion + "" + messageName + "" + + resource.getRequester().getIdentifier().getValue() + "" + businessKey + + "" + DATE_TIME_DISPLAY_FORMAT.format(resource.getMeta().getLastUpdated()) + ""; + } + + private Predicate isStringParam(String system, String code) + { + return p -> p.hasType() && p.getType().hasCoding() + && p.getType().getCoding().stream() + .anyMatch(c -> system.equals(c.getSystem()) && code.equals(c.getCode())) + && p.hasValue() && p.getValue() instanceof StringType; + } + + private String getQuestionnaireResponseRow(QuestionnaireResponse resource) + { + String businessKey = resource.getItem().stream() + .filter(i -> "business-key".equals(i.getLinkId()) && i.hasAnswer() && i.getAnswer().size() == 1 + && i.getAnswerFirstRep().hasValueStringType()) + .map(i -> i.getAnswerFirstRep().getValueStringType().getValue()).findFirst().orElse(""); + + return "" + + resource.getIdElement().getIdPart() + "" + resource.getStatus().toCode() + "" + + resource.getQuestionnaire().replaceAll("\\|", " \\| ") + "" + businessKey + + "" + DATE_TIME_DISPLAY_FORMAT.format(resource.getMeta().getLastUpdated()) + ""; + } +} diff --git a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/TaskHtmlGenerator.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/TaskHtmlGenerator.java similarity index 64% rename from dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/TaskHtmlGenerator.java rename to dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/TaskHtmlGenerator.java index 3654ab1a2..0ca30c161 100644 --- a/dsf-fhir/dsf-fhir-rest-adapter/src/main/java/dev/dsf/fhir/adapter/TaskHtmlGenerator.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/adapter/TaskHtmlGenerator.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; +import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -13,6 +14,7 @@ import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Task.ParameterComponent; @@ -32,42 +34,47 @@ public Class getResourceType() } @Override - public void writeHtml(String basePath, Task task, OutputStreamWriter out) throws IOException + public boolean isResourceSupported(String basePath, URI resourceUri, Resource resource) + { + return resource != null && resource instanceof Task; + } + + @Override + public void writeHtml(String basePath, URI resourceUri, Task task, OutputStreamWriter out) throws IOException { boolean draft = Task.TaskStatus.DRAFT.equals(task.getStatus()); out.write("
"); - out.write("
\n"); - out.write("
\n"); + out.write("\n"); + out.write("
\n"); out.write("
"); out.write(""); out.write("Info\n"); - out.write(""); + out.write( + ""); out.write(""); out.write("
\n"); - String[] taskCanonicalSplit = task.getInstantiatesCanonical().split("\\|"); - String href = basePath + "ActivityDefinition?url=" + taskCanonicalSplit[0] + "&version=" - + taskCanonicalSplit[1]; - out.write("
"); - out.write("

\n"); - out.write("This Task resource " + (draft ? "can be used" : "was used") - + " to instantiate the following process:"); - out.write("

\n"); out.write("
    \n"); - out.write("
  • Process URL: " - + taskCanonicalSplit[0] + "
  • \n"); - out.write("
  • Process Version: " - + taskCanonicalSplit[1] + "
  • \n"); - out.write("
  • Task Profile: " - + task.getMeta().getProfile().stream().map(CanonicalType::getValue).collect(Collectors.joining(", ")) - + "
  • \n"); - out.write("
  • Task Status: " + task.getStatus().toCode() + "
  • \n"); + out.write("
  • ID / Version: " + (task.getIdElement() == null ? "" : task.getIdElement().getIdPart()) + + " / " + (task.getIdElement() == null ? "" : task.getIdElement().getVersionIdPart()) + "
  • \n"); + out.write("
  • Last Updated: " + (task.getMeta().getLastUpdated() == null ? "" + : DATE_TIME_DISPLAY_FORMAT.format(task.getMeta().getLastUpdated())) + "
  • \n"); + out.write("
  • Status: " + (task.getStatus() == null ? "" : task.getStatus().toCode()) + "
  • \n"); + out.write("
  • Process: " + + (task.getInstantiatesCanonical() == null ? "" + : task.getInstantiatesCanonical().replaceAll("\\|", " | ")) + + "
  • \n"); + out.write( + "
  • Task Profile: " + + task.getMeta().getProfile().stream().map(CanonicalType::getValue) + .map(v -> "" + + v.replaceAll("\\|", " | ") + "") + .collect(Collectors.joining(", ")) + + "
  • \n"); getInput(task, isMessageName()).ifPresent(m -> silentWrite(out, "
  • Message-Name: " + m + "
  • \n")); getInput(task, isBusinessKey()).ifPresent(k -> silentWrite(out, "
  • Business-Key: " + k + "
  • \n")); getInput(task, isCorrelationKey()) @@ -76,17 +83,17 @@ public void writeHtml(String basePath, Task task, OutputStreamWriter out) throws out.write("
\n"); out.write("
\n"); - out.write("
\n"); + out.write("
\n"); out.write("
\n"); out.write("\n"); - out.write("\n"); out.write("
\n"); out.write("
\n"); out.write("\n"); - out.write("\n"); out.write("
\n"); @@ -182,64 +189,10 @@ private Predicate isCorrelationKey() && CODESYSTEM_BPMN_MESSAGE_CORRELATION_KEY.equals(c.getCode())); } - private String getColorClass(Task.TaskStatus status, String elementType) - { - switch (status) - { - case DRAFT: - case REQUESTED: - { - if (ELEMENT_TYPE_ROW.equals(elementType)) - return "info-color-draft-requested"; - else if (ELEMENT_TYPE_LINK.equals(elementType)) - return "info-link-draft-requested"; - else if (ELEMENT_TYPE_PATH.equals(elementType)) - return "info-path-draft-requested"; - } - case INPROGRESS: - { - if (ELEMENT_TYPE_ROW.equals(elementType)) - return "info-color-progress"; - else if (ELEMENT_TYPE_LINK.equals(elementType)) - return "info-link-progress"; - else if (ELEMENT_TYPE_PATH.equals(elementType)) - return "info-path-progress"; - } - case COMPLETED: - { - if (ELEMENT_TYPE_ROW.equals(elementType)) - return "info-color-completed"; - else if (ELEMENT_TYPE_LINK.equals(elementType)) - return "info-link-completed"; - else if (ELEMENT_TYPE_PATH.equals(elementType)) - return "info-path-completed"; - } - case ENTEREDINERROR: - case REJECTED: - case CANCELLED: - case FAILED: - { - if (ELEMENT_TYPE_ROW.equals(elementType)) - return "info-color-stopped-failed"; - else if (ELEMENT_TYPE_LINK.equals(elementType)) - return "info-link-stopped-failed"; - else if (ELEMENT_TYPE_PATH.equals(elementType)) - return "info-path-stopped-failed"; - } - case RECEIVED: - case ACCEPTED: - case READY: - case ONHOLD: - case NULL: - default: - return ""; - } - } - private void writeInput(Task.ParameterComponent input, Map elemenIndexMap, boolean draft, OutputStreamWriter out) throws IOException { - String typeCode = getTypeCode(input); + String typeCode = getCode(input.getType()); boolean display = display(draft, typeCode); if (input.hasValue()) @@ -252,7 +205,7 @@ private void writeInput(Task.ParameterComponent input, Map elem private void writeOutput(Task.TaskOutputComponent output, Map elemenIndexMap, OutputStreamWriter out) throws IOException { - String typeCode = getTypeCode(output); + String typeCode = getCode(output.getType()); if (output.hasValue()) { writeInputRow(output.getValue(), output.getExtension(), typeCode, elemenIndexMap, typeCode, true, false, @@ -270,16 +223,6 @@ private boolean display(boolean draft, String typeCode) return true; } - private String getTypeCode(Task.ParameterComponent input) - { - return getCode(input.getType()); - } - - private String getTypeCode(Task.TaskOutputComponent output) - { - return getCode(output.getType()); - } - private String getCode(CodeableConcept codeableConcept) { return codeableConcept.getCoding().stream().findFirst() diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQuery.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQuery.java index fbd7a2b91..8373cd71d 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQuery.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/search/SearchQuery.java @@ -293,7 +293,7 @@ private String createSortSql(List sortParameterValues) { if (value != null && !value.isBlank()) { - SearchQueryParameterFactory sortParameterFactory = searchParameterFactoriesByParameterName + SearchQueryParameterFactory sortParameterFactory = searchParameterFactoriesBySortParameterName .get(value); if (sortParameterFactory != null) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java index 990641421..b231e3a2e 100755 --- a/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java +++ b/dsf-fhir/dsf-fhir-server/src/main/java/dev/dsf/fhir/spring/config/AdapterConfig.java @@ -9,6 +9,7 @@ import dev.dsf.fhir.adapter.FhirAdapter; import dev.dsf.fhir.adapter.HtmlFhirAdapter; import dev.dsf.fhir.adapter.QuestionnaireResponseHtmlGenerator; +import dev.dsf.fhir.adapter.SearchBundleHtmlGenerator; import dev.dsf.fhir.adapter.TaskHtmlGenerator; @Configuration @@ -30,6 +31,7 @@ public FhirAdapter fhirAdapter() public HtmlFhirAdapter htmlFhirAdapter() { return new HtmlFhirAdapter(fhirConfig.fhirContext(), () -> propertiesConfig.getServerBaseUrl(), - List.of(new QuestionnaireResponseHtmlGenerator(), new TaskHtmlGenerator())); + List.of(new QuestionnaireResponseHtmlGenerator(), new TaskHtmlGenerator(), + new SearchBundleHtmlGenerator(propertiesConfig.getDefaultPageCount()))); } } diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/static/bookmarks.js b/dsf-fhir/dsf-fhir-server/src/main/resources/static/bookmarks.js index ac633ff12..63ef2ade1 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/static/bookmarks.js +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/bookmarks.js @@ -114,9 +114,9 @@ function getInitialBookmarks() { 'NamingSystem': ['/fhir/NamingSystem'], 'Organization': ['/fhir/Organization', '/fhir/Organization?identifier=highmed.org', '/fhir/Organization?identifier=medizininformatik-initiative.de', '/fhir/Organization?identifier=netzwerk-universitaetsmedizin.de'], 'OrganizationAffiliation': ['/fhir/OrganizationAffiliation'], - 'QuestionnaireResponse': ['/fhir/QuestionnaireResponse?status=in-progress'], + 'QuestionnaireResponse': ['/fhir/QuestionnaireResponse?_sort=-_lastUpdated', '/fhir/QuestionnaireResponse?_sort=-_lastUpdated&status=in-progress'], 'Subscription': ['/fhir/Subscription'], - 'Task': ['/fhir/Task', '/fhir/Task?_sort=-_lastUpdated', '/fhir/Task?_sort=-_lastUpdated&_count=1', '/fhir/Task?status=draft'], + 'Task': ['/fhir/Task', '/fhir/Task?_sort=-_lastUpdated', '/fhir/Task?_sort=_profile&status=draft'], 'ValueSet': ['/fhir/ValueSet'] }; } diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/static/dsf.css b/dsf-fhir/dsf-fhir-server/src/main/resources/static/dsf.css index 5e9c75f1f..423c02e1e 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/static/dsf.css +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/dsf.css @@ -1,6 +1,89 @@ +[theme="light"] { + --color-prime: #326F95; + --color-background: #fff; + --color-alt-background: #eee; + --color-tab-button: #000; + --color-tab-button-background: #f1f1f1; + --color-tab-button-hover: #000; + --color-tab-button-background-hover: #ddd; + --color-tab-button-background-active: #ccc; + --color-row-background: #f2f2f2; + --color-row-background-deep: #e2e2e2; + --color-row-background-hover: #ddd; + --color-input-text: #000; + --color-input-background: #fff; + --color-input-border: #ccc; + --color-input-radio-border: #000; + --color-input-placeholder: #aaa; + --color-info-background-draft: #deecf9; + --color-info-text-draft: #326F95; + --color-info-background-requested: #f2f2f2; + --color-info-text-requested: #545454; + --color-info-background-progress: #fff7c7; + --color-info-text-progress: #a25b36; + --color-info-background-completed: #e4fde1; + --color-info-text-completed: #357a38; + --color-info-background-failed: #ffe1e1; + --color-info-text-failed: #761137; + --color-info-background-stopped: #ffe1e1; + --color-info-text-stopped: #761137; + --color-error-background: #ffe1e1; + --color-error-text: #761137; + --color-disabled-background: #f2f2f2; + --color-disabled-text: #545454; + --color-row-border-draft: #326F95; + --color-row-border-requested: #545454; + --color-row-border-progress: #a25b36; + --color-row-border-completed: #357a38; + --color-row-border-failed: #761137; + --color-row-border-stopped: #761137; +} + +[theme="dark"] { + --color-prime: #fff; + --color-background: #000; + --color-alt-background: #181818; + --color-tab-button: #fff; + --color-tab-button-background: #181818; + --color-tab-button-background-hover: #ddd; + --color-tab-button-hover: #000; + --color-tab-button-background-active: #888; + --color-row-background: #333; + --color-row-background-deep: #444; + --color-row-background-hover: #777; + --color-input-text: #fff; + --color-input-background: #181818; + --color-input-border: #666; + --color-input-radio-border: #fff; + --color-input-placeholder: #666; + --color-info-background-draft: #326F95; + --color-info-text-draft: #deecf9; + --color-info-background-requested: #333; + --color-info-text-requested: #aaa; + --color-info-background-progress: #a25b36; + --color-info-text-progress: #fff7c7; + --color-info-background-completed: #14452f; + --color-info-text-completed: #e4fde1; + --color-info-background-failed: #761137; + --color-info-text-failed: #ffe1e1; + --color-info-background-stopped: #761137; + --color-info-text-stopped: #ffe1e1; + --color-error-background: #761137; + --color-error-text: #ffe1e1; + --color-disabled-background: #333; + --color-disabled-text: #aaa; + --color-row-border-draft: #326F95; + --color-row-border-requested: #aaa; + --color-row-border-progress: #a25b36; + --color-row-border-completed: #14452f; + --color-row-border-failed: #761137; + --color-row-border-stopped: #761137; +} + body { margin: 2em; font-family: sans-serif; + background-color: var(--color-background); } table#header { @@ -19,13 +102,15 @@ td#url { td#url h1 { font-family: monospace; - color: #326F95; + color: var(--color-prime); margin: 0 0 0 1em; word-break: break-word; } -td#url h1 a:link, td#url h1 a:visited, td#url h1 a:active { - color: #326F95; +td#url h1 a:link, +td#url h1 a:visited, +td#url h1 a:active { + color: var(--color-prime); text-decoration: none; } @@ -39,7 +124,8 @@ td#url h1 a:hover { .tab button { padding: 0.2em 0.6em; - background-color: #f1f1f1; + background-color: var(--color-tab-button-background); + color: var(--color-tab-button); border: none; outline: none; cursor: pointer; @@ -47,11 +133,13 @@ td#url h1 a:hover { } .tab button:hover { - background-color: #ddd; + background-color: var(--color-tab-button-background-hover); + color: var(--color-tab-button-hover); } .tab button.active { - background-color: #ccc; + background-color: var(--color-tab-button-background-active); + color: var(--color-tab-button); } pre { @@ -65,10 +153,6 @@ pre.lang-html { font-family: monospace; } -li.L0, li.L1, li.L2, li.L3, li.L5, li.L6, li.L7, li.L8 { - list-style-type: decimal !important; -} - #icons { position: absolute; top: 2em; @@ -94,7 +178,16 @@ li.L0, li.L1, li.L2, li.L3, li.L5, li.L6, li.L7, li.L8 { } .icon:hover>path { - fill: #326F95; + fill: var(--color-prime); +} + +.icon[disabled] { + height: 1.4em; + cursor: auto; +} + +.icon[disabled]:hover>path { + fill: #aaa; } #help { @@ -103,14 +196,16 @@ li.L0, li.L1, li.L2, li.L3, li.L5, li.L6, li.L7, li.L8 { right: 1em; padding: 1em; border: 1px solid #ccc; - background: white; + color: var(--color-prime); + background: var(--color-background); padding-top: 2em; max-width: 83%; min-width: 12em; + z-index: 1; } #help>#help-title { - color: #326F95; + color: var(--color-prime); position: relative; top: -1.9em; font-family: sans-serif; @@ -148,31 +243,31 @@ li.L0, li.L1, li.L2, li.L3, li.L5, li.L6, li.L7, li.L8 { right: 1em; padding: 1em; border: 1px solid #ccc; - background: white; + background: var(--color-background); padding-top: 2em; max-width: 83%; min-width: 8em; + z-index: 1; } #bookmarks>#bookmarks-title { - color: #326F95; + color: var(--color-prime); position: relative; top: -1.9em; font-family: sans-serif; } -#bookmarks a:link, #bookmarks a:visited, #bookmarks a:active { - color: #326F95; +#bookmarks a:link, +#bookmarks a:visited, +#bookmarks a:active { + color: var(--color-prime); text-decoration: none; vertical-align: super; font-family: monospace; } #bookmarks a:hover { - /* color: #326F95; */ text-decoration: underline; - /* vertical-align: super; - font-family: monospace; */ } #bookmarks>#bookmarks-list { @@ -203,36 +298,158 @@ li.L0, li.L1, li.L2, li.L3, li.L5, li.L6, li.L7, li.L8 { right: 1em; } -.bookmarks-list-entry-removed>a:link, .bookmarks-list-entry-removed>a:visited, - .bookmarks-list-entry-removed>a:active { +.bookmarks-list-entry-removed>a:link, +.bookmarks-list-entry-removed>a:visited, +.bookmarks-list-entry-removed>a:active { color: #aaa !important; } +.bundle>#header #resources { + padding-right: 1em; + color: #aaa +} + +.bundle>#header #page { + padding-left: 1em; + color: #aaa +} + +.bundle { + border: 1px solid #ccc; + padding: 20px 20px 10px 20px; + color: var(--color-prime); + overflow-x: auto; +} + +.bundle>#list { + border-collapse: separate; + border-spacing: 0 1rem; + width: 100%; +} + +.bundle>#list tr:not(:first-child) { + cursor: pointer; +} + +.bundle>#list th:first-child { + border-left: 0.5rem solid var(--color-row-background-deep); +} + +.bundle>#list td:first-child { + border-left: 0.5rem solid var(--color-row-background); +} + +.bundle>#list th { + color: var(--color-prime); + background-color: var(--color-row-background-deep); + padding: 1rem 0 1rem 1rem; + white-space: nowrap; + text-align: left; +} + +.bundle>#list td { + background-color: var(--color-row-background); + padding: 1rem 0 1rem 1rem; + white-space: nowrap; + text-align: left; +} + +.bundle>#list tr:hover td { + background-color: var(--color-row-background-hover); +} + +.bundle>#list th:last-child { + padding-right: 1rem; +} + +.bundle>#list td:last-child { + padding-right: 1rem; +} + +.bundle>#list td[status="draft"]:first-child { + border-left-color: var(--color-row-border-draft); +} + +.bundle>#list td[status="requested"]:first-child { + border-left-color: var(--color-row-border-requested); +} + +.bundle>#list td[status="in-progress"]:first-child { + border-left-color: var(--color-row-border-progress); +} + +.bundle>#list td[status="completed"]:first-child { + border-left-color: var(--color-row-border-completed); +} + +.bundle>#list td[status="failed"]:first-child { + border-left-color: var(--color-row-border-failed); +} + +.bundle>#list td[status="stopped"]:first-child { + border-left-color: var(--color-row-border-stopped); +} + +.bundle>#list td.id-value { + font-family: monospace; +} + +.bundle a:link, +.bundle a:visited, +.bundle a:active { + color: var(--color-prime); + text-decoration: none; +} + +.bundle a:hover { + text-decoration: underline; +} + +.bundle>#list td:first-child, +.bundle>#list th:first-child { + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; +} + +.bundle>#list td:last-child, +.bundle>#list th:last-child { + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +} + @media print { body { margin: 0; } + table#header { margin-bottom: 2em; } + table#header img { height: 3em; } + td#url h1 { font-size: 1.5em; } + .tab { display: none; } + pre.prettyprint { border: 0 !important; } + #icons { display: none; } + #bookmarks { display: none; } + #help { display: none; } diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.css b/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.css index 0ce7c2171..47e192c0d 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.css +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.css @@ -1,351 +1,421 @@ form { - border: 1px solid #ccc; - background-color: #ffffff; - padding: 20px 20px 10px 20px; - font-family: Epilogue, sans-serif; + border: 1px solid #ccc; + padding: 20px 20px 10px 20px; + font-family: Epilogue, sans-serif; } fieldset#form-fieldset { - display: block; - margin: 0; - padding: 0; - min-inline-size: min-content; - border: none; + display: block; + margin: 0; + padding: 0; + min-inline-size: min-content; + border: none; } .row { - border-radius: 5px; - padding: 0 1em 1em 1em; - margin-bottom: 0.7em; - background-color: #f2f2f2; + border-radius: 5px; + padding: 0 1em 1em 1em; + margin-bottom: 0.7em; + background-color: var(--color-row-background); } .row-extension-0 { - margin-top: 12px; - padding: 0 0 12px 12px; - border-radius: 5px; - background-color: #e2e2e2; + margin-top: 12px; + padding: 0 0 12px 12px; + border-radius: 5px; + background-color: var(--color-row-background-deep); } .row-label-extension-no-value { - margin-bottom: -10px; + margin-bottom: -10px; } .row-extension { - padding: 10px 15px 0 15px; + padding: 10px 15px 0 15px; } .row-display { - padding-top: 15px; + padding-top: 15px; } .p-display { - margin: 0; + margin: 0; + color: var(--color-input-text) } .error { - background-color: #ffa590; + background-color: var(--color-error-background); } .error-list-not-visible { - display: none; + display: none; } .error-list-visible { - display: block; - font-size: small; - color: #470003; - padding-left: 20px; - margin-bottom: 0; + display: block; + font-size: small; + color: var(--color-error-text); + padding-left: 20px; + margin-bottom: 0; } .row-info { - display: flex; - padding-top: 15px; + display: flex; + padding-top: 15px; } -.info-color-draft-requested { - background-color: #deecf9; - color: #12395d; +form[status=draft] .row-info { + background-color: var(--color-info-background-draft); + color: var(--color-info-text-draft); } -.info-color-progress { - background-color: #edd273; - color: #864401; +form[status=requested] .row-info { + background-color: var(--color-info-background-requested); + color: var(--color-info-text-requested); } -.info-color-completed { - background-color: #a3c585; - color: #4b6043; +form[status=in-progress] .row-info { + background-color: var(--color-info-background-progress); + color: var(--color-info-text-progress); } -.info-color-stopped-failed { - background-color: #ffb3b3; - color: #761137; +form[status=completed] .row-info { + background-color: var(--color-info-background-completed); + color: var(--color-info-text-completed); } -.info-link { - font-family: monospace; - font-size: 130%; +form[status=failed] .row-info { + background-color: var(--color-info-background-failed); + color: var(--color-info-text-failed); } -.info-link-task { - color: #326F95; +form[status=stopped] .row-info { + background-color: var(--color-info-background-stopped); + color: var(--color-info-text-stopped); } -.info-link-draft-requested { - color: #12395d; +form[status=draft] .row-info .info-icon>path { + fill: var(--color-info-text-draft); } -.info-link-progress { - color: #864401; +form[status=requested] .row-info .info-icon>path { + fill: var(--color-info-text-requested); } -.info-link-completed { - color: #4b6043; +form[status=in-progress] .row-info .info-icon>path { + fill: var(--color-info-text-progress); } -.info-link-stopped-failed { - color: #761137; +form[status=completed] .row-info .info-icon>path { + fill: var(--color-info-text-completed); } -.info-link:active { - font-family: monospace; - font-size: 130%; +form[status=failed] .row-info .info-icon>path { + fill: var(--color-info-text-failed); } -.info-link-draft-requested:active { - color: #12395d; +form[status=stopped] .row-info .info-icon>path { + fill: var(--color-info-text-stopped); } -.info-link-progress:active { - color: #864401; +form[status=draft] .row-info a:link, +form[status=draft] .row-info a:visited, +form[status=draft] .row-info a:active { + color: var(--color-info-text-draft); } -.info-link-completed:active { - color: #4b6043; +form[status=requested] .row-info a:link, +form[status=requested] .row-info a:visited, +form[status=requested] .row-info a:active { + color: var(--color-info-text-requested); } -.info-link-stopped-failed:active { - color: #761137; +form[status=in-progress] .row-info a:link, +form[status=in-progress] .row-info a:visited, +form[status=in-progress] .row-info a:active { + color: var(--color-info-text-progress); } -.info-icon { - padding-top: 15px; - padding-right: 15px; - height: 35px; - width: 35px; +form[status=completed] .row-info a:link, +form[status=completed] .row-info a:visited, +form[status=completed] .row-info a:active { + color: var(--color-info-text-completed); +} + +form[status=failed] .row-info a:link, +form[status=failed] .row-info a:visited, +form[status=failed] .row-info a:active { + color: var(--color-info-text-failed); } -.info-icon > path.info-path-draft-requested { - fill: #12395d; +form[status=stopped] .row-info a:link, +form[status=stopped] .row-info a:visited, +form[status=stopped] .row-info a:active { + color: var(--color-info-text-stopped); } -.info-icon > path.info-path-progress { - fill: #864401; +form .row-info a:link, +form .row-info a:visited, +form .row-info a:active { + text-decoration: none; } -.info-icon > path.info-path-completed { - fill: #4b6043; +form .row-info a:hover { + text-decoration: underline; } -.info-icon > path.info-path-stopped-failed { - fill: #761137; +.info-icon { + padding-top: 15px; + padding-right: 15px; + height: 35px; + width: 35px; } .info-list { - padding-left: 20px; + padding-left: 20px; } -.info-list > li:not(:last-child) { - margin-bottom: 0.3em; +.info-list>li:not(:last-child) { + margin-bottom: 0.3em; } .row:after { - content: ""; - display: table; - clear: both; + content: ""; + display: table; + clear: both; } .row-submit { - background-color: #ffffff; - padding: 0; + background-color: transparent; + padding: 0; } label { - display: block; - padding: 12px 12px 12px 0; - font-weight: regular; + display: block; + padding: 12px 12px 12px 0; + font-weight: regular; } label.radio { - padding: 5px 12px 5px 0; - font-weight: 100; + padding: 5px 12px 5px 0; + font-weight: 100; + color: var(--color-input-text) +} + +input { + background-color: var(--color-input-background); + color: var(--color-input-text); } input[type=radio] { - margin-right: 7px; + margin-right: 7px; } -input[type=text], input[type=url], select, textarea { - width: 100%; - min-width: 200px; - padding: 12px; - border: 1px solid #ccc; - border-radius: 4px; - box-sizing: border-box; - resize: vertical; - display: block; +input[type="radio"] { + border: 0.1em solid var(--color-input-radio-border); + border-radius: 9px; + height: 18px; + width: 18px; + margin-bottom: -0.2em; + -webkit-appearance: none; + appearance: none; } -input[type=date], input[type=time], input[type=datetime-local], input[type=number] { - width: 100%; - min-width: 200px; - padding: 12px; - border: 1px solid #ccc; - border-radius: 4px; - box-sizing: border-box; - resize: none; - display: block; +input[type="radio"][disabled], +fieldset#form-fieldset[disabled] input[type="radio"] { + border: 1px solid var(--color-disabled-text); } -input.identifier-coding-code { - margin-top: 6px; +input[type="radio"]:checked { + border: 0.1em solid var(--color-prime); + background-color: var(--color-prime); + box-shadow: inset 0 0 0 0.2em var(--color-background); } -.invisible { - display: none; +input[type="radio"][disabled]:checked, +fieldset#form-fieldset[disabled] input[type="radio"]:checked { + border: 0.1em solid var(--color-disabled-text); + background-color: var(--color-disabled-text); + box-shadow: inset 0 0 0 0.2em var(--color-disabled-background); } -button.submit { - background-color: #29235c; - color: white; - padding: 12px 60px; - border: none; - border-radius: 4px; - cursor: pointer; - float: left; +input[disabled], +fieldset#form-fieldset[disabled] input { + cursor: default; + background-color: var(--color-disabled-background); + color: var(--color-disabled-text); + border-color: rgba(118, 118, 118, 0.5); +} + +input[type=text], +input[type=url], +select, +textarea { + width: 100%; + min-width: 200px; + padding: 12px; + border: 1px solid var(--color-input-border); + border-radius: 4px; + box-sizing: border-box; + resize: vertical; + display: block; +} + +input[type=date], +input[type=time], +input[type=datetime-local], +input[type=number] { + width: 100%; + min-width: 200px; + padding: 12px; + border: 1px solid var(--color-input-border); + border-radius: 4px; + box-sizing: border-box; + resize: none; + display: block; +} + +input[type=number] { + -webkit-appearance: textfield; + -moz-appearance: textfield; + -o-appearance: textfield; + appearance: textfield; +} + +.input-group { + display: flex; + flex-direction: row; } -button.submit:disabled, -button.submit[disabled] { - background-color: #ccc; - color: #555; - cursor: not-allowed; +.input-group-svg { + cursor: pointer; + align-self: center; + margin-left: 0.46em; + rotate: 180deg; +} + +.input-group-svg>path { + fill: #aaa; +} + +.input-group-svg:hover>path { + fill: #326F95; +} + +input.identifier-coding-code { + margin-top: 6px; } .input-output-header { - font-family: monospace; - font-size: 1.75em; + font-family: monospace; + font-size: 1.75em; color: #326F95; padding-left: 5px; margin-bottom: 12px; } +.invisible { + display: none; +} + +button.submit { + background-color: #326F95; + color: #fff; + padding: 12px 60px; + border: none; + border-radius: 4px; + cursor: pointer; + float: left; +} + .spinner-enabled { - display: block; + display: block; } .spinner-disabled { - display: none; + display: none; } .spinner { - position: fixed; - width: 100%; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: rgba(255,255,255,0.7); - z-index: 9999; + position: fixed; + width: 100%; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.7); + z-index: 9999; } @-webkit-keyframes spin { - from { -webkit-transform: rotate(0deg); } - to { -webkit-transform: rotate(360deg); } + from { + -webkit-transform: rotate(0deg); + } + + to { + -webkit-transform: rotate(360deg); + } } @keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } } .spinner::after { - content: ''; - position: absolute; - left: 48%; - top: 40%; - width: 40px; - height: 40px; - border-style: solid; - border-color: #29235c; - border-top-color: transparent; - border-width: 4px; - border-radius: 50%; - -webkit-animation: spin .8s linear infinite; - animation: spin .8s linear infinite; + content: ''; + position: absolute; + left: 48%; + top: 40%; + width: 40px; + height: 40px; + border-style: solid; + border-color: #29235c; + border-top-color: transparent; + border-width: 4px; + border-radius: 50%; + -webkit-animation: spin .8s linear infinite; + animation: spin .8s linear infinite; } .cardinalities { - font-weight: normal; - font-size: x-small; - color: gray; + font-weight: normal; + font-size: x-small; + color: #aaa; } .row-label { - padding: 12px 0 6px 6px; - font-weight: bold; - font-size: small; - position: relative; + color: var(--color-input-text); + padding: 12px 0 6px 6px; + font-weight: bold; + font-size: small; + position: relative; } .plus-minus-icon { - position: absolute; - bottom: 0.2em; - right: 0.1em; - cursor: pointer; + position: absolute; + bottom: 0.2em; + right: 0.1em; + cursor: pointer; } -.plus-minus-icon:hover > svg > path { +.plus-minus-icon:hover>svg>path { fill: #326F95; } -.plus-minus-icon > svg > path { - fill: #aaa; -} - -.input-group { - display: flex; - flex-direction: row; -} - -.input-group-svg { - cursor: pointer; - align-self: center; - margin-left: 0.46em; - rotate: 180deg; -} - -.input-group-svg > path { - fill: #aaa; -} - -.input-group-svg:hover > path { - fill: #326F95; -} - -input[type=number] { - -webkit-appearance: textfield; - -moz-appearance: textfield; - -o-appearance: textfield; - appearance: textfield; +.plus-minus-icon>svg>path { + fill: #aaa; } ::placeholder { - color: #ddd; + color: var(--color-input-placeholder); } ::-ms-input-placeholder { - color: #ddd; + color: var(--color-input-placeholder); } \ No newline at end of file diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.js b/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.js index c629d1004..bb8831d44 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.js +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/form.js @@ -487,29 +487,34 @@ function getValueOfDifferential(differentials, path, property) { function modifyInputRow(definition, indices) { const row = document.querySelector("[name='" + definition.typeCode + "-input-row']") - const index = parseInt(row.getAttribute("index")) - indices.set(getDefinitionId(definition), index) + if (row) { + const rowIndex = row.getAttribute("index") + if (rowIndex) { + const index = parseInt(rowIndex) + indices.set(getDefinitionId(definition), index) + } - const label = row.querySelector("label") - if (label) { - const cardinalities = htmlToElement('', " [" + definition.min + ".." + definition.max + "]") - label.appendChild(cardinalities) + const label = row.querySelector("label") + if (label) { + const cardinalities = htmlToElement('', " [" + definition.min + ".." + definition.max + "]") + label.appendChild(cardinalities) - if (definition.max !== "1") { - const plusIcon = htmlToElement('') - const plusIconSvg = htmlToElement('Add additional input') + if (definition.max !== "1") { + const plusIcon = htmlToElement('') + const plusIconSvg = htmlToElement('Add additional input') - plusIconSvg.addEventListener("click", () => { - appendInputRowAfter(row, definition, indices) - }) + plusIconSvg.addEventListener("click", () => { + appendInputRowAfter(row, definition, indices) + }) - plusIcon.appendChild(plusIconSvg) - label.appendChild(plusIcon) + plusIcon.appendChild(plusIconSvg) + label.appendChild(plusIcon) + } } - } - if (definition.min < 1 || definition.min === undefined) - row.setAttribute("optional", "") + if (definition.min < 1 || definition.min === undefined) + row.setAttribute("optional", "") + } } function appendInputRowAfter(inputRow, definition, indices) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/static/help.js b/dsf-fhir/dsf-fhir-server/src/main/resources/static/help.js index e6a2eb622..22fc06a25 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/static/help.js +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/help.js @@ -36,7 +36,7 @@ function createAndShowHelp(httpRequest) { const searchParam = metadata.rest[0].resource.filter(r => r.type === resourceType[1])[0].searchParam; //Resource if (resourceType[1] !== undefined && resourceType[2] === undefined && resourceType[3] === undefined && resourceType[4] === undefined) { - createHelp(searchParam); + createHelp(searchParam.filter(p => !['_at', '_since'].includes(p.name))); } //Resource/_history else if (resourceType[1] !== undefined && resourceType[2] === undefined && resourceType[3] !== undefined && resourceType[4] === undefined) { diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/static/prettify.css b/dsf-fhir/dsf-fhir-server/src/main/resources/static/prettify.css index b655dce96..50e8167ef 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/static/prettify.css +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/prettify.css @@ -60,13 +60,17 @@ li.L0, li.L1, li.L2, li.L3, +li.L4, li.L5, li.L6, li.L7, -li.L8 { list-style-type: none } +li.L8, +li.L9 { + list-style-type: decimal; color: var(--color-prime); +} /* Alternate shading for lines */ li.L1, li.L3, li.L5, li.L7, -li.L9 { background: #eee } +li.L9 { background: var(--color-alt-background) } diff --git a/dsf-fhir/dsf-fhir-server/src/main/resources/static/tabs.js b/dsf-fhir/dsf-fhir-server/src/main/resources/static/tabs.js index 7ff4ba911..57fa21c4b 100644 --- a/dsf-fhir/dsf-fhir-server/src/main/resources/static/tabs.js +++ b/dsf-fhir/dsf-fhir-server/src/main/resources/static/tabs.js @@ -1,79 +1,94 @@ function openTab(lang) { - const tabcontent = document.getElementsByClassName("prettyprint"); - for (let i = 0; i < tabcontent.length; i++) { - tabcontent[i].style.display = "none"; - } + const tabcontent = document.getElementsByClassName("prettyprint") + for (let i = 0; i < tabcontent.length; i++) + tabcontent[i].style.display = "none" - const tablinks = document.getElementsByClassName("tablinks"); - for (let i = 0; i < tablinks.length; i++) { - tablinks[i].className = tablinks[i].className.replace(" active", ""); - } + const tablinks = document.getElementsByClassName("tablinks") + for (let i = 0; i < tablinks.length; i++) + tablinks[i].className = tablinks[i].className.replace(" active", "") - document.getElementById(lang).style.display = "block"; - document.getElementById(lang + "-button").className += " active"; + document.getElementById(lang).style.display = "block" + document.getElementById(lang + "-button").className += " active" if (lang != "html" && localStorage != null) - localStorage.setItem('lang', lang); + localStorage.setItem('lang', lang) if (lang == "html") - lang = localStorage != null && localStorage.getItem("lang") != null ? localStorage.getItem("lang") : "xml"; + lang = localStorage != null && localStorage.getItem("lang") != null ? localStorage.getItem("lang") : "xml" - setDownloadLink(lang); + setDownloadLink(lang) } function openInitialTab(htmlEnabled) { if (htmlEnabled) - openTab("html"); + openTab("html") else { - const lang = localStorage != null && localStorage.getItem("lang") != null ? localStorage.getItem("lang") : "xml"; + const lang = localStorage != null && localStorage.getItem("lang") != null ? localStorage.getItem("lang") : "xml" if (lang == "xml" || lang == "json") - openTab(lang); + openTab(lang); } } function setDownloadLink(lang) { - const searchParams = new URLSearchParams(document.location.search); - searchParams.set('_format', lang); - searchParams.set('_pretty', 'true'); + const searchParams = new URLSearchParams(document.location.search) + searchParams.set('_format', lang) + searchParams.set('_pretty', 'true') - const downloadLink = document.getElementById('download-link'); - downloadLink.href = '?' + searchParams.toString(); - downloadLink.download = getDownloadFileName(lang); - downloadLink.title = 'Download as ' + lang.toUpperCase(); + const downloadLink = document.getElementById('download-link') + downloadLink.href = '?' + searchParams.toString() + downloadLink.download = getDownloadFileName(lang) + downloadLink.title = 'Download as ' + lang.toUpperCase() } function getDownloadFileName(lang) { - const resourceType = getResourceTypeForCurrentUrl(); + const resourceType = getResourceTypeForCurrentUrl() /* /, /metadata, /_history */ if (resourceType == null) { - if (window.location.pathname.endsWith('/metadata')) { - return "metadata." + lang; - } else if (window.location.pathname.endsWith('/_history')) { - return "history." + lang; - } else { - return "root." + lang; - } + if (window.location.pathname.endsWith('/metadata')) + return "metadata." + lang + else if (window.location.pathname.endsWith('/_history')) + return "history." + lang + else + return "root." + lang } else { //Resource - if (resourceType[1] !== undefined && resourceType[2] === undefined && resourceType[3] === undefined && resourceType[4] === undefined) { - return resourceType[1] + '_Search.' + lang; - } + if (resourceType[1] !== undefined && resourceType[2] === undefined && resourceType[3] === undefined && resourceType[4] === undefined) + return resourceType[1] + '_Search.' + lang //Resource/_history - else if (resourceType[1] !== undefined && resourceType[2] === undefined && resourceType[3] !== undefined && resourceType[4] === undefined) { - return resourceType[1] + '_History.' + lang; - } + else if (resourceType[1] !== undefined && resourceType[2] === undefined && resourceType[3] !== undefined && resourceType[4] === undefined) + return resourceType[1] + '_History.' + lang //Resource/id - else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] === undefined && resourceType[4] === undefined) { - return resourceType[1] + '_' + resourceType[2].replace('/', '') + '.' + lang; - } + else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] === undefined && resourceType[4] === undefined) + return resourceType[1] + '_' + resourceType[2].replace('/', '') + '.' + lang //Resource/id/_history - else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] !== undefined && resourceType[4] === undefined) { - return resourceType[1] + '_' + resourceType[2].replace('/', '') + '_History.' + lang; - } + else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] !== undefined && resourceType[4] === undefined) + return resourceType[1] + '_' + resourceType[2].replace('/', '') + '_History.' + lang //Resource/id/_history/version - else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] !== undefined && resourceType[4] !== undefined) { - return resourceType[1] + '_' + resourceType[2].replace('/', '') + '_v' + resourceType[4].replace('/', '') + '.' + lang; - } + else if (resourceType[1] !== undefined && resourceType[2] !== undefined && resourceType[3] !== undefined && resourceType[4] !== undefined) + return resourceType[1] + '_' + resourceType[2].replace('/', '') + '_v' + resourceType[4].replace('/', '') + '.' + lang + } +} + +function setUiTheme(theme = getUiTheme()) { + if (theme === 'dark') { + document.getElementById('light-mode').style.display = 'block' + document.getElementById('dark-mode').style.display = 'none' } + else { + document.getElementById('light-mode').style.display = 'none' + document.getElementById('dark-mode').style.display = 'block' + } + + document.querySelector("html").setAttribute("theme", theme); + localStorage.setItem("theme", theme); +} + +function getUiTheme() { + if (localStorage !== null && localStorage.getItem("theme") !== null) + return localStorage.getItem("theme") + else if (window.matchMedia("(prefers-color-scheme: dark)").matches) + return "dark" + else + return "light" } \ No newline at end of file diff --git a/dsf-tools/dsf-tools-test-data-generator/src/main/java/dev/dsf/tools/generator/ConfigGenerator.java b/dsf-tools/dsf-tools-test-data-generator/src/main/java/dev/dsf/tools/generator/ConfigGenerator.java index ea0c8ac3e..1d9df72c0 100755 --- a/dsf-tools/dsf-tools-test-data-generator/src/main/java/dev/dsf/tools/generator/ConfigGenerator.java +++ b/dsf-tools/dsf-tools-test-data-generator/src/main/java/dev/dsf/tools/generator/ConfigGenerator.java @@ -90,6 +90,8 @@ public void modifyJavaTestFhirConfigProperties(Map cli - SEARCH - HISTORY - PERMANENT_DELETE + practitioner-role: + - http://dsf.dev/fhir/CodeSystem/practitioner-role|DSF_ADMIN """, webbrowserTestUser.getCertificateSha512ThumbprintHex())); writeProperties(Paths.get("config/java-test-fhir-config.properties"), javaTestFhirConfigProperties);