From 89f5ccc7f5fc030e6db514f79726788770c6fa50 Mon Sep 17 00:00:00 2001
From: Boris Baldassari <boris@chrysalice.org>
Date: Sun, 29 Aug 2021 22:14:59 +0200
Subject: [PATCH] loader: add new maven-jar loader

The maven loader loads jar and zip files as Maven artefacts into the software heritage archive.

Note:
Supersedes D6158 and addresses the review done in that diff.

Related to T1724
---
 conftest.py                                   |   1 +
 docs/package-loader-specifications.rst        |   9 +
 setup.py                                      |   1 +
 swh/loader/package/maven/__init__.py          |  17 +
 swh/loader/package/maven/loader.py            | 231 +++++++
 swh/loader/package/maven/tasks.py             |  15 +
 swh/loader/package/maven/tests/__init__.py    |   0
 .../sprova4j-0.1.0-sources.jar                | Bin 0 -> 14316 bytes
 .../data/https_maven.org/sprova4j-0.1.0.pom   |  86 +++
 .../sprova4j-0.1.1-sources.jar                | Bin 0 -> 14510 bytes
 .../data/https_maven.org/sprova4j-0.1.1.pom   |  86 +++
 swh/loader/package/maven/tests/test_maven.py  | 615 ++++++++++++++++++
 swh/loader/package/maven/tests/test_tasks.py  |  50 ++
 13 files changed, 1111 insertions(+)
 create mode 100644 swh/loader/package/maven/__init__.py
 create mode 100644 swh/loader/package/maven/loader.py
 create mode 100644 swh/loader/package/maven/tasks.py
 create mode 100644 swh/loader/package/maven/tests/__init__.py
 create mode 100644 swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.0-sources.jar
 create mode 100644 swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.0.pom
 create mode 100644 swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.1-sources.jar
 create mode 100644 swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.1.pom
 create mode 100644 swh/loader/package/maven/tests/test_maven.py
 create mode 100644 swh/loader/package/maven/tests/test_tasks.py

diff --git a/conftest.py b/conftest.py
index 2d4f2f76..b4a6d0ae 100644
--- a/conftest.py
+++ b/conftest.py
@@ -22,4 +22,5 @@ def swh_scheduler_celery_includes(swh_scheduler_celery_includes):
         "swh.loader.package.npm.tasks",
         "swh.loader.package.pypi.tasks",
         "swh.loader.package.nixguix.tasks",
+        "swh.loader.package.maven.tasks",
     ]
diff --git a/docs/package-loader-specifications.rst b/docs/package-loader-specifications.rst
index 9609a8bb..aceed29c 100644
--- a/docs/package-loader-specifications.rst
+++ b/docs/package-loader-specifications.rst
@@ -56,6 +56,15 @@ Here is an overview of the fields (+ internal version name + branch name) used b
      - original author
      - ``<codemeta: dateCreated>`` from SWORD XML
      - revisions had parents
+   * - maven-loader
+     - passed as arg
+     - HEAD
+     - ``release_name(version)``
+     - "Synthetic release for archive at {p_info.url}\n"
+     - true
+     - ""
+     - passed as arg
+     - Only one artefact per url (jar/zip src)
    * - nixguix
      - URL
      - URL
diff --git a/setup.py b/setup.py
index cebead9e..81f04816 100755
--- a/setup.py
+++ b/setup.py
@@ -63,6 +63,7 @@ setup(
         loader.npm=swh.loader.package.npm:register
         loader.opam=swh.loader.package.opam:register
         loader.pypi=swh.loader.package.pypi:register
+        loader.maven=swh.loader.package.maven:register
     """,
     classifiers=[
         "Programming Language :: Python :: 3",
diff --git a/swh/loader/package/maven/__init__.py b/swh/loader/package/maven/__init__.py
new file mode 100644
index 00000000..1e5b016d
--- /dev/null
+++ b/swh/loader/package/maven/__init__.py
@@ -0,0 +1,17 @@
+# Copyright (C) 2021  The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+
+from typing import Any, Mapping
+
+
+def register() -> Mapping[str, Any]:
+    """Register the current worker module's definition"""
+    from .loader import MavenLoader
+
+    return {
+        "task_modules": [f"{__name__}.tasks"],
+        "loader": MavenLoader,
+    }
diff --git a/swh/loader/package/maven/loader.py b/swh/loader/package/maven/loader.py
new file mode 100644
index 00000000..bf09e823
--- /dev/null
+++ b/swh/loader/package/maven/loader.py
@@ -0,0 +1,231 @@
+# Copyright (C) 2021  The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from datetime import datetime, timezone
+import hashlib
+import json
+import logging
+from os import path
+import string
+from typing import (
+    Any,
+    Dict,
+    Iterator,
+    List,
+    Mapping,
+    Optional,
+    OrderedDict,
+    Sequence,
+    Tuple,
+)
+from urllib.parse import urlparse
+
+import attr
+import iso8601
+import requests
+
+from swh.loader.package.loader import (
+    BasePackageInfo,
+    PackageLoader,
+    PartialExtID,
+    RawExtrinsicMetadataCore,
+)
+from swh.loader.package.utils import EMPTY_AUTHOR, release_name
+from swh.model.model import (
+    MetadataAuthority,
+    MetadataAuthorityType,
+    ObjectType,
+    RawExtrinsicMetadata,
+    Release,
+    Sha1Git,
+    TimestampWithTimezone,
+)
+from swh.storage.interface import StorageInterface
+
+logger = logging.getLogger(__name__)
+
+
+@attr.s
+class MavenPackageInfo(BasePackageInfo):
+    time = attr.ib(type=datetime)
+    """Timestamp of the last update of jar file on the server."""
+    gid = attr.ib(type=str)
+    """Group ID of the maven artifact"""
+    aid = attr.ib(type=str)
+    """Artifact ID of the maven artifact"""
+    version = attr.ib(type=str)
+    """Version of the maven artifact"""
+
+    # default format for maven artifacts
+    MANIFEST_FORMAT = string.Template("$gid $aid $version $url $time")
+
+    def extid(self, manifest_format: Optional[string.Template] = None) -> PartialExtID:
+        """Returns a unique intrinsic identifier of this package info
+
+        ``manifest_format`` allows overriding the class' default MANIFEST_FORMAT"""
+        manifest_format = manifest_format or self.MANIFEST_FORMAT
+        manifest = manifest_format.substitute(
+            {
+                "gid": self.gid,
+                "aid": self.aid,
+                "version": self.version,
+                "url": self.url,
+                "time": str(self.time),
+            }
+        )
+        return ("maven-jar", hashlib.sha256(manifest.encode()).digest())
+
+    @classmethod
+    def from_metadata(cls, a_metadata: Dict[str, Any]) -> "MavenPackageInfo":
+        url = a_metadata["url"]
+        filename = a_metadata.get("filename")
+        time = iso8601.parse_date(a_metadata["time"])
+        time = time.astimezone(tz=timezone.utc)
+        gid = a_metadata["gid"]
+        aid = a_metadata["aid"]
+        version = a_metadata["version"]
+        return cls(
+            url=url,
+            filename=filename or path.split(url)[-1],
+            time=time,
+            gid=gid,
+            aid=aid,
+            version=version,
+            directory_extrinsic_metadata=[
+                RawExtrinsicMetadataCore(
+                    format="maven-json", metadata=json.dumps(a_metadata).encode(),
+                ),
+            ],
+        )
+
+
+class MavenLoader(PackageLoader[MavenPackageInfo]):
+    """Load source code jar origin's artifact files into swh archive
+
+    """
+
+    visit_type = "maven"
+
+    def __init__(
+        self,
+        storage: StorageInterface,
+        url: str,
+        artifacts: Sequence[Dict[str, Any]],
+        extid_manifest_format: Optional[str] = None,
+        max_content_size: Optional[int] = None,
+    ):
+        f"""Loader constructor.
+
+        For now, this is the lister's task output.
+        There is one, and only one, artefact (jar or zip) per version, as guaranteed by
+        the Maven coordinates system.
+
+        Args:
+            url: Origin url
+            artifacts: List of single artifact information with keys:
+
+               - **time**: the time of the last update of jar file on the server
+                 as an iso8601 date string
+
+               - **url**: the artifact url to retrieve filename
+
+               - **filename**: optionally, the file's name
+
+               - **gid**: artifact's groupId
+
+               - **aid**: artifact's artifactId
+
+               - **version**: artifact's version
+
+            extid_manifest_format: template string used to format a manifest,
+                which is hashed to get the extid of a package.
+                Defaults to {MavenPackageInfo.MANIFEST_FORMAT!r}
+
+        """
+        super().__init__(storage=storage, url=url, max_content_size=max_content_size)
+        self.artifacts = artifacts  # assume order is enforced in the lister
+        self.version_artifact: OrderedDict[str, Dict[str, Any]]
+        self.version_artifact = OrderedDict(
+            {str(jar["version"]): jar for jar in artifacts if jar["version"]}
+        )
+
+    def get_versions(self) -> Sequence[str]:
+        return list(self.version_artifact.keys())
+
+    def get_default_version(self) -> str:
+        # Default version is the last item
+        return self.artifacts[-1]["version"]
+
+    def get_metadata_authority(self):
+        p_url = urlparse(self.url)
+        return MetadataAuthority(
+            type=MetadataAuthorityType.FORGE,
+            url=f"{p_url.scheme}://{p_url.netloc}/",
+            metadata={},
+        )
+
+    def build_extrinsic_directory_metadata(
+        self, p_info: MavenPackageInfo, release_id: Sha1Git, directory_id: Sha1Git,
+    ) -> List[RawExtrinsicMetadata]:
+        if not p_info.directory_extrinsic_metadata:
+            # If this package loader doesn't write metadata, no need to require
+            # an implementation for get_metadata_authority.
+            return []
+
+        # Get artifacts
+        dir_ext_metadata = p_info.directory_extrinsic_metadata[0]
+        a_metadata = json.loads(dir_ext_metadata.metadata)
+        aid = a_metadata["aid"]
+        version = a_metadata["version"]
+
+        # Rebuild POM URL.
+        pom_url = path.dirname(p_info.url)
+        pom_url = f"{pom_url}/{aid}-{version}.pom"
+
+        r = requests.get(pom_url, allow_redirects=True)
+        if r.status_code == 200:
+            metadata_pom = r.content
+        else:
+            metadata_pom = b""
+
+        return super().build_extrinsic_directory_metadata(
+            attr.evolve(
+                p_info,
+                directory_extrinsic_metadata=[
+                    RawExtrinsicMetadataCore(
+                        format="maven-pom", metadata=metadata_pom,
+                    ),
+                    dir_ext_metadata,
+                ],
+            ),
+            release_id=release_id,
+            directory_id=directory_id,
+        )
+
+    def get_package_info(self, version: str) -> Iterator[Tuple[str, MavenPackageInfo]]:
+        a_metadata = self.version_artifact[version]
+        yield release_name(a_metadata["version"]), MavenPackageInfo.from_metadata(
+            a_metadata
+        )
+
+    def build_release(
+        self, p_info: MavenPackageInfo, uncompressed_path: str, directory: Sha1Git
+    ) -> Optional[Release]:
+        msg = f"Synthetic release for archive at {p_info.url}\n".encode("utf-8")
+        # time is an iso8601 date
+        normalized_time = TimestampWithTimezone.from_datetime(p_info.time)
+        return Release(
+            name=p_info.version.encode(),
+            message=msg,
+            date=normalized_time,
+            author=EMPTY_AUTHOR,
+            target=directory,
+            target_type=ObjectType.DIRECTORY,
+            synthetic=True,
+        )
+
+    def extra_branches(self) -> Dict[bytes, Mapping[str, Any]]:
+        last_snapshot = self.last_snapshot()
+        return last_snapshot.to_dict()["branches"] if last_snapshot else {}
diff --git a/swh/loader/package/maven/tasks.py b/swh/loader/package/maven/tasks.py
new file mode 100644
index 00000000..5be462d7
--- /dev/null
+++ b/swh/loader/package/maven/tasks.py
@@ -0,0 +1,15 @@
+# Copyright (C) 2021  The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from celery import shared_task
+
+from swh.loader.package.maven.loader import MavenLoader
+
+
+@shared_task(name=__name__ + ".LoadMaven")
+def load_jar_file(*, url=None, artifacts=None):
+    """Load jar's artifacts."""
+    loader = MavenLoader.from_configfile(url=url, artifacts=artifacts)
+    return loader.load()
diff --git a/swh/loader/package/maven/tests/__init__.py b/swh/loader/package/maven/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.0-sources.jar b/swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.0-sources.jar
new file mode 100644
index 0000000000000000000000000000000000000000..2a15a031042cbd1bc41408dd9df3d14dc387a8a3
GIT binary patch
literal 14316
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
Yk77UqJQ5HPQoz>|;C4TuyZ`O~18lWG5dZ)H

literal 0
HcmV?d00001

diff --git a/swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.0.pom b/swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.0.pom
new file mode 100644
index 00000000..bc1a35b3
--- /dev/null
+++ b/swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.0.pom
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>al.aldi</groupId>
+  <artifactId>sprova4j</artifactId>
+  <version>0.1.0</version>
+  <name>sprova4j</name>
+  <description>Java client for Sprova Test Management</description>
+  <url>https://github.com/aldialimucaj/sprova4j</url>
+  <inceptionYear>2018</inceptionYear>
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+  <developers>
+    <developer>
+      <id>aldi</id>
+      <name>Aldi Alimucaj</name>
+      <email>aldi.alimucaj@gmail.com</email>
+    </developer>
+  </developers>
+  <scm>
+    <connection>scm:git:git://github.com/aldialimucaj/sprova4j.git</connection>
+    <developerConnection>scm:git:git://github.com/aldialimucaj/sprova4j.git</developerConnection>
+    <url>https://github.com/aldialimucaj/sprova4j</url>
+  </scm>
+  <dependencies>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>1.2.3</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>2.8.3</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp3</groupId>
+      <artifactId>okhttp</artifactId>
+      <version>3.10.0</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okio</groupId>
+      <artifactId>okio</artifactId>
+      <version>1.0.0</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.glassfish</groupId>
+      <artifactId>javax.json</artifactId>
+      <version>1.1.2</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.json</groupId>
+      <artifactId>javax.json-api</artifactId>
+      <version>1.1.2</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.validation</groupId>
+      <artifactId>validation-api</artifactId>
+      <version>2.0.1.Final</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp3</groupId>
+      <artifactId>mockwebserver</artifactId>
+      <version>3.10.0</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.1-sources.jar b/swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.1-sources.jar
new file mode 100644
index 0000000000000000000000000000000000000000..06fbedbf5d37a73e12efc3bdd74828201ed43982
GIT binary patch
literal 14510
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
Dlw_a(

literal 0
HcmV?d00001

diff --git a/swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.1.pom b/swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.1.pom
new file mode 100644
index 00000000..05e5a71f
--- /dev/null
+++ b/swh/loader/package/maven/tests/data/https_maven.org/sprova4j-0.1.1.pom
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>al.aldi</groupId>
+  <artifactId>sprova4j</artifactId>
+  <version>0.1.1</version>
+  <name>sprova4j</name>
+  <description>Java client for Sprova Test Management</description>
+  <url>https://github.com/aldialimucaj/sprova4j</url>
+  <inceptionYear>2018</inceptionYear>
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+  <developers>
+    <developer>
+      <id>aldi</id>
+      <name>Aldi Alimucaj</name>
+      <email>aldi.alimucaj@gmail.com</email>
+    </developer>
+  </developers>
+  <scm>
+    <connection>https://github.com/aldialimucaj/sprova4j.git</connection>
+    <developerConnection>https://github.com/aldialimucaj/sprova4j.git</developerConnection>
+    <url>https://github.com/aldialimucaj/sprova4j</url>
+  </scm>
+  <dependencies>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>1.2.3</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>2.8.5</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp3</groupId>
+      <artifactId>okhttp</artifactId>
+      <version>3.10.0</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okio</groupId>
+      <artifactId>okio</artifactId>
+      <version>1.14.1</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.glassfish</groupId>
+      <artifactId>javax.json</artifactId>
+      <version>1.1.2</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.json</groupId>
+      <artifactId>javax.json-api</artifactId>
+      <version>1.1.2</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.validation</groupId>
+      <artifactId>validation-api</artifactId>
+      <version>2.0.1.Final</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp3</groupId>
+      <artifactId>mockwebserver</artifactId>
+      <version>3.10.0</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/swh/loader/package/maven/tests/test_maven.py b/swh/loader/package/maven/tests/test_maven.py
new file mode 100644
index 00000000..3d2dff49
--- /dev/null
+++ b/swh/loader/package/maven/tests/test_maven.py
@@ -0,0 +1,615 @@
+# Copyright (C) 2019-2021 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+import hashlib
+import json
+from pathlib import Path
+import string
+
+import pytest
+
+from swh.loader.package import __version__
+from swh.loader.package.maven.loader import MavenLoader, MavenPackageInfo
+from swh.loader.package.utils import EMPTY_AUTHOR
+from swh.loader.tests import assert_last_visit_matches, check_snapshot, get_stats
+from swh.model.hashutil import hash_to_bytes
+from swh.model.model import (
+    RawExtrinsicMetadata,
+    Release,
+    Snapshot,
+    SnapshotBranch,
+    TargetType,
+    Timestamp,
+    TimestampWithTimezone,
+)
+from swh.model.model import MetadataAuthority, MetadataAuthorityType, MetadataFetcher
+from swh.model.model import ObjectType as ModelObjectType
+from swh.model.swhids import CoreSWHID, ExtendedObjectType, ExtendedSWHID, ObjectType
+from swh.storage.algos.snapshot import snapshot_get_all_branches
+
+URL = "https://repo1.maven.org/maven2/"
+MVN_ARTIFACTS = [
+    {
+        "time": "2021-07-12 19:06:59.335000",
+        "url": "https://repo1.maven.org/maven2/al/aldi/sprova4j/0.1.0/"
+        + "sprova4j-0.1.0-sources.jar",
+        "gid": "al.aldi",
+        "aid": "sprova4j",
+        "filename": "sprova4j-0.1.0-sources.jar",
+        "version": "0.1.0",
+    },
+    {
+        "time": "2021-07-12 19:37:05.534000",
+        "url": "https://repo1.maven.org/maven2/al/aldi/sprova4j/0.1.1/"
+        + "sprova4j-0.1.1-sources.jar",
+        "gid": "al.aldi",
+        "aid": "sprova4j",
+        "filename": "sprova4j-0.1.1-sources.jar",
+        "version": "0.1.1",
+    },
+]
+
+MVN_ARTIFACTS_POM = [
+    "https://repo1.maven.org/maven2/al/aldi/sprova4j/0.1.0/sprova4j-0.1.0.pom",
+    "https://repo1.maven.org/maven2/al/aldi/sprova4j/0.1.1/sprova4j-0.1.1.pom",
+]
+
+_expected_new_contents_first_visit = [
+    "cd807364cd7730022b3849f90ccf4bababbada84",
+    "79e33dd52ebdf615e6696ae69add91cb990d81e2",
+    "8002bd514156f05a0940ae14ef86eb0179cbd510",
+    "23479553a6ccec30d377dee0496123a65d23fd8c",
+    "07ffbebb933bc1660e448f07d8196c2b083797f9",
+    "abf021b581f80035b56153c9aa27195b8d7ebbb8",
+    "eec70ba80a6862ed2619727663b17eb0d9dfe131",
+    "81a493dacb44dedf623f29ecf62c0e035bf698de",
+    "bda85ed0bbecf8cddfea04234bee16f476f64fe4",
+    "1ec91d561f5bdf59acb417086e04c54ead94e94e",
+    "d517b423da707fa21378623f35facebff53cb59d",
+    "3f0f21a764972d79e583908991c893c999613354",
+    "a2dd4d7dfe6043baf9619081e4e29966989211af",
+    "f62685cf0c6825a4097c949280b584cf0e16d047",
+    "56afc1ea60cef6548ce0a34f44e91b0e4b063835",
+    "cf7c740926e7ebc9ac8978a5c4f0e1e7a0e9e3af",
+    "86ff828bea1c22ca3d50ed82569b9c59ce2c41a1",
+    "1d0fa04454d9fec31d8ee3f35b58158ca1e28b15",
+    "e90239a2c8d9ede61a29671a8b397a743e18fa34",
+    "ce8851005d084aea089bcd8cf01052f4b234a823",
+    "2c34ce622aa7fa68d104900840f66671718e6249",
+    "e6a6fec32dcb3bee93c34fc11b0174a6b0b0ec6d",
+    "405d3e1be4b658bf26de37f2c90c597b2796b9d7",
+    "d0d2f5848721e04300e537826ef7d2d6d9441df0",
+    "399c67e33e38c475fd724d283dd340f6a2e8dc91",
+    "dea10c1111cc61ac1809fb7e88857e3db054959f",
+]
+
+_expected_json_metadata = {
+    "time": "2021-07-12 19:06:59.335000",
+    "url": (
+        "https://repo1.maven.org/maven2/al/aldi/sprova4j/0.1.0/"
+        "sprova4j-0.1.0-sources.jar"
+    ),
+    "gid": "al.aldi",
+    "aid": "sprova4j",
+    "filename": "sprova4j-0.1.0-sources.jar",
+    "version": "0.1.0",
+}
+_expected_pom_metadata = (
+    """<?xml version="1.0" encoding="UTF-8"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 """
+    'http://maven.apache.org/xsd/maven-4.0.0.xsd" '
+    'xmlns="http://maven.apache.org/POM/4.0.0" '
+    """xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>al.aldi</groupId>
+  <artifactId>sprova4j</artifactId>
+  <version>0.1.0</version>
+  <name>sprova4j</name>
+  <description>Java client for Sprova Test Management</description>
+  <url>https://github.com/aldialimucaj/sprova4j</url>
+  <inceptionYear>2018</inceptionYear>
+  <licenses>
+    <license>
+      <name>The Apache Software License, Version 2.0</name>
+      <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+  <developers>
+    <developer>
+      <id>aldi</id>
+      <name>Aldi Alimucaj</name>
+      <email>aldi.alimucaj@gmail.com</email>
+    </developer>
+  </developers>
+  <scm>
+    <connection>scm:git:git://github.com/aldialimucaj/sprova4j.git</connection>
+    <developerConnection>scm:git:git://github.com/aldialimucaj/sprova4j.git</developerConnection>
+    <url>https://github.com/aldialimucaj/sprova4j</url>
+  </scm>
+  <dependencies>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <version>1.2.3</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>2.8.3</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp3</groupId>
+      <artifactId>okhttp</artifactId>
+      <version>3.10.0</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okio</groupId>
+      <artifactId>okio</artifactId>
+      <version>1.0.0</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.glassfish</groupId>
+      <artifactId>javax.json</artifactId>
+      <version>1.1.2</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.json</groupId>
+      <artifactId>javax.json-api</artifactId>
+      <version>1.1.2</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.validation</groupId>
+      <artifactId>validation-api</artifactId>
+      <version>2.0.1.Final</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.12</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.squareup.okhttp3</groupId>
+      <artifactId>mockwebserver</artifactId>
+      <version>3.10.0</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
+"""
+)
+
+_expected_new_directories_first_visit = [
+    "6c9de41e4cebb91a8368da1d89ae9873bd540ec3",
+    "c1a2ee97fc47426d0179f94d223405336b5cd075",
+    "9e1bdca292765a9528af18743bd793b80362c768",
+    "193a7af634592ef27fb341762806f61e8fb8eab3",
+    "a297aa21e3dbf138b370be3aae7a852dd403bbbb",
+    "da84026119ae04022f007d5b3362e98d46d09045",
+    "75bb915942a9c441ca62aeffc3b634f1ec9ce5e2",
+    "0851d359283b2ad82b116c8d1b55ab14b1ec219c",
+    "2bcbb8b723a025ee9a36b719cea229ed38c37e46",
+]
+
+_expected_new_release_first_visit = "02e83c29ec094db581f939d2e238d0613a4f59ac"
+
+REL_MSG = (
+    b"Synthetic release for archive at https://repo1.maven.org/maven2/al/aldi/"
+    b"sprova4j/0.1.0/sprova4j-0.1.0-sources.jar\n"
+)
+
+REVISION_DATE = TimestampWithTimezone(
+    timestamp=Timestamp(seconds=1626116819, microseconds=335000),
+    offset=0,
+    negative_utc=False,
+)
+
+
+@pytest.fixture
+def data_jar_1(datadir):
+    content = Path(
+        datadir, "https_maven.org", "sprova4j-0.1.0-sources.jar"
+    ).read_bytes()
+    return content
+
+
+@pytest.fixture
+def data_pom_1(datadir):
+    content = Path(datadir, "https_maven.org", "sprova4j-0.1.0.pom").read_bytes()
+    return content
+
+
+@pytest.fixture
+def data_jar_2(datadir):
+    content = Path(
+        datadir, "https_maven.org", "sprova4j-0.1.1-sources.jar"
+    ).read_bytes()
+    return content
+
+
+@pytest.fixture
+def data_pom_2(datadir):
+    content = Path(datadir, "https_maven.org", "sprova4j-0.1.1.pom").read_bytes()
+    return content
+
+
+def test_jar_visit_with_no_artifact_found(swh_storage, requests_mock_datadir):
+    unknown_artifact_url = "https://ftp.g.o/unknown/8sync-0.1.0.tar.gz"
+    loader = MavenLoader(
+        swh_storage,
+        unknown_artifact_url,
+        artifacts=[
+            {
+                "time": "2021-07-18 08:05:05.187000",
+                "url": unknown_artifact_url,  # unknown artifact
+                "filename": "8sync-0.1.0.tar.gz",
+                "gid": "al/aldi",
+                "aid": "sprova4j",
+                "version": "0.1.0",
+            }
+        ],
+    )
+
+    actual_load_status = loader.load()
+    assert actual_load_status["status"] == "uneventful"
+    assert actual_load_status["snapshot_id"] is not None
+
+    expected_snapshot_id = "1a8893e6a86f444e8be8e7bda6cb34fb1735a00e"
+    assert actual_load_status["snapshot_id"] == expected_snapshot_id
+
+    stats = get_stats(swh_storage)
+
+    assert_last_visit_matches(
+        swh_storage, unknown_artifact_url, status="partial", type="maven"
+    )
+
+    assert {
+        "content": 0,
+        "directory": 0,
+        "origin": 1,
+        "origin_visit": 1,
+        "release": 0,
+        "revision": 0,
+        "skipped_content": 0,
+        "snapshot": 1,
+    } == stats
+
+
+def test_jar_visit_with_release_artifact_no_prior_visit(
+    swh_storage, requests_mock, data_jar_1, data_pom_1
+):
+    """With no prior visit, loading a jar ends up with 1 snapshot
+
+    """
+    requests_mock.get(MVN_ARTIFACTS[0]["url"], content=data_jar_1)
+    requests_mock.get(MVN_ARTIFACTS_POM[0], content=data_pom_1)
+    loader = MavenLoader(
+        swh_storage, MVN_ARTIFACTS[0]["url"], artifacts=[MVN_ARTIFACTS[0]]
+    )
+
+    actual_load_status = loader.load()
+    assert actual_load_status["status"] == "eventful"
+
+    expected_snapshot_first_visit_id = hash_to_bytes(
+        "c5195b8ebd148649bf094561877964b131ab27e0"
+    )
+
+    expected_snapshot = Snapshot(
+        id=expected_snapshot_first_visit_id,
+        branches={
+            b"HEAD": SnapshotBranch(
+                target_type=TargetType.ALIAS, target=b"releases/0.1.0",
+            ),
+            b"releases/0.1.0": SnapshotBranch(
+                target_type=TargetType.RELEASE,
+                target=hash_to_bytes(_expected_new_release_first_visit),
+            ),
+        },
+    )
+    actual_snapshot = snapshot_get_all_branches(
+        swh_storage, hash_to_bytes(actual_load_status["snapshot_id"])
+    )
+
+    assert actual_snapshot == expected_snapshot
+    check_snapshot(expected_snapshot, swh_storage)
+
+    assert (
+        hash_to_bytes(actual_load_status["snapshot_id"])
+        == expected_snapshot_first_visit_id
+    )
+
+    stats = get_stats(swh_storage)
+    assert_last_visit_matches(
+        swh_storage, MVN_ARTIFACTS[0]["url"], status="full", type="maven"
+    )
+
+    expected_contents = map(hash_to_bytes, _expected_new_contents_first_visit)
+    assert list(swh_storage.content_missing_per_sha1(expected_contents)) == []
+
+    expected_dirs = map(hash_to_bytes, _expected_new_directories_first_visit)
+    assert list(swh_storage.directory_missing(expected_dirs)) == []
+
+    expected_rels = map(hash_to_bytes, {_expected_new_release_first_visit})
+    assert list(swh_storage.release_missing(expected_rels)) == []
+
+    rel_id = actual_snapshot.branches[b"releases/0.1.0"].target
+    (rel,) = swh_storage.release_get([rel_id])
+
+    assert rel == Release(
+        id=hash_to_bytes(_expected_new_release_first_visit),
+        name=b"0.1.0",
+        message=REL_MSG,
+        author=EMPTY_AUTHOR,
+        date=REVISION_DATE,
+        target_type=ModelObjectType.DIRECTORY,
+        target=hash_to_bytes("6c9de41e4cebb91a8368da1d89ae9873bd540ec3"),
+        synthetic=True,
+        metadata=None,
+    )
+
+    assert {
+        "content": len(_expected_new_contents_first_visit),
+        "directory": len(_expected_new_directories_first_visit),
+        "origin": 1,
+        "origin_visit": 1,
+        "release": 1,
+        "revision": 0,
+        "skipped_content": 0,
+        "snapshot": 1,
+    } == stats
+
+
+def test_jar_2_visits_without_change(
+    swh_storage, requests_mock_datadir, requests_mock, data_jar_2, data_pom_2
+):
+    """With no prior visit, load a gnu project ends up with 1 snapshot
+
+    """
+    requests_mock.get(MVN_ARTIFACTS[1]["url"], content=data_jar_2)
+    requests_mock.get(MVN_ARTIFACTS_POM[1], content=data_pom_2)
+    loader = MavenLoader(
+        swh_storage, MVN_ARTIFACTS[1]["url"], artifacts=[MVN_ARTIFACTS[1]]
+    )
+
+    actual_load_status = loader.load()
+    assert actual_load_status["status"] == "eventful"
+
+    expected_snapshot_first_visit_id = hash_to_bytes(
+        "91dcacee7a6d2b54f9cab14bc14cb86d22d2ac2b"
+    )
+
+    assert (
+        hash_to_bytes(actual_load_status["snapshot_id"])
+        == expected_snapshot_first_visit_id
+    )
+
+    assert_last_visit_matches(
+        swh_storage, MVN_ARTIFACTS[1]["url"], status="full", type="maven"
+    )
+
+    actual_load_status2 = loader.load()
+    assert actual_load_status2["status"] == "uneventful"
+    assert actual_load_status2["snapshot_id"] is not None
+    assert actual_load_status["snapshot_id"] == actual_load_status2["snapshot_id"]
+
+    assert_last_visit_matches(
+        swh_storage, MVN_ARTIFACTS[1]["url"], status="full", type="maven"
+    )
+
+    # Make sure we have only one entry in history for the pom fetch, one for
+    # the actual download of jar, and that they're correct.
+    urls_history = [str(req.url) for req in list(requests_mock_datadir.request_history)]
+    assert urls_history == [
+        MVN_ARTIFACTS[1]["url"],
+        MVN_ARTIFACTS_POM[1],
+    ]
+
+
+def test_metadatata(swh_storage, requests_mock, data_jar_1, data_pom_1):
+    """With no prior visit, loading a jar ends up with 1 snapshot.
+    Extrinsic metadata is the pom file associated to the source jar.
+    """
+    requests_mock.get(MVN_ARTIFACTS[0]["url"], content=data_jar_1)
+    requests_mock.get(MVN_ARTIFACTS_POM[0], content=data_pom_1)
+    loader = MavenLoader(
+        swh_storage, MVN_ARTIFACTS[0]["url"], artifacts=[MVN_ARTIFACTS[0]]
+    )
+
+    actual_load_status = loader.load()
+    assert actual_load_status["status"] == "eventful"
+
+    expected_release_id = hash_to_bytes(_expected_new_release_first_visit)
+    release = swh_storage.release_get([expected_release_id])[0]
+    assert release is not None
+
+    release_swhid = CoreSWHID(
+        object_type=ObjectType.RELEASE, object_id=expected_release_id
+    )
+    directory_swhid = ExtendedSWHID(
+        object_type=ExtendedObjectType.DIRECTORY, object_id=release.target
+    )
+    metadata_authority = MetadataAuthority(
+        type=MetadataAuthorityType.FORGE, url="https://repo1.maven.org/",
+    )
+
+    expected_metadata = [
+        RawExtrinsicMetadata(
+            target=directory_swhid,
+            authority=metadata_authority,
+            fetcher=MetadataFetcher(
+                name="swh.loader.package.maven.loader.MavenLoader", version=__version__,
+            ),
+            discovery_date=loader.visit_date,
+            format="maven-pom",
+            metadata=_expected_pom_metadata.encode(),
+            origin=MVN_ARTIFACTS[0]["url"],
+            release=release_swhid,
+        ),
+        RawExtrinsicMetadata(
+            target=directory_swhid,
+            authority=metadata_authority,
+            fetcher=MetadataFetcher(
+                name="swh.loader.package.maven.loader.MavenLoader", version=__version__,
+            ),
+            discovery_date=loader.visit_date,
+            format="maven-json",
+            metadata=json.dumps(_expected_json_metadata).encode(),
+            origin=MVN_ARTIFACTS[0]["url"],
+            release=release_swhid,
+        ),
+    ]
+
+    res = swh_storage.raw_extrinsic_metadata_get(directory_swhid, metadata_authority)
+    assert res.next_page_token is None
+    assert set(res.results) == set(expected_metadata)
+
+
+def test_metadatata_no_pom(swh_storage, requests_mock, data_jar_1):
+    """With no prior visit, loading a jar ends up with 1 snapshot.
+    Extrinsic metadata is None if the pom file cannot be retrieved.
+    """
+    requests_mock.get(MVN_ARTIFACTS[0]["url"], content=data_jar_1)
+    requests_mock.get(MVN_ARTIFACTS_POM[0], status_code="404")
+    loader = MavenLoader(
+        swh_storage, MVN_ARTIFACTS[0]["url"], artifacts=[MVN_ARTIFACTS[0]]
+    )
+
+    actual_load_status = loader.load()
+    assert actual_load_status["status"] == "eventful"
+
+    expected_release_id = hash_to_bytes(_expected_new_release_first_visit)
+    release = swh_storage.release_get([expected_release_id])[0]
+    assert release is not None
+
+    release_swhid = CoreSWHID(
+        object_type=ObjectType.RELEASE, object_id=expected_release_id
+    )
+    directory_swhid = ExtendedSWHID(
+        object_type=ExtendedObjectType.DIRECTORY, object_id=release.target
+    )
+    metadata_authority = MetadataAuthority(
+        type=MetadataAuthorityType.FORGE, url="https://repo1.maven.org/",
+    )
+
+    expected_metadata = [
+        RawExtrinsicMetadata(
+            target=directory_swhid,
+            authority=metadata_authority,
+            fetcher=MetadataFetcher(
+                name="swh.loader.package.maven.loader.MavenLoader", version=__version__,
+            ),
+            discovery_date=loader.visit_date,
+            format="maven-pom",
+            metadata=b"",
+            origin=MVN_ARTIFACTS[0]["url"],
+            release=release_swhid,
+        ),
+        RawExtrinsicMetadata(
+            target=directory_swhid,
+            authority=metadata_authority,
+            fetcher=MetadataFetcher(
+                name="swh.loader.package.maven.loader.MavenLoader", version=__version__,
+            ),
+            discovery_date=loader.visit_date,
+            format="maven-json",
+            metadata=json.dumps(_expected_json_metadata).encode(),
+            origin=MVN_ARTIFACTS[0]["url"],
+            release=release_swhid,
+        ),
+    ]
+    res = swh_storage.raw_extrinsic_metadata_get(directory_swhid, metadata_authority)
+    assert res.next_page_token is None
+    assert set(res.results) == set(expected_metadata)
+
+
+def test_jar_extid():
+    """Compute primary key should return the right identity
+
+    """
+
+    metadata = MVN_ARTIFACTS[0]
+
+    p_info = MavenPackageInfo(**metadata)
+
+    expected_manifest = (
+        b"al.aldi sprova4j 0.1.0 "
+        b"https://repo1.maven.org/maven2/al/aldi/sprova4j/0.1.0/sprova4j-0.1.0"
+        b"-sources.jar 1626109619335"
+    )
+    for manifest_format in [
+        string.Template("$aid $gid $version"),
+        string.Template("$gid $aid"),
+        string.Template("$gid $aid $version"),
+    ]:
+        actual_id = p_info.extid(manifest_format=manifest_format)
+        assert actual_id != ("maven-jar", hashlib.sha256(expected_manifest).digest(),)
+
+    for manifest_format, expected_manifest in [
+        (None, "{gid} {aid} {version} {url} {time}".format(**metadata).encode()),
+    ]:
+        actual_id = p_info.extid(manifest_format=manifest_format)
+        assert actual_id == ("maven-jar", hashlib.sha256(expected_manifest).digest(),)
+
+    with pytest.raises(KeyError):
+        p_info.extid(manifest_format=string.Template("$a $unknown_key"))
+
+
+def test_jar_snapshot_append(
+    swh_storage,
+    requests_mock_datadir,
+    requests_mock,
+    data_jar_1,
+    data_pom_1,
+    data_jar_2,
+    data_pom_2,
+):
+
+    # first loading with a first artifact
+    artifact1 = MVN_ARTIFACTS[0]
+    url1 = artifact1["url"]
+    requests_mock.get(url1, content=data_jar_1)
+    requests_mock.get(MVN_ARTIFACTS_POM[0], content=data_pom_1)
+    loader = MavenLoader(swh_storage, url1, [artifact1])
+    actual_load_status = loader.load()
+    assert actual_load_status["status"] == "eventful"
+    assert actual_load_status["snapshot_id"] is not None
+    assert_last_visit_matches(swh_storage, url1, status="full", type="maven")
+
+    # check expected snapshot
+    snapshot = loader.last_snapshot()
+    assert len(snapshot.branches) == 2
+    branch_artifact1_name = f"releases/{artifact1['version']}".encode()
+    assert b"HEAD" in snapshot.branches
+    assert branch_artifact1_name in snapshot.branches
+    assert snapshot.branches[b"HEAD"].target == branch_artifact1_name
+
+    # second loading with a second artifact
+    artifact2 = MVN_ARTIFACTS[1]
+    url2 = artifact2["url"]
+    requests_mock.get(url2, content=data_jar_2)
+    requests_mock.get(MVN_ARTIFACTS_POM[1], content=data_pom_2)
+    loader = MavenLoader(swh_storage, url2, [artifact2])
+    actual_load_status = loader.load()
+    assert actual_load_status["status"] == "eventful"
+    assert actual_load_status["snapshot_id"] is not None
+    assert_last_visit_matches(swh_storage, url2, status="full", type="maven")
+
+    # check expected snapshot, should contain a new branch and the
+    # branch for the first artifact
+    snapshot = loader.last_snapshot()
+    assert len(snapshot.branches) == 2
+    branch_artifact2_name = f"releases/{artifact2['version']}".encode()
+    assert b"HEAD" in snapshot.branches
+    assert branch_artifact2_name in snapshot.branches
+    assert branch_artifact1_name not in snapshot.branches
+    assert snapshot.branches[b"HEAD"].target == branch_artifact2_name
diff --git a/swh/loader/package/maven/tests/test_tasks.py b/swh/loader/package/maven/tests/test_tasks.py
new file mode 100644
index 00000000..17212198
--- /dev/null
+++ b/swh/loader/package/maven/tests/test_tasks.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2019-2021  The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+MVN_ARTIFACTS = [
+    {
+        "time": 1626109619335,
+        "url": "https://repo1.maven.org/maven2/al/aldi/sprova4j/0.1.0/"
+        + "sprova4j-0.1.0.jar",
+        "gid": "al.aldi",
+        "aid": "sprova4j",
+        "filename": "sprova4j-0.1.0.jar",
+        "version": "0.1.0",
+    },
+]
+
+
+def test_tasks_jar_loader(
+    mocker, swh_scheduler_celery_app, swh_scheduler_celery_worker, swh_config
+):
+    mock_load = mocker.patch("swh.loader.package.maven.loader.MavenLoader.load")
+    mock_load.return_value = {"status": "eventful"}
+
+    res = swh_scheduler_celery_app.send_task(
+        "swh.loader.package.maven.tasks.LoadMaven",
+        kwargs=dict(url=MVN_ARTIFACTS[0]["url"], artifacts=MVN_ARTIFACTS,),
+    )
+    assert res
+    res.wait()
+    assert res.successful()
+    assert mock_load.called
+    assert res.result == {"status": "eventful"}
+
+
+def test_tasks_jar_loader_snapshot_append(
+    mocker, swh_scheduler_celery_app, swh_scheduler_celery_worker, swh_config
+):
+    mock_load = mocker.patch("swh.loader.package.maven.loader.MavenLoader.load")
+    mock_load.return_value = {"status": "eventful"}
+
+    res = swh_scheduler_celery_app.send_task(
+        "swh.loader.package.maven.tasks.LoadMaven",
+        kwargs=dict(url=MVN_ARTIFACTS[0]["url"], artifacts=[]),
+    )
+    assert res
+    res.wait()
+    assert res.successful()
+    assert mock_load.called
+    assert res.result == {"status": "eventful"}
-- 
GitLab