dev.fitko.fitconnect.client.util.DestinationValidator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of client Show documentation
Show all versions of client Show documentation
Library that provides client access to the FIT-Connect api-endpoints for sending, subscribing and
routing
package dev.fitko.fitconnect.client.util;
import dev.fitko.fitconnect.api.FitConnectService;
import dev.fitko.fitconnect.api.config.chunking.AttachmentChunkingConfig;
import dev.fitko.fitconnect.api.config.Version;
import dev.fitko.fitconnect.api.domain.model.attachment.Attachment;
import dev.fitko.fitconnect.api.domain.model.callback.Callback;
import dev.fitko.fitconnect.api.domain.model.destination.Destination;
import dev.fitko.fitconnect.api.domain.model.destination.DestinationService;
import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose;
import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType;
import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema;
import dev.fitko.fitconnect.api.domain.model.reply.replychannel.FitConnect;
import dev.fitko.fitconnect.api.domain.model.reply.replychannel.ReplyChannel;
import dev.fitko.fitconnect.api.domain.sender.SendableEncryptedSubmission;
import dev.fitko.fitconnect.api.domain.sender.SendableSubmission;
import dev.fitko.fitconnect.api.domain.subscriber.SendableReply;
import dev.fitko.fitconnect.api.domain.validation.ValidationResult;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static dev.fitko.fitconnect.api.config.ApplicationConfig.METADATA_VERSION_SUPPORTING_CHUNKING;
import static dev.fitko.fitconnect.core.utils.Preconditions.checkArgumentAndThrow;
/**
* Validate submission and reply payload against the properties and services of a destination.
*/
public class DestinationValidator {
private static final Pattern LEIKA_KEY_PATTERN = Pattern.compile("^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$");
private final FitConnectService fitConnectService;
public DestinationValidator(final FitConnectService fitConnectService) {
this.fitConnectService = fitConnectService;
}
/**
* Checks if the encrypted submission payload is in a valid state for sending.
*
* @param sendableEncryptedSubmission payload to be checked
* @throws IllegalArgumentException if one of the checks fails
*/
public void ensureValidDataPayload(final SendableEncryptedSubmission sendableEncryptedSubmission, Destination destination) {
checkArgumentAndThrow(sendableEncryptedSubmission == null, "Encrypted payload must not be null.");
checkArgumentAndThrow(sendableEncryptedSubmission.getData() == null, "Encrypted data is mandatory, but was null.");
checkArgumentAndThrow(sendableEncryptedSubmission.getMetadata() == null, "Encrypted metadata must not be null.");
testDefaults(destination, sendableEncryptedSubmission.getServiceIdentifier(), sendableEncryptedSubmission.getCallback());
}
/**
* Checks if the unencrypted submission payload is in a valid state for sending.
*
* @param sendableSubmission payload to be checked
* @throws IllegalArgumentException if one of the checks fails
*/
public void ensureValidDataPayload(final SendableSubmission sendableSubmission, Destination destination) {
checkArgumentAndThrow(sendableSubmission == null, "Submission must not be null.");
testDataFormat(sendableSubmission.getAttachments(), sendableSubmission.getSubmissionSchema(), sendableSubmission.getData());
testDefaults(sendableSubmission, destination);
}
private void testDataFormat(List attachments, SubmissionSchema submissionSchema, byte[] data) {
final List dataAttachments = attachments.stream().filter(a -> a.getPurpose().equals(Purpose.DATA)).collect(Collectors.toList());
if (dataAttachments.size() > 1) {
throw new IllegalArgumentException("Data as an attachment can only be set once, found " + dataAttachments.size() + " entries");
} else if (dataAttachments.size() == 1) {
testOnValidDataFormat(dataAttachments.get(0).getDataAsBytes(), submissionSchema);
} else {
checkArgumentAndThrow(data == null || data.length == 0, "Data payload is mandatory, but was null or empty.");
testOnValidDataFormat(data, submissionSchema);
}
}
/**
* Checks if the unencrypted reply payload is in a valid state for sending.
*
* @param sendableReply reply payload to be checked
* @throws IllegalArgumentException if one of the checks fails
*/
public void ensureValidDataPayload(final SendableReply sendableReply, Destination destination) {
checkArgumentAndThrow(sendableReply == null, "Reply must not be null.");
checkArgumentAndThrow(sendableReply.getReplyChannel() == null || sendableReply.getReplyChannel().getFitConnect() == null,
"FitConnect reply channel is mandatory for sending replies, but was null on original submission " + sendableReply.getOriginalSubmissionId());
checkMatchingDataSchemaOnDestination(sendableReply.getSubmissionSchema(), destination);
testDataFormat(sendableReply.getAttachments(), sendableReply.getSubmissionSchema(), sendableReply.getData());
checkMatchingProcessingStandardsOnDestination(destination.getServices(), sendableReply.getReplyChannel(), sendableReply.getServiceIdentifier(), sendableReply.getSubmissionSchema());
}
/**
* Tests if the given destination has support for attachment chunking.
*
* @param destination the destination to be checked
* @return boolean if chucking is provided by the destination
*/
public boolean destinationSupportsAttachmentChunking(Destination destination) {
checkArgumentAndThrow(destination.getMetadataVersions() == null, "Metadata versions not present on destination " + destination.getDestinationId());
return destination.getMetadataVersions().stream()
.map(Version::new)
.anyMatch(version -> version.isGreaterOrEqualThan(METADATA_VERSION_SUPPORTING_CHUNKING));
}
/**
* Throw an exception if a destination does not support chunking and any of these preconditions is true:
*
* - all attachments should be chunked
* - any large attachments exist that should be chunked automatically
*
*
* @param config chunking config
* @param attachments list of attachments that are checked for large attachments
* @throws IllegalArgumentException when attachments exist that should be chunked
*/
public static void throwIfAttachmentsAreEligibleForChunking(AttachmentChunkingConfig config, List attachments) {
if (!attachments.isEmpty() && (config.isChunkAllAttachments() || attachments.stream().anyMatch(Attachment::isLargeAttachment))) {
throw new IllegalArgumentException("Destination does not support chunking. At least metadata version " + METADATA_VERSION_SUPPORTING_CHUNKING + " is required.");
}
}
private void testDefaults(final Destination destination, final String serviceIdentifier, final Callback callback) {
checkArgumentAndThrow(destination == null, "DestinationId is mandatory, but was null.");
checkArgumentAndThrow(serviceIdentifier == null, "Leika key is mandatory, but was null.");
checkArgumentAndThrow(invalidLeikaKeyPattern(serviceIdentifier), "LeikaKey has invalid format, please follow: ^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$.");
if (serviceTypeDoesNotMatchDestination(destination, serviceIdentifier)) {
throw new IllegalArgumentException("Provided service type '" + serviceIdentifier + "' is not allowed by the destination ");
}
if (callback != null) {
checkCallbackFormat(callback);
}
}
private void testDefaults(final SendableSubmission sendableSubmission, Destination destination) {
checkArgumentAndThrow(sendableSubmission.getDestinationId() == null, "DestinationId is mandatory, but was null.");
checkArgumentAndThrow(sendableSubmission.getServiceIdentifier() == null, "Leika key is mandatory, but was null.");
checkArgumentAndThrow(invalidLeikaKeyPattern(sendableSubmission.getServiceIdentifier()), "LeikaKey has invalid format, please follow: ^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$.\")");
if (serviceTypeDoesNotMatchDestination(destination, sendableSubmission.getServiceIdentifier())) {
throw new IllegalArgumentException("Provided service type '" + sendableSubmission.getServiceIdentifier() + "' is not allowed by the destination ");
}
checkMatchingDataSchemaOnDestination(sendableSubmission.getSubmissionSchema(), destination);
checkMatchingProcessingStandardsOnDestination(destination.getServices(), sendableSubmission.getReplyChannel(), sendableSubmission.getServiceIdentifier(), sendableSubmission.getSubmissionSchema());
if (sendableSubmission.getCallback() != null) {
checkCallbackFormat(sendableSubmission.getCallback());
}
}
private void checkMatchingProcessingStandardsOnDestination(final Set destinationServices, ReplyChannel submissionReplyChannel, String serviceIdentifier, final SubmissionSchema submissionSchema) {
if (processingStandardsDoNotMatchDestination(destinationServices, submissionReplyChannel, serviceIdentifier, submissionSchema)) {
throw new IllegalArgumentException("FIT-Connect reply channel processing standard(s) do not match any of the destination services");
}
}
private boolean processingStandardsDoNotMatchDestination(final Set destinationServices, ReplyChannel submissionReplyChannel, String serviceIdentifier, final SubmissionSchema submissionSchema) {
if (destinationServices == null
|| destinationServices.isEmpty()
|| destinationServices.stream().noneMatch(s -> s.getReplyChannels() != null)
|| submissionReplyChannel == null
|| submissionReplyChannel.getFitConnect() == null) {
return false;
}
final Optional matchingStandardsFound = destinationServices.stream()
.filter(service -> service.getIdentifier().equals(serviceIdentifier))
.filter(service -> service.getSubmissionSchemas().stream().anyMatch(schema -> schema.equals(submissionSchema)))
.map(DestinationService::getReplyChannels)
.map(ReplyChannel::getFitConnect)
.filter(Objects::nonNull)
.filter(matchingProcessingStandards(submissionReplyChannel))
.findAny();
return matchingStandardsFound.isEmpty();
}
private static Predicate matchingProcessingStandards(ReplyChannel submissionReplyChannel) {
return fitConnect -> new HashSet<>(fitConnect.getProcessStandards()).containsAll(submissionReplyChannel.getFitConnect().getProcessStandards());
}
private void checkMatchingDataSchemaOnDestination(final SubmissionSchema submissionSchema, final Destination destination) {
final MimeType dataMimeType = submissionSchema.getMimeType();
if (mimeTypeAndSchemaUriDoNotMatchDestination(destination, dataMimeType, submissionSchema.getSchemaUri())) {
throw new IllegalArgumentException("Combination of provided MIME type '" + dataMimeType.value() + "' and schema URI '" + submissionSchema.getSchemaUri() + "' is not allowed by the destination");
}
}
private boolean serviceTypeDoesNotMatchDestination(final Destination destination, final String serviceIdentifier) {
return destination.getServices().stream()
.map(DestinationService::getIdentifier)
.filter(Objects::nonNull)
.filter(destinationServiceIdentifier -> destinationServiceIdentifier.equals(serviceIdentifier))
.findFirst()
.isEmpty();
}
private boolean mimeTypeAndSchemaUriDoNotMatchDestination(final Destination destination, final MimeType mimeType, final URI schemaUri) {
return destination.getServices().stream()
.flatMap(service -> service.getSubmissionSchemas().stream())
.filter(Objects::nonNull)
.filter(schema -> schema.getMimeType().equals(mimeType))
.filter(schema -> schema.getSchemaUri().equals(schemaUri))
.findFirst()
.isEmpty();
}
private void testOnValidDataFormat(final byte[] data, SubmissionSchema submissionSchema) {
final MimeType dataMimeType = submissionSchema.getMimeType();
if (dataMimeType.equals(MimeType.APPLICATION_JSON)) {
checkJsonFormat(data, submissionSchema.getSchemaUri());
} else if (dataMimeType.equals(MimeType.APPLICATION_XML)) {
checkXmlFormat(data);
}
}
private void checkXmlFormat(final byte[] xmlData) {
final ValidationResult validationResult = fitConnectService.validateXmlFormat(xmlData);
if (validationResult.hasError()) {
throw new IllegalArgumentException("Data is not in expected xml format, please provide valid xml: " + validationResult.getError().getMessage());
}
}
private void checkJsonFormat(final byte[] jsonData, final URI dataSchemaUri) {
final ValidationResult validationResult = fitConnectService.validateJsonFormat(jsonData, dataSchemaUri);
if (validationResult.hasError()) {
throw new IllegalArgumentException("Data is not in expected json format, please provide valid json: " + validationResult.getError().getMessage());
}
}
private void checkCallbackFormat(final Callback callback) {
if (callback.getUri() == null || !callback.getUri().getScheme().equals("https")) {
throw new IllegalArgumentException("Callback URI " + callback.getUri() + " must be a secure https connection");
}
if (callback.getSecret() == null || callback.getSecret().length() < 32) {
throw new IllegalArgumentException("Callback secret must have a length of at least 32 characters");
}
}
private boolean invalidLeikaKeyPattern(final String leikaKey) {
if (leikaKey.length() < 7 || leikaKey.length() > 255) {
return true;
}
return !LEIKA_KEY_PATTERN.matcher(leikaKey).matches();
}
}