All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.fitko.fitconnect.client.util.DestinationValidator Maven / Gradle / Ivy

Go to download

Library that provides client access to the FIT-Connect api-endpoints for sending, subscribing and routing

There is a newer version: 2.3.5
Show newest version
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(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy