def validate_archive(self, filename):
        reader = CombineArchiveReader()
        name = os.path.relpath(filename, EXAMPLES_DIR)
        temp_dirname = os.path.join(self.temp_dirname, name)
        if not os.path.isdir(temp_dirname):
            os.makedirs(temp_dirname)
        archive = reader.run(filename, temp_dirname)

        config = Config(
            OMEX_METADATA_SCHEMA=OmexMetadataSchema.biosimulations, )

        error_msgs, warning_msgs = validate(
            archive,
            temp_dirname,
            formats_to_validate=list(
                CombineArchiveContentFormat.__members__.values()),
            config=config,
        )

        if warning_msgs:
            msg = 'The COMBINE/OMEX archive may be invalid.\n  {}'.format(
                flatten_nested_list_of_strings(warning_msgs).replace(
                    '\n', '\n  '))
            warnings.warn(msg, BioSimulatorsWarning)

        if error_msgs:
            msg = 'The COMBINE/OMEX archive is not valid.\n  {}'.format(
                flatten_nested_list_of_strings(error_msgs).replace(
                    '\n', '\n  '))
            raise ValueError(msg)
    def test_validate_no_metadata(self):
        os.remove(os.path.join(self.tmp_dir, 'thumbnail.png'))

        config = Config(VALIDATE_OMEX_METADATA=True)
        archive = CombineArchiveReader().run(os.path.join(
            self.FIXTURES_DIR, 'no-metadata.omex'),
                                             self.tmp_dir,
                                             config=config)
        errors, warnings = validate(archive, self.tmp_dir, config=config)
        self.assertEqual(errors, [])

        config = Config(VALIDATE_OMEX_METADATA=False)
        archive = CombineArchiveReader().run(os.path.join(
            self.FIXTURES_DIR, 'no-metadata.omex'),
                                             self.tmp_dir,
                                             config=config)
        errors, warnings = validate(archive, self.tmp_dir, config=config)
        self.assertEqual(errors, [])

        config = Config(VALIDATE_OMEX_METADATA=True)
        archive = CombineArchiveReader().run(os.path.join(
            self.FIXTURES_DIR, 'no-metadata.omex'),
                                             self.tmp_dir,
                                             config=config)
        errors, warnings = validate(
            archive,
            self.tmp_dir,
            formats_to_validate=list(
                CombineArchiveContentFormat.__members__.values()),
            config=config)
        self.assertNotEqual(errors, [])

        config = Config(VALIDATE_OMEX_METADATA=False)
        archive = CombineArchiveReader().run(os.path.join(
            self.FIXTURES_DIR, 'no-metadata.omex'),
                                             self.tmp_dir,
                                             config=config)
        errors, warnings = validate(
            archive,
            self.tmp_dir,
            formats_to_validate=list(
                CombineArchiveContentFormat.__members__.values()),
            config=config)
        self.assertEqual(errors, [])
    def test_manifest_in_manifest(self):
        out_dir = os.path.join(self.tmp_dir, 'out')
        archive = CombineArchiveReader().run(
            os.path.join(os.path.dirname(__file__), '..', 'fixtures',
                         'manifest-in-manifest.omex'), out_dir)
        errors, warnings = validate(archive, out_dir)
        self.assertEqual(errors, [])
        self.assertIn(
            'manifests should not contain content entries for themselves',
            flatten_nested_list_of_strings(warnings))

        out_dir = os.path.join(self.tmp_dir, 'out')
        archive = CombineArchiveReader().run(
            os.path.join(os.path.dirname(__file__), '..', 'fixtures',
                         'multiple-manifests.omex'), out_dir)
        errors, warnings = validate(archive, out_dir)
        self.assertEqual(errors, [])
        self.assertIn(
            'manifests should not contain content entries for themselves',
            flatten_nested_list_of_strings(warnings))
    def test_validate(self):
        os.remove(os.path.join(self.tmp_dir, 'thumbnail.png'))

        archive = CombineArchiveReader().run(self.OMEX_FIXTURE, self.tmp_dir)
        errors, warnings = validate(archive, self.tmp_dir)
        self.assertEqual(errors, [])
        self.assertNotEqual(warnings, [])

        archive2 = copy.deepcopy(archive)
        for content in archive.contents:
            archive2.contents.append(content)
        errors, warnings = validate(archive2, self.tmp_dir)
        self.assertIn('contains repeated content items',
                      flatten_nested_list_of_strings(errors))

        archive2 = copy.deepcopy(archive)
        archive2.contents = []
        errors, warnings = validate(archive2, self.tmp_dir)
        self.assertIn('does not contain content items',
                      flatten_nested_list_of_strings(errors))
    def test_sedml_validation_examples(self):
        dirname = os.path.join(os.path.dirname(__file__), '..', 'fixtures',
                               'sedml-validation')

        filename = os.path.join(
            dirname, 'invalid-omex-manifest-missing-attribute.omex')
        with self.assertRaisesRegex(ValueError,
                                    'must have the required attributes'):
            io.CombineArchiveReader().run(filename,
                                          os.path.join(self.temp_dir, 'a'))

        filename = os.path.join(dirname,
                                'invalid-sedml-missing-attribute.omex')
        archive = io.CombineArchiveReader().run(
            filename, os.path.join(self.temp_dir, 'b'))
        errors, warnings = validation.validate(
            archive, os.path.join(self.temp_dir, 'b'))
        self.assertNotEqual(errors, [])

        filename = os.path.join(dirname,
                                'invalid-sedml-missing-namespace.omex')
        archive = io.CombineArchiveReader().run(
            filename, os.path.join(self.temp_dir, 'c'))
        errors, warnings = validation.validate(
            archive, os.path.join(self.temp_dir, 'c'))
        self.assertNotEqual(errors, [])

        filename = os.path.join(dirname, 'invalid-sedml-multiple-errors.omex')
        archive = io.CombineArchiveReader().run(
            filename, os.path.join(self.temp_dir, 'd'))
        errors, warnings = validation.validate(
            archive, os.path.join(self.temp_dir, 'd'))
        self.assertNotEqual(errors, [])

        filename = os.path.join(dirname, 'warnings-sedml-sbml.omex')
        archive = io.CombineArchiveReader().run(
            filename, os.path.join(self.temp_dir, 'e'))
        errors, warnings = validation.validate(
            archive, os.path.join(self.temp_dir, 'e'))
        self.assertEqual(errors, [])
        self.assertNotEqual(warnings, [])

        filename = os.path.join(dirname, 'valid-sedml-sbml-qual.omex')
        archive = io.CombineArchiveReader().run(
            filename, os.path.join(self.temp_dir, 'f'))
        errors, warnings = validation.validate(
            archive, os.path.join(self.temp_dir, 'f'))
        self.assertEqual(errors, [])

        filename = os.path.join(dirname, 'valid-sedml-bngl.omex')
        archive = io.CombineArchiveReader().run(
            filename, os.path.join(self.temp_dir, 'g'))
        errors, warnings = validation.validate(
            archive, os.path.join(self.temp_dir, 'g'))
        self.assertEqual(errors, [])
def handler(body, file=None):
    ''' Validate a COMBINE/OMEX archive

    Args:
        body (:obj:`dict`): dictionary in schema ``ValidateCombineArchiveFileOrUrl`` with keys

            * ``url`` whose value has schema ``Url`` with the URL for a COMBINE/OMEX archive
            * ``omexMetadataFormat`` (:obj:`str`): format of the OMEX Metadata files
            * ``omexMetadataSchema`` (:obj:`str`): schema for validating the OMEX Metadata files
            * ``validateOmexManifest`` (:obj:`bool`, optional): Whether to validate the OMEX manifest file in the archive
            * ``validateSedml`` (:obj:`bool`, optional): Whether to validate the SED-ML files in the archive
            * ``validateSedmlModels`` (:obj:`bool`, optional): Whether to validate the sources of the models in the SED-ML files in the archive
            * ``validateOmexMetadata`` (:obj:`bool`, optional): Whether to validate the OMEX metdata files in the archive according to
                `BioSimulators' conventions <https://docs.biosimulations.org/concepts/conventions/simulation-project-metadata/>`_
            * ``validateImages`` (:obj:`bool`, optional): Whether to validate the images (BMP, GIF, JPEG, PNG, TIFF WEBP) files in the archive

        file (:obj:`werkzeug.datastructures.FileStorage`): COMBINE/OMEX archive file

    Returns:
        ``ValidationReport``: information about the validity or
            lack thereof of a COMBINE/OMEX archive
    '''
    try:
        omexMetadataInputFormat = OmexMetadataInputFormat(
            body['omexMetadataFormat'])
    except ValueError as exception:
        raise BadRequestException(
            title='`omexMetadataFormat` must be a recognized format.',
            exception=exception)

    try:
        omexMetadataSchema = OmexMetadataSchema(body['omexMetadataSchema'])
    except ValueError as exception:
        raise BadRequestException(
            title='`omexMetadataSchema` must be a recognized schema.',
            exception=exception)

    config = Config(
        OMEX_METADATA_INPUT_FORMAT=omexMetadataInputFormat,
        OMEX_METADATA_SCHEMA=omexMetadataSchema,
        VALIDATE_OMEX_MANIFESTS=body.get('validateOmexManifest', True),
        VALIDATE_SEDML=body.get('validateSedml', True),
        VALIDATE_SEDML_MODELS=body.get('validateSedmlModels', True),
        VALIDATE_OMEX_METADATA=body.get('validateOmexMetadata', True),
        VALIDATE_IMAGES=body.get('validateImages', True),
    )

    archive_file = file
    archive_url = body.get('url', None)
    if archive_url and archive_file:
        raise BadRequestException(
            title='Only one of `file` or `url` can be used at a time.',
            instance=ValueError(),
        )
    if not archive_url and not archive_file:
        raise BadRequestException(
            title='One of `file` or `url` must be used.',
            instance=ValueError(),
        )

    # create temporary working directory
    temp_dirname = get_temp_dir()
    archive_filename = os.path.join(temp_dirname, 'archive.omex')

    # get COMBINE/OMEX archive
    if archive_file:
        archive_file.save(archive_filename)

    else:
        try:
            response = requests.get(archive_url)
            response.raise_for_status()
        except requests.exceptions.RequestException as exception:
            title = 'COMBINE/OMEX archive could not be loaded from `{}`'.format(
                archive_url)
            raise BadRequestException(
                title=title,
                instance=exception,
            )

        # save archive to local temporary file
        with open(archive_filename, 'wb') as file:
            file.write(response.content)

    # read archive
    archive_dirname = os.path.join(temp_dirname, 'archive')
    reader = CombineArchiveReader()
    errors = []
    warnings = []
    try:
        archive = reader.run(archive_filename, archive_dirname, config=config)
    except Exception as exception:
        errors = [[
            'The file could not be parsed as a COMBINE/OMEX archive.',
            [[str(exception)]]
        ]]

    if not errors:
        errors, warnings = validate(
            archive,
            archive_dirname,
            formats_to_validate=list(
                CombineArchiveContentFormat.__members__.values()),
            config=config,
        )

    return make_validation_report(errors,
                                  warnings,
                                  filenames=[archive_filename])
    def test_no_validation(self):
        archive_dirname = os.path.join(self.tmp_dir, 'archive')
        os.mkdir(archive_dirname)

        # OMEX manifests
        archive = CombineArchive()

        errors, warnings = validate(archive, archive_dirname)
        self.assertIn('must have at least one content',
                      flatten_nested_list_of_strings(errors))
        self.assertNotEqual(warnings, [])

        with mock.patch.dict('os.environ', {'VALIDATE_OMEX_MANIFESTS': '0'}):
            errors, warnings = validate(archive, archive_dirname)
        self.assertEqual(errors, [])
        self.assertEqual(warnings, [])

        # SED-ML
        archive = CombineArchive()
        archive.contents.append(
            CombineArchiveContent(
                location='simulation.sedml',
                format=CombineArchiveContentFormat.SED_ML.value,
            ))

        sedml_filename = os.path.join(archive_dirname, 'simulation.sedml')
        with open(sedml_filename, 'w') as file:
            file.write('invalid')

        errors, warnings = validate(archive, archive_dirname)
        self.assertIn('Missing XML declaration',
                      flatten_nested_list_of_strings(errors))
        self.assertEqual(warnings, [])

        with mock.patch.dict('os.environ', {
                'VALIDATE_OMEX_MANIFESTS': '0',
                'VALIDATE_SEDML': '0',
        }):
            errors, warnings = validate(archive, archive_dirname)
        self.assertEqual(errors, [])
        self.assertEqual(warnings, [])

        os.remove(sedml_filename)

        # models
        archive = CombineArchive()
        archive.contents.append(
            CombineArchiveContent(
                location='simulation.sedml',
                format=CombineArchiveContentFormat.SED_ML.value,
            ))

        model_filename = os.path.join(archive_dirname, 'model.xml')
        shutil.copyfile(
            os.path.join(os.path.dirname(__file__), '..', 'fixtures',
                         'BIOMD0000000297.xml'), model_filename)

        sed_doc = SedDocument()
        sed_doc.models.append(
            Model(id='model',
                  source=model_filename,
                  language=ModelLanguage.SBML.value))

        sedml_filename = os.path.join(archive_dirname, 'simulation.sedml')
        SedmlSimulationWriter().run(sed_doc, sedml_filename)

        with open(model_filename, 'w') as file:
            file.write('invalid')

        errors, warnings = validate(archive, archive_dirname)
        self.assertIn('Missing XML declaration',
                      flatten_nested_list_of_strings(errors))
        self.assertEqual(warnings, [])

        with mock.patch.dict('os.environ', {
                'VALIDATE_OMEX_MANIFESTS': '0',
                'VALIDATE_SEDML_MODELS': '0',
        }):
            errors, warnings = validate(archive, archive_dirname)
        self.assertEqual(errors, [])
        self.assertEqual(warnings, [])

        os.remove(sedml_filename)
        os.remove(model_filename)

        # images
        archive = CombineArchive()
        archive.contents.append(
            CombineArchiveContent(
                location='image.png',
                format=CombineArchiveContentFormat.PNG.value,
            ))

        errors, warnings = validate(
            archive,
            archive_dirname,
            formats_to_validate=[CombineArchiveContentFormat.PNG])
        self.assertIn('The PNG file at location `image.png` is invalid.',
                      flatten_nested_list_of_strings(errors))
        self.assertNotEqual(warnings, [])

        with mock.patch.dict('os.environ', {
                'VALIDATE_OMEX_MANIFESTS': '0',
                'VALIDATE_IMAGES': '0',
        }):
            errors, warnings = validate(
                archive,
                archive_dirname,
                formats_to_validate=[CombineArchiveContentFormat.PNG])
        self.assertEqual(errors, [])
        self.assertEqual(warnings, [])

        # OMEX metadata
        archive = CombineArchive()
        archive.contents.append(
            CombineArchiveContent(
                location='metadata.rdf',
                format=CombineArchiveContentFormat.OMEX_METADATA.value,
            ))

        metadata_file = os.path.join(archive_dirname, 'metadata.rdf')
        shutil.copyfile(
            os.path.join(os.path.dirname(__file__), '..', 'fixtures',
                         'omex-metadata', 'invalid.rdf'), metadata_file)

        errors, warnings = validate(
            archive,
            archive_dirname,
            formats_to_validate=[CombineArchiveContentFormat.OMEX_METADATA])
        self.assertIn(
            'The OMEX Metadata file at location `metadata.rdf` is invalid.',
            flatten_nested_list_of_strings(errors))
        self.assertNotEqual(warnings, [])

        with mock.patch.dict('os.environ', {
                'VALIDATE_OMEX_MANIFESTS': '0',
                'VALIDATE_OMEX_METADATA': '0',
        }):
            errors, warnings = validate(
                archive,
                archive_dirname,
                formats_to_validate=[
                    CombineArchiveContentFormat.OMEX_METADATA
                ])
        self.assertEqual(errors, [])
        self.assertEqual(warnings, [])

        os.remove(metadata_file)
    def test_error_handling(self):
        os.remove(os.path.join(self.tmp_dir, 'thumbnail.png'))

        archive = CombineArchive()
        errors, warnings = validate(archive, self.tmp_dir)
        self.assertEqual(len(errors), 1)
        self.assertEqual(len(errors[0]), 1)
        self.assertIn('must have at least one content element', errors[0][0])
        self.assertEqual(len(warnings), 1)
        self.assertEqual(len(warnings[0]), 1)
        self.assertIn('does not contain any SED-ML files', warnings[0][0])

        archive = CombineArchive(contents=[
            None,
        ])
        errors, warnings = validate(archive, self.tmp_dir)
        self.assertEqual(len(errors), 1)
        self.assertEqual(len(warnings), 1)
        self.assertIn('must be an instance of',
                      flatten_nested_list_of_strings(errors))
        self.assertIn('does not contain any SED-ML files',
                      flatten_nested_list_of_strings(warnings))

        archive = CombineArchive(contents=[
            CombineArchiveContent(),
        ])
        errors, warnings = validate(archive, self.tmp_dir)
        self.assertEqual(len(errors), 1)
        self.assertEqual(len(warnings), 1)
        self.assertIn('must have a location',
                      flatten_nested_list_of_strings(errors))
        self.assertIn('must have a format',
                      flatten_nested_list_of_strings(errors))
        self.assertIn('does not contain any SED-ML files',
                      flatten_nested_list_of_strings(warnings))

        archive = CombineArchive(contents=[
            CombineArchiveContent(
                location='plain.txt',
                format='plain/text',
            ),
        ])
        errors, warnings = validate(archive, self.tmp_dir)
        self.assertEqual(len(errors), 1)
        self.assertEqual(len(warnings), 1)
        self.assertIn('is not a file', flatten_nested_list_of_strings(errors))
        self.assertIn('does not contain any SED-ML files',
                      flatten_nested_list_of_strings(warnings))

        with open(os.path.join(self.tmp_dir, 'sim.sedml'), 'w') as file:
            pass

        archive = CombineArchive(contents=[
            CombineArchiveContent(
                location='sim.sedml',
                format=CombineArchiveContentFormat.SED_ML,
            ),
        ])
        errors, warnings = validate(archive, self.tmp_dir)
        self.assertEqual(len(errors), 1)
        self.assertEqual(warnings, [])
        self.assertIn('is invalid', flatten_nested_list_of_strings(errors))

        archive = CombineArchive(contents=[
            CombineArchiveContent(
                location='sim.sedml',
                format=CombineArchiveContentFormat.SED_ML,
            ),
        ])
        with mock.patch.object(SedmlSimulationReader,
                               'run',
                               side_effect=ValueError('other error')):
            with self.assertRaisesRegex(ValueError, 'other error'):
                validate(archive, self.tmp_dir)

        def side_effect(self,
                        filename,
                        validate_models_with_languages=False,
                        config=None):
            self.warnings = [['my warning']]

        with mock.patch.object(SedmlSimulationReader, 'run', side_effect):
            errors, warnings = validate(archive, self.tmp_dir)
        self.assertEqual(errors, [])
        self.assertEqual(len(warnings), 1)
        self.assertIn('may be invalid',
                      flatten_nested_list_of_strings(warnings))
        self.assertIn('my warning', flatten_nested_list_of_strings(warnings))