diff --git a/DEPENDENCIES b/DEPENDENCIES index cd7b50306..337a62525 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -45,11 +45,8 @@ maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.15.1, Apache-2.0, approve maven/mavencentral/com.fasterxml.jackson/jackson-bom/2.16.0, , restricted, clearlydefined maven/mavencentral/com.fasterxml.uuid/java-uuid-generator/4.1.0, Apache-2.0, approved, clearlydefined maven/mavencentral/com.github.cliftonlabs/json-simple/3.0.2, Apache-2.0, approved, clearlydefined -maven/mavencentral/com.github.docker-java/docker-java-api/3.3.3, Apache-2.0, approved, #10346 maven/mavencentral/com.github.docker-java/docker-java-api/3.3.4, Apache-2.0, approved, #10346 -maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.3, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #7946 maven/mavencentral/com.github.docker-java/docker-java-transport-zerodep/3.3.4, Apache-2.0 AND (Apache-2.0 AND BSD-3-Clause), approved, #7946 -maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.3, Apache-2.0, approved, #7942 maven/mavencentral/com.github.docker-java/docker-java-transport/3.3.4, Apache-2.0, approved, #7942 maven/mavencentral/com.github.java-json-tools/btf/1.3, Apache-2.0 OR LGPL-3.0-or-later, approved, #2721 maven/mavencentral/com.github.java-json-tools/jackson-coreutils-equivalence/1.0, LGPL-3.0 OR Apache-2.0, approved, clearlydefined @@ -182,7 +179,6 @@ maven/mavencentral/junit/junit/4.13.2, EPL-2.0, approved, CQ23636 maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.1, Apache-2.0, approved, #7164 maven/mavencentral/net.bytebuddy/byte-buddy/1.12.21, Apache-2.0 AND BSD-3-Clause, approved, #1811 maven/mavencentral/net.bytebuddy/byte-buddy/1.14.1, Apache-2.0 AND BSD-3-Clause, approved, #7163 -maven/mavencentral/net.java.dev.jna/jna/5.12.1, Apache-2.0 OR LGPL-2.1-or-later, approved, #3217 maven/mavencentral/net.java.dev.jna/jna/5.13.0, Apache-2.0 AND LGPL-2.1-or-later, approved, #6709 maven/mavencentral/net.javacrumbs.json-unit/json-unit-core/2.36.0, Apache-2.0, approved, clearlydefined maven/mavencentral/net.minidev/accessors-smart/2.4.7, Apache-2.0, approved, #7515 @@ -218,6 +214,7 @@ maven/mavencentral/org.bouncycastle/bcutil-jdk18on/1.72, MIT, approved, #3790 maven/mavencentral/org.bouncycastle/bcutil-jdk18on/1.77, MIT, approved, #11596 maven/mavencentral/org.ccil.cowan.tagsoup/tagsoup/1.2.1, Apache-2.0, approved, clearlydefined maven/mavencentral/org.checkerframework/checker-qual/3.12.0, MIT, approved, clearlydefined +maven/mavencentral/org.checkerframework/checker-qual/3.31.0, MIT, approved, clearlydefined maven/mavencentral/org.eclipse.angus/angus-activation/1.0.0, EPL-2.0 OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.angus maven/mavencentral/org.eclipse.edc/api-observability/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/autodoc-processor/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc @@ -254,6 +251,7 @@ maven/mavencentral/org.eclipse.edc/policy-evaluator/0.4.2-SNAPSHOT, Apache-2.0, maven/mavencentral/org.eclipse.edc/policy-model/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/policy-spi/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/runtime-metamodel/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc +maven/mavencentral/org.eclipse.edc/sql-core/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/state-machine/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transaction-datasource-spi/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc maven/mavencentral/org.eclipse.edc/transaction-spi/0.4.2-SNAPSHOT, Apache-2.0, approved, technology.edc @@ -345,6 +343,7 @@ maven/mavencentral/org.ow2.asm/asm-tree/9.6, BSD-3-Clause, approved, #10773 maven/mavencentral/org.ow2.asm/asm/9.1, BSD-3-Clause, approved, CQ23029 maven/mavencentral/org.ow2.asm/asm/9.2, BSD-3-Clause, approved, CQ23635 maven/mavencentral/org.ow2.asm/asm/9.6, BSD-3-Clause, approved, #10776 +maven/mavencentral/org.postgresql/postgresql/42.7.0, BSD-2-Clause AND LicenseRef-scancode-free-unknown AND Apache-2.0, restricted, #11681 maven/mavencentral/org.reflections/reflections/0.10.2, Apache-2.0 AND WTFPL, approved, clearlydefined maven/mavencentral/org.rnorth.duct-tape/duct-tape/1.0.8, MIT, approved, clearlydefined maven/mavencentral/org.slf4j/slf4j-api/1.7.22, MIT, approved, CQ11943 @@ -356,9 +355,10 @@ maven/mavencentral/org.slf4j/slf4j-api/1.7.36, MIT, approved, CQ13368 maven/mavencentral/org.slf4j/slf4j-api/2.0.5, MIT, approved, #5915 maven/mavencentral/org.slf4j/slf4j-api/2.0.6, MIT, approved, #5915 maven/mavencentral/org.slf4j/slf4j-api/2.0.9, MIT, approved, #5915 -maven/mavencentral/org.testcontainers/junit-jupiter/1.19.1, MIT, approved, #10344 +maven/mavencentral/org.testcontainers/database-commons/1.19.3, Apache-2.0, approved, #10345 +maven/mavencentral/org.testcontainers/jdbc/1.19.3, Apache-2.0, approved, #10348 maven/mavencentral/org.testcontainers/junit-jupiter/1.19.3, MIT, approved, #10344 -maven/mavencentral/org.testcontainers/testcontainers/1.19.1, Apache-2.0 AND MIT, approved, #10347 +maven/mavencentral/org.testcontainers/postgresql/1.19.3, MIT, approved, #10350 maven/mavencentral/org.testcontainers/testcontainers/1.19.3, Apache-2.0 AND MIT, approved, #10347 maven/mavencentral/org.xmlunit/xmlunit-core/2.9.1, Apache-2.0, approved, #6272 maven/mavencentral/org.xmlunit/xmlunit-placeholders/2.9.1, Apache-2.0, approved, clearlydefined diff --git a/core/identity-hub-did-store-sql/build.gradle.kts b/core/identity-hub-did-store-sql/build.gradle.kts new file mode 100644 index 000000000..9494ed612 --- /dev/null +++ b/core/identity-hub-did-store-sql/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metform Systems, Inc. - initial API and implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + api(project(":spi:identity-hub-did-spi")) + implementation(libs.edc.core.sql) // for the SqlStatements + implementation(libs.edc.spi.transaction.datasource) + + testImplementation(testFixtures(project(":spi:identity-hub-did-spi"))) + testImplementation(testFixtures(libs.edc.core.sql)) + testImplementation(libs.edc.junit) +} diff --git a/core/identity-hub-did-store-sql/docs/schema.sql b/core/identity-hub-did-store-sql/docs/schema.sql new file mode 100644 index 000000000..5bb560348 --- /dev/null +++ b/core/identity-hub-did-store-sql/docs/schema.sql @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metform Systems, Inc. - initial API and implementation + * + */ + +-- +-- +-- This program and the accompanying materials are made available under the +-- terms of the Apache License, Version 2.0 which is available at +-- https://www.apache.org/licenses/LICENSE-2.0 +-- +-- SPDX-License-Identifier: Apache-2.0 +-- +-- Contributors: +-- Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation +-- + +-- only intended for and tested with Postgres! +CREATE TABLE IF NOT EXISTS did_resources +( + did VARCHAR NOT NULL, + create_timestamp BIGINT NOT NULL, + state_timestamp BIGINT NOT NULL, + state INT NOT NULL, + did_document JSON NOT NULL, + PRIMARY KEY (did) +); diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/BaseSqlDialectStatements.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/BaseSqlDialectStatements.java new file mode 100644 index 000000000..6fcbd9a0e --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/BaseSqlDialectStatements.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.store.sql; + +import org.eclipse.edc.identityhub.did.store.sql.schema.postgres.DidResourceMapping; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +import static java.lang.String.format; + +public class BaseSqlDialectStatements implements DidResourceStatements { + @Override + public String getInsertTemplate() { + return executeStatement() + .column(getIdColumn()) + .column(getStateColumn()) + .column(getCreateTimestampColumn()) + .column(getStateTimestampColumn()) + .jsonColumn(getDidDocumentColumn()) + .insertInto(getDidResourceTableName()); + } + + @Override + public String getUpdateTemplate() { + return executeStatement() + .column(getIdColumn()) + .column(getStateColumn()) + .column(getCreateTimestampColumn()) + .column(getStateTimestampColumn()) + .jsonColumn(getDidDocumentColumn()) + .update(getDidResourceTableName(), getIdColumn()); + } + + @Override + public String getDeleteByIdTemplate() { + return executeStatement().delete(getDidResourceTableName(), getIdColumn()); + } + + @Override + public String getFindByIdTemplate() { + return format("SELECT * FROM %s WHERE %s = ?", getDidResourceTableName(), getIdColumn()); + + } + + @Override + public SqlQueryStatement createQuery(QuerySpec querySpec) { + var select = getSelectStatement(); + return new SqlQueryStatement(select, querySpec, new DidResourceMapping(this)); + } + + @Override + public String getSelectStatement() { + return format("SELECT * FROM %s", getDidResourceTableName()); + } +} diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/DidResourceStatements.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/DidResourceStatements.java new file mode 100644 index 000000000..cc94ce915 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/DidResourceStatements.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.store.sql; + +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.statement.SqlStatements; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +/** + * The DidResourceStatements interface defines the SQL statements required to interact with the `did_resources` table. + * It extends the SqlStatements interface. + */ +public interface DidResourceStatements extends SqlStatements { + default String getDidResourceTableName() { + return "did_resources"; + } + + default String getIdColumn() { + return "did"; + } + + default String getStateColumn() { + return "state"; + } + + default String getStateTimestampColumn() { + return "state_timestamp"; + } + + default String getCreateTimestampColumn() { + return "create_timestamp"; + } + + default String getDidDocumentColumn() { + return "did_document"; + } + + String getInsertTemplate(); + + String getUpdateTemplate(); + + String getDeleteByIdTemplate(); + + String getFindByIdTemplate(); + + SqlQueryStatement createQuery(QuerySpec query); + + String getSelectStatement(); +} diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStore.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStore.java new file mode 100644 index 000000000..dad7596d1 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStore.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.store.sql; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.iam.did.spi.document.DidDocument; +import org.eclipse.edc.identithub.did.spi.model.DidResource; +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.spi.persistence.EdcPersistenceException; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.sql.QueryExecutor; +import org.eclipse.edc.sql.store.AbstractSqlStore; +import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry; +import org.eclipse.edc.transaction.spi.TransactionContext; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Objects; + + +/** + * The SqlDidResourceStore class is an implementation of the DidResourceStore interface that interacts with a SQL database. + * It extends the AbstractSqlStore class and provides CRUD methods for managing DidResource objects. + */ +public class SqlDidResourceStore extends AbstractSqlStore implements DidResourceStore { + + private final DidResourceStatements statements; + + public SqlDidResourceStore(DataSourceRegistry dataSourceRegistry, String dataSourceName, TransactionContext transactionContext, + ObjectMapper objectMapper, QueryExecutor queryExecutor, DidResourceStatements statements) { + super(dataSourceRegistry, dataSourceName, transactionContext, objectMapper, queryExecutor); + this.statements = statements; + } + + + @Override + public StoreResult save(DidResource resource) { + var did = resource.getDid(); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findById(did) != null) { + return StoreResult.alreadyExists(alreadyExistsErrorMessage(did)); + } + + var stmt = statements.getInsertTemplate(); + queryExecutor.execute(connection, stmt, + did, + resource.getState(), + resource.getCreateTimestamp(), + resource.getStateTimestamp(), + toJson(resource.getDocument())); + return StoreResult.success(); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public StoreResult update(DidResource resource) { + var did = resource.getDid(); + Objects.requireNonNull(resource); + Objects.requireNonNull(did); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findById(did) != null) { + queryExecutor.execute(connection, statements.getUpdateTemplate(), + did, + resource.getState(), + resource.getCreateTimestamp(), + resource.getStateTimestamp(), + toJson(resource.getDocument()), + did); + return StoreResult.success(); + } + return StoreResult.notFound(notFoundErrorMessage(did)); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + @Override + public DidResource findById(String did) { + Objects.requireNonNull(did); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + var sql = statements.getFindByIdTemplate(); + return queryExecutor.single(connection, false, this::mapResultSet, sql, did); + } catch (Exception exception) { + throw new EdcPersistenceException(exception); + } + }); + } + + @Override + public Collection query(QuerySpec query) { + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + var sql = statements.createQuery(query); + return queryExecutor.query(connection, true, this::mapResultSet, sql.getQueryAsString(), sql.getParameters()).toList(); + } catch (Exception exception) { + throw new EdcPersistenceException(exception); + } + }); + } + + @Override + public StoreResult deleteById(String did) { + Objects.requireNonNull(did); + return transactionContext.execute(() -> { + try (var connection = getConnection()) { + if (findById(did) != null) { + var stmt = statements.getDeleteByIdTemplate(); + queryExecutor.execute(connection, stmt, did); + return StoreResult.success(); + } + return StoreResult.notFound(notFoundErrorMessage(did)); + } catch (SQLException e) { + throw new EdcPersistenceException(e); + } + }); + } + + private DidResource mapResultSet(ResultSet resultSet) throws Exception { + return DidResource.Builder.newInstance() + .did(resultSet.getString(statements.getIdColumn())) + .createTimestamp(resultSet.getLong(statements.getCreateTimestampColumn())) + .stateTimeStamp(resultSet.getLong(statements.getStateTimestampColumn())) + .document(fromJson(resultSet.getString(statements.getDidDocumentColumn()), DidDocument.class)) + .state(resultSet.getInt(statements.getStateColumn())) + .build(); + } +} diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreExtension.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreExtension.java new file mode 100644 index 000000000..f03189342 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreExtension.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.store.sql; + +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.identityhub.did.store.sql.schema.postgres.PostgresDialectStatements; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.sql.QueryExecutor; +import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry; +import org.eclipse.edc.transaction.spi.TransactionContext; + +import static org.eclipse.edc.identityhub.did.store.sql.SqlDidResourceStoreExtension.NAME; + +@Extension(value = NAME) +public class SqlDidResourceStoreExtension implements ServiceExtension { + public static final String NAME = "DID Resource SQL Store Extension"; + + @Setting(value = "Datasource name for the DidResource database", defaultValue = DataSourceRegistry.DEFAULT_DATASOURCE) + public static final String DATASOURCE_SETTING_NAME = "edc.datasource.didresource.name"; + @Inject + private DataSourceRegistry dataSourceRegistry; + @Inject + private TransactionContext transactionContext; + @Inject + private TypeManager typemanager; + @Inject + private QueryExecutor queryExecutor; + @Inject(required = false) + private DidResourceStatements statements; + + + @Provider + public DidResourceStore createSqlStore(ServiceExtensionContext context) { + return new SqlDidResourceStore(dataSourceRegistry, getDataSourceName(context), transactionContext, typemanager.getMapper(), + queryExecutor, getStatementImpl()); + } + + private DidResourceStatements getStatementImpl() { + return statements != null ? statements : new PostgresDialectStatements(); + } + + private String getDataSourceName(ServiceExtensionContext context) { + return context.getConfig().getString(DATASOURCE_SETTING_NAME, DataSourceRegistry.DEFAULT_DATASOURCE); + } +} diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidDocumentMapping.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidDocumentMapping.java new file mode 100644 index 000000000..9fabc8ef6 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidDocumentMapping.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.store.sql.schema.postgres; + +import org.eclipse.edc.identityhub.did.store.sql.DidResourceStatements; +import org.eclipse.edc.sql.translation.JsonFieldMapping; +import org.eclipse.edc.sql.translation.TranslationMapping; + +/** + * The DidDocumentMapping class extends the TranslationMapping class and represents the mapping of the DidDocument + * object to the database table. It defines the fields and their corresponding database columns for the DidDocument. + */ +public class DidDocumentMapping extends TranslationMapping { + + public static final String FIELD_ID = "id"; + public static final String FIELD_SERVICE = "service"; + public static final String FIELD_VERIFICATION_METHOD = "verificationMethod"; + public static final String FIELD_AUTHENTICATION = "authentication"; + + public DidDocumentMapping(DidResourceStatements statements) { + add(FIELD_ID, statements.getIdColumn()); + add(FIELD_SERVICE, new JsonFieldMapping(FIELD_SERVICE)); + add(FIELD_VERIFICATION_METHOD, new JsonFieldMapping(FIELD_VERIFICATION_METHOD)); + add(FIELD_AUTHENTICATION, FIELD_AUTHENTICATION); + } +} diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidResourceMapping.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidResourceMapping.java new file mode 100644 index 000000000..0a37931dc --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/DidResourceMapping.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.store.sql.schema.postgres; + +import org.eclipse.edc.identityhub.did.store.sql.DidResourceStatements; +import org.eclipse.edc.sql.translation.TranslationMapping; + +/** + * The DidResourceMapping class extends the TranslationMapping class and represents the mapping of the DidResource + * object to the database table. It defines the fields and their corresponding database columns for the DidResource, + * as well as the mapping for the DidDocument object. + */ +public class DidResourceMapping extends TranslationMapping { + + public static final String FIELD_DID = "did"; + public static final String FIELD_STATE = "state"; + public static final String FIELD_CREATE_TIMESTAMP = "create_timestamp"; + public static final String FIELD_STATE_TIMESTAMP = "state_timestamp"; + public static final String FIELD_DOCUMENT = "document"; + + public DidResourceMapping(DidResourceStatements statements) { + add(FIELD_DID, statements.getIdColumn()); + add(FIELD_STATE, statements.getStateColumn()); + add(FIELD_CREATE_TIMESTAMP, statements.getCreateTimestampColumn()); + add(FIELD_STATE_TIMESTAMP, statements.getStateTimestampColumn()); + add(FIELD_DOCUMENT, new DidDocumentMapping(statements)); + } +} \ No newline at end of file diff --git a/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/PostgresDialectStatements.java b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/PostgresDialectStatements.java new file mode 100644 index 000000000..5aecbb2a4 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/java/org/eclipse/edc/identityhub/did/store/sql/schema/postgres/PostgresDialectStatements.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.store.sql.schema.postgres; + +import org.eclipse.edc.identityhub.did.store.sql.BaseSqlDialectStatements; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.sql.dialect.PostgresDialect; +import org.eclipse.edc.sql.translation.SqlQueryStatement; + +import static java.lang.String.format; +import static org.eclipse.edc.sql.dialect.PostgresDialect.getSelectFromJsonArrayTemplate; + +/** + * The `PostgresStatementsImpl` class is an implementation of the `DidResourceStatements` interface that provides + * PostgreSQL specific SQL statements for interacting with the `did_resources` table. + */ +public class PostgresDialectStatements extends BaseSqlDialectStatements { + @Override + public String getFormatAsJsonOperator() { + return PostgresDialect.getJsonCastOperator(); + } + + @Override + public SqlQueryStatement createQuery(QuerySpec querySpec) { + if (querySpec.containsAnyLeftOperand("document.service")) { + var select = getSelectFromJsonArrayTemplate(getSelectStatement(), "%s -> '%s'".formatted(getDidDocumentColumn(), "service"), DidDocumentMapping.FIELD_SERVICE); + return new SqlQueryStatement(select, querySpec, new DidResourceMapping(this)); + } else if (querySpec.containsAnyLeftOperand("document.verificationMethod")) { + var select = getSelectFromJsonArrayTemplate(getSelectStatement(), "%s -> '%s'".formatted(getDidDocumentColumn(), "verificationMethod"), DidDocumentMapping.FIELD_VERIFICATION_METHOD); + return new SqlQueryStatement(select, querySpec, new DidResourceMapping(this)); + } else if (querySpec.containsAnyLeftOperand("document.authentication")) { + var select = getSelectFromJsonArrayTextTemplate(getSelectStatement(), "%s -> '%s'".formatted(getDidDocumentColumn(), "authentication"), DidDocumentMapping.FIELD_AUTHENTICATION); + return new SqlQueryStatement(select, querySpec, new DidResourceMapping(this)); + } + return super.createQuery(querySpec); + } + + private String getSelectFromJsonArrayTextTemplate(String selectStatement, String jsonPath, String aliasName) { + return format("%s, json_array_elements_text(%s) as %s", selectStatement, jsonPath, aliasName); + } +} diff --git a/core/identity-hub-did-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/core/identity-hub-did-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 000000000..f79b4486a --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2023 Metaform Systems, Inc. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Metform Systems, Inc. - initial API and implementation +# +# + +org.eclipse.edc.identityhub.did.store.sql.SqlDidResourceStoreExtension \ No newline at end of file diff --git a/core/identity-hub-did-store-sql/src/main/resources/did.json b/core/identity-hub-did-store-sql/src/main/resources/did.json new file mode 100644 index 000000000..511d12ef5 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/main/resources/did.json @@ -0,0 +1,57 @@ +{ + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "alsoKnownAs": { + "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", + "@type": "@id" + }, + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + }, + "service": { + "@id": "https://www.w3.org/ns/did#service", + "@type": "@id", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "serviceEndpoint": { + "@id": "https://www.w3.org/ns/did#serviceEndpoint", + "@type": "@id" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } +} \ No newline at end of file diff --git a/core/identity-hub-did-store-sql/src/test/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreTest.java b/core/identity-hub-did-store-sql/src/test/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreTest.java new file mode 100644 index 000000000..2bb335b08 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/test/java/org/eclipse/edc/identityhub/did/store/sql/SqlDidResourceStoreTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.store.sql; + +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.identityhub.did.store.sql.schema.postgres.PostgresDialectStatements; +import org.eclipse.edc.identityhub.did.store.test.DidResourceStoreTestBase; +import org.eclipse.edc.junit.annotations.ComponentTest; +import org.eclipse.edc.spi.types.TypeManager; +import org.eclipse.edc.sql.QueryExecutor; +import org.eclipse.edc.sql.testfixtures.PostgresqlStoreSetupExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +@ComponentTest +@ExtendWith(PostgresqlStoreSetupExtension.class) +class SqlDidResourceStoreTest extends DidResourceStoreTestBase { + + private final DidResourceStatements statements = new PostgresDialectStatements(); + private SqlDidResourceStore store; + + @BeforeEach + void setup(PostgresqlStoreSetupExtension extension, QueryExecutor queryExecutor) throws IOException { + var typeManager = new TypeManager(); + store = new SqlDidResourceStore(extension.getDataSourceRegistry(), extension.getDatasourceName(), + extension.getTransactionContext(), typeManager.getMapper(), queryExecutor, statements); + + var schema = Files.readString(Paths.get("./docs/schema.sql")); + extension.runQuery(schema); + } + + @AfterEach + void tearDown(PostgresqlStoreSetupExtension extension) { + extension.runQuery("DROP TABLE " + statements.getDidResourceTableName() + " CASCADE"); + } + + @Override + protected DidResourceStore getStore() { + return store; + } +} \ No newline at end of file diff --git a/core/identity-hub-did-store-sql/src/test/resources/did.json b/core/identity-hub-did-store-sql/src/test/resources/did.json new file mode 100644 index 000000000..511d12ef5 --- /dev/null +++ b/core/identity-hub-did-store-sql/src/test/resources/did.json @@ -0,0 +1,57 @@ +{ + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "alsoKnownAs": { + "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", + "@type": "@id" + }, + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + }, + "service": { + "@id": "https://www.w3.org/ns/did#service", + "@type": "@id", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "serviceEndpoint": { + "@id": "https://www.w3.org/ns/did#serviceEndpoint", + "@type": "@id" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } +} \ No newline at end of file diff --git a/core/identity-hub-did/build.gradle.kts b/core/identity-hub-did/build.gradle.kts index 4d4943e90..41f9be43d 100644 --- a/core/identity-hub-did/build.gradle.kts +++ b/core/identity-hub-did/build.gradle.kts @@ -4,9 +4,12 @@ plugins { dependencies { api(project(":spi:identity-hub-spi")) + api(project(":spi:identity-hub-did-spi")) + + implementation(libs.edc.core.connector) // for the reflection-based query resolver testImplementation(libs.edc.junit) testImplementation(libs.edc.ext.jsonld) testImplementation(testFixtures(project(":spi:identity-hub-spi"))) - testImplementation(libs.edc.identity.did.crypto) // EC private key wrapper + testImplementation(testFixtures(project(":spi:identity-hub-did-spi"))) } diff --git a/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/DidDefaultServicesExtension.java b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/DidDefaultServicesExtension.java new file mode 100644 index 000000000..bf16805ca --- /dev/null +++ b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/DidDefaultServicesExtension.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.defaults; + +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.spi.system.ServiceExtension; + +import static org.eclipse.edc.identityhub.did.defaults.DidDefaultServicesExtension.NAME; + +@Extension(value = NAME) +public class DidDefaultServicesExtension implements ServiceExtension { + public static final String NAME = "DID Default Services Extension"; + + @Provider(isDefault = true) + public DidResourceStore createInMemoryDidResourceStore() { + return new InMemoryDidResourceStore(); + } +} diff --git a/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStore.java b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStore.java new file mode 100644 index 000000000..d9d4b21f2 --- /dev/null +++ b/core/identity-hub-did/src/main/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStore.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.defaults; + +import org.eclipse.edc.connector.core.store.ReflectionBasedQueryResolver; +import org.eclipse.edc.identithub.did.spi.model.DidResource; +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.spi.query.QueryResolver; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class InMemoryDidResourceStore implements DidResourceStore { + private final Map store = new HashMap<>(); + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final QueryResolver queryResolver = new ReflectionBasedQueryResolver<>(DidResource.class); + + + @Override + public StoreResult save(DidResource resource) { + lock.writeLock().lock(); + try { + var did = resource.getDid(); + if (store.containsKey(did)) { + return StoreResult.alreadyExists(alreadyExistsErrorMessage(did)); + } + store.put(did, resource); + return StoreResult.success(); + } finally { + lock.writeLock().unlock(); + } + } + + + @Override + public StoreResult update(DidResource resource) { + lock.writeLock().lock(); + try { + var did = resource.getDid(); + if (!store.containsKey(did)) { + return StoreResult.notFound(notFoundErrorMessage(did)); + } + store.put(did, resource); + return StoreResult.success(); + } finally { + lock.writeLock().unlock(); + } + } + + + @Override + public DidResource findById(String did) { + lock.readLock().lock(); + try { + return store.get(did); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public Collection query(QuerySpec query) { + lock.readLock().lock(); + try { + return queryResolver.query(store.values().stream(), query).toList(); + } finally { + lock.readLock().unlock(); + } + } + + @Override + public StoreResult deleteById(String did) { + lock.writeLock().lock(); + try { + return store.remove(did) == null + ? StoreResult.notFound(notFoundErrorMessage(did)) + : StoreResult.success(); + } finally { + lock.writeLock().unlock(); + } + } +} diff --git a/core/identity-hub-did/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/core/identity-hub-did/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index 308271903..58586ce19 100644 --- a/core/identity-hub-did/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/core/identity-hub-did/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -12,4 +12,5 @@ # # +org.eclipse.edc.identityhub.did.defaults.DidDefaultServicesExtension org.eclipse.edc.identityhub.did.DidServicesExtension \ No newline at end of file diff --git a/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStoreTest.java b/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStoreTest.java new file mode 100644 index 000000000..36538fb7c --- /dev/null +++ b/core/identity-hub-did/src/test/java/org/eclipse/edc/identityhub/did/defaults/InMemoryDidResourceStoreTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.defaults; + +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.identityhub.did.store.test.DidResourceStoreTestBase; + +class InMemoryDidResourceStoreTest extends DidResourceStoreTestBase { + + private final DidResourceStore store = new InMemoryDidResourceStore(); + + @Override + protected DidResourceStore getStore() { + return store; + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f0bce472..d75869779 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ postgres = "42.6.0" restAssured = "5.3.2" swagger = "2.2.18" rsApi = "3.1.0" -testcontainers = "1.19.1" +testcontainers = "1.19.3" [libraries] edc-util = { module = "org.eclipse.edc:util", version.ref = "edc" } @@ -74,6 +74,8 @@ postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" } restAssured = { module = "io.rest-assured:rest-assured", version.ref = "restAssured" } swagger-jaxrs = { module = "io.swagger.core.v3:swagger-jaxrs2-jakarta", version.ref = "swagger" } testcontainers-junit = { module = "org.testcontainers:junit-jupiter", version.ref = "testcontainers" } +testcontainers-postgres = { module = "org.testcontainers:postgres", version.ref = "testcontainers" } + [bundles] connector = ["edc-boot", "edc-core-connector", "edc-ext-http", "edc-ext-observability", "edc-ext-jsonld"] diff --git a/settings.gradle.kts b/settings.gradle.kts index 2f9eddaef..1df621b7c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,6 +31,7 @@ include(":spi:identity-hub-did-spi") include(":core:identity-hub-api") include(":core:identity-hub-credentials") include(":core:identity-hub-did") +include(":core:identity-hub-did-store-sql") // extension modules include(":extensions:cryptography:public-key-provider") diff --git a/spi/identity-hub-did-spi/build.gradle.kts b/spi/identity-hub-did-spi/build.gradle.kts index 133467508..cc0e3bf1f 100644 --- a/spi/identity-hub-did-spi/build.gradle.kts +++ b/spi/identity-hub-did-spi/build.gradle.kts @@ -18,10 +18,12 @@ plugins { `maven-publish` } -val swagger: String by project - dependencies { - api(libs.edc.spi.identitytrust) - implementation(libs.edc.spi.identity.did) + api(libs.edc.spi.identity.did) + + testFixturesImplementation(libs.edc.spi.identity.did) + testFixturesImplementation(libs.junit.jupiter.api) + testFixturesImplementation(libs.edc.junit) + testFixturesImplementation(libs.assertj) } diff --git a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java index 80d66e6c8..92efecddc 100644 --- a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java +++ b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidResource.java @@ -27,7 +27,7 @@ public class DidResource { @JsonIgnore private Clock clock = Clock.systemUTC(); private String did; - private DidState state = DidState.INITIAL; + private int state = DidState.INITIAL.code(); private long stateTimestamp; private long createTimestamp; private DidDocument document; @@ -41,10 +41,14 @@ public String getDid() { return did; } - public DidState getState() { + public int getState() { return state; } + public DidState getStateAsEnum() { + return DidState.from(state); + } + public long getStateTimestamp() { return stateTimestamp; } @@ -70,7 +74,7 @@ public Builder did(String did) { } public Builder state(DidState state) { - this.resource.state = state; + this.resource.state = state.code(); return this; } @@ -105,6 +109,11 @@ public DidResource build() { return resource; } + public Builder state(int code) { + this.resource.state = code; + return this; + } + public static Builder newInstance() { return new Builder(); } diff --git a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidState.java b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidState.java index a15e16cab..d7d8efbd6 100644 --- a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidState.java +++ b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/model/DidState.java @@ -14,6 +14,8 @@ package org.eclipse.edc.identithub.did.spi.model; +import java.util.Arrays; + /** * The DidState enum represents the state of a DID resource in the internal store. */ @@ -45,4 +47,8 @@ public enum DidState { public int code() { return code; } + + public static DidState from(int code) { + return Arrays.stream(values()).filter(tps -> tps.code == code).findFirst().orElse(null); + } } diff --git a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/store/DidResourceStore.java b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/store/DidResourceStore.java index 8541d32ad..02d96f5e8 100644 --- a/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/store/DidResourceStore.java +++ b/spi/identity-hub-did-spi/src/main/java/org/eclipse/edc/identithub/did/spi/store/DidResourceStore.java @@ -16,8 +16,10 @@ import org.eclipse.edc.identithub.did.spi.model.DidResource; import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.StoreResult; +import java.util.Collection; import java.util.List; /** @@ -51,14 +53,14 @@ public interface DidResourceStore { * @param did The DID to search for. * @return The {@link DidResource} object found in the store, or null if no matching object is found. */ - DidResource getById(String did); + DidResource findById(String did); /** * Retrieves all {@link DidResource} objects from the store. * * @return A {@link List} containing {@link DidResource} objects retrieved from the store. */ - List getAll(); + Collection query(QuerySpec query); /** * Deletes a {@link DidResource} object from the store with the specified DID. If the specified DID document does not @@ -69,4 +71,11 @@ public interface DidResourceStore { */ StoreResult deleteById(String did); + default String alreadyExistsErrorMessage(String did) { + return "A DidResource with ID %s already exists.".formatted(did); + } + + default String notFoundErrorMessage(String did) { + return "A DidResource with ID %s was not found.".formatted(did); + } } diff --git a/spi/identity-hub-did-spi/src/testFixtures/java/org/eclipse/edc/identityhub/did/store/test/DidResourceStoreTestBase.java b/spi/identity-hub-did-spi/src/testFixtures/java/org/eclipse/edc/identityhub/did/store/test/DidResourceStoreTestBase.java new file mode 100644 index 000000000..18cb600f3 --- /dev/null +++ b/spi/identity-hub-did-spi/src/testFixtures/java/org/eclipse/edc/identityhub/did/store/test/DidResourceStoreTestBase.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2023 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.did.store.test; + +import org.assertj.core.api.Assertions; +import org.eclipse.edc.iam.did.spi.document.DidDocument; +import org.eclipse.edc.iam.did.spi.document.Service; +import org.eclipse.edc.iam.did.spi.document.VerificationMethod; +import org.eclipse.edc.identithub.did.spi.model.DidResource; +import org.eclipse.edc.identithub.did.spi.model.DidState; +import org.eclipse.edc.identithub.did.spi.store.DidResourceStore; +import org.eclipse.edc.spi.message.Range; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.query.SortOrder; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import static java.util.stream.IntStream.range; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; + +/** + * Base test class for DidResourceStore implementations. + */ +public abstract class DidResourceStoreTestBase { + + public static final String DID = "did:web:test"; + + @Test + void save() { + var didResource = createDidResource(DID).build(); + assertThat(getStore().save(didResource)).isSucceeded(); + } + + @Test + void save_alreadyExists() { + var didResource = createDidResource(DID).build(); + assertThat(getStore().save(didResource)).isSucceeded(); + assertThat(getStore().save(didResource)).isFailed().detail().isEqualTo("A DidResource with ID %s already exists.".formatted(DID)); + } + + @Test + void update() { + var builder = createDidResource(DID); + var didResource = builder.build(); + getStore().save(didResource); + + var didResource2 = builder.state(DidState.GENERATED).build(); + assertThat(getStore().update(didResource2)).isSucceeded(); + var fromDb = getStore().findById(DID); + Assertions.assertThat(fromDb).usingRecursiveComparison().isEqualTo(didResource2); + } + + @Test + void update_notExists() { + assertThat(getStore().update(createDidResource(DID).build())).isFailed() + .detail().isEqualTo("A DidResource with ID %s was not found.".formatted(DID)); + } + + @Test + void findById() { + var didResource = createDidResource(DID).build(); + getStore().save(didResource); + + Assertions.assertThat(getStore().findById(DID)) + .isNotNull() + .usingRecursiveComparison() + .isEqualTo(didResource); + } + + @Test + void findById_notExists() { + Assertions.assertThat(getStore().findById("did:web:notexist")).isNull(); + } + + @Test + void query() { + var dids = range(0, 10) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList(); + dids.forEach(getStore()::save); + + Assertions.assertThat(getStore().query(QuerySpec.none())) + .hasSize(10) + .usingRecursiveFieldByFieldElementComparator() + .containsExactlyInAnyOrderElementsOf(dids); + } + + @Test + void query_withPage() { + var dids = range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList(); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().range(new Range(25, 35)).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(10) + .usingRecursiveFieldByFieldElementComparator() + .containsAnyElementsOf(dids); + } + + @Test + void query_withSorting() { + var dids = range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList(); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().sortOrder(SortOrder.DESC).sortField("did").build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(50) + .usingRecursiveFieldByFieldElementComparator() + .containsAnyElementsOf(dids) + .extracting(DidResource::getDid) + .isSortedAccordingTo(Comparator.reverseOrder()); + } + + @Test + void query_bySimpleProperty() { + var dids = new ArrayList<>(range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList()); + + var expected = createDidResource(DID + "69").state(DidState.PUBLISHED).build(); + dids.add(expected); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().filter(new Criterion("state", "=", DidState.PUBLISHED.code())).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(expected); + } + + @Test + void query_byComplexProperty_service() { + var dids = new ArrayList<>(range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList()); + + var expected = createDidResource(DID + "69") + .document(DidDocument.Builder.newInstance() + .id(DID + "69") + .service(List.of(new Service("test-service", "foo-type", "https://foo.bar"))) + .build()) + .build(); + + dids.add(expected); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().filter(new Criterion("document.service.type", "=", "foo-type")).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(expected); + } + + @Test + void query_byComplexProperty_verificationMethod() { + var dids = new ArrayList<>(range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList()); + + var expected = createDidResource(DID + "69") + .document(DidDocument.Builder.newInstance() + .id(DID + "69") + .service(List.of(new Service("test-service", "foo-type", "https://foo.bar"))) + .verificationMethod(List.of(VerificationMethod.Builder.newInstance().id("vm-1").type("test-type").publicKeyMultibase("asdfl;aksdflaskdfj").build())) + .build()) + .build(); + + dids.add(expected); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().filter(new Criterion("document.verificationMethod.type", "=", "test-type")).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(expected); + } + + @Test + void query_byComplexProperty_authentication() { + var dids = new ArrayList<>(range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList()); + + var expected = createDidResource(DID + "69") + .document(DidDocument.Builder.newInstance() + .id(DID + "69") + .service(List.of(new Service("test-service", "foo-type", "https://foo.bar"))) + .verificationMethod(List.of(VerificationMethod.Builder.newInstance().id("vm-1").type("test-type").publicKeyMultibase("asdfl;aksdflaskdfj").build())) + .authentication(List.of("auth1", "auth2")) + .build()) + .build(); + + dids.add(expected); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().filter(new Criterion("document.authentication", "=", "auth1")).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(expected); + } + + @Test + void query_byComplexProperty_id() { + var dids = new ArrayList<>(range(0, 50) + .mapToObj(i -> createDidResource(DID + i).build()) + .toList()); + + var expected = createDidResource(DID + "69") + .document(DidDocument.Builder.newInstance() + .id(DID + "69") + .build()) + .build(); + + dids.add(expected); + dids.forEach(getStore()::save); + + var q = QuerySpec.Builder.newInstance().filter(new Criterion("document.id", "=", DID + "69")).build(); + Assertions.assertThat(getStore().query(q)) + .hasSize(1) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(expected); + } + + @Test + void deleteById() { + var didResource = createDidResource(DID).build(); + getStore().save(didResource); + assertThat(getStore().deleteById(DID)).isSucceeded(); + Assertions.assertThat(getStore().query(QuerySpec.none())).isEmpty(); + } + + @Test + void deleteById_notExist() { + assertThat(getStore().deleteById(DID)).isFailed() + .detail().isEqualTo("A DidResource with ID %s was not found.".formatted(DID)); + } + + protected abstract DidResourceStore getStore(); + + private DidResource.Builder createDidResource(String did) { + return DidResource.Builder.newInstance() + .did(did) + .document(DidDocument.Builder.newInstance() + .id(did) + .build()) + .state(DidState.INITIAL); + } +}