diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/AbstractImportExportCsvConceptMapCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/AbstractImportExportCsvConceptMapCommand.java new file mode 100644 index 00000000000..dfccb459251 --- /dev/null +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/AbstractImportExportCsvConceptMapCommand.java @@ -0,0 +1,369 @@ +package ca.uhn.fhir.cli; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.StringUtils.*; + +public abstract class AbstractImportExportCsvConceptMapCommand extends BaseCommand { + // TODO: Don't use qualified names for loggers in HAPI CLI. + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(AbstractImportExportCsvConceptMapCommand.class); + + protected static final String CONCEPTMAP_URL_PARAM = "u"; + protected static final String CONCEPTMAP_URL_PARAM_LONGOPT = "url"; + protected static final String CONCEPTMAP_URL_PARAM_NAME = "url"; + protected static final String CONCEPTMAP_URL_PARAM_DESC = "The URL of the ConceptMap resource to be imported/exported (i.e. ConceptMap.url)."; + protected static final String FILE_PARAM = "f"; + protected static final String FILE_PARAM_LONGOPT = "filename"; + protected static final String FILE_PARAM_NAME = "filename"; + protected static final String FILE_PARAM_DESC = "The path and filename of the CSV file to be imported/exported (e.g. ./input.csv, ./output.csv, etc.)."; + + protected IGenericClient client; + protected String conceptMapUrl; + protected FhirVersionEnum fhirVersion; + protected String file; + + @Override + protected void addFhirVersionOption(Options theOptions) { + String versions = Arrays.stream(FhirVersionEnum.values()) + .filter(t -> t != FhirVersionEnum.DSTU2_1 && t != FhirVersionEnum.DSTU2_HL7ORG && t != FhirVersionEnum.DSTU2) + .map(t -> t.name().toLowerCase()) + .sorted() + .collect(Collectors.joining(", ")); + addRequiredOption(theOptions, FHIR_VERSION_PARAM, FHIR_VERSION_PARAM_LONGOPT, FHIR_VERSION_PARAM_NAME, FHIR_VERSION_PARAM_DESC + versions); + } + + @Override + public void run(CommandLine theCommandLine) throws ParseException, ExecutionException { + parseFhirContext(theCommandLine); + FhirContext ctx = getFhirContext(); + + String targetServer = theCommandLine.getOptionValue(BASE_URL_PARAM); + if (isBlank(targetServer)) { + throw new ParseException("No target server (-" + BASE_URL_PARAM + ") specified."); + } else if (!targetServer.startsWith("http") && !targetServer.startsWith("file")) { + throw new ParseException("Invalid target server specified, must begin with 'http' or 'file'."); + } + + conceptMapUrl = theCommandLine.getOptionValue(CONCEPTMAP_URL_PARAM); + if (isBlank(conceptMapUrl)) { + throw new ParseException("No ConceptMap URL (" + CONCEPTMAP_URL_PARAM + ") specified."); + } else { + ourLog.info("Specified ConceptMap URL (ConceptMap.url): {}", conceptMapUrl); + } + + file = theCommandLine.getOptionValue(FILE_PARAM); + if (isBlank(file)) { + throw new ParseException("No file (" + FILE_PARAM + ") specified."); + } + if (!file.endsWith(".csv")) { + file = file.concat(".csv"); + } + + parseAdditionalParameters(theCommandLine); + + client = super.newClient(theCommandLine); + fhirVersion = ctx.getVersion().getVersion(); + if (fhirVersion != FhirVersionEnum.DSTU3 + && fhirVersion != FhirVersionEnum.R4) { + throw new ParseException("This command does not support FHIR version " + fhirVersion + "."); + } + + if (theCommandLine.hasOption(VERBOSE_LOGGING_PARAM)) { + client.registerInterceptor(new LoggingInterceptor(true)); + } + + process(); + } + + protected void parseAdditionalParameters(CommandLine theCommandLine) throws ParseException {} + + protected abstract void process() throws ParseException, ExecutionException; + + protected enum Header { + SOURCE_CODE_SYSTEM, + SOURCE_CODE_SYSTEM_VERSION, + TARGET_CODE_SYSTEM, + TARGET_CODE_SYSTEM_VERSION, + SOURCE_CODE, + SOURCE_DISPLAY, + TARGET_CODE, + TARGET_DISPLAY, + EQUIVALENCE, + COMMENT + } + + protected class TemporaryConceptMapGroup { + private String source; + private String sourceVersion; + private String target; + private String targetVersion; + + public TemporaryConceptMapGroup() { + } + + public TemporaryConceptMapGroup(String theSource, String theSourceVersion, String theTarget, String theTargetVersion) { + this.source = theSource; + this.sourceVersion = theSourceVersion; + this.target = theTarget; + this.targetVersion = theTargetVersion; + } + + public boolean hasSource() { + return isNotBlank(source); + } + + public String getSource() { + return source; + } + + public TemporaryConceptMapGroup setSource(String theSource) { + this.source = theSource; + return this; + } + + public boolean hasSourceVersion() { + return isNotBlank(sourceVersion); + } + + public String getSourceVersion() { + return sourceVersion; + } + + public TemporaryConceptMapGroup setSourceVersion(String theSourceVersion) { + this.sourceVersion = theSourceVersion; + return this; + } + + public boolean hasTarget() { + return isNotBlank(target); + } + + public String getTarget() { + return target; + } + + public TemporaryConceptMapGroup setTarget(String theTarget) { + this.target = theTarget; + return this; + } + + public boolean hasTargetVersion() { + return isNotBlank(targetVersion); + } + + public String getTargetVersion() { + return targetVersion; + } + + public TemporaryConceptMapGroup setTargetVersion(String theTargetVersion) { + this.targetVersion = theTargetVersion; + return this; + } + + public boolean hasValues() { + return !isAllBlank(getSource(), getSourceVersion(), getTarget(), getTargetVersion()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof TemporaryConceptMapGroup)) return false; + + TemporaryConceptMapGroup that = (TemporaryConceptMapGroup) o; + + return new EqualsBuilder() + .append(getSource(), that.getSource()) + .append(getSourceVersion(), that.getSourceVersion()) + .append(getTarget(), that.getTarget()) + .append(getTargetVersion(), that.getTargetVersion()) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(getSource()) + .append(getSourceVersion()) + .append(getTarget()) + .append(getTargetVersion()) + .toHashCode(); + } + } + + protected class TemporaryConceptMapGroupElement { + private String code; + private String display; + + public TemporaryConceptMapGroupElement() { + } + + public TemporaryConceptMapGroupElement(String theCode, String theDisplay) { + this.code = theCode; + this.display = theDisplay; + } + + public boolean hasCode() { + return isNotBlank(code); + } + + public String getCode() { + return code; + } + + public TemporaryConceptMapGroupElement setCode(String theCode) { + this.code = theCode; + return this; + } + + public boolean hasDisplay() { + return isNotBlank(display); + } + + public String getDisplay() { + return display; + } + + public TemporaryConceptMapGroupElement setDisplay(String theDisplay) { + this.display = theDisplay; + return this; + } + + public boolean hasValues() { + return !isAllBlank(getCode(), getDisplay()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof TemporaryConceptMapGroupElement)) return false; + + TemporaryConceptMapGroupElement that = (TemporaryConceptMapGroupElement) o; + + return new EqualsBuilder() + .append(getCode(), that.getCode()) + .append(getDisplay(), that.getDisplay()) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(getCode()) + .append(getDisplay()) + .toHashCode(); + } + } + + protected class TemporaryConceptMapGroupElementTarget { + private String code; + private String display; + private String equivalence; + private String comment; + + public TemporaryConceptMapGroupElementTarget() { + } + + public TemporaryConceptMapGroupElementTarget(String theCode, String theDisplay, String theEquivalence, String theComment) { + this.code = theCode; + this.display = theDisplay; + this.equivalence = theEquivalence; + this.comment = theComment; + } + + public boolean hasCode() { + return isNotBlank(code); + } + + public String getCode() { + return code; + } + + public TemporaryConceptMapGroupElementTarget setCode(String theCode) { + this.code = theCode; + return this; + } + + public boolean hasDisplay() { + return isNotBlank(display); + } + + public String getDisplay() { + return display; + } + + public TemporaryConceptMapGroupElementTarget setDisplay(String theDisplay) { + this.display = theDisplay; + return this; + } + + public boolean hasEquivalence() { + return isNotBlank(equivalence); + } + + public String getEquivalence() { + return equivalence; + } + + public TemporaryConceptMapGroupElementTarget setEquivalence(String theEquivalence) { + this.equivalence = theEquivalence; + return this; + } + + public boolean hasComment() { + return isNotBlank(comment); + } + + public String getComment() { + return comment; + } + + public TemporaryConceptMapGroupElementTarget setComment(String theComment) { + this.comment = theComment; + return this; + } + + public boolean hasValues() { + return !isAllBlank(getCode(), getDisplay(), getEquivalence(), getComment()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (!(o instanceof TemporaryConceptMapGroupElementTarget)) return false; + + TemporaryConceptMapGroupElementTarget that = (TemporaryConceptMapGroupElementTarget) o; + + return new EqualsBuilder() + .append(getCode(), that.getCode()) + .append(getDisplay(), that.getDisplay()) + .append(getEquivalence(), that.getEquivalence()) + .append(getComment(), that.getComment()) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(getCode()) + .append(getDisplay()) + .append(getEquivalence()) + .append(getComment()) + .toHashCode(); + } + } +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java index 18017162127..6284cdb5995 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseApp.java @@ -132,6 +132,8 @@ public abstract class BaseApp { commands.add(new WebsocketSubscribeCommand()); commands.add(new UploadTerminologyCommand()); commands.add(new IgPackUploader()); + commands.add(new ExportConceptMapToCsvCommand()); + commands.add(new ImportCsvToConceptMapCommand()); return commands; } diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java index 59f9204c994..5ae8c825299 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java @@ -57,21 +57,42 @@ import java.util.zip.GZIPInputStream; import static org.apache.commons.lang3.StringUtils.*; public abstract class BaseCommand implements Comparable { - public static final String BASE_URL_PARAM = "t"; - public static final String BASIC_AUTH_OPTION = "b"; - public static final String BASIC_AUTH_LONGOPT = "basic-auth"; - public static final String BEARER_TOKEN_LONGOPT = "bearer-token"; - public static final String FHIR_VERSION_OPTION = "v"; + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final Logger ourLog = LoggerFactory.getLogger(BaseCommand.class); - private FhirContext myFhirCtx; + + protected static final String BASE_URL_PARAM = "t"; + protected static final String BASE_URL_PARAM_LONGOPT = "target"; + protected static final String BASE_URL_PARAM_NAME = "target"; + protected static final String BASE_URL_PARAM_DESC = "Base URL for the target server (e.g. \"http://example.com/fhir\")."; + protected static final String BASIC_AUTH_PARAM = "b"; + protected static final String BASIC_AUTH_PARAM_LONGOPT = "basic-auth"; + protected static final String BASIC_AUTH_PARAM_NAME = "basic-auth"; + protected static final String BASIC_AUTH_PARAM_DESC = "If specified, this parameter supplies a username and password (in the format \"username:password\") to include in an HTTP Basic Auth header."; + protected static final String BEARER_TOKEN_PARAM_LONGOPT = "bearer-token"; + protected static final String BEARER_TOKEN_PARAM_NAME = "bearer-token"; + protected static final String BEARER_TOKEN_PARAM_DESC = "If specified, this parameter supplies a Bearer Token to supply with the request."; + protected static final String FHIR_VERSION_PARAM = "v"; + protected static final String FHIR_VERSION_PARAM_LONGOPT = "fhir-version"; + protected static final String FHIR_VERSION_PARAM_NAME = "version"; + protected static final String FHIR_VERSION_PARAM_DESC = "The FHIR version being used. Valid values: "; + protected static final String VERBOSE_LOGGING_PARAM = "l"; + protected static final String VERBOSE_LOGGING_PARAM_LONGOPT = "logging"; + protected static final String VERBOSE_LOGGING_PARAM_DESC = "If specified, verbose logging will be used."; + + + protected FhirContext myFhirCtx; public BaseCommand() { super(); } + protected void addBaseUrlOption(Options theOptions) { + addRequiredOption(theOptions, BASE_URL_PARAM, BASE_URL_PARAM_LONGOPT, BASE_URL_PARAM_NAME, BASE_URL_PARAM_DESC); + } + protected void addBasicAuthOption(Options theOptions) { - addOptionalOption(theOptions, BASIC_AUTH_OPTION, BASIC_AUTH_LONGOPT, true, "If specified, this parameter supplies a username and password (in the format \"username:password\") to include in an HTTP Basic Auth header"); - addOptionalOption(theOptions, null, BEARER_TOKEN_LONGOPT, true, "If specified, this parameter supplies a Bearer Token to supply with the request"); + addOptionalOption(theOptions, BASIC_AUTH_PARAM, BASIC_AUTH_PARAM_LONGOPT, BASIC_AUTH_PARAM_NAME, BASIC_AUTH_PARAM_DESC); + addOptionalOption(theOptions, null, BEARER_TOKEN_PARAM_LONGOPT, BEARER_TOKEN_PARAM_NAME, BEARER_TOKEN_PARAM_DESC); } protected void addFhirVersionOption(Options theOptions) { @@ -80,7 +101,11 @@ public abstract class BaseCommand implements Comparable { .map(t -> t.name().toLowerCase()) .sorted() .collect(Collectors.joining(", ")); - addRequiredOption(theOptions, FHIR_VERSION_OPTION, "fhir-version", "version", "The FHIR version being used. Valid values: " + versions); + addRequiredOption(theOptions, FHIR_VERSION_PARAM, FHIR_VERSION_PARAM_LONGOPT, FHIR_VERSION_PARAM_NAME, FHIR_VERSION_PARAM_DESC + versions); + } + + protected void addVerboseLoggingOption(Options theOptions) { + addOptionalOption(theOptions, VERBOSE_LOGGING_PARAM, VERBOSE_LOGGING_PARAM_LONGOPT, false, VERBOSE_LOGGING_PARAM_DESC); } private void addOption(Options theOptions, boolean theRequired, String theOpt, String theLong, boolean theHasArgument, String theArgumentName, String theDescription) { @@ -117,9 +142,7 @@ public abstract class BaseCommand implements Comparable { } protected void addRequiredOption(Options theOptions, String theOpt, String theLong, String theArgumentName, String theDescription) { - boolean hasArgument = isNotBlank(theArgumentName); - boolean required = true; - addOption(theOptions, required, theOpt, theLong, hasArgument, theArgumentName, theDescription); + addOption(theOptions, true, theOpt, theLong, isNotBlank(theArgumentName), theArgumentName, theDescription); } @Override @@ -182,7 +205,7 @@ public abstract class BaseCommand implements Comparable { * @return Returns the complete authorization header value using the "-b" option */ protected String getAndParseOptionBasicAuthHeader(CommandLine theCommandLine) { - return getAndParseOptionBasicAuthHeader(theCommandLine, BASIC_AUTH_OPTION); + return getAndParseOptionBasicAuthHeader(theCommandLine, BASIC_AUTH_PARAM); } /** @@ -307,12 +330,17 @@ public abstract class BaseCommand implements Comparable { return inputFiles; } - protected IGenericClient newClient(CommandLine theCommandLine) { - return newClient(theCommandLine, BASE_URL_PARAM, BASIC_AUTH_OPTION, BEARER_TOKEN_LONGOPT); + protected IGenericClient newClient(CommandLine theCommandLine) throws ParseException { + return newClient(theCommandLine, BASE_URL_PARAM, BASIC_AUTH_PARAM, BEARER_TOKEN_PARAM_LONGOPT); } - protected IGenericClient newClient(CommandLine theCommandLine, String theBaseUrlParamName, String theBasicAuthOptionName, String theBearerTokenOptionName) { + protected IGenericClient newClient(CommandLine theCommandLine, String theBaseUrlParamName, String theBasicAuthOptionName, String theBearerTokenOptionName) throws ParseException { String baseUrl = theCommandLine.getOptionValue(theBaseUrlParamName); + if (isBlank(baseUrl)) { + throw new ParseException("No target server (-" + BASE_URL_PARAM + ") specified."); + } else if (!baseUrl.startsWith("http") && !baseUrl.startsWith("file")) { + throw new ParseException("Invalid target server specified, must begin with 'http' or 'file'."); + } myFhirCtx.getRestfulClientFactory().setSocketTimeout(10 * 60 * 1000); IGenericClient retVal = myFhirCtx.newRestfulGenericClient(baseUrl); @@ -333,9 +361,9 @@ public abstract class BaseCommand implements Comparable { } protected void parseFhirContext(CommandLine theCommandLine) throws ParseException { - String version = theCommandLine.getOptionValue(FHIR_VERSION_OPTION); + String version = theCommandLine.getOptionValue(FHIR_VERSION_PARAM); if (isBlank(version)) { - throw new ParseException("Missing required option: -" + FHIR_VERSION_OPTION); + throw new ParseException("Missing required option: -" + FHIR_VERSION_PARAM); } try { @@ -348,5 +376,4 @@ public abstract class BaseCommand implements Comparable { public abstract void run(CommandLine theCommandLine) throws ParseException, ExecutionException; - } diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ExampleDataUploader.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ExampleDataUploader.java index 07a3c395ca5..266d6d76667 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ExampleDataUploader.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ExampleDataUploader.java @@ -63,7 +63,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; public class ExampleDataUploader extends BaseCommand { - + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExampleDataUploader.class); private IBaseBundle getBundleFromFile(Integer theLimit, File theSuppliedFile, FhirContext theCtx) throws ParseException, IOException { diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ExportConceptMapToCsvCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ExportConceptMapToCsvCommand.java new file mode 100644 index 00000000000..4b252979be8 --- /dev/null +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ExportConceptMapToCsvCommand.java @@ -0,0 +1,196 @@ +package ca.uhn.fhir.cli; + +/*- + * #%L + * HAPI FHIR - Command Line Client - API + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.io.IOUtils; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.ConceptMap; +import org.hl7.fhir.r4.model.ConceptMap.ConceptMapGroupComponent; +import org.hl7.fhir.r4.model.ConceptMap.SourceElementComponent; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.lang3.StringUtils.defaultString; + +public class ExportConceptMapToCsvCommand extends AbstractImportExportCsvConceptMapCommand { + // TODO: Don't use qualified names for loggers in HAPI CLI. + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExportConceptMapToCsvCommand.class); + + @Override + public String getCommandDescription() { + return "Exports a specific ConceptMap resource to a CSV file."; + } + + @Override + public String getCommandName() { + return "export-conceptmap-to-csv"; + } + + @Override + public Options getOptions() { + Options options = new Options(); + + this.addFhirVersionOption(options); + addBaseUrlOption(options); + addRequiredOption(options, CONCEPTMAP_URL_PARAM, CONCEPTMAP_URL_PARAM_LONGOPT, CONCEPTMAP_URL_PARAM_NAME, CONCEPTMAP_URL_PARAM_DESC); + addRequiredOption(options, FILE_PARAM, FILE_PARAM_LONGOPT, FILE_PARAM_NAME, FILE_PARAM_DESC); + addBasicAuthOption(options); + addVerboseLoggingOption(options); + + return options; + } + + @Override + protected void process() throws ParseException { + searchForConceptMapByUrl(); + } + + private void searchForConceptMapByUrl() { + ourLog.info("Searching for ConceptMap with specified URL (i.e. ConceptMap.url): {}", conceptMapUrl); + if (fhirVersion == FhirVersionEnum.DSTU3) { + org.hl7.fhir.dstu3.model.Bundle response = client + .search() + .forResource(org.hl7.fhir.dstu3.model.ConceptMap.class) + .where(org.hl7.fhir.dstu3.model.ConceptMap.URL.matches().value(conceptMapUrl)) + .returnBundle(org.hl7.fhir.dstu3.model.Bundle.class) + .execute(); + + if (response.hasEntry()) { + ourLog.info("Found ConceptMap with specified URL (i.e. ConceptMap.url): {}", conceptMapUrl); + org.hl7.fhir.dstu3.model.ConceptMap conceptMap = (org.hl7.fhir.dstu3.model.ConceptMap) response.getEntryFirstRep().getResource(); + convertConceptMapToCsv(conceptMap); + } else { + ourLog.info("No ConceptMap exists with specified URL (i.e. ConceptMap.url): {}", conceptMapUrl); + } + } else if (fhirVersion == FhirVersionEnum.R4) { + Bundle response = client + .search() + .forResource(ConceptMap.class) + .where(ConceptMap.URL.matches().value(conceptMapUrl)) + .returnBundle(Bundle.class) + .execute(); + + if (response.hasEntry()) { + ourLog.info("Found ConceptMap with specified URL (i.e. ConceptMap.url): {}", conceptMapUrl); + ConceptMap conceptMap = (ConceptMap) response.getEntryFirstRep().getResource(); + convertConceptMapToCsv(conceptMap); + } else { + ourLog.info("No ConceptMap exists with specified URL (i.e. ConceptMap.url): {}", conceptMapUrl); + } + } + } + + private void convertConceptMapToCsv(org.hl7.fhir.dstu3.model.ConceptMap theConceptMap) { + ourLog.info("Exporting ConceptMap to CSV..."); + BufferedWriter bufferedWriter = null; + CSVPrinter csvPrinter = null; + try { + bufferedWriter = Files.newBufferedWriter(Paths.get(file)); + csvPrinter = new CSVPrinter( + bufferedWriter, + CSVFormat + .DEFAULT + .withRecordSeparator("\n") + .withHeader(Header.class)); + + for (org.hl7.fhir.dstu3.model.ConceptMap.ConceptMapGroupComponent group : theConceptMap.getGroup()) { + for (org.hl7.fhir.dstu3.model.ConceptMap.SourceElementComponent element : group.getElement()) { + for (org.hl7.fhir.dstu3.model.ConceptMap.TargetElementComponent target : element.getTarget()) { + + List columns = new ArrayList<>(); + columns.add(defaultString(group.getSource())); + columns.add(defaultString(group.getSourceVersion())); + columns.add(defaultString(group.getTarget())); + columns.add(defaultString(group.getTargetVersion())); + columns.add(defaultString(element.getCode())); + columns.add(defaultString(element.getDisplay())); + columns.add(defaultString(target.getCode())); + columns.add(defaultString(target.getDisplay())); + columns.add(defaultString(target.getEquivalence().toCode())); + columns.add(defaultString(target.getComment())); + + csvPrinter.print(columns); + } + } + } + } catch (IOException ioe) { + throw new InternalErrorException(ioe); + } finally { + IOUtils.closeQuietly(csvPrinter); + IOUtils.closeQuietly(bufferedWriter); + } + ourLog.info("Finished exporting to {}", file); + } + + private void convertConceptMapToCsv(ConceptMap theConceptMap) { + ourLog.info("Exporting ConceptMap to CSV..."); + Writer writer = null; + CSVPrinter csvPrinter = null; + try { + writer = Files.newBufferedWriter(Paths.get(file)); + csvPrinter = new CSVPrinter( + writer, + CSVFormat + .DEFAULT + .withRecordSeparator("\n") + .withHeader(Header.class)); + + for (ConceptMapGroupComponent group : theConceptMap.getGroup()) { + for (SourceElementComponent element : group.getElement()) { + for (ConceptMap.TargetElementComponent target : element.getTarget()) { + + List columns = new ArrayList<>(); + columns.add(defaultString(group.getSource())); + columns.add(defaultString(group.getSourceVersion())); + columns.add(defaultString(group.getTarget())); + columns.add(defaultString(group.getTargetVersion())); + columns.add(defaultString(element.getCode())); + columns.add(defaultString(element.getDisplay())); + columns.add(defaultString(target.getCode())); + columns.add(defaultString(target.getDisplay())); + columns.add(defaultString(target.getEquivalence().toCode())); + columns.add(defaultString(target.getComment())); + + csvPrinter.printRecord(columns); + } + } + } + } catch (IOException ioe) { + throw new InternalErrorException(ioe); + } finally { + IOUtils.closeQuietly(csvPrinter); + IOUtils.closeQuietly(writer); + } + ourLog.info("Finished exporting to {}", file); + } +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/IgPackUploader.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/IgPackUploader.java index 19b13eef5ed..496b133af37 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/IgPackUploader.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/IgPackUploader.java @@ -23,7 +23,6 @@ package ca.uhn.fhir.cli; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.igpacks.parser.IgPackParserDstu3; import ca.uhn.fhir.rest.client.api.IGenericClient; -import net.sf.ehcache.transaction.xa.commands.Command; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; @@ -42,6 +41,7 @@ import java.io.IOException; import java.util.Collection; public class IgPackUploader extends BaseCommand { + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final Logger ourLog = LoggerFactory.getLogger(IgPackUploader.class); @Override diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ImportCsvToConceptMapCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ImportCsvToConceptMapCommand.java new file mode 100644 index 00000000000..608c05187c1 --- /dev/null +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ImportCsvToConceptMapCommand.java @@ -0,0 +1,336 @@ +package ca.uhn.fhir.cli; + +/*- + * #%L + * HAPI FHIR - Command Line Client - API + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; +import org.apache.commons.io.IOUtils; +import org.hl7.fhir.convertors.VersionConvertor_30_40; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r4.model.ConceptMap; +import org.hl7.fhir.r4.model.ConceptMap.ConceptMapGroupComponent; +import org.hl7.fhir.r4.model.ConceptMap.SourceElementComponent; +import org.hl7.fhir.r4.model.ConceptMap.TargetElementComponent; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.UriType; + +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.ExecutionException; + +import static org.apache.commons.lang3.StringUtils.*; + +public class ImportCsvToConceptMapCommand extends AbstractImportExportCsvConceptMapCommand { + // TODO: Don't use qualified names for loggers in HAPI CLI. + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ImportCsvToConceptMapCommand.class); + + protected static final String SOURCE_VALUE_SET_PARAM = "i"; + protected static final String SOURCE_VALUE_SET_PARAM_LONGOPT = "input"; + protected static final String SOURCE_VALUE_SET_PARAM_NAME = "input"; + protected static final String SOURCE_VALUE_SET_PARAM_DESC = "The source value set of the ConceptMap to be imported (i.e. ConceptMap.sourceUri)."; + protected static final String TARGET_VALUE_SET_PARAM = "o"; + protected static final String TARGET_VALUE_SET_PARAM_LONGOPT = "output"; + protected static final String TARGET_VALUE_SET_PARAM_NAME = "output"; + protected static final String TARGET_VALUE_SET_PARAM_DESC = "The target value set of the ConceptMap to be imported (i.e. ConceptMap.targetUri)."; + + protected String sourceValueSet; + protected String targetValueSet; + + private boolean hasElements; + private boolean hasTargets; + + @Override + public String getCommandDescription() { + return "Imports a CSV file to a ConceptMap resource."; + } + + @Override + public String getCommandName() { + return "import-csv-to-conceptmap"; + } + + @Override + public Options getOptions() { + Options options = new Options(); + + this.addFhirVersionOption(options); + addBaseUrlOption(options); + addRequiredOption(options, CONCEPTMAP_URL_PARAM, CONCEPTMAP_URL_PARAM_LONGOPT, CONCEPTMAP_URL_PARAM_NAME, CONCEPTMAP_URL_PARAM_DESC); + // + addOptionalOption(options, SOURCE_VALUE_SET_PARAM, SOURCE_VALUE_SET_PARAM_LONGOPT, SOURCE_VALUE_SET_PARAM_NAME, SOURCE_VALUE_SET_PARAM_DESC); + addOptionalOption(options, TARGET_VALUE_SET_PARAM, TARGET_VALUE_SET_PARAM_LONGOPT, TARGET_VALUE_SET_PARAM_NAME, TARGET_VALUE_SET_PARAM_DESC); + // + addRequiredOption(options, FILE_PARAM, FILE_PARAM_LONGOPT, FILE_PARAM_NAME, FILE_PARAM_DESC); + addBasicAuthOption(options); + addVerboseLoggingOption(options); + + return options; + } + + @Override + protected void parseAdditionalParameters(CommandLine theCommandLine) throws ParseException { + sourceValueSet = theCommandLine.getOptionValue(SOURCE_VALUE_SET_PARAM); + if (isBlank(sourceValueSet)) { + ourLog.info("Source value set is not specified (i.e. ConceptMap.sourceUri)."); + } else { + ourLog.info("Specified source value set (i.e. ConceptMap.sourceUri): {}", sourceValueSet); + } + + targetValueSet = theCommandLine.getOptionValue(TARGET_VALUE_SET_PARAM); + if (isBlank(targetValueSet)) { + ourLog.info("Target value set is not specified (i.e. ConceptMap.targetUri)."); + } else { + ourLog.info("Specified target value set (i.e. ConceptMap.targetUri): {}", targetValueSet); + } + } + + @Override + protected void process() throws ParseException, ExecutionException { + searchForConceptMapByUrl(); + } + + private void searchForConceptMapByUrl() throws ParseException, ExecutionException { + if (fhirVersion == FhirVersionEnum.DSTU3) { + org.hl7.fhir.dstu3.model.ConceptMap conceptMap = convertCsvToConceptMapDstu3(); + + ourLog.info("Searching for existing ConceptMap with specified URL (i.e. ConceptMap.url): {}", conceptMapUrl); + MethodOutcome methodOutcome = client + .update() + .resource(conceptMap) + .conditional() + .where(org.hl7.fhir.dstu3.model.ConceptMap.URL.matches().value(conceptMapUrl)) + .execute(); + + if (Boolean.TRUE.equals(methodOutcome.getCreated())) { + ourLog.info("Created new ConceptMap: {}", methodOutcome.getId().getValue()); + } else { + ourLog.info("Updated existing ConceptMap: {}", methodOutcome.getId().getValue()); + } + } else if (fhirVersion == FhirVersionEnum.R4) { + ConceptMap conceptMap = convertCsvToConceptMapR4(); + + ourLog.info("Searching for existing ConceptMap with specified URL (i.e. ConceptMap.url): {}", conceptMapUrl); + MethodOutcome methodOutcome = client + .update() + .resource(conceptMap) + .conditional() + .where(ConceptMap.URL.matches().value(conceptMapUrl)) + .execute(); + + if (Boolean.TRUE.equals(methodOutcome.getCreated())) { + ourLog.info("Created new ConceptMap: {}", methodOutcome.getId().getValue()); + } else { + ourLog.info("Updated existing ConceptMap: {}", methodOutcome.getId().getValue()); + } + } + } + + private org.hl7.fhir.dstu3.model.ConceptMap convertCsvToConceptMapDstu3() throws ParseException, ExecutionException { + try { + return VersionConvertor_30_40.convertConceptMap(convertCsvToConceptMapR4()); + } catch (FHIRException fe) { + throw new ExecutionException(fe); + } + } + + private ConceptMap convertCsvToConceptMapR4() throws ParseException, ExecutionException { + ourLog.info("Converting CSV to ConceptMap..."); + ConceptMap retVal = new ConceptMap(); + Reader reader = null; + CSVParser csvParser = null; + try { + reader = Files.newBufferedReader(Paths.get(file)); + csvParser = new CSVParser( + reader, + CSVFormat + .DEFAULT + .withRecordSeparator("\n") + .withHeader(Header.class) + .withFirstRecordAsHeader() + .withIgnoreHeaderCase() + .withTrim()); + + retVal.setUrl(conceptMapUrl); + + if (isNotBlank(sourceValueSet)) { + retVal.setSource(new UriType(sourceValueSet)); + } + + if (isNotBlank(targetValueSet)) { + retVal.setTarget(new UriType(targetValueSet)); + } + + TemporaryConceptMapGroup temporaryConceptMapGroup; + TemporaryConceptMapGroupElement temporaryConceptMapGroupElement; + Map>> groupMap = parseCsvRecords(csvParser); + Map> elementMap; + Set targetSet; + ConceptMapGroupComponent conceptMapGroupComponent; + SourceElementComponent sourceElementComponent; + TargetElementComponent targetElementComponent; + for (Map.Entry>> groupEntry : groupMap.entrySet()) { + + hasElements = false; + hasTargets = false; + + temporaryConceptMapGroup = groupEntry.getKey(); + conceptMapGroupComponent = new ConceptMapGroupComponent(); + + if (temporaryConceptMapGroup.hasSource()) { + conceptMapGroupComponent.setSource(temporaryConceptMapGroup.getSource()); + } + + if (temporaryConceptMapGroup.hasSourceVersion()) { + conceptMapGroupComponent.setSourceVersion(temporaryConceptMapGroup.getSourceVersion()); + } + + if (temporaryConceptMapGroup.hasTarget()) { + conceptMapGroupComponent.setTarget(temporaryConceptMapGroup.getTarget()); + } + + if (temporaryConceptMapGroup.hasTargetVersion()) { + conceptMapGroupComponent.setTargetVersion(temporaryConceptMapGroup.getTargetVersion()); + } + + elementMap = groupEntry.getValue(); + for (Map.Entry> elementEntry : elementMap.entrySet()) { + + temporaryConceptMapGroupElement = elementEntry.getKey(); + sourceElementComponent = new SourceElementComponent(); + + if (temporaryConceptMapGroupElement.hasCode()) { + sourceElementComponent.setCode(temporaryConceptMapGroupElement.getCode()); + } + + if (temporaryConceptMapGroupElement.hasDisplay()) { + sourceElementComponent.setDisplay(temporaryConceptMapGroupElement.getDisplay()); + } + + targetSet = elementEntry.getValue(); + for (TemporaryConceptMapGroupElementTarget temporaryConceptMapGroupElementTarget : targetSet) { + + targetElementComponent = new TargetElementComponent(); + + if (temporaryConceptMapGroupElementTarget.hasCode()) { + targetElementComponent.setCode(temporaryConceptMapGroupElementTarget.getCode()); + } + + if (temporaryConceptMapGroupElementTarget.hasDisplay()) { + targetElementComponent.setDisplay(temporaryConceptMapGroupElementTarget.getDisplay()); + } + + if (temporaryConceptMapGroupElementTarget.hasEquivalence()) { + try { + targetElementComponent.setEquivalence(Enumerations.ConceptMapEquivalence.fromCode(temporaryConceptMapGroupElementTarget.getEquivalence())); + } catch (FHIRException fe) { + throw new ExecutionException(fe); + } + } + + if (temporaryConceptMapGroupElementTarget.hasComment()) { + targetElementComponent.setComment(temporaryConceptMapGroupElementTarget.getComment()); + } + + if (temporaryConceptMapGroupElementTarget.hasValues()) { + sourceElementComponent.addTarget(targetElementComponent); + hasTargets = true; + } + } + + if (temporaryConceptMapGroupElement.hasValues() || hasTargets) { + conceptMapGroupComponent.addElement(sourceElementComponent); + hasElements = true; + } + } + + if (temporaryConceptMapGroup.hasValues() || hasElements || hasTargets) { + retVal.addGroup(conceptMapGroupComponent); + } + } + } catch (IOException e) { + throw new InternalErrorException(e); + } finally { + IOUtils.closeQuietly(csvParser); + IOUtils.closeQuietly(reader); + } + + ourLog.info("Finished converting CSV to ConceptMap."); + return retVal; + } + + private Map>> parseCsvRecords(CSVParser theCsvParser) { + Map>> retVal = new LinkedHashMap<>(); + + TemporaryConceptMapGroup group; + TemporaryConceptMapGroupElement element; + TemporaryConceptMapGroupElementTarget target; + Map> elementMap; + Set targetSet; + + for (CSVRecord csvRecord : theCsvParser) { + + group = new TemporaryConceptMapGroup( + defaultString(csvRecord.get(Header.SOURCE_CODE_SYSTEM)), + defaultString(csvRecord.get(Header.SOURCE_CODE_SYSTEM_VERSION)), + defaultString(csvRecord.get(Header.TARGET_CODE_SYSTEM)), + defaultString(csvRecord.get(Header.TARGET_CODE_SYSTEM_VERSION))); + + element = new TemporaryConceptMapGroupElement( + defaultString(csvRecord.get(Header.SOURCE_CODE)), + defaultString(csvRecord.get(Header.SOURCE_DISPLAY))); + + target = new TemporaryConceptMapGroupElementTarget( + defaultString(csvRecord.get(Header.TARGET_CODE)), + defaultString(csvRecord.get(Header.TARGET_DISPLAY)), + defaultString(csvRecord.get(Header.EQUIVALENCE)), + defaultString(csvRecord.get(Header.COMMENT))); + + if (!retVal.containsKey(group)) { + targetSet = new LinkedHashSet<>(); + targetSet.add(target); + + elementMap = new LinkedHashMap<>(); + elementMap.put(element, targetSet); + + retVal.put(group, elementMap); + } else if (!retVal.get(group).containsKey(element)) { + targetSet = new LinkedHashSet<>(); + targetSet.add(target); + + retVal.get(group).put(element, targetSet); + } else { + retVal.get(group).get(element).add(target); + } + } + + return retVal; + } +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu2.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu2.java index 3fef32991e3..e515f12b248 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu2.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu2.java @@ -35,6 +35,7 @@ public class LoadingValidationSupportDstu2 implements IValidationSupport { private FhirContext myCtx = FhirContext.forDstu2Hl7Org(); + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LoadingValidationSupportDstu2.class); @Override diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu3.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu3.java index 47bea91c635..c5825017477 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu3.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportDstu3.java @@ -38,6 +38,7 @@ public class LoadingValidationSupportDstu3 implements IValidationSupport { private FhirContext myCtx = FhirContext.forDstu3(); + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LoadingValidationSupportDstu3.class); @Override diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportR4.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportR4.java index 5484ddc2546..4dfa5839480 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportR4.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/LoadingValidationSupportR4.java @@ -34,7 +34,7 @@ import java.util.Collections; import java.util.List; public class LoadingValidationSupportR4 implements org.hl7.fhir.r4.hapi.ctx.IValidationSupport { - + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LoadingValidationSupportR4.class); private FhirContext myCtx = FhirContext.forR4(); diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/RunServerCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/RunServerCommand.java index b89e4da3a1e..163021438a8 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/RunServerCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/RunServerCommand.java @@ -20,17 +20,11 @@ package ca.uhn.fhir.cli; * #L% */ -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.SocketException; - -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; - +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.demo.ContextHolder; +import ca.uhn.fhir.jpa.demo.FhirServerConfig; +import ca.uhn.fhir.jpa.demo.FhirServerConfigDstu3; +import ca.uhn.fhir.jpa.demo.FhirServerConfigR4; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; @@ -41,8 +35,10 @@ import org.springframework.web.context.ContextLoader; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import ca.uhn.fhir.jpa.dao.DaoConfig; -import ca.uhn.fhir.jpa.demo.*; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import java.io.*; +import java.net.SocketException; public class RunServerCommand extends BaseCommand { @@ -53,6 +49,7 @@ public class RunServerCommand extends BaseCommand { private static final int DEFAULT_PORT = 8080; private static final String OPTION_P = "p"; + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RunServerCommand.class); public static final String RUN_SERVER_COMMAND = "run-server"; private int myPort; diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java index a79ba4378a3..756567ed7d4 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java @@ -24,10 +24,8 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.jpa.term.IHapiTerminologyLoaderSvc; import ca.uhn.fhir.rest.client.api.IGenericClient; -import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.hl7.fhir.dstu3.model.Parameters; @@ -36,12 +34,10 @@ import org.hl7.fhir.dstu3.model.UriType; import org.hl7.fhir.instance.model.api.IBaseParameters; import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; public class UploadTerminologyCommand extends BaseCommand { - + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UploadTerminologyCommand.class); - private static final String BASE_URL_PARAM = "t"; private static final String UPLOAD_EXTERNAL_CODE_SYSTEM = "upload-external-code-system"; @Override @@ -59,10 +55,11 @@ public class UploadTerminologyCommand extends BaseCommand { Options options = new Options(); addFhirVersionOption(options); - addRequiredOption(options, "t", "target", true, "Base URL for the target server (e.g. \"http://example.com/fhir\")"); + addBaseUrlOption(options); addRequiredOption(options, "u", "url", true, "The code system URL associated with this upload (e.g. " + IHapiTerminologyLoaderSvc.SCT_URI + ")"); addOptionalOption(options, "d", "data", true, "Local file to use to upload (can be a raw file or a ZIP containing the raw file)"); addBasicAuthOption(options); + addVerboseLoggingOption(options); return options; } @@ -72,13 +69,6 @@ public class UploadTerminologyCommand extends BaseCommand { parseFhirContext(theCommandLine); FhirContext ctx = getFhirContext(); - String targetServer = theCommandLine.getOptionValue(BASE_URL_PARAM); - if (isBlank(targetServer)) { - throw new ParseException("No target server (-" + BASE_URL_PARAM + ") specified"); - } else if (targetServer.startsWith("http") == false && targetServer.startsWith("file") == false) { - throw new ParseException("Invalid target server specified, must begin with 'http' or 'file'"); - } - String termUrl = theCommandLine.getOptionValue("u"); if (isBlank(termUrl)) { throw new ParseException("No URL provided"); @@ -89,8 +79,6 @@ public class UploadTerminologyCommand extends BaseCommand { throw new ParseException("No data file provided"); } - String bearerToken = theCommandLine.getOptionValue("b"); - IGenericClient client = super.newClient(theCommandLine); IBaseParameters inputParameters; if (ctx.getVersion().getVersion() == FhirVersionEnum.DSTU3) { @@ -104,11 +92,7 @@ public class UploadTerminologyCommand extends BaseCommand { throw new ParseException("This command does not support FHIR version " + ctx.getVersion().getVersion()); } - if (isNotBlank(bearerToken)) { - client.registerInterceptor(new BearerTokenAuthInterceptor(bearerToken)); - } - - if (theCommandLine.hasOption('v')) { + if (theCommandLine.hasOption(VERBOSE_LOGGING_PARAM)) { client.registerInterceptor(new LoggingInterceptor(true)); } diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidateCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidateCommand.java index 8a3c6df6901..ec80fe58065 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidateCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidateCommand.java @@ -41,7 +41,7 @@ import static org.apache.commons.lang3.StringUtils.*; import static org.fusesource.jansi.Ansi.ansi; public class ValidateCommand extends BaseCommand { - + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ValidateCommand.class); @Override diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidationDataUploader.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidationDataUploader.java index a5c38b9116c..b72bc998e18 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidationDataUploader.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidationDataUploader.java @@ -53,7 +53,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; public class ValidationDataUploader extends BaseCommand { - + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ValidationDataUploader.class); private ArrayList myExcludes = new ArrayList<>(); @@ -167,7 +167,7 @@ public class ValidationDataUploader extends BaseCommand { } - private void uploadDefinitionsDstu2(CommandLine theCommandLine, FhirContext ctx) throws CommandFailureException { + private void uploadDefinitionsDstu2(CommandLine theCommandLine, FhirContext ctx) throws CommandFailureException, ParseException { IGenericClient client = newClient(theCommandLine); ourLog.info("Uploading definitions to server"); @@ -267,7 +267,7 @@ public class ValidationDataUploader extends BaseCommand { ourLog.info("Finished uploading definitions to server (took {} ms)", delay); } - private void uploadDefinitionsDstu3(CommandLine theCommandLine, FhirContext theCtx) throws CommandFailureException { + private void uploadDefinitionsDstu3(CommandLine theCommandLine, FhirContext theCtx) throws CommandFailureException, ParseException { IGenericClient client = newClient(theCommandLine); ourLog.info("Uploading definitions to server"); @@ -365,7 +365,7 @@ public class ValidationDataUploader extends BaseCommand { ourLog.info("Finished uploading definitions to server (took {} ms)", delay); } - private void uploadDefinitionsR4(CommandLine theCommandLine, FhirContext theCtx) throws CommandFailureException { + private void uploadDefinitionsR4(CommandLine theCommandLine, FhirContext theCtx) throws CommandFailureException, ParseException { IGenericClient client = newClient(theCommandLine); ourLog.info("Uploading definitions to server"); diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/WebsocketSubscribeCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/WebsocketSubscribeCommand.java index a30f2c91e7e..47d3554b288 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/WebsocketSubscribeCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/WebsocketSubscribeCommand.java @@ -37,6 +37,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; public class WebsocketSubscribeCommand extends BaseCommand { private static final org.slf4j.Logger LOG_RECV = org.slf4j.LoggerFactory.getLogger("websocket.RECV"); private static final org.slf4j.Logger LOG_SEND = org.slf4j.LoggerFactory.getLogger("websocket.SEND"); + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(WebsocketSubscribeCommand.class); private boolean myQuit; diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/ExportConceptMapToCsvCommandTest.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/ExportConceptMapToCsvCommandTest.java new file mode 100644 index 00000000000..3eafc068710 --- /dev/null +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/ExportConceptMapToCsvCommandTest.java @@ -0,0 +1,282 @@ +package ca.uhn.fhir.cli; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.interceptor.VerboseLoggingInterceptor; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; +import com.google.common.base.Charsets; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.r4.model.ConceptMap; +import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence; +import org.hl7.fhir.r4.model.UriType; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; + +public class ExportConceptMapToCsvCommandTest { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ExportConceptMapToCsvCommandTest.class); + private static final String CM_URL = "http://example.com/conceptmap"; + private static final String VS_URL_1 = "http://example.com/valueset/1"; + private static final String VS_URL_2 = "http://example.com/valueset/2"; + private static final String CS_URL_1 = "http://example.com/codesystem/1"; + private static final String CS_URL_2 = "http://example.com/codesystem/2"; + private static final String CS_URL_3 = "http://example.com/codesystem/3"; + private static final String FILE = "./target/output.csv"; + + private static String ourBase; + private static IGenericClient ourClient; + private static FhirContext ourCtx = FhirContext.forR4(); + private static int ourPort; + private static Server ourServer; + + static { + System.setProperty("test", "true"); + } + + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + ServletHandler servletHandler = new ServletHandler(); + + RestfulServer restfulServer = new RestfulServer(ourCtx); + restfulServer.registerInterceptor(new VerboseLoggingInterceptor()); + restfulServer.setResourceProviders(new HashMapResourceProviderConceptMapR4(ourCtx)); + + ServletHolder servletHolder = new ServletHolder(restfulServer); + servletHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(servletHandler); + + ourServer.start(); + + ourBase = "http://localhost:" + ourPort; + + ourClient = ourCtx.newRestfulGenericClient(ourBase); + + ourClient.create().resource(createConceptMap()).execute(); + } + + @Test + public void testExportConceptMapToCsvCommand() throws IOException { + ourLog.info("ConceptMap:\n" + ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(createConceptMap())); + + App.main(new String[] {"export-conceptmap-to-csv", + "-v", "r4", + "-t", ourBase, + "-u", CM_URL, + "-f", FILE, + "-l"}); + + String expected = "SOURCE_CODE_SYSTEM,SOURCE_CODE_SYSTEM_VERSION,TARGET_CODE_SYSTEM,TARGET_CODE_SYSTEM_VERSION,SOURCE_CODE,SOURCE_DISPLAY,TARGET_CODE,TARGET_DISPLAY,EQUIVALENCE,COMMENT\n" + + "http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/2,Version 2t,Code 1a,Display 1a,Code 2a,Display 2a,equal,2a This is a comment.\n" + + "http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/2,Version 2t,Code 1b,Display 1b,Code 2b,Display 2b,equal,2b This is a comment.\n" + + "http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/2,Version 2t,Code 1c,Display 1c,Code 2c,Display 2c,equal,2c This is a comment.\n" + + "http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/2,Version 2t,Code 1d,Display 1d,Code 2d,Display 2d,equal,2d This is a comment.\n" + + "http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/3,Version 3t,Code 1a,Display 1a,Code 3a,Display 3a,equal,3a This is a comment.\n" + + "http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/3,Version 3t,Code 1b,Display 1b,Code 3b,Display 3b,equal,3b This is a comment.\n" + + "http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/3,Version 3t,Code 1c,Display 1c,Code 3c,Display 3c,equal,3c This is a comment.\n" + + "http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/3,Version 3t,Code 1d,Display 1d,Code 3d,Display 3d,equal,3d This is a comment.\n" + + "http://example.com/codesystem/2,Version 2s,http://example.com/codesystem/3,Version 3t,Code 2a,Display 2a,Code 3a,Display 3a,equal,3a This is a comment.\n" + + "http://example.com/codesystem/2,Version 2s,http://example.com/codesystem/3,Version 3t,Code 2b,Display 2b,Code 3b,Display 3b,equal,3b This is a comment.\n" + + "http://example.com/codesystem/2,Version 2s,http://example.com/codesystem/3,Version 3t,Code 2c,Display 2c,Code 3c,Display 3c,equal,3c This is a comment.\n" + + "http://example.com/codesystem/2,Version 2s,http://example.com/codesystem/3,Version 3t,Code 2d,Display 2d,Code 3d,Display 3d,equal,3d This is a comment.\n"; + String result = IOUtils.toString(new FileInputStream(FILE), Charsets.UTF_8); + assertEquals(expected, result); + + FileUtils.deleteQuietly(new File(FILE)); + } + + static ConceptMap createConceptMap() { + ConceptMap conceptMap = new ConceptMap(); + conceptMap + .setUrl(CM_URL) + .setSource(new UriType(VS_URL_1)) + .setTarget(new UriType(VS_URL_2)); + + ConceptMap.ConceptMapGroupComponent group = conceptMap.addGroup(); + group + .setSource(CS_URL_1) + .setSourceVersion("Version 1s") + .setTarget(CS_URL_2) + .setTargetVersion("Version 2t"); + + ConceptMap.SourceElementComponent element = group.addElement(); + element + .setCode("Code 1a") + .setDisplay("Display 1a"); + + ConceptMap.TargetElementComponent target = element.addTarget(); + target + .setCode("Code 2a") + .setDisplay("Display 2a") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("2a This is a comment."); + + element = group.addElement(); + element + .setCode("Code 1b") + .setDisplay("Display 1b"); + + target = element.addTarget(); + target + .setCode("Code 2b") + .setDisplay("Display 2b") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("2b This is a comment."); + + element = group.addElement(); + element + .setCode("Code 1c") + .setDisplay("Display 1c"); + + target = element.addTarget(); + target + .setCode("Code 2c") + .setDisplay("Display 2c") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("2c This is a comment."); + + element = group.addElement(); + element + .setCode("Code 1d") + .setDisplay("Display 1d"); + + target = element.addTarget(); + target + .setCode("Code 2d") + .setDisplay("Display 2d") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("2d This is a comment."); + + group = conceptMap.addGroup(); + group + .setSource(CS_URL_1) + .setSourceVersion("Version 1s") + .setTarget(CS_URL_3) + .setTargetVersion("Version 3t"); + + element = group.addElement(); + element + .setCode("Code 1a") + .setDisplay("Display 1a"); + + target = element.addTarget(); + target + .setCode("Code 3a") + .setDisplay("Display 3a") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("3a This is a comment."); + + element = group.addElement(); + element + .setCode("Code 1b") + .setDisplay("Display 1b"); + + target = element.addTarget(); + target + .setCode("Code 3b") + .setDisplay("Display 3b") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("3b This is a comment."); + + element = group.addElement(); + element + .setCode("Code 1c") + .setDisplay("Display 1c"); + + target = element.addTarget(); + target + .setCode("Code 3c") + .setDisplay("Display 3c") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("3c This is a comment."); + + element = group.addElement(); + element + .setCode("Code 1d") + .setDisplay("Display 1d"); + + target = element.addTarget(); + target + .setCode("Code 3d") + .setDisplay("Display 3d") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("3d This is a comment."); + + group = conceptMap.addGroup(); + group + .setSource(CS_URL_2) + .setSourceVersion("Version 2s") + .setTarget(CS_URL_3) + .setTargetVersion("Version 3t"); + + element = group.addElement(); + element + .setCode("Code 2a") + .setDisplay("Display 2a"); + + target = element.addTarget(); + target + .setCode("Code 3a") + .setDisplay("Display 3a") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("3a This is a comment."); + + element = group.addElement(); + element + .setCode("Code 2b") + .setDisplay("Display 2b"); + + target = element.addTarget(); + target + .setCode("Code 3b") + .setDisplay("Display 3b") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("3b This is a comment."); + + element = group.addElement(); + element + .setCode("Code 2c") + .setDisplay("Display 2c"); + + target = element.addTarget(); + target + .setCode("Code 3c") + .setDisplay("Display 3c") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("3c This is a comment."); + + element = group.addElement(); + element + .setCode("Code 2d") + .setDisplay("Display 2d"); + + target = element.addTarget(); + target + .setCode("Code 3d") + .setDisplay("Display 3d") + .setEquivalence(ConceptMapEquivalence.EQUAL) + .setComment("3d This is a comment."); + + return conceptMap; + } +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/HashMapResourceProviderConceptMapR4.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/HashMapResourceProviderConceptMapR4.java new file mode 100644 index 00000000000..d958ae5a3c4 --- /dev/null +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/HashMapResourceProviderConceptMapR4.java @@ -0,0 +1,127 @@ +package ca.uhn.fhir.cli; + +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.annotation.*; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.provider.AbstractHashMapResourceProvider; +import com.google.common.base.Charsets; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.hl7.fhir.r4.model.ConceptMap; +import org.hl7.fhir.r4.model.IdType; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeMap; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +/** + * This is a subclass to implement FHIR operations specific to R4 ConceptMap + * resources. Its superclass, {@link AbstractHashMapResourceProvider}, is a simple + * implementation of the resource provider interface that uses a HashMap to + * store all resources in memory. + *

+ * This subclass currently supports the following FHIR operations: + *

+ *
    + *
  • Search for R4 ConceptMap resources by ConceptMap.url
  • + *
  • Conditional update for R4 ConceptMap resources by ConceptMap.url
  • + *
+ */ +public class HashMapResourceProviderConceptMapR4 extends AbstractHashMapResourceProvider { + @SuppressWarnings("unchecked") + public HashMapResourceProviderConceptMapR4(FhirContext theFhirContext) { + super(theFhirContext, ConceptMap.class); + + FhirVersionEnum fhirVersion = theFhirContext.getVersion().getVersion(); + if (fhirVersion != FhirVersionEnum.R4) { + throw new IllegalStateException("Requires FHIR version R4. Unsupported FHIR version provided: " + fhirVersion); + } + + + } + + @Search + public List searchByUrl( + @RequiredParam(name=ConceptMap.SP_URL) String theConceptMapUrl) { + + List retVal = new ArrayList<>(); + + for (TreeMap next : myIdToVersionToResourceMap.values()) { + if (!next.isEmpty()) { + ConceptMap conceptMap = next.lastEntry().getValue(); + if (theConceptMapUrl.equals(conceptMap.getUrl())) + retVal.add(conceptMap); + break; + } + } + + return retVal; + } + + @Update + public MethodOutcome updateConceptMapConditional( + @ResourceParam ConceptMap theConceptMap, + @IdParam IdType theId, + @ConditionalUrlParam String theConditional) { + + MethodOutcome methodOutcome = new MethodOutcome(); + + if (theConditional != null) { + + String url = null; + + try { + List params = URLEncodedUtils.parse(new URI(theConditional), Charsets.UTF_8); + for (NameValuePair param : params) { + if (param.getName().equalsIgnoreCase("url")) { + url = param.getValue(); + break; + } + } + } catch (URISyntaxException urise) { + throw new InvalidRequestException(urise); + } + + if (isNotBlank(url)) { + List conceptMaps = searchByUrl(url); + + if (!conceptMaps.isEmpty()) { + methodOutcome = update(conceptMaps.get(0)); + } else { + methodOutcome = create(theConceptMap); + } + } + + } else { + methodOutcome = update(theConceptMap); + } + + return methodOutcome; + } +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/ImportCsvToConceptMapCommandTest.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/ImportCsvToConceptMapCommandTest.java new file mode 100644 index 00000000000..231ea5716a3 --- /dev/null +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/java/ca/uhn/fhir/cli/ImportCsvToConceptMapCommandTest.java @@ -0,0 +1,371 @@ +package ca.uhn.fhir.cli; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.interceptor.VerboseLoggingInterceptor; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.ConceptMap; +import org.hl7.fhir.r4.model.ConceptMap.ConceptMapGroupComponent; +import org.hl7.fhir.r4.model.ConceptMap.SourceElementComponent; +import org.hl7.fhir.r4.model.ConceptMap.TargetElementComponent; +import org.hl7.fhir.r4.model.Enumerations.ConceptMapEquivalence; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; + +import static org.junit.Assert.*; + +public class ImportCsvToConceptMapCommandTest { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ImportCsvToConceptMapCommandTest.class); + private static final String CM_URL = "http://example.com/conceptmap"; + private static final String VS_URL_1 = "http://example.com/valueset/1"; + private static final String VS_URL_2 = "http://example.com/valueset/2"; + private static final String CS_URL_1 = "http://example.com/codesystem/1"; + private static final String CS_URL_2 = "http://example.com/codesystem/2"; + private static final String CS_URL_3 = "http://example.com/codesystem/3"; + private static final String FILENAME = "import-csv-to-conceptmap-command-test-input.csv"; + + private static String file; + private static String ourBase; + private static IGenericClient ourClient; + private static FhirContext ourCtx = FhirContext.forR4(); + private static int ourPort; + private static Server ourServer; + + private static RestfulServer restfulServer; + + private static HashMapResourceProviderConceptMapR4 hashMapResourceProviderConceptMapR4; + + static { + System.setProperty("test", "true"); + } + + @After + public void afterClearResourceProvider() { + HashMapResourceProviderConceptMapR4 resourceProvider = (HashMapResourceProviderConceptMapR4) restfulServer.getResourceProviders().iterator().next(); + resourceProvider.clear(); + } + + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + ServletHandler servletHandler = new ServletHandler(); + + restfulServer = new RestfulServer(ourCtx); + restfulServer.registerInterceptor(new VerboseLoggingInterceptor()); + restfulServer.setResourceProviders(new HashMapResourceProviderConceptMapR4(ourCtx)); + + ServletHolder servletHolder = new ServletHolder(restfulServer); + servletHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(servletHandler); + + ourServer.start(); + + ourBase = "http://localhost:" + ourPort; + + ourClient = ourCtx.newRestfulGenericClient(ourBase); + } + + @Test + public void testConditionalUpdateResultsInCreate() { + ConceptMap conceptMap = ExportConceptMapToCsvCommandTest.createConceptMap(); + String conceptMapUrl = conceptMap.getUrl(); + + ourLog.info("Searching for existing ConceptMap with specified URL (i.e. ConceptMap.url): {}", conceptMapUrl); + MethodOutcome methodOutcome = ourClient + .update() + .resource(conceptMap) + .conditional() + .where(ConceptMap.URL.matches().value(conceptMapUrl)) + .execute(); + + // Do not simplify to assertEquals(...) + assertTrue(Boolean.TRUE.equals(methodOutcome.getCreated())); + } + + @Test + public void testConditionalUpdateResultsInUpdate() { + ConceptMap conceptMap = ExportConceptMapToCsvCommandTest.createConceptMap(); + ourClient.create().resource(conceptMap).execute(); + String conceptMapUrl = conceptMap.getUrl(); + + ourLog.info("Searching for existing ConceptMap with specified URL (i.e. ConceptMap.url): {}", conceptMapUrl); + MethodOutcome methodOutcome = ourClient + .update() + .resource(conceptMap) + .conditional() + .where(ConceptMap.URL.matches().value(conceptMapUrl)) + .execute(); + + // Do not simplify to assertEquals(...) + assertTrue(!Boolean.TRUE.equals(methodOutcome.getCreated())); + } + + @Test + public void testNonConditionalUpdate() { + ConceptMap conceptMap = ExportConceptMapToCsvCommandTest.createConceptMap(); + ourClient.create().resource(conceptMap).execute(); + + Bundle response = ourClient + .search() + .forResource(ConceptMap.class) + .where(ConceptMap.URL.matches().value(CM_URL)) + .returnBundle(Bundle.class) + .execute(); + + ConceptMap resultConceptMap = (ConceptMap) response.getEntryFirstRep().getResource(); + + MethodOutcome methodOutcome = ourClient + .update() + .resource(resultConceptMap) + .withId(resultConceptMap.getIdElement()) + .execute(); + + assertNull(methodOutcome.getCreated()); + + // Do not simplify to assertEquals(...) + assertTrue(!Boolean.TRUE.equals(methodOutcome.getCreated())); + } + + @Test + public void testImportCsvToConceptMapCommand() throws FHIRException { + ClassLoader classLoader = getClass().getClassLoader(); + File fileToImport = new File(classLoader.getResource(FILENAME).getFile()); + ImportCsvToConceptMapCommandTest.file = fileToImport.getAbsolutePath(); + + App.main(new String[] {"import-csv-to-conceptmap", + "-v", "r4", + "-t", ourBase, + "-u", CM_URL, + "-i", VS_URL_1, + "-o", VS_URL_2, + "-f", file, + "-l"}); + + Bundle response = ourClient + .search() + .forResource(ConceptMap.class) + .where(ConceptMap.URL.matches().value(CM_URL)) + .returnBundle(Bundle.class) + .execute(); + + ConceptMap conceptMap = (ConceptMap) response.getEntryFirstRep().getResource(); + + ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(conceptMap)); + + assertEquals("http://localhost:" + ourPort + "/ConceptMap/1/_history/1", conceptMap.getId()); + + assertEquals(CM_URL, conceptMap.getUrl()); + assertEquals(VS_URL_1, conceptMap.getSourceUriType().getValueAsString()); + assertEquals(VS_URL_2, conceptMap.getTargetUriType().getValueAsString()); + + assertEquals(3, conceptMap.getGroup().size()); + + ConceptMapGroupComponent group = conceptMap.getGroup().get(0); + assertEquals(CS_URL_1, group.getSource()); + assertEquals("Version 1s", group.getSourceVersion()); + assertEquals(CS_URL_2, group.getTarget()); + assertEquals("Version 2t", group.getTargetVersion()); + + assertEquals(4, group.getElement().size()); + + SourceElementComponent source = group.getElement().get(0); + assertEquals("Code 1a", source.getCode()); + assertEquals("Display 1a", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + TargetElementComponent target = source.getTarget().get(0); + assertEquals("Code 2a", target.getCode()); + assertEquals("Display 2a", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("2a This is a comment.", target.getComment()); + + source = group.getElement().get(1); + assertEquals("Code 1b", source.getCode()); + assertEquals("Display 1b", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + target = source.getTarget().get(0); + assertEquals("Code 2b", target.getCode()); + assertEquals("Display 2b", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("2b This is a comment.", target.getComment()); + + source = group.getElement().get(2); + assertEquals("Code 1c", source.getCode()); + assertEquals("Display 1c", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + target = source.getTarget().get(0); + assertEquals("Code 2c", target.getCode()); + assertEquals("Display 2c", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("2c This is a comment.", target.getComment()); + + source = group.getElement().get(3); + assertEquals("Code 1d", source.getCode()); + assertEquals("Display 1d", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + target = source.getTarget().get(0); + assertEquals("Code 2d", target.getCode()); + assertEquals("Display 2d", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("2d This is a comment.", target.getComment()); + + group = conceptMap.getGroup().get(1); + assertEquals(CS_URL_1, group.getSource()); + assertEquals("Version 1s", group.getSourceVersion()); + assertEquals(CS_URL_3, group.getTarget()); + assertEquals("Version 3t", group.getTargetVersion()); + + assertEquals(4, group.getElement().size()); + + source = group.getElement().get(0); + assertEquals("Code 1a", source.getCode()); + assertEquals("Display 1a", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + target = source.getTarget().get(0); + assertEquals("Code 3a", target.getCode()); + assertEquals("Display 3a", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("3a This is a comment.", target.getComment()); + + source = group.getElement().get(1); + assertEquals("Code 1b", source.getCode()); + assertEquals("Display 1b", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + target = source.getTarget().get(0); + assertEquals("Code 3b", target.getCode()); + assertEquals("Display 3b", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("3b This is a comment.", target.getComment()); + + source = group.getElement().get(2); + assertEquals("Code 1c", source.getCode()); + assertEquals("Display 1c", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + target = source.getTarget().get(0); + assertEquals("Code 3c", target.getCode()); + assertEquals("Display 3c", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("3c This is a comment.", target.getComment()); + + source = group.getElement().get(3); + assertEquals("Code 1d", source.getCode()); + assertEquals("Display 1d", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + target = source.getTarget().get(0); + assertEquals("Code 3d", target.getCode()); + assertEquals("Display 3d", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("3d This is a comment.", target.getComment()); + + group = conceptMap.getGroup().get(2); + assertEquals(CS_URL_2, group.getSource()); + assertEquals("Version 2s", group.getSourceVersion()); + assertEquals(CS_URL_3, group.getTarget()); + assertEquals("Version 3t", group.getTargetVersion()); + + assertEquals(4, group.getElement().size()); + + source = group.getElement().get(0); + assertEquals("Code 2a", source.getCode()); + assertEquals("Display 2a", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + target = source.getTarget().get(0); + assertEquals("Code 3a", target.getCode()); + assertEquals("Display 3a", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("3a This is a comment.", target.getComment()); + + source = group.getElement().get(1); + assertEquals("Code 2b", source.getCode()); + assertEquals("Display 2b", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + target = source.getTarget().get(0); + assertEquals("Code 3b", target.getCode()); + assertEquals("Display 3b", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("3b This is a comment.", target.getComment()); + + source = group.getElement().get(2); + assertEquals("Code 2c", source.getCode()); + assertEquals("Display 2c", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + target = source.getTarget().get(0); + assertEquals("Code 3c", target.getCode()); + assertEquals("Display 3c", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("3c This is a comment.", target.getComment()); + + source = group.getElement().get(3); + assertEquals("Code 2d", source.getCode()); + assertEquals("Display 2d", source.getDisplay()); + + assertEquals(1, source.getTarget().size()); + + target = source.getTarget().get(0); + assertEquals("Code 3d", target.getCode()); + assertEquals("Display 3d", target.getDisplay()); + assertEquals(ConceptMapEquivalence.EQUAL, target.getEquivalence()); + assertEquals("3d This is a comment.", target.getComment()); + + App.main(new String[] {"import-csv-to-conceptmap", + "-v", "r4", + "-t", ourBase, + "-u", CM_URL, + "-i", VS_URL_1, + "-o", VS_URL_2, + "-f", file, + "-l"}); + + response = ourClient + .search() + .forResource(ConceptMap.class) + .where(ConceptMap.URL.matches().value(CM_URL)) + .returnBundle(Bundle.class) + .execute(); + + conceptMap = (ConceptMap) response.getEntryFirstRep().getResource(); + + assertEquals("http://localhost:" + ourPort + "/ConceptMap/1/_history/2", conceptMap.getId()); + } +} diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/test/resources/import-csv-to-conceptmap-command-test-input.csv b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/resources/import-csv-to-conceptmap-command-test-input.csv new file mode 100644 index 00000000000..3ffd4cefc74 --- /dev/null +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/test/resources/import-csv-to-conceptmap-command-test-input.csv @@ -0,0 +1,13 @@ +SOURCE_CODE_SYSTEM,SOURCE_CODE_SYSTEM_VERSION,TARGET_CODE_SYSTEM,TARGET_CODE_SYSTEM_VERSION,SOURCE_CODE,SOURCE_DISPLAY,TARGET_CODE,TARGET_DISPLAY,EQUIVALENCE,COMMENT +http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/2,Version 2t,Code 1a,Display 1a,Code 2a,Display 2a,equal,2a This is a comment. +http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/2,Version 2t,Code 1b,Display 1b,Code 2b,Display 2b,equal,2b This is a comment. +http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/2,Version 2t,Code 1c,Display 1c,Code 2c,Display 2c,equal,2c This is a comment. +http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/2,Version 2t,Code 1d,Display 1d,Code 2d,Display 2d,equal,2d This is a comment. +http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/3,Version 3t,Code 1a,Display 1a,Code 3a,Display 3a,equal,3a This is a comment. +http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/3,Version 3t,Code 1b,Display 1b,Code 3b,Display 3b,equal,3b This is a comment. +http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/3,Version 3t,Code 1c,Display 1c,Code 3c,Display 3c,equal,3c This is a comment. +http://example.com/codesystem/1,Version 1s,http://example.com/codesystem/3,Version 3t,Code 1d,Display 1d,Code 3d,Display 3d,equal,3d This is a comment. +http://example.com/codesystem/2,Version 2s,http://example.com/codesystem/3,Version 3t,Code 2a,Display 2a,Code 3a,Display 3a,equal,3a This is a comment. +http://example.com/codesystem/2,Version 2s,http://example.com/codesystem/3,Version 3t,Code 2b,Display 2b,Code 3b,Display 3b,equal,3b This is a comment. +http://example.com/codesystem/2,Version 2s,http://example.com/codesystem/3,Version 3t,Code 2c,Display 2c,Code 3c,Display 3c,equal,3c This is a comment. +http://example.com/codesystem/2,Version 2s,http://example.com/codesystem/3,Version 3t,Code 2d,Display 2d,Code 3d,Display 3d,equal,3d This is a comment. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/AbstractHashMapResourceProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/AbstractHashMapResourceProvider.java new file mode 100644 index 00000000000..0756072e34b --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/AbstractHashMapResourceProvider.java @@ -0,0 +1,198 @@ +package ca.uhn.fhir.rest.server.provider; + +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * Copyright (C) 2014 - 2018 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.*; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +/** + * This class is a simple implementation of the resource provider + * interface that uses a HashMap to store all resources in memory. + * It is essentially a copy of {@link ca.uhn.fhir.rest.server.provider.HashMapResourceProvider} + * with the {@link Update} and {@link ResourceParam} annotations removed from method + * {@link ca.uhn.fhir.rest.server.provider.HashMapResourceProvider#update(IBaseResource)}. + * Non-generic subclasses of this abstract class may implement their own annotated methods (e.g. a conditional + * update method specifically for ConceptMap resources). + *

+ * This class currently supports the following FHIR operations: + *

+ *
    + *
  • Create
  • + *
  • Update existing resource
  • + *
  • Update non-existing resource (e.g. create with client-supplied ID)
  • + *
  • Delete
  • + *
  • Search by resource type with no parameters
  • + *
+ * + * @param The resource type to support + */ +public class AbstractHashMapResourceProvider implements IResourceProvider { + private static final Logger ourLog = LoggerFactory.getLogger(AbstractHashMapResourceProvider.class); + private final Class myResourceType; + private final FhirContext myFhirContext; + private final String myResourceName; + protected Map> myIdToVersionToResourceMap = new HashMap<>(); + private long myNextId; + + /** + * Constructor + * + * @param theFhirContext The FHIR context + * @param theResourceType The resource type to support + */ + @SuppressWarnings("WeakerAccess") + public AbstractHashMapResourceProvider(FhirContext theFhirContext, Class theResourceType) { + myFhirContext = theFhirContext; + myResourceType = theResourceType; + myResourceName = myFhirContext.getResourceDefinition(theResourceType).getName(); + clear(); + } + + /** + * Clear all data held in this resource provider + */ + public void clear() { + myNextId = 1; + myIdToVersionToResourceMap.clear(); + } + + @Create + public MethodOutcome create(@ResourceParam T theResource) { + long idPart = myNextId++; + String idPartAsString = Long.toString(idPart); + Long versionIdPart = 1L; + + IIdType id = store(theResource, idPartAsString, versionIdPart); + + return new MethodOutcome() + .setCreated(true) + .setId(id); + } + + @Delete + public MethodOutcome delete(@IdParam IIdType theId) { + TreeMap versions = myIdToVersionToResourceMap.get(theId.getIdPart()); + if (versions == null || versions.isEmpty()) { + throw new ResourceNotFoundException(theId); + } + + long nextVersion = versions.lastEntry().getKey() + 1L; + IIdType id = store(null, theId.getIdPart(), nextVersion); + + return new MethodOutcome() + .setId(id); + } + + @Override + public Class getResourceType() { + return myResourceType; + } + + private synchronized TreeMap getVersionToResource(String theIdPart) { + if (!myIdToVersionToResourceMap.containsKey(theIdPart)) { + myIdToVersionToResourceMap.put(theIdPart, new TreeMap()); + } + return myIdToVersionToResourceMap.get(theIdPart); + } + + @Read(version = true) + public IBaseResource read(@IdParam IIdType theId) { + TreeMap versions = myIdToVersionToResourceMap.get(theId.getIdPart()); + if (versions == null || versions.isEmpty()) { + throw new ResourceNotFoundException(theId); + } + + if (theId.hasVersionIdPart()) { + Long versionId = theId.getVersionIdPartAsLong(); + if (!versions.containsKey(versionId)) { + throw new ResourceNotFoundException(theId); + } else { + T resource = versions.get(versionId); + if (resource == null) { + throw new ResourceGoneException(theId); + } + return resource; + } + + } else { + return versions.lastEntry().getValue(); + } + } + + @Search + public List search() { + List retVal = new ArrayList<>(); + + for (TreeMap next : myIdToVersionToResourceMap.values()) { + if (next.isEmpty() == false) { + retVal.add(next.lastEntry().getValue()); + } + } + + return retVal; + } + + private IIdType store(@ResourceParam T theResource, String theIdPart, Long theVersionIdPart) { + IIdType id = myFhirContext.getVersion().newIdType(); + id.setParts(null, myResourceName, theIdPart, Long.toString(theVersionIdPart)); + if (theResource != null) { + theResource.setId(id); + } + + TreeMap versionToResource = getVersionToResource(theIdPart); + versionToResource.put(theVersionIdPart, theResource); + + ourLog.info("Storing resource with ID: {}", id.getValue()); + + return id; + } + + public MethodOutcome update(T theResource) { + String idPartAsString = theResource.getIdElement().getIdPart(); + TreeMap versionToResource = getVersionToResource(idPartAsString); + + Long versionIdPart; + Boolean created; + if (versionToResource.isEmpty()) { + versionIdPart = 1L; + created = true; + } else { + versionIdPart = versionToResource.lastKey() + 1L; + created = false; + } + + IIdType id = store(theResource, idPartAsString, versionIdPart); + + return new MethodOutcome() + .setCreated(created) + .setId(id); + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java index f7bdec0f74f..4ad1fc3d8a7 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java @@ -54,7 +54,7 @@ public class HashMapResourceProvider implements IResour private final Class myResourceType; private final FhirContext myFhirContext; private final String myResourceName; - private Map> myIdToVersionToResourceMap = new HashMap<>(); + protected Map> myIdToVersionToResourceMap = new HashMap<>(); private long myNextId; /** diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 80676fabdc5..379ccc15779 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -124,6 +124,11 @@ The ConceptMap]] operation $translate]]> has been implemented. + + HAPI-FHIR_CLI now includes two new commands: one for importing and populating a + ConceptMap]]> resource from a CSV; and one for exporting a + ConceptMap]]> resource to a CSV. +