diff --git a/docs/src/pages/guides/FHIRServerUsersGuide.md b/docs/src/pages/guides/FHIRServerUsersGuide.md
index f07953eb7f7..3f349697a86 100644
--- a/docs/src/pages/guides/FHIRServerUsersGuide.md
+++ b/docs/src/pages/guides/FHIRServerUsersGuide.md
@@ -1476,7 +1476,7 @@ The Bulk Data web application writes the exported FHIR resources to an IBM Cloud
"useServerTruststore": true
},
"pageSize": 100,
- "batchIdEncryptionKey": "example-password",
+ "batchIdEncodingKey": "example-password",
"maxPartitions": 3,
"maxInputs": 5
},
@@ -2356,7 +2356,7 @@ This section contains reference information about each of the configuration prop
|`fhirServer/bulkdata/core/file/resourceCountThreshold`|number|The number of resources at which to finish writing a given file. The actual number of resources written to a single file may be slightly above this number, dependent on the configured page size. Use `0` to indicate that there is no limit to the number of resources to be written to a single file.|
|`fhirServer/bulkdata/core/azure/objectSizeThresholdMB`|number|The size, in megabytes, at which to finish writing a given object.|
|`fhirServer/bulkdata/core/azure/objectResourceCountThreshold`|number|The number of resources at which to finish writing a given object. The actual number of resources written to a single object may be slightly above this number, dependent on the configured page size.|
-|`fhirServer/bulkdata/core/batchIdEncryptionKey`|string|Encoding key for JavaBatch job id |
+|`fhirServer/bulkdata/core/batchIdEncodingKey`|string|Encoding key for JavaBatch job id |
|`fhirServer/bulkdata/core/pageSize`|number|The search page size for patient/group export and the legacy export, the default value is 1000 |
|`fhirServer/bulkdata/core/maxPartitions`|number| The maximum number of simultaneous partitions that are processed per Export and Import |
|`fhirServer/bulkdata/core/maxInputs`|number| The number of inputs allowed for $import |
@@ -2535,6 +2535,7 @@ This section contains reference information about each of the configuration prop
|`fhirServer/bulkdata/core/cos/presignedExpiry`|86400|
|`fhirServer/bulkdata/core/azure/objectSizeThresholdMB`|200|
|`fhirServer/bulkdata/core/azure/objectResourceCountThreshold`|200000|
+|`fhirServer/bulkdata/core/batchIdEncodingKey`|""|
|`fhirServer/bulkdata/core/pageSize`|1000|
|`fhirServer/bulkdata/core/maxPartitions`|5|
|`fhirServer/bulkdata/core/maxInputs`|5|
@@ -2717,7 +2718,7 @@ Cases where that behavior is not supported are marked below with an `N` in the `
|`fhirServer/bulkdata/core/cos/presignedExpiry`|Y|Y|Y|
|`fhirServer/bulkdata/core/azure/objectSizeThresholdMB`|N|N||
|`fhirServer/bulkdata/core/azure/objectResourceCountThreshold`|N|N||
-|`fhirServer/bulkdata/core/batchIdEncryptionKey`|Y|N|Y|
+|`fhirServer/bulkdata/core/batchIdEncodingKey`|Y|N|Y|
|`fhirServer/bulkdata/core/pageSize`|Y|Y|Y|
|`fhirServer/bulkdata/core/maxPartitions`|Y|Y|Y|
|`fhirServer/bulkdata/core/maxInputs`|Y|Y|Y|
diff --git a/docs/src/pages/guides/FHIRValidationGuide.md b/docs/src/pages/guides/FHIRValidationGuide.md
index a3989837051..35e82a2e722 100644
--- a/docs/src/pages/guides/FHIRValidationGuide.md
+++ b/docs/src/pages/guides/FHIRValidationGuide.md
@@ -219,7 +219,7 @@ Below is the list of implementation guides that are packaged as part of the proj
|--------------------|-----------------|
|US Core|`3.1.1`, `4.0.0`, `5.0.1`|
|CARIN Consumer Directed Payer Data Exchange (CARIN IG for BlueButton®)|`1.0.0`, `1.1.0`|
-|minimal Common Oncology Data Elements (mCODE)|`1.0.0`
+|minimal Common Oncology Data Elements (mCODE)|`1.0.0`|
|Da Vinci Health Record Exchange (HREX)|`1.0.0`|
|Da Vinci Payer Data Exchange (PDEX)|`1.0.0`, `2.0.0`|
|Da Vinci Payer Data Exchange (PDEX) Plan Net|`1.0.0`, `1.1.0`|
diff --git a/fhir-parent/pom.xml b/fhir-parent/pom.xml
index 4208aa62b32..98a1c230608 100644
--- a/fhir-parent/pom.xml
+++ b/fhir-parent/pom.xml
@@ -726,6 +726,11 @@
icu4j
71.1
+
+ org.hashids
+ hashids
+ 1.0.3
+
diff --git a/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json b/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json
index 3fa58e72b70..27cb4e457b6 100644
--- a/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json
+++ b/fhir-server-webapp/src/main/liberty/config/config/default/fhir-server-config.json
@@ -156,7 +156,7 @@
"resourceCountThreshold": 200000
},
"pageSize": 100,
- "batchIdEncryptionKey": "change-password",
+ "batchIdEncodingKey": "change-password",
"maxPartitions": 5,
"maxInputs": 5,
"maxChunkReadTime": "90000",
diff --git a/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-azurite.json b/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-azurite.json
index 37813b9bffe..22a03e4a643 100644
--- a/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-azurite.json
+++ b/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-azurite.json
@@ -340,7 +340,7 @@
"resourceCountThreshold": 200000
},
"pageSize": 100,
- "batchIdEncryptionKey": "change-password",
+ "batchIdEncodingKey": "change-password",
"maxPartitions": 5,
"maxInputs": 5
},
diff --git a/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-minio.json b/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-minio.json
index d009481f7a0..149c5c8d8b7 100644
--- a/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-minio.json
+++ b/fhir-server-webapp/src/test/liberty/config/config/default/fhir-server-config-postgresql-minio.json
@@ -330,7 +330,7 @@
"resourceCountThreshold": 200000
},
"pageSize": 100,
- "batchIdEncryptionKey": "change-password",
+ "batchIdEncodingKey": "change-password",
"maxPartitions": 5,
"maxInputs": 5
},
diff --git a/operation/fhir-operation-bulkdata/pom.xml b/operation/fhir-operation-bulkdata/pom.xml
index 237c454f03f..1847a9ad529 100644
--- a/operation/fhir-operation-bulkdata/pom.xml
+++ b/operation/fhir-operation-bulkdata/pom.xml
@@ -19,6 +19,18 @@
fhir-server
${project.version}
+
+ org.apache.httpcomponents
+ httpclient
+
+
+ org.apache.httpcomponents
+ httpcore
+
+
+ org.hashids
+ hashids
+
jakarta.ws.rs
jakarta.ws.rs-api
@@ -34,14 +46,6 @@
jsonassert
test
-
- org.apache.httpcomponents
- httpclient
-
-
- org.apache.httpcomponents
- httpcore
-
org.apache.cxf
cxf-rt-frontend-jaxrs
diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/client/BulkDataClient.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/client/BulkDataClient.java
index 4db2e54e26c..9a7bab445a4 100644
--- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/client/BulkDataClient.java
+++ b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/client/BulkDataClient.java
@@ -39,7 +39,6 @@
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
-
import org.linuxforhealth.fhir.config.FHIRRequestContext;
import org.linuxforhealth.fhir.exception.FHIROperationException;
import org.linuxforhealth.fhir.model.generator.exception.FHIRGeneratorException;
@@ -55,7 +54,6 @@
import org.linuxforhealth.fhir.operation.bulkdata.model.JobInstanceRequest;
import org.linuxforhealth.fhir.operation.bulkdata.model.JobInstanceResponse;
import org.linuxforhealth.fhir.operation.bulkdata.model.PollingLocationResponse;
-import org.linuxforhealth.fhir.operation.bulkdata.model.transformer.JobIdEncodingTransformer;
import org.linuxforhealth.fhir.operation.bulkdata.model.type.Input;
import org.linuxforhealth.fhir.operation.bulkdata.model.type.JobParameter;
import org.linuxforhealth.fhir.operation.bulkdata.model.type.JobType;
@@ -63,6 +61,7 @@
import org.linuxforhealth.fhir.operation.bulkdata.model.type.StorageType;
import org.linuxforhealth.fhir.operation.bulkdata.model.url.DownloadUrl;
import org.linuxforhealth.fhir.operation.bulkdata.util.BulkDataExportUtil;
+import org.linuxforhealth.fhir.operation.bulkdata.util.CommonUtil;
/**
* BulkData Client to connect to the other server.
@@ -227,7 +226,7 @@ public String submitExport(Instant since, Set types, ExportType exportTy
CloseableHttpResponse jobResponse = cli.execute(jobPost);
int status = -1;
- String jobId = "-1";
+ int jobId = -1;
try {
status = jobResponse.getStatusLine().getStatusCode();
handleStandardResponseStatus(status);
@@ -243,7 +242,7 @@ public String submitExport(Instant since, Set types, ExportType exportTy
String responseString = new BasicResponseHandler().handleResponse(jobResponse);
JobInstanceResponse response = JobInstanceResponse.Parser.parse(responseString);
- jobId = Integer.toString(response.getInstanceId());
+ jobId = response.getInstanceId();
} finally {
jobPost.releaseConnection();
@@ -251,7 +250,7 @@ public String submitExport(Instant since, Set types, ExportType exportTy
}
cli.close();
- return baseUri + "/$bulkdata-status?job=" + JobIdEncodingTransformer.getInstance().encodeJobId(jobId);
+ return baseUri + "/$bulkdata-status?job=" + CommonUtil.encodeJobId(jobId);
}
/**
@@ -663,7 +662,7 @@ public String submitImport(String inputFormat, String inputSource, List i
CloseableHttpResponse jobResponse = cli.execute(jobPost);
int status = -1;
- String jobId = "-1";
+ int jobId = -1;
try {
status = jobResponse.getStatusLine().getStatusCode();
handleStandardResponseStatus(status);
@@ -682,7 +681,7 @@ public String submitImport(String inputFormat, String inputSource, List i
JobInstanceResponse response = JobInstanceResponse.Parser.parse(responseString);
- jobId = Integer.toString(response.getInstanceId());
+ jobId = response.getInstanceId();
} finally {
jobPost.releaseConnection();
@@ -690,7 +689,7 @@ public String submitImport(String inputFormat, String inputSource, List i
}
cli.close();
- return baseUri + "/$bulkdata-status?job=" + JobIdEncodingTransformer.getInstance().encodeJobId(jobId);
+ return baseUri + "/$bulkdata-status?job=" + CommonUtil.encodeJobId(jobId);
}
/**
diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/ConfigurationAdapter.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/ConfigurationAdapter.java
index 5985f816a68..9dd7deb87ea 100644
--- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/ConfigurationAdapter.java
+++ b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/ConfigurationAdapter.java
@@ -215,13 +215,13 @@ public interface ConfigurationAdapter {
int getCorePageSize();
/**
- * get core batch id encryption key for the job id that is returned
+ * get core batch id encoding key for the job id that is returned
*
* @implNote System value. We want to minimize the conflict possiblity.
*
* @return
*/
- String getCoreBatchIdEncryptionKey();
+ String getCoreBatchIdEncodingKey();
/**
* get core max partitions
@@ -557,10 +557,10 @@ default boolean isStorageProviderParquetEnabled(String provider) {
boolean getStorageProviderUsesRequestAccessToken(String provider);
/**
- * allows multiple resources in a single file.
- *
- * @implNote this default is false.
- *
+ * allows multiple resources in a single file.
+ *
+ * @implNote this default is false.
+ *
* @param source
* @return
*/
diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/impl/AbstractSystemConfigurationImpl.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/impl/AbstractSystemConfigurationImpl.java
index d2ac8e9247d..7c94679366a 100644
--- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/impl/AbstractSystemConfigurationImpl.java
+++ b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/config/impl/AbstractSystemConfigurationImpl.java
@@ -107,8 +107,8 @@ private static final int defaultCoreMaxParititions() {
}
@Override
- public String getCoreBatchIdEncryptionKey() {
- return FHIRConfigHelper.getStringProperty("fhirServer/bulkdata/core/batchIdEncryptionKey", null);
+ public String getCoreBatchIdEncodingKey() {
+ return FHIRConfigHelper.getStringProperty("fhirServer/bulkdata/core/batchIdEncodingKey", "");
}
@Override
@@ -403,7 +403,7 @@ public String getStorageProviderAuthTypeConnectionString(String provider) {
@Override
public String getProviderAzureServiceVersion(String provider) {
- return FHIRConfigHelper.getStringProperty("fhirServer/bulkdata/storageProviders/" + provider + "/serviceVersion", null);
+ return FHIRConfigHelper.getStringProperty("fhirServer/bulkdata/storageProviders/" + provider + "/serviceVersion", null);
}
@Override
diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformer.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformer.java
deleted file mode 100644
index f28279f8df7..00000000000
--- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformer.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * (C) Copyright IBM Corp. 2021
- *
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package org.linuxforhealth.fhir.operation.bulkdata.model.transformer;
-
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.util.Arrays;
-import java.util.Base64;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import javax.crypto.Cipher;
-import javax.crypto.spec.SecretKeySpec;
-
-import org.linuxforhealth.fhir.config.FHIRRequestContext;
-import org.linuxforhealth.fhir.operation.bulkdata.config.ConfigurationFactory;
-
-/**
- * Manages the Job Id Encryption.
- */
-public class JobIdEncodingTransformer {
-
- private static final String CLASSNAME = JobIdEncodingTransformer.class.getName();
- private static final Logger logger = Logger.getLogger(CLASSNAME);
-
- // Tenant Encryption key used for JavaBatch Job ID
- private static ConcurrentMap KEY_MAP = new ConcurrentHashMap<>();
-
- private static JobIdEncodingTransformer transformer = null;
-
- private JobIdEncodingTransformer() {
- // No Operation
- }
-
- /**
- * get the instance
- *
- * @return
- */
- public static JobIdEncodingTransformer getInstance() {
- if (transformer == null) {
- transformer = new JobIdEncodingTransformer();
- }
- return transformer;
- }
-
- /*
- * gets the tenant specific encryption key.
- * @return
- */
- private SecretKeySpec getJobIdEncryptionKey() {
- String encryptionKey = ConfigurationFactory.getInstance().getCoreBatchIdEncryptionKey();
- SecretKeySpec secretKey = null;
-
- if (encryptionKey != null && !encryptionKey.isEmpty()) {
- try {
- byte[] keyBytes = encryptionKey.getBytes("UTF-8");
- keyBytes = Arrays.copyOf(MessageDigest.getInstance("SHA-1").digest(keyBytes), 16);
- secretKey = new SecretKeySpec(keyBytes, "AES");
- } catch (Exception e) {
- logger.log(Level.WARNING, "Fail to generate encryption key from config!", e);
- }
- }
-
- if (secretKey == null) {
- logger.warning("Failed to get encryption key, JavaBatch Job ids will not be encrypted!");
- }
- return secretKey;
- }
-
- /**
- * encodes the job id
- *
- * @implNote note a Cipher is used here, however it is not used to encrypt sensitive information rather encode the jobId.
- *
- * @param jobId
- * @return
- */
- public String encodeJobId(String jobId) {
- String tenantId = FHIRRequestContext.get().getTenantId();
- SecretKeySpec key = KEY_MAP.computeIfAbsent(tenantId, k -> getJobIdEncryptionKey());
- // Encrypt and UrlEncode the batch job id.
- if (key == null) {
- return jobId;
- } else {
- try {
- // Use light weight encryption without salt to simplify both the encryption/decryption and also config.
- Cipher cp = Cipher.getInstance("AES/ECB/PKCS5Padding");
- cp.init(Cipher.ENCRYPT_MODE, key);
-
- // Encrypt the job id, base64-encode it, and replace all `/` chars with the less problematic `_` char
- String encodedJobId = Base64.getEncoder().withoutPadding().encodeToString(cp.doFinal(jobId.getBytes("UTF-8"))).replaceAll("/", "_");
- // The encrypted job id is used in the polling content location url directly, so urlencode here.
- return java.net.URLEncoder.encode(encodedJobId, StandardCharsets.UTF_8.name());
- } catch (Exception e) {
- return jobId;
- }
- }
- }
-
- /**
- * decodes the job id.
- *
- * @implNote note a Cipher is used here, however it is not used to encrypt sensitive information rather encode the
- * jobId.
- *
- * @param encodedJobId
- * @return
- */
- public String decodeJobId(String encodedJobId) {
- String tenantId = FHIRRequestContext.get().getTenantId();
- SecretKeySpec key = KEY_MAP.computeIfAbsent(tenantId, k -> getJobIdEncryptionKey());
- // Decrypt to get the batch job id.
- if (key == null) {
- return encodedJobId;
- } else {
- try {
- // Use light weight encryption without salt to simplify both the encryption/decryption and also config.
- Cipher cp = Cipher.getInstance("AES/ECB/PKCS5PADDING");
- cp.init(Cipher.DECRYPT_MODE, key);
- // The encrypted job id has already been urldecoded by liberty runtime before reaching this function,
- // so, we don't do urldecode here.)
- return new String(cp.doFinal(Base64.getDecoder().decode(encodedJobId.replaceAll("_", "/"))), "UTF-8");
- } catch (Exception e) {
- return encodedJobId;
- }
- }
- }
-}
diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtil.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtil.java
index 9174319f427..4eb03da41bd 100644
--- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtil.java
+++ b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtil.java
@@ -18,8 +18,6 @@
import javax.ws.rs.core.MediaType;
-import org.owasp.encoder.Encode;
-
import org.linuxforhealth.fhir.config.FHIRConfigHelper;
import org.linuxforhealth.fhir.core.FHIRMediaType;
import org.linuxforhealth.fhir.core.FHIRVersionParam;
@@ -34,9 +32,9 @@
import org.linuxforhealth.fhir.operation.bulkdata.OperationConstants;
import org.linuxforhealth.fhir.operation.bulkdata.OperationConstants.ExportType;
import org.linuxforhealth.fhir.operation.bulkdata.model.PollingLocationResponse;
-import org.linuxforhealth.fhir.operation.bulkdata.model.transformer.JobIdEncodingTransformer;
import org.linuxforhealth.fhir.search.compartment.CompartmentHelper;
import org.linuxforhealth.fhir.server.spi.operation.FHIROperationContext;
+import org.owasp.encoder.Encode;
/**
* BulkData Util captures common methods
@@ -319,15 +317,14 @@ public String checkAndValidateJob(Parameters parameters) throws FHIROperationExc
if (parameters != null) {
for (Parameters.Parameter parameter : parameters.getParameter()) {
if (OperationConstants.PARAM_JOB.equals(parameter.getName().getValue())
- && parameter.getValue() != null && parameter.getValue().is(org.linuxforhealth.fhir.model.type.String.class)) {
- String job = JobIdEncodingTransformer.getInstance().decodeJobId(parameter.getValue().as(org.linuxforhealth.fhir.model.type.String.class).getValue());
-
- // The job is never going to be empty or null as STRING is never empty at this point.
- if (job.contains("/") || job.contains("?")) {
- throw new FHIROperationException("job passed is invalid and is not supported");
+ && parameter.getValue() != null && parameter.getValue().is(FHIR_STRING)) {
+ try {
+ // Don't look at any other parameters.
+ return CommonUtil.decodeJobId(parameter.getValue().as(FHIR_STRING).getValue());
+ } catch(IllegalArgumentException e) {
+ String msg = "invalid job id was passed";
+ throw buildOperationException(msg, IssueType.INVALID, e);
}
- // Don't look at any other parameters.
- return job;
}
}
}
diff --git a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/CommonUtil.java b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/CommonUtil.java
index b75ecb43c72..bc6c22b04c1 100644
--- a/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/CommonUtil.java
+++ b/operation/fhir-operation-bulkdata/src/main/java/org/linuxforhealth/fhir/operation/bulkdata/util/CommonUtil.java
@@ -1,11 +1,12 @@
/*
- * (C) Copyright IBM Corp. 2019, 2021
+ * (C) Copyright IBM Corp. 2019, 2022
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.linuxforhealth.fhir.operation.bulkdata.util;
+import org.hashids.Hashids;
import org.linuxforhealth.fhir.config.FHIRRequestContext;
import org.linuxforhealth.fhir.exception.FHIROperationException;
import org.linuxforhealth.fhir.model.resource.OperationOutcome;
@@ -21,6 +22,8 @@
* Common Util captures common methods
*/
public class CommonUtil {
+ // min length constant for the "Hashids" we use to obfuscate job ids
+ private static final int HASHID_MIN_LENGTH = 6;
/**
* Type of Operation Call
@@ -115,14 +118,73 @@ public void verifyAllowedType(String storageType) throws FHIROperationException
}
}
- public static FHIROperationException buildExceptionWithIssue(String msg, IssueType issueType)
- throws FHIROperationException {
+ /**
+ * encode the job id as a short string for use in URLs
+ *
+ * @param jobId the numeric job id to encode
+ * @return
+ * @implNote The implementation uses the tenantId from the request context and the
+ * corresponding coreBatchIdEncodingKey from fhir-server-config as a seed for
+ * the encoding.
+ */
+ public static String encodeJobId(long jobId) {
+ Hashids encoder = getHashids();
+ return encoder.encode(jobId);
+ }
+
+ /**
+ * decode the job id back into the string representation of its numeric job id
+ *
+ * @param encodedJobId a job id encoded via {@link #encodeJobId(long, String, String)}
+ * @return
+ * @throws IllegalArgumentException if the passed encodedJobId could not be decoded
+ * @implNote The implementation uses the tenantId from the request context and the
+ * corresponding coreBatchIdEncodingKey from fhir-server-config to decode.
+ */
+ public static String decodeJobId(String encodedJobId) {
+ Hashids decoder = getHashids();
+ long[] decodedArray = decoder.decode(encodedJobId);
+ if (decodedArray.length != 1) {
+ throw new IllegalArgumentException("expected an encodedJobId with a single part, but found " + decodedArray.length);
+ }
+ return Long.toString(decodedArray[0]);
+ }
+
+ /**
+ * Get the tenantId from the request context and the corresponding coreBatchIdEncodingKey from
+ * fhir-server-config and use that as the seed for the returned Hashids
+ */
+ private static Hashids getHashids() {
+ String configSecret = ConfigurationFactory.getInstance().getCoreBatchIdEncodingKey();
+ String seed = FHIRRequestContext.get().getTenantId() + ":" + configSecret;
+ return new Hashids(seed, HASHID_MIN_LENGTH);
+ }
+
+ /**
+ * Construct a FHIROperationException with the passed {@code msg} and
+ * a single OperationOutcome.Issue of type {@code issueType}
+ *
+ * @param msg
+ * @param issueType
+ * @return
+ * @throws FHIROperationException
+ */
+ public static FHIROperationException buildExceptionWithIssue(String msg, IssueType issueType) {
OperationOutcome.Issue ooi = FHIRUtil.buildOperationOutcomeIssue(msg, issueType);
return new FHIROperationException(msg).withIssue(ooi);
}
- public static FHIROperationException buildExceptionWithIssue(String msg, Throwable cause, IssueType issueType)
- throws FHIROperationException {
+ /**
+ * Construct a FHIROperationException with the passed {@code msg}, caused by (@code cause}, and
+ * a single OperationOutcome.Issue of type {@code issueType}
+ *
+ * @param msg
+ * @param cause
+ * @param issueType
+ * @return
+ * @throws FHIROperationException
+ */
+ public static FHIROperationException buildExceptionWithIssue(String msg, Throwable cause, IssueType issueType) {
OperationOutcome.Issue ooi = FHIRUtil.buildOperationOutcomeIssue(msg, issueType);
return new FHIROperationException(msg, cause).withIssue(ooi);
}
diff --git a/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformerTest.java b/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformerTest.java
deleted file mode 100644
index e7d763557f0..00000000000
--- a/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/model/transformer/JobIdEncodingTransformerTest.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * (C) Copyright IBM Corp. 2021, 2022
- *
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package org.linuxforhealth.fhir.operation.bulkdata.model.transformer;
-
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertNotNull;
-
-import java.lang.reflect.Method;
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-import org.linuxforhealth.fhir.config.FHIRConfiguration;
-import org.linuxforhealth.fhir.config.FHIRRequestContext;
-import org.linuxforhealth.fhir.exception.FHIRException;
-
-/**
- * Tests the Job ID Encoding Transformer.
- */
-public class JobIdEncodingTransformerTest {
-
- @BeforeClass
- public void setup() {
- FHIRConfiguration.setConfigHome("target/test-classes");
- }
-
- @BeforeMethod
- public void startMethod(Method method) throws FHIRException {
-
- // Configure the request context for our search tests
- FHIRRequestContext context = FHIRRequestContext.get();
- context.setTenantId("default");
- }
-
- @AfterMethod
- public void clearThreadLocal() {
- FHIRRequestContext.remove();
- }
-
- @Test
- public void testTransformerRoundTrip() throws Exception {
- // Using the legacy implementation for the configuration the encode/decode uses change-password
- final JobIdEncodingTransformer transformer = JobIdEncodingTransformer.getInstance();
-
- String jobId = transformer.decodeJobId("1");
- assertNotNull(jobId);
- assertEquals(jobId, "1");
-
- // This results in at least one case where the naive base64 encoding of the encoded jobId would
- // 1. have a leading '/' which is prohibited by the S3 client; and
- // 2. have consecutive '/' which can makes it harder to get
- for (int i = 0; i < 2000; i++) {
- jobId = String.valueOf(i);
-
- String encodedJobId = transformer.encodeJobId(jobId);
- assertNotNull(encodedJobId);
- assertFalse(encodedJobId.equals(jobId));
- assertFalse(encodedJobId.startsWith("/"));
- assertFalse(encodedJobId.contains("//"));
-
- encodedJobId = URLDecoder.decode(encodedJobId, StandardCharsets.UTF_8.toString());
- assertNotNull(encodedJobId);
-
- String decodedJobId = transformer.decodeJobId(encodedJobId);
- assertNotNull(decodedJobId);
- assertEquals(decodedJobId, jobId);
- }
- }
-}
\ No newline at end of file
diff --git a/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataCommonUtilTest.java b/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataCommonUtilTest.java
new file mode 100644
index 00000000000..dc1a2c9b09a
--- /dev/null
+++ b/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataCommonUtilTest.java
@@ -0,0 +1,68 @@
+/*
+ * (C) Copyright IBM Corp. 2021, 2022
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.linuxforhealth.fhir.operation.bulkdata.util;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.lang.reflect.Method;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.linuxforhealth.fhir.config.FHIRRequestContext;
+import org.linuxforhealth.fhir.exception.FHIRException;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/**
+ * Tests the Job ID Encoding Transformer.
+ */
+public class BulkDataCommonUtilTest {
+ @BeforeMethod
+ public void startMethod(Method method) throws FHIRException {
+
+ // Configure the request context for our search tests
+ FHIRRequestContext context = FHIRRequestContext.get();
+ context.setTenantId("default");
+ }
+
+ @AfterMethod
+ public void clearThreadLocal() {
+ FHIRRequestContext.remove();
+ }
+
+ @Test
+ public void testJobIdEncodingRoundTrip() throws Exception {
+ Set encodedIds = new HashSet<>();
+ for (int i = 0; i < Math.pow(10, 5); i++) {
+ String jobId = String.valueOf(i);
+
+ String encodedJobId = CommonUtil.encodeJobId(i);
+ assertNotNull(encodedJobId);
+ assertFalse(encodedJobId.equals(jobId));
+ assertFalse(encodedJobId.contains("/"), "can't contain a slash");
+ assertTrue(encodedIds.add(encodedJobId), "each encoded id must be unique");
+
+ // Ensure all the chars are URL-safe
+ assertEquals(encodedJobId, URLDecoder.decode(encodedJobId, StandardCharsets.UTF_8));
+
+ String decodedJobId = CommonUtil.decodeJobId(encodedJobId);
+ assertNotNull(decodedJobId);
+ assertEquals(decodedJobId, jobId);
+ }
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void testJobIdDecodingInvalid() throws Exception {
+ CommonUtil.decodeJobId("bogus");
+ }
+}
\ No newline at end of file
diff --git a/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtilTest.java b/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtilTest.java
index a223ba5f9ed..b746e62e483 100644
--- a/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtilTest.java
+++ b/operation/fhir-operation-bulkdata/src/test/java/org/linuxforhealth/fhir/operation/bulkdata/util/BulkDataExportUtilTest.java
@@ -26,11 +26,6 @@
import javax.ws.rs.core.MediaType;
-import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
import org.linuxforhealth.fhir.config.FHIRConfiguration;
import org.linuxforhealth.fhir.config.FHIRRequestContext;
import org.linuxforhealth.fhir.core.FHIRVersionParam;
@@ -50,6 +45,10 @@
import org.linuxforhealth.fhir.operation.bulkdata.model.PollingLocationResponse;
import org.linuxforhealth.fhir.server.spi.operation.FHIROperationContext;
import org.linuxforhealth.fhir.server.spi.operation.FHIROperationContext.Type;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
/**
* Test Export util
@@ -504,15 +503,14 @@ public void testCheckAndValidateJobInvalidSlash() throws FHIROperationException
@Test
public void testCheckAndValidateJobValid() throws FHIROperationException {
- List parameters = List.of(Parameter.builder().name(string("job")).value(string("1234q346")).build());
- Parameters.Builder builder = Parameters.builder();
- builder.id(UUID.randomUUID().toString());
- builder.parameter(parameters);
- Parameters ps = builder.build();
+ String encodedJobId = CommonUtil.encodeJobId(555L);
+ Parameters ps = Parameters.builder()
+ .parameter(Parameter.builder().name("job").value(encodedJobId).build())
+ .build();
String result = util.checkAndValidateJob(ps);
assertNotNull(result);
- assertEquals(result, "1234q346");
+ assertEquals(result, "555");
}
@Test
diff --git a/operation/fhir-operation-bulkdata/src/test/resources/config/config/fhir-server-config.json b/operation/fhir-operation-bulkdata/src/test/resources/config/config/fhir-server-config.json
index 4930e26e25b..c2d90dd4f8b 100644
--- a/operation/fhir-operation-bulkdata/src/test/resources/config/config/fhir-server-config.json
+++ b/operation/fhir-operation-bulkdata/src/test/resources/config/config/fhir-server-config.json
@@ -49,7 +49,7 @@
},
"pageSize": 100,
"_comment": "max of 1000",
- "batchIdEncryptionKey": "change-password",
+ "batchIdEncodingKey": "change-password",
"maxPartitions": 3,
"maxInputs": 5,
"systemExportImpl": "none"
diff --git a/operation/fhir-operation-bulkdata/src/test/resources/config/default/fhir-server-config.json b/operation/fhir-operation-bulkdata/src/test/resources/config/default/fhir-server-config.json
index 4927754695c..99fba683e38 100644
--- a/operation/fhir-operation-bulkdata/src/test/resources/config/default/fhir-server-config.json
+++ b/operation/fhir-operation-bulkdata/src/test/resources/config/default/fhir-server-config.json
@@ -21,7 +21,7 @@
},
"pageSize": 100,
"_comment": "max of 1000",
- "batchIdEncryptionKey": "change-password",
+ "batchIdEncodingKey": "change-password",
"maxPartitions": 3,
"maxInputs": 5,
"systemExportImpl": "none"
diff --git a/operation/fhir-operation-bulkdata/src/test/resources/config/not-config/fhir-server-config.json b/operation/fhir-operation-bulkdata/src/test/resources/config/not-config/fhir-server-config.json
index 4930e26e25b..c2d90dd4f8b 100644
--- a/operation/fhir-operation-bulkdata/src/test/resources/config/not-config/fhir-server-config.json
+++ b/operation/fhir-operation-bulkdata/src/test/resources/config/not-config/fhir-server-config.json
@@ -49,7 +49,7 @@
},
"pageSize": 100,
"_comment": "max of 1000",
- "batchIdEncryptionKey": "change-password",
+ "batchIdEncodingKey": "change-password",
"maxPartitions": 3,
"maxInputs": 5,
"systemExportImpl": "none"
diff --git a/operation/fhir-operation-bulkdata/src/test/resources/config/provider/fhir-server-config.json b/operation/fhir-operation-bulkdata/src/test/resources/config/provider/fhir-server-config.json
index a74d3df863d..2365a31d165 100644
--- a/operation/fhir-operation-bulkdata/src/test/resources/config/provider/fhir-server-config.json
+++ b/operation/fhir-operation-bulkdata/src/test/resources/config/provider/fhir-server-config.json
@@ -25,7 +25,7 @@
"resourceCountThreshold": 200000
},
"pageSize": 100,
- "batchIdEncryptionKey": "change-password",
+ "batchIdEncodingKey": "change-password",
"maxPartitions": 3,
"maxInputs": 5,
"maxChunkReadTime": "90000",