From a470db1bcf171f718549d4c8f8166f638cc76cf5 Mon Sep 17 00:00:00 2001
From: Martin Weise <martin.weise@tuwien.ac.at>
Date: Sat, 27 May 2023 21:19:44 +0200
Subject: [PATCH] Fixed the label retrieval

---
 .../dbrepo-realm.json                         |  70 +++++----
 .../table/columns/concepts/ConceptDto.java    |   3 +-
 .../columns/concepts/ConceptSaveDto.java      |   6 +
 .../table/columns/concepts/UnitDto.java       |   3 +-
 .../table/columns/concepts/UnitSaveDto.java   |   6 +
 .../at/tuwien/api/semantics/EntityDto.java    |   3 +-
 .../api/semantics/TableColumnEntityDto.java   |   2 +-
 .../table/columns/TableColumnConcept.java     |   3 +
 .../table/columns/TableColumnUnit.java        |   3 +
 dbrepo-metadata-db/setup-schema.sql           |  41 +++---
 .../tuwien/endpoints/SemanticsEndpoint.java   |  41 ++----
 .../at/tuwien/endpoints/TableEndpoint.java    |   2 +-
 .../java/at/tuwien/mapper/OntologyMapper.java |  83 +++++++----
 .../tuwien/service/impl/QueryServiceImpl.java |  21 ++-
 .../service/impl/SemanticServiceImpl.java     |   6 +-
 .../tuwien/service/impl/TableServiceImpl.java |   2 +-
 .../tuwien/endpoints/TableColumnEndpoint.java |   4 +-
 .../endpoint/TableColumnEndpointUnitTest.java |  18 ++-
 .../SemanticEntityNotFoundException.java      |  21 +++
 .../gateway/SemanticServiceGateway.java       |   8 ++
 .../impl/SemanticServiceGatewayImpl.java      |  49 +++++++
 .../java/at/tuwien/mapper/TableMapper.java    |   9 +-
 .../repository/jpa/OntologyRepository.java    |  10 ++
 .../at/tuwien/service/SemanticService.java    |  32 +++++
 .../java/at/tuwien/service/TableService.java  |   8 +-
 .../service/impl/SemanticServiceImpl.java     | 107 ++++++++++++++
 .../tuwien/service/impl/TableServiceImpl.java |  77 +++-------
 dbrepo-ui/.env.example                        |   1 +
 dbrepo-ui/Dockerfile                          |   1 +
 dbrepo-ui/api/semantic.service.js             |  34 +++++
 .../components/dialogs/EditSemantics.vue      |  96 -------------
 dbrepo-ui/components/dialogs/Semantics.vue    | 135 +++++++++++-------
 .../components/dialogs/ViewSemanticEntity.vue |  72 ++++++++++
 dbrepo-ui/config.js                           |   1 +
 dbrepo-ui/layouts/default.vue                 |   5 +-
 dbrepo-ui/nuxt.config.js                      |   5 +-
 dbrepo-ui/pages/semantic/index.vue            |  35 +++--
 .../semantic/ontology/_ontology_id/index.vue  |  17 +--
 dbrepo-ui/pages/semantic/ontology/index.vue   |   5 +
 39 files changed, 673 insertions(+), 372 deletions(-)
 create mode 100644 dbrepo-table-service/services/src/main/java/at/tuwien/exception/SemanticEntityNotFoundException.java
 create mode 100644 dbrepo-table-service/services/src/main/java/at/tuwien/gateway/SemanticServiceGateway.java
 create mode 100644 dbrepo-table-service/services/src/main/java/at/tuwien/gateway/impl/SemanticServiceGatewayImpl.java
 create mode 100644 dbrepo-table-service/services/src/main/java/at/tuwien/repository/jpa/OntologyRepository.java
 create mode 100644 dbrepo-table-service/services/src/main/java/at/tuwien/service/SemanticService.java
 create mode 100644 dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java
 delete mode 100644 dbrepo-ui/components/dialogs/EditSemantics.vue
 create mode 100644 dbrepo-ui/components/dialogs/ViewSemanticEntity.vue

diff --git a/dbrepo-authentication-service/dbrepo-realm.json b/dbrepo-authentication-service/dbrepo-realm.json
index d6f63ede24..359cfccf6c 100644
--- a/dbrepo-authentication-service/dbrepo-realm.json
+++ b/dbrepo-authentication-service/dbrepo-realm.json
@@ -66,6 +66,14 @@
       "clientRole" : false,
       "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
       "attributes" : { }
+    }, {
+      "id" : "143ba359-5fa2-451e-8296-43ecf20bb251",
+      "name" : "update-semantic-concept",
+      "description" : "${update-semantic-concept}",
+      "composite" : false,
+      "clientRole" : false,
+      "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
+      "attributes" : { }
     }, {
       "id" : "5136d7a3-e3f0-4585-bacd-15cb8a56095c",
       "name" : "escalated-container-handling",
@@ -220,6 +228,14 @@
       "clientRole" : false,
       "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
       "attributes" : { }
+    }, {
+      "id" : "b60a5694-4099-4f7d-a7e9-4c433e0eb9c9",
+      "name" : "update-semantic-unit",
+      "description" : "${update-semantic-unit}",
+      "composite" : false,
+      "clientRole" : false,
+      "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
+      "attributes" : { }
     }, {
       "id" : "e9854bbb-4580-4757-b1ae-305934173249",
       "name" : "create-database-access",
@@ -258,7 +274,7 @@
       "description" : "${default-data-steward-roles}",
       "composite" : true,
       "composites" : {
-        "realm" : [ "escalated-identifier-handling", "escalated-semantics-handling", "default-user-handling" ]
+        "realm" : [ "escalated-identifier-handling", "default-semantics-handling", "escalated-semantics-handling", "default-user-handling" ]
       },
       "clientRole" : false,
       "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
@@ -293,7 +309,7 @@
       "description" : "${escalated-semantics-handling}",
       "composite" : true,
       "composites" : {
-        "realm" : [ "create-ontology", "update-ontology", "list-ontologies", "delete-ontology", "modify-foreign-table-column-semantics" ]
+        "realm" : [ "update-semantic-unit", "create-ontology", "update-ontology", "list-ontologies", "delete-ontology", "modify-foreign-table-column-semantics", "update-semantic-concept" ]
       },
       "clientRole" : false,
       "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0",
@@ -1054,7 +1070,7 @@
   "otpPolicyLookAheadWindow" : 1,
   "otpPolicyPeriod" : 30,
   "otpPolicyCodeReusable" : false,
-  "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName", "totpAppFreeOTPName" ],
+  "otpSupportedApplications" : [ "totpAppGoogleName", "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName" ],
   "webAuthnPolicyRpEntityName" : "keycloak",
   "webAuthnPolicySignatureAlgorithms" : [ "ES256" ],
   "webAuthnPolicyRpId" : "",
@@ -2077,7 +2093,7 @@
       "subType" : "authenticated",
       "subComponents" : { },
       "config" : {
-        "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-address-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper" ]
+        "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "saml-user-property-mapper" ]
       }
     }, {
       "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979",
@@ -2086,7 +2102,7 @@
       "subType" : "anonymous",
       "subComponents" : { },
       "config" : {
-        "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper" ]
+        "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "saml-role-list-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper" ]
       }
     } ],
     "org.keycloak.keys.KeyProvider" : [ {
@@ -2138,7 +2154,7 @@
   "internationalizationEnabled" : false,
   "supportedLocales" : [ ],
   "authenticationFlows" : [ {
-    "id" : "7dddcfca-7bcd-477f-8329-50a43455fd87",
+    "id" : "b21432ee-a5a0-44f1-89a5-0a7649bb5e99",
     "alias" : "Account verification options",
     "description" : "Method with which to verity the existing account",
     "providerId" : "basic-flow",
@@ -2160,7 +2176,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "8dffaa2e-8dbe-4276-8cd9-7a81eaece233",
+    "id" : "fe07cb6c-0ffe-4353-9184-6a7bd51940ea",
     "alias" : "Authentication Options",
     "description" : "Authentication options.",
     "providerId" : "basic-flow",
@@ -2189,7 +2205,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "7e3d1f77-19a9-421c-b58c-d38f4d03fe63",
+    "id" : "ebefc325-3ee1-45ea-8837-0503a13784b1",
     "alias" : "Browser - Conditional OTP",
     "description" : "Flow to determine if the OTP is required for the authentication",
     "providerId" : "basic-flow",
@@ -2211,7 +2227,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "e60fa1bd-3905-4c5d-a754-396dbf02d3db",
+    "id" : "006c167c-32bf-41fe-80a2-0e01f9f158d5",
     "alias" : "Direct Grant - Conditional OTP",
     "description" : "Flow to determine if the OTP is required for the authentication",
     "providerId" : "basic-flow",
@@ -2233,7 +2249,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "6570bc4c-7804-4a5c-87da-4ae57e9e26dd",
+    "id" : "b00fef58-fcdb-4e63-8efc-7f45b2d916fa",
     "alias" : "First broker login - Conditional OTP",
     "description" : "Flow to determine if the OTP is required for the authentication",
     "providerId" : "basic-flow",
@@ -2255,7 +2271,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "6c011108-9cf4-4973-8bb4-afbf15dd44a2",
+    "id" : "ac9e7d88-fef2-4292-9e92-469a3b44b252",
     "alias" : "Handle Existing Account",
     "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider",
     "providerId" : "basic-flow",
@@ -2277,7 +2293,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "9acc6184-a192-44b1-aed8-9cf500456c68",
+    "id" : "b346f859-4f7c-4383-ad6e-22ee648568f7",
     "alias" : "Reset - Conditional OTP",
     "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
     "providerId" : "basic-flow",
@@ -2299,7 +2315,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "78dac59d-dca1-401e-973b-083694df75df",
+    "id" : "44618916-6ad5-4d89-98b5-02ca56c4e94e",
     "alias" : "User creation or linking",
     "description" : "Flow for the existing/non-existing user alternatives",
     "providerId" : "basic-flow",
@@ -2322,7 +2338,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "fe5875af-12d3-4027-b6dc-62aa2e4f4ec9",
+    "id" : "cd705e48-31b2-4b44-93a1-ac64366d99e7",
     "alias" : "Verify Existing Account by Re-authentication",
     "description" : "Reauthentication of existing account",
     "providerId" : "basic-flow",
@@ -2344,7 +2360,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "9a61c442-6393-4d10-bbb4-bb19a03f4560",
+    "id" : "1f8c5799-9c25-490d-bc81-8a75fcf88ce3",
     "alias" : "browser",
     "description" : "browser based authentication",
     "providerId" : "basic-flow",
@@ -2380,7 +2396,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "76db50d0-143c-499a-8256-e4229c5f81d6",
+    "id" : "6ac53bd6-6665-42f6-8c9b-4bc0d6e39b83",
     "alias" : "clients",
     "description" : "Base authentication for clients",
     "providerId" : "client-flow",
@@ -2416,7 +2432,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "4529a603-cc30-46e4-b821-f8fe88352048",
+    "id" : "76b187f5-0c6c-4cb8-ac56-b54712ca290d",
     "alias" : "direct grant",
     "description" : "OpenID Connect Resource Owner Grant",
     "providerId" : "basic-flow",
@@ -2445,7 +2461,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "dc9cda33-523a-4b76-ba21-7055c0f1aaea",
+    "id" : "42e03bec-71a8-4517-8fc0-1601397f8e17",
     "alias" : "docker auth",
     "description" : "Used by Docker clients to authenticate against the IDP",
     "providerId" : "basic-flow",
@@ -2460,7 +2476,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "27eb9e18-c28b-43c1-8148-9b46b3ed103e",
+    "id" : "9ec63a08-04e2-4736-a87a-46057598466e",
     "alias" : "first broker login",
     "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
     "providerId" : "basic-flow",
@@ -2483,7 +2499,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "9f83ad1f-cb1a-422b-802e-b4d7f2800fc5",
+    "id" : "715aef9a-fbc1-4a8a-969b-0df5e81ea536",
     "alias" : "forms",
     "description" : "Username, password, otp and other auth forms.",
     "providerId" : "basic-flow",
@@ -2505,7 +2521,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "fadcf245-9bab-4b43-ae93-5a2fe685970f",
+    "id" : "ad7f41c0-abb5-492d-9de1-f11a9c0e3090",
     "alias" : "http challenge",
     "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes",
     "providerId" : "basic-flow",
@@ -2527,7 +2543,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "3509bd76-4ae6-4f70-816c-415668d7000d",
+    "id" : "96d9faa7-7c92-4b34-9433-6b8198cab651",
     "alias" : "registration",
     "description" : "registration flow",
     "providerId" : "basic-flow",
@@ -2543,7 +2559,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "e18b1d97-8ce5-43f1-bb4e-9540c303f6da",
+    "id" : "6cfc2735-d8a8-41da-ab4f-5c8f2c6d3ff1",
     "alias" : "registration form",
     "description" : "registration form",
     "providerId" : "form-flow",
@@ -2579,7 +2595,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "bf867f8e-95a7-4dc3-8569-07b53874b41c",
+    "id" : "2bc9fcb2-2b4a-480d-a4f8-d41885ef4bf9",
     "alias" : "reset credentials",
     "description" : "Reset credentials for a user if they forgot their password or something",
     "providerId" : "basic-flow",
@@ -2615,7 +2631,7 @@
       "userSetupAllowed" : false
     } ]
   }, {
-    "id" : "74b260ac-7de8-41f0-a0ab-3b26bc6b4d75",
+    "id" : "4e4b9e79-7aff-4de8-869f-04dbd1a5bd1e",
     "alias" : "saml ecp",
     "description" : "SAML ECP Profile Authentication Flow",
     "providerId" : "basic-flow",
@@ -2631,13 +2647,13 @@
     } ]
   } ],
   "authenticatorConfig" : [ {
-    "id" : "e22bf5a6-9a63-425c-91f2-684a10f7f436",
+    "id" : "b65b03a6-9a9e-49f0-9887-7942996c8345",
     "alias" : "create unique user config",
     "config" : {
       "require.password.update.after.registration" : "false"
     }
   }, {
-    "id" : "a06a0326-c8e5-4c05-bb63-545b9f4932f2",
+    "id" : "33d1c568-b4cd-43e1-870a-f14778834744",
     "alias" : "review profile config",
     "config" : {
       "update.profile.on.first.login" : "missing"
diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/ConceptDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/ConceptDto.java
index e031cf5944..590cd825e7 100644
--- a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/ConceptDto.java
+++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/ConceptDto.java
@@ -25,9 +25,10 @@ public class ConceptDto {
     @NotBlank
     private String uri;
 
-    @NotBlank
     private String name;
 
+    private String description;
+
     @NotNull
     @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
     private Instant created;
diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/ConceptSaveDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/ConceptSaveDto.java
index e38927af8f..159e07823c 100644
--- a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/ConceptSaveDto.java
+++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/ConceptSaveDto.java
@@ -16,4 +16,10 @@ public class ConceptSaveDto {
     @NotBlank
     private String uri;
 
+    @NotBlank
+    private String name;
+
+    @NotBlank
+    private String description;
+
 }
diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/UnitDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/UnitDto.java
index 65b6c2a0bd..9e0fd69360 100644
--- a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/UnitDto.java
+++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/UnitDto.java
@@ -23,9 +23,10 @@ public class UnitDto {
     @NotBlank
     private String uri;
 
-    @NotBlank
     private String name;
 
+    private String description;
+
     @NotNull
     @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
     private Instant created;
diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/UnitSaveDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/UnitSaveDto.java
index 41cfd8caaf..326efc48b5 100644
--- a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/UnitSaveDto.java
+++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/columns/concepts/UnitSaveDto.java
@@ -16,4 +16,10 @@ public class UnitSaveDto {
     @NotBlank
     private String uri;
 
+    @NotBlank
+    private String name;
+
+    @NotBlank
+    private String description;
+
 }
diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/semantics/EntityDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/semantics/EntityDto.java
index 728b9979da..5c1d6cc13a 100644
--- a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/semantics/EntityDto.java
+++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/semantics/EntityDto.java
@@ -22,8 +22,7 @@ public class EntityDto {
     @Schema(example = "Apache Jena")
     private String label;
 
-    @NotBlank
     @Schema(example = "open source semantic web framework for Java")
-    private String comment;
+    private String description;
 
 }
diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/semantics/TableColumnEntityDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/semantics/TableColumnEntityDto.java
index 6e314aba1c..b71a0ae0be 100644
--- a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/semantics/TableColumnEntityDto.java
+++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/semantics/TableColumnEntityDto.java
@@ -35,6 +35,6 @@ public class TableColumnEntityDto {
     private String label;
 
     @Schema(example = "open source semantic web framework for Java")
-    private String comment;
+    private String description;
 
 }
diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnConcept.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnConcept.java
index fa2ae5a066..8304898a1d 100644
--- a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnConcept.java
+++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnConcept.java
@@ -33,6 +33,9 @@ public class TableColumnConcept {
     @Column(name = "name")
     private String name;
 
+    @Column(name = "description")
+    private String description;
+
     @Column(nullable = false, updatable = false, columnDefinition = "TIMESTAMP")
     @CreatedDate
     private Instant created;
diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnUnit.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnUnit.java
index aecbcd9dd3..922f48217f 100644
--- a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnUnit.java
+++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/columns/TableColumnUnit.java
@@ -33,6 +33,9 @@ public class TableColumnUnit {
     @Column(name = "name")
     private String name;
 
+    @Column(name = "description")
+    private String description;
+
     @Column(nullable = false, updatable = false, columnDefinition = "TIMESTAMP")
     @CreatedDate
     private Instant created;
diff --git a/dbrepo-metadata-db/setup-schema.sql b/dbrepo-metadata-db/setup-schema.sql
index 9642c98f61..92afa5c8ca 100644
--- a/dbrepo-metadata-db/setup-schema.sql
+++ b/dbrepo-metadata-db/setup-schema.sql
@@ -275,19 +275,21 @@ CREATE TABLE IF NOT EXISTS `fda`.`mdb_constraints_checks`
 
 CREATE TABLE IF NOT EXISTS `fda`.`mdb_concepts`
 (
-    uri        text         not null,
-    name       VARCHAR(255) null,
-    created    timestamp    NOT NULL DEFAULT NOW(),
-    created_by character varying(255),
+    uri         text         not null,
+    name        VARCHAR(255) null,
+    description TEXT         null,
+    created     timestamp    NOT NULL DEFAULT NOW(),
+    created_by  character varying(255),
     PRIMARY KEY (uri(200))
 ) WITH SYSTEM VERSIONING;
 
 CREATE TABLE IF NOT EXISTS `fda`.`mdb_units`
 (
-    uri        text         not null,
-    name       VARCHAR(255) null,
-    created    timestamp    NOT NULL DEFAULT NOW(),
-    created_by character varying(255),
+    uri         text         not null,
+    name        VARCHAR(255) null,
+    description TEXT         null,
+    created     timestamp    NOT NULL DEFAULT NOW(),
+    created_by  character varying(255),
     PRIMARY KEY (uri(200))
 ) WITH SYSTEM VERSIONING;
 
@@ -499,13 +501,18 @@ VALUES (1, '%Y-%c-%d %H:%i:%S.%f', 'yyyy-MM-dd HH:mm:ss.SSSSSS', '2022-01-30 13:
        (1, '%Y-%c-%d %H:%i:%S', 'yyyy-MM-dd HH:mm:ss', '2022-01-30 13:44:25', true),
        (1, '%Y-%c-%d', 'yyyy-MM-dd', '2022-01-30', false);
 
-INSERT INTO `fda`.`mdb_ontologies` (uri, prefix, sparql_endpoint)
-VALUES ('http://www.ontology-of-units-of-measure.org/resource/om-2/', 'om2', null),
-       ('http://www.wikidata.org/', 'wd', 'https://query.wikidata.org/sparql'),
-       ('http://purl.org/ontology/mo/', 'mo', null),
-       ('http://purl.org/dc/elements/1.1/', 'dc', null),
-       ('http://www.w3.org/2001/XMLSchema#', 'xsd', null),
-       ('http://purl.org/NET/c4dm/timeline.owl#', 'tl', null),
-       ('http://xmlns.com/foaf/0.1/', 'foaf', null),
-       ('http://dbpedia.org', 'db', 'http://dbpedia.org/sparql');
+INSERT INTO `fda`.`mdb_ontologies` (prefix, uri, sparql_endpoint)
+VALUES ('om2', 'http://www.ontology-of-units-of-measure.org/resource/om-2/', null),
+       ('wd', 'http://www.wikidata.org/', 'https://query.wikidata.org/sparql'),
+       ('mo', 'http://purl.org/ontology/mo/', null),
+       ('dc', 'http://purl.org/dc/elements/1.1/', null),
+       ('xsd', 'http://www.w3.org/2001/XMLSchema#', null),
+       ('tl', 'http://purl.org/NET/c4dm/timeline.owl#', null),
+       ('foaf', 'http://xmlns.com/foaf/0.1/', null),
+       ('schema', 'http://schema.org/', null),
+       ('rdf', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', null),
+       ('rdfs', 'http://www.w3.org/2000/01/rdf-schema#', null),
+       ('owl', 'http://www.w3.org/2002/07/owl#', null),
+       ('prov', 'http://www.w3.org/ns/prov#', null),
+       ('db', 'http://dbpedia.org', 'http://dbpedia.org/sparql');
 COMMIT;
diff --git a/dbrepo-semantics-service/rest-service/src/main/java/at/tuwien/endpoints/SemanticsEndpoint.java b/dbrepo-semantics-service/rest-service/src/main/java/at/tuwien/endpoints/SemanticsEndpoint.java
index 6567d5ee38..5bcf6abdf0 100644
--- a/dbrepo-semantics-service/rest-service/src/main/java/at/tuwien/endpoints/SemanticsEndpoint.java
+++ b/dbrepo-semantics-service/rest-service/src/main/java/at/tuwien/endpoints/SemanticsEndpoint.java
@@ -1,9 +1,6 @@
 package at.tuwien.endpoints;
 
-import at.tuwien.api.database.table.columns.concepts.ConceptDto;
-import at.tuwien.api.database.table.columns.concepts.ConceptSaveDto;
-import at.tuwien.api.database.table.columns.concepts.UnitDto;
-import at.tuwien.api.database.table.columns.concepts.UnitSaveDto;
+import at.tuwien.api.database.table.columns.concepts.*;
 import at.tuwien.mapper.OntologyMapper;
 import at.tuwien.mapper.SemanticMapper;
 import at.tuwien.service.SemanticService;
@@ -67,39 +64,19 @@ public class SemanticsEndpoint {
     @PostMapping("/concept")
     @Transactional
     @PreAuthorize("hasAuthority('create-semantic-concept')")
-    @Timed(value = "semantics.concept.save", description = "Time needed to create or update a semantic concept")
+    @Timed(value = "semantics.concept.save", description = "Time needed to save a semantic concept")
     @Operation(summary = "Create or update a semantic concept", security = @SecurityRequirement(name = "bearerAuth"))
     @ApiResponses(value = {
             @ApiResponse(responseCode = "202",
-                    description = "Created or updated a semantic concept",
+                    description = "Saved a semantic concept",
                     content = {@Content(
                             mediaType = "application/json",
                             schema = @Schema(implementation = ConceptDto.class))}),
     })
     public ResponseEntity<ConceptDto> saveConcept(@NotNull @Valid @RequestBody ConceptSaveDto data) {
-        log.debug("endpoint save or update concept, data={}", data);
+        log.debug("endpoint save concept, data={}", data);
         final ConceptDto dto = ontologyMapper.tableColumnConceptToConceptDto(semanticService.saveConcept(data));
-        log.trace("save or update concept resulted in dto {}", dto);
-        return ResponseEntity.ok()
-                .body(dto);
-    }
-
-    @PutMapping("/concept")
-    @Transactional
-    @PreAuthorize("hasAuthority('create-semantic-concept')")
-    @Timed(value = "semantics.concept.save", description = "Time needed to create or update a semantic concept")
-    @Operation(summary = "Create or update a semantic concept", security = @SecurityRequirement(name = "bearerAuth"))
-    @ApiResponses(value = {
-            @ApiResponse(responseCode = "202",
-                    description = "Created or updated a semantic concept",
-                    content = {@Content(
-                            mediaType = "application/json",
-                            schema = @Schema(implementation = ConceptDto.class))}),
-    })
-    public ResponseEntity<ConceptDto> saveConcept(@NotNull @Valid @RequestBody ConceptSaveDto data) {
-        log.debug("endpoint save or update concept, data={}", data);
-        final ConceptDto dto = ontologyMapper.tableColumnConceptToConceptDto(semanticService.saveConcept(data));
-        log.trace("save or update concept resulted in dto {}", dto);
+        log.trace("save concept resulted in dto {}", dto);
         return ResponseEntity.ok()
                 .body(dto);
     }
@@ -129,11 +106,11 @@ public class SemanticsEndpoint {
     @PostMapping("/unit")
     @Transactional
     @PreAuthorize("hasAuthority('create-semantic-unit')")
-    @Timed(value = "semantics.unit.save", description = "Time needed to create or update a semantic unit")
-    @Operation(summary = "Create or update a semantic unit", security = @SecurityRequirement(name = "bearerAuth"))
+    @Timed(value = "semantics.unit.save", description = "Time needed to save a semantic unit")
+    @Operation(summary = "Save a semantic unit", security = @SecurityRequirement(name = "bearerAuth"))
     @ApiResponses(value = {
             @ApiResponse(responseCode = "202",
-                    description = "Created or updated a semantic unit",
+                    description = "Saved a semantic unit",
                     content = {@Content(
                             mediaType = "application/json",
                             schema = @Schema(implementation = UnitDto.class))}),
@@ -141,7 +118,7 @@ public class SemanticsEndpoint {
     public ResponseEntity<UnitDto> saveConcept(@NotNull @Valid @RequestBody UnitSaveDto data) {
         log.debug("endpoint save or update unit, data={}", data);
         final UnitDto dto = ontologyMapper.tableColumnUnitToUnitDto(semanticService.saveUnit(data));
-        log.trace("save or update unit resulted in dto {}", dto);
+        log.trace("save unit resulted in dto {}", dto);
         return ResponseEntity.accepted()
                 .body(dto);
     }
diff --git a/dbrepo-semantics-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java b/dbrepo-semantics-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java
index f9581b2993..cc3689fded 100644
--- a/dbrepo-semantics-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java
+++ b/dbrepo-semantics-service/rest-service/src/main/java/at/tuwien/endpoints/TableEndpoint.java
@@ -95,7 +95,7 @@ public class TableEndpoint {
                                                                          @NotNull @PathVariable("tableId") Long tableId,
                                                                          @NotNull @PathVariable("columnId") Long columnId)
             throws QueryMalformedException, TableColumnNotFoundException {
-        log.debug("endpoint analyse table semantics, databaseId={}, tableId={}, columnId={}", databaseId, tableId, columnId);
+        log.debug("endpoint analyse table column semantics, databaseId={}, tableId={}, columnId={}", databaseId, tableId, columnId);
         final List<TableColumnEntityDto> dtos = tableService.suggestTableColumnSemantics(databaseId, tableId, columnId);
         log.trace("analyse table semantics resulted in dtos {}", dtos);
         return ResponseEntity.ok()
diff --git a/dbrepo-semantics-service/services/src/main/java/at/tuwien/mapper/OntologyMapper.java b/dbrepo-semantics-service/services/src/main/java/at/tuwien/mapper/OntologyMapper.java
index 7268311727..aeec69284c 100644
--- a/dbrepo-semantics-service/services/src/main/java/at/tuwien/mapper/OntologyMapper.java
+++ b/dbrepo-semantics-service/services/src/main/java/at/tuwien/mapper/OntologyMapper.java
@@ -1,9 +1,6 @@
 package at.tuwien.mapper;
 
-import at.tuwien.api.database.table.columns.concepts.ConceptDto;
-import at.tuwien.api.database.table.columns.concepts.ConceptSaveDto;
-import at.tuwien.api.database.table.columns.concepts.UnitDto;
-import at.tuwien.api.database.table.columns.concepts.UnitSaveDto;
+import at.tuwien.api.database.table.columns.concepts.*;
 import at.tuwien.api.semantics.OntologyBriefDto;
 import at.tuwien.api.semantics.OntologyCreateDto;
 import at.tuwien.api.semantics.OntologyDto;
@@ -14,6 +11,8 @@ import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.Mappings;
 
+import java.util.List;
+
 
 @Mapper(componentModel = "spring")
 public interface OntologyMapper {
@@ -42,57 +41,79 @@ public interface OntologyMapper {
 
     TableColumnConcept conceptSaveDtoToTableColumnConcept(ConceptSaveDto data);
 
-    default String ontologyToFindByLabelQuery(Ontology ontology, String label, Integer limit) {
+    default String defaultNamespaces(List<Ontology> data) {
+        return String.join("\n",
+                data.stream()
+                        .map(o -> "PREFIX " + o.getPrefix() + ": <" + o.getUri() + ">")
+                        .toList());
+    }
+
+    default String ontologyToFindByLabelQuery(List<Ontology> ontologies, Ontology ontology, String label, Integer limit) {
         if (ontology.getSparqlEndpoint() != null) {
             /* prefer SPARQL endpoint over rdf */
             return String.join("\n",
-                    "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>",
+                    defaultNamespaces(ontologies),
                     "SELECT * {",
                     "  SERVICE <" + ontology.getSparqlEndpoint() + "> {",
-                    "    SELECT ?o ?label ?comment {",
-                    "      ?o rdfs:label \"" + label.replace("\"", "") + "\"@en .",
-                    "      ?o rdfs:label ?label .",
-                    "      OPTIONAL {?o rdfs:comment ?comment} .",
-                    "      FILTER (langMatches(lang(?label), \"EN\" ) )",
-                    "      FILTER (langMatches(lang(?comment), \"EN\" ) )",
-                    "     } LIMIT " + limit,
+                    "    SELECT ?o ?label ?description {",
+                    "      OPTIONAL {",
+                    "        ?o rdfs:label ?label",
+                    "        FILTER (LANG(?label) = 'en')",
+                    "      }",
+                    "      OPTIONAL {",
+                    "        ?o schema:description ?description",
+                    "        FILTER (LANG(?description) = 'en')",
+                    "      }",
+                    "    } LIMIT " + limit,
                     "  }",
                     "}");
         }
         return String.join("\n",
-                "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>",
+                defaultNamespaces(ontologies),
                 "SELECT ?o ?label ?comment {",
                 "  ?o rdfs:label \"" + label.replace("\"", "") + "\"@en .",
-                "  ?o rdfs:label ?label .",
-                "  OPTIONAL {?o rdfs:comment ?comment} .",
-                "  FILTER (langMatches(lang(?label), \"EN\" ) )",
-                "  FILTER (langMatches(lang(?comment), \"EN\" ) )",
+                "  OPTIONAL {",
+                "    ?o rdfs:label ?label",
+                "    FILTER (LANG(?label) = 'en')",
+                "  }",
+                "  OPTIONAL {",
+                "    ?o schema:description ?description",
+                "    FILTER (LANG(?description) = 'en')",
+                "  }",
                 "} LIMIT " + limit);
     }
 
-    default String ontologyToFindByUriQuery(Ontology ontology, String uri) {
+    default String ontologyToFindByUriQuery(List<Ontology> ontologies, Ontology ontology, String uri) {
         if (ontology.getSparqlEndpoint() != null) {
             /* prefer SPARQL endpoint over rdf */
             return String.join("\n",
-                    "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>",
+                    defaultNamespaces(ontologies),
                     "SELECT * {",
                     "  SERVICE <" + ontology.getSparqlEndpoint() + "> {",
-                    "    SELECT ?label ?comment {",
-                    "      <" + uri + "> rdfs:label ?label .",
-                    "      OPTIONAL {<" + uri + "> rdfs:comment ?comment} .",
-                    "      FILTER (langMatches(lang(?label), \"EN\" ) )",
-                    "      FILTER (langMatches(lang(?comment), \"EN\" ) )",
+                    "    SELECT ?label ?description {",
+                    "      OPTIONAL {",
+                    "        <" + uri + "> rdfs:label ?label",
+                    "        FILTER (LANG(?label) = 'en')",
+                    "      }",
+                    "      OPTIONAL {",
+                    "        <" + uri + "> schema:description ?description",
+                    "        FILTER (LANG(?description) = 'en')",
+                    "      }",
                     "     } LIMIT 1",
                     "  }",
                     "}");
         }
         return String.join("\n",
-                "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>",
-                "SELECT ?label ?comment {",
-                "  <" + uri + "> rdfs:label ?label .",
-                "  OPTIONAL {<" + uri + "> rdfs:comment ?comment} .",
-                "  FILTER (langMatches(lang(?label), \"EN\" ) )",
-                "  FILTER (langMatches(lang(?comment), \"EN\" ) )",
+                defaultNamespaces(ontologies),
+                "SELECT ?label ?description {",
+                "  OPTIONAL {",
+                "    <" + uri + "> rdfs:label ?label",
+                "    FILTER (LANG(?label) = 'en')",
+                "  }",
+                "  OPTIONAL {",
+                "    <" + uri + "> schema:description ?description",
+                "    FILTER (LANG(?description) = 'en')",
+                "  }",
                 "} LIMIT 1");
     }
 
diff --git a/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java b/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java
index 68e9d93e2a..5b51cfdf8c 100644
--- a/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java
+++ b/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java
@@ -30,10 +30,13 @@ public class QueryServiceImpl implements QueryService {
 
     private final Dataset dataset;
     private final OntologyMapper ontologyMapper;
+    private final OntologyRepository ontologyRepository;
 
     @Autowired
-    public QueryServiceImpl(OntologyRepository ontologyRepository, OntologyMapper ontologyMapper) {
+    public QueryServiceImpl(OntologyRepository ontologyRepository, OntologyMapper ontologyMapper,
+                            OntologyRepository ontologyRepository1) {
         this.ontologyMapper = ontologyMapper;
+        this.ontologyRepository = ontologyRepository1;
         final Context context = ARQ.getContext().copy();
         this.dataset = DatasetFactory.create();
         /* registry */
@@ -62,17 +65,19 @@ public class QueryServiceImpl implements QueryService {
 
     @Override
     public List<EntityDto> findByLabel(Ontology ontology, String label, Integer limit) throws QueryMalformedException {
-        final String statement = ontologyMapper.ontologyToFindByLabelQuery(ontology, label, limit);
+        final List<Ontology> ontologies = ontologyRepository.findAll();
+        final String statement = ontologyMapper.ontologyToFindByLabelQuery(ontologies, ontology, label, limit);
+        log.trace("execute sparql query:\n{}", statement);
         final List<EntityDto> results = new LinkedList<>();
         try (QueryExecution execution = QueryExecutionFactory.create(statement, this.dataset.getDefaultModel())) {
             final Iterator<QuerySolution> resultSet = execution.execSelect();
             while (resultSet.hasNext()) {
                 final QuerySolution solution = resultSet.next();
-                final RDFNode comment = solution.get("comment");
+                final RDFNode description = solution.get("description");
                 final EntityDto entity = EntityDto.builder()
                         .uri(solution.get("o").toString())
                         .label(label)
-                        .comment(comment != null ? comment.asLiteral().getLexicalForm() : null)
+                        .description(description != null ? description.asLiteral().getLexicalForm() : null)
                         .build();
                 results.add(entity);
             }
@@ -85,18 +90,20 @@ public class QueryServiceImpl implements QueryService {
 
     @Override
     public List<EntityDto> findByUri(Ontology ontology, String uri) throws QueryMalformedException {
-        final String statement = ontologyMapper.ontologyToFindByUriQuery(ontology, uri);
+        final List<Ontology> ontologies = ontologyRepository.findAll();
+        final String statement = ontologyMapper.ontologyToFindByUriQuery(ontologies, ontology, uri);
+        log.trace("execute sparql query:\n{}", statement);
         try (QueryExecution execution = QueryExecutionFactory.create(statement, this.dataset.getDefaultModel())) {
             final Iterator<QuerySolution> resultSet = execution.execSelect();
             final List<EntityDto> results = new LinkedList<>();
             while (resultSet.hasNext()) {
                 final QuerySolution solution = resultSet.next();
                 final RDFNode label = solution.get("label");
-                final RDFNode comment = solution.get("comment");
+                final RDFNode description = solution.get("description");
                 final EntityDto entity = EntityDto.builder()
                         .uri(uri)
                         .label(label != null ? label.asLiteral().getLexicalForm() : null)
-                        .comment(comment != null ? comment.asLiteral().getLexicalForm() : null)
+                        .description(description != null ? description.asLiteral().getLexicalForm() : null)
                         .build();
                 results.add(entity);
             }
diff --git a/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java b/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java
index 8c8cab0469..94a55ee511 100644
--- a/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java
+++ b/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java
@@ -34,15 +34,13 @@ public class SemanticServiceImpl implements SemanticService {
     @Override
     @Transactional(readOnly = true)
     public List<TableColumnConcept> findAllConcepts() {
-        final List<TableColumnConcept> concepts = tableColumnConceptRepository.findAll();
-        return concepts;
+        return tableColumnConceptRepository.findAll();
     }
 
     @Override
     @Transactional(readOnly = true)
     public List<TableColumnUnit> findAllUnits() {
-        final List<TableColumnUnit> units = tableColumnUnitRepository.findAll();
-        return units;
+        return tableColumnUnitRepository.findAll();
     }
 
     @Override
diff --git a/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java b/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java
index d51df7fcf1..8f58625094 100644
--- a/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java
+++ b/dbrepo-semantics-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java
@@ -88,7 +88,7 @@ public class TableServiceImpl implements TableService {
                             .columnId(optional.get().getId())
                             .label(e.getLabel())
                             .uri(e.getUri())
-                            .comment(e.getComment())
+                            .description(e.getDescription())
                             .build())
                     .toList());
         }
diff --git a/dbrepo-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java b/dbrepo-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java
index 82d41b6d86..5d025939ba 100644
--- a/dbrepo-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java
+++ b/dbrepo-table-service/rest-service/src/main/java/at/tuwien/endpoints/TableColumnEndpoint.java
@@ -85,8 +85,8 @@ public class TableColumnEndpoint {
                                             @NotNull Principal principal,
                                             @NotNull @RequestHeader("Authorization") String authorization)
             throws TableNotFoundException, TableMalformedException, DatabaseNotFoundException,
-            ContainerNotFoundException, UnitNotFoundException, ConceptNotFoundException, NotAllowedException,
-            SemanticEntityPersistException {
+            ContainerNotFoundException, NotAllowedException, SemanticEntityPersistException,
+            SemanticEntityNotFoundException {
         log.debug("endpoint update table, containerId={}, databaseId={}, tableId={}, principal={}", containerId,
                 databaseId, tableId, principal);
         if (!User.hasRole(principal, "modify-foreign-table-column-semantics")) {
diff --git a/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableColumnEndpointUnitTest.java b/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableColumnEndpointUnitTest.java
index 38f3e9547f..2dd1a03a5a 100644
--- a/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableColumnEndpointUnitTest.java
+++ b/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableColumnEndpointUnitTest.java
@@ -101,7 +101,9 @@ public class TableColumnEndpointUnitTest extends BaseUnitTest {
 
     @Test
     @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-table-column-semantics"})
-    public void update_publicHasRoleHasOwnWriteAccess_succeeds() throws TableNotFoundException, NotAllowedException, TableMalformedException, UnitNotFoundException, DatabaseNotFoundException, ConceptNotFoundException, ContainerNotFoundException, SemanticEntityPersistException {
+    public void update_publicHasRoleHasOwnWriteAccess_succeeds() throws TableNotFoundException, NotAllowedException,
+            TableMalformedException, DatabaseNotFoundException, ContainerNotFoundException,
+            SemanticEntityPersistException, SemanticEntityNotFoundException {
 
         /* test */
         generic_update(CONTAINER_3_ID, DATABASE_3_ID, TABLE_8_ID, COLUMN_1_1_ID, DATABASE_3, TABLE_8, COLUMN_8_2_WITH_SEMANTICS, COLUMN_8_2_SEMANTICS_UPDATE_DTO, USER_1_USERNAME, USER_1_PRINCIPAL, DATABASE_3_USER_1_WRITE_OWN_ACCESS);
@@ -140,8 +142,8 @@ public class TableColumnEndpointUnitTest extends BaseUnitTest {
     @Test
     @WithMockUser(username = USER_2_USERNAME, authorities = {"modify-table-column-semantics"})
     public void update_publicHasRoleForeignHasAllWriteAccess_succeeds() throws TableNotFoundException,
-            NotAllowedException, TableMalformedException, UnitNotFoundException, DatabaseNotFoundException,
-            ConceptNotFoundException, ContainerNotFoundException, SemanticEntityPersistException {
+            NotAllowedException, TableMalformedException, DatabaseNotFoundException,
+            ContainerNotFoundException, SemanticEntityPersistException, SemanticEntityNotFoundException {
 
         /* test */
         generic_update(CONTAINER_3_ID, DATABASE_3_ID, TABLE_8_ID, COLUMN_1_1_ID, DATABASE_3, TABLE_8, COLUMN_8_2_WITH_SEMANTICS, COLUMN_8_2_SEMANTICS_UPDATE_DTO, USER_2_USERNAME, USER_2_PRINCIPAL, DATABASE_3_USER_2_WRITE_ALL_ACCESS);
@@ -183,7 +185,9 @@ public class TableColumnEndpointUnitTest extends BaseUnitTest {
 
     @Test
     @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-table-column-semantics"})
-    public void update_privateHasRoleHasOwnWriteAccess_succeeds() throws TableNotFoundException, NotAllowedException, TableMalformedException, UnitNotFoundException, DatabaseNotFoundException, ConceptNotFoundException, ContainerNotFoundException, SemanticEntityPersistException {
+    public void update_privateHasRoleHasOwnWriteAccess_succeeds() throws TableNotFoundException, NotAllowedException,
+            TableMalformedException, DatabaseNotFoundException, ContainerNotFoundException,
+            SemanticEntityPersistException, SemanticEntityNotFoundException {
 
         /* test */
         generic_update(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, COLUMN_1_1_ID, DATABASE_1, TABLE_1, COLUMN_1_4_WITH_SEMANTICS, COLUMN_1_4_SEMANTICS_UPDATE_DTO, USER_1_USERNAME, USER_1_PRINCIPAL, DATABASE_1_USER_1_WRITE_OWN_ACCESS);
@@ -222,8 +226,8 @@ public class TableColumnEndpointUnitTest extends BaseUnitTest {
     @Test
     @WithMockUser(username = USER_2_USERNAME, authorities = {"modify-table-column-semantics"})
     public void update_privateHasRoleForeignHasAllWriteAccess_succeeds() throws TableNotFoundException,
-            NotAllowedException, TableMalformedException, UnitNotFoundException, DatabaseNotFoundException,
-            ConceptNotFoundException, ContainerNotFoundException, SemanticEntityPersistException {
+            NotAllowedException, TableMalformedException, DatabaseNotFoundException,
+            ContainerNotFoundException, SemanticEntityPersistException, SemanticEntityNotFoundException {
 
         /* test */
         generic_update(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, COLUMN_1_1_ID, DATABASE_1, TABLE_1, COLUMN_1_4_WITH_SEMANTICS, COLUMN_1_4_SEMANTICS_UPDATE_DTO, USER_2_USERNAME, USER_2_PRINCIPAL, DATABASE_1_USER_2_WRITE_ALL_ACCESS);
@@ -238,7 +242,7 @@ public class TableColumnEndpointUnitTest extends BaseUnitTest {
                                                        ColumnSemanticsUpdateDto data, String username,
                                                        Principal principal, DatabaseAccess access)
             throws DatabaseNotFoundException, NotAllowedException, TableNotFoundException, TableMalformedException,
-            UnitNotFoundException, ConceptNotFoundException, ContainerNotFoundException, SemanticEntityPersistException {
+            ContainerNotFoundException, SemanticEntityPersistException, SemanticEntityNotFoundException {
 
         /* mock */
         if (database != null) {
diff --git a/dbrepo-table-service/services/src/main/java/at/tuwien/exception/SemanticEntityNotFoundException.java b/dbrepo-table-service/services/src/main/java/at/tuwien/exception/SemanticEntityNotFoundException.java
new file mode 100644
index 0000000000..70098f61da
--- /dev/null
+++ b/dbrepo-table-service/services/src/main/java/at/tuwien/exception/SemanticEntityNotFoundException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND)
+public class SemanticEntityNotFoundException extends Exception {
+
+    public SemanticEntityNotFoundException(String msg) {
+        super(msg);
+    }
+
+    public SemanticEntityNotFoundException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public SemanticEntityNotFoundException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/dbrepo-table-service/services/src/main/java/at/tuwien/gateway/SemanticServiceGateway.java b/dbrepo-table-service/services/src/main/java/at/tuwien/gateway/SemanticServiceGateway.java
new file mode 100644
index 0000000000..df54e70226
--- /dev/null
+++ b/dbrepo-table-service/services/src/main/java/at/tuwien/gateway/SemanticServiceGateway.java
@@ -0,0 +1,8 @@
+package at.tuwien.gateway;
+
+import at.tuwien.api.semantics.EntityDto;
+import at.tuwien.exception.SemanticEntityNotFoundException;
+
+public interface SemanticServiceGateway {
+    EntityDto getEntity(Long ontologyId, String uri, String authorization) throws SemanticEntityNotFoundException;
+}
diff --git a/dbrepo-table-service/services/src/main/java/at/tuwien/gateway/impl/SemanticServiceGatewayImpl.java b/dbrepo-table-service/services/src/main/java/at/tuwien/gateway/impl/SemanticServiceGatewayImpl.java
new file mode 100644
index 0000000000..7b4f839984
--- /dev/null
+++ b/dbrepo-table-service/services/src/main/java/at/tuwien/gateway/impl/SemanticServiceGatewayImpl.java
@@ -0,0 +1,49 @@
+package at.tuwien.gateway.impl;
+
+import at.tuwien.api.semantics.EntityDto;
+import at.tuwien.exception.SemanticEntityNotFoundException;
+import at.tuwien.gateway.SemanticServiceGateway;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+@Slf4j
+@Service
+public class SemanticServiceGatewayImpl implements SemanticServiceGateway {
+
+    private final RestTemplate restTemplate;
+
+    @Autowired
+    public SemanticServiceGatewayImpl(RestTemplate restTemplate) {
+        this.restTemplate = restTemplate;
+    }
+
+    @Override
+    public EntityDto getEntity(Long ontologyId, String uri, String authorization) throws SemanticEntityNotFoundException {
+        final String url = "/api/semantic/ontology/" + ontologyId + "/entity?uri=" + uri;
+        final HttpHeaders headers = new HttpHeaders();
+        headers.set("Authorization", authorization);
+        final ResponseEntity<EntityDto[]> response = restTemplate.exchange(url, HttpMethod.GET,
+                new HttpEntity<>(null, headers), EntityDto[].class);
+        if (!response.getStatusCode().equals(HttpStatus.OK) || response.getBody() == null) {
+            log.error("Failed to get semantic entity for uri {}", uri);
+            throw new SemanticEntityNotFoundException("Failed to get concept for uri " + uri);
+        }
+        if (response.getBody().length != 1) {
+            log.error("None or multiple entities found for uri {}", uri);
+            throw new SemanticEntityNotFoundException("None or multiple entities found for uri " + uri);
+        }
+        return response.getBody()[0];
+    }
+
+    private String urlEncode(String value) {
+        return URLEncoder.encode(value, StandardCharsets.UTF_8);
+    }
+
+}
diff --git a/dbrepo-table-service/services/src/main/java/at/tuwien/mapper/TableMapper.java b/dbrepo-table-service/services/src/main/java/at/tuwien/mapper/TableMapper.java
index 5dd576ce0f..688e44c832 100644
--- a/dbrepo-table-service/services/src/main/java/at/tuwien/mapper/TableMapper.java
+++ b/dbrepo-table-service/services/src/main/java/at/tuwien/mapper/TableMapper.java
@@ -14,6 +14,7 @@ import at.tuwien.api.database.table.constraints.ConstraintsCreateDto;
 import at.tuwien.api.database.table.constraints.foreignKey.ForeignKeyCreateDto;
 import at.tuwien.api.database.table.constraints.foreignKey.ForeignKeyDto;
 import at.tuwien.api.database.table.constraints.foreignKey.ReferenceTypeDto;
+import at.tuwien.api.semantics.EntityDto;
 import at.tuwien.entities.database.Database;
 import at.tuwien.entities.database.table.Table;
 import at.tuwien.entities.database.table.columns.TableColumn;
@@ -85,14 +86,14 @@ public interface TableMapper {
     Table tableCreateDtoToTable(TableCreateDto data);
 
     @Mappings({
-            @Mapping(source = "conceptUri", target = "uri")
+            @Mapping(source = "label", target = "name")
     })
-    TableColumnConcept columnSemanticsUpdateDtoToTableColumnConcept(ColumnSemanticsUpdateDto data);
+    TableColumnConcept entityDtoToTableColumnConcept(EntityDto data);
 
     @Mappings({
-            @Mapping(source = "unitUri", target = "uri")
+            @Mapping(source = "label", target = "name")
     })
-    TableColumnUnit columnSemanticsUpdateDtoToTableColumnUnit(ColumnSemanticsUpdateDto data);
+    TableColumnUnit entityDtoToTableColumnUnit(EntityDto data);
 
     default TableColumn columnNameToTableColumn(Table table, String name) throws TableMalformedException {
         String internalName = nameToInternalName(name);
diff --git a/dbrepo-table-service/services/src/main/java/at/tuwien/repository/jpa/OntologyRepository.java b/dbrepo-table-service/services/src/main/java/at/tuwien/repository/jpa/OntologyRepository.java
new file mode 100644
index 0000000000..75c296f527
--- /dev/null
+++ b/dbrepo-table-service/services/src/main/java/at/tuwien/repository/jpa/OntologyRepository.java
@@ -0,0 +1,10 @@
+package at.tuwien.repository.jpa;
+
+import at.tuwien.entities.semantics.Ontology;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface OntologyRepository extends JpaRepository<Ontology, Long> {
+
+}
diff --git a/dbrepo-table-service/services/src/main/java/at/tuwien/service/SemanticService.java b/dbrepo-table-service/services/src/main/java/at/tuwien/service/SemanticService.java
new file mode 100644
index 0000000000..343a27347d
--- /dev/null
+++ b/dbrepo-table-service/services/src/main/java/at/tuwien/service/SemanticService.java
@@ -0,0 +1,32 @@
+package at.tuwien.service;
+
+import at.tuwien.entities.database.table.columns.TableColumnConcept;
+import at.tuwien.entities.database.table.columns.TableColumnUnit;
+import at.tuwien.exception.ConceptNotFoundException;
+import at.tuwien.exception.SemanticEntityNotFoundException;
+import at.tuwien.exception.UnitNotFoundException;
+
+public interface SemanticService {
+
+    /**
+     * Finds a ColumnConcept with given uri
+     *
+     * @param uri The uri.
+     * @return The concept, if successful.
+     * @throws ConceptNotFoundException The ColumnConcept was not found in the metadata database.
+     */
+    TableColumnConcept findConcept(String uri) throws ConceptNotFoundException;
+
+    /**
+     * Finds a unit with given uri
+     *
+     * @param uri The uri.
+     * @return The unit, if successful.
+     * @throws UnitNotFoundException The unit was not found in the metadata database.
+     */
+    TableColumnUnit findUnit(String uri) throws UnitNotFoundException;
+
+    TableColumnConcept saveConcept(String uri, String authorization) throws SemanticEntityNotFoundException;
+
+    TableColumnUnit saveUnit(String uri, String authorization) throws SemanticEntityNotFoundException;
+}
diff --git a/dbrepo-table-service/services/src/main/java/at/tuwien/service/TableService.java b/dbrepo-table-service/services/src/main/java/at/tuwien/service/TableService.java
index dae9cb07f6..3bc0a73af5 100644
--- a/dbrepo-table-service/services/src/main/java/at/tuwien/service/TableService.java
+++ b/dbrepo-table-service/services/src/main/java/at/tuwien/service/TableService.java
@@ -78,16 +78,14 @@ public interface TableService {
      * @param tableId     The table id.
      * @param columnId    The column id.
      * @param updateDto   The update data containing unit and concept uris.
-     * @return The updated table column.
-     * @throws TableNotFoundException
+     * @return The updated table column, if successful.
+     * @throws TableNotFoundException     The table was not found in the metadata database.
      * @throws DatabaseNotFoundException  The database was not found in the metadata database.
      * @throws ContainerNotFoundException The container was not found.
      * @throws TableMalformedException    The table seems malformed by the mapper.
-     * @throws UnitNotFoundException
-     * @throws ConceptNotFoundException
      */
     TableColumn update(Long containerId, Long databaseId, Long tableId, Long columnId,
                        ColumnSemanticsUpdateDto updateDto, String authorization)
             throws TableNotFoundException, DatabaseNotFoundException, ContainerNotFoundException,
-            TableMalformedException, UnitNotFoundException, ConceptNotFoundException, SemanticEntityPersistException;
+            TableMalformedException, SemanticEntityPersistException, SemanticEntityNotFoundException;
 }
diff --git a/dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java b/dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java
new file mode 100644
index 0000000000..94cd509ae8
--- /dev/null
+++ b/dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/SemanticServiceImpl.java
@@ -0,0 +1,107 @@
+package at.tuwien.service.impl;
+
+import at.tuwien.entities.database.table.columns.TableColumnConcept;
+import at.tuwien.entities.database.table.columns.TableColumnUnit;
+import at.tuwien.entities.semantics.Ontology;
+import at.tuwien.exception.ConceptNotFoundException;
+import at.tuwien.exception.SemanticEntityNotFoundException;
+import at.tuwien.exception.UnitNotFoundException;
+import at.tuwien.gateway.SemanticServiceGateway;
+import at.tuwien.mapper.TableMapper;
+import at.tuwien.repository.jpa.ConceptRepository;
+import at.tuwien.repository.jpa.OntologyRepository;
+import at.tuwien.repository.jpa.UnitRepository;
+import at.tuwien.service.SemanticService;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+@Log4j2
+@Service
+public class SemanticServiceImpl implements SemanticService {
+
+    private final TableMapper tableMapper;
+    private final UnitRepository unitRepository;
+    private final ConceptRepository conceptRepository;
+    private final OntologyRepository ontologyRepository;
+    private final SemanticServiceGateway semanticServiceGateway;
+
+    @Autowired
+    public SemanticServiceImpl(TableMapper tableMapper, UnitRepository unitRepository, ConceptRepository conceptRepository,
+                               OntologyRepository ontologyRepository, SemanticServiceGateway semanticServiceGateway) {
+        this.tableMapper = tableMapper;
+        this.unitRepository = unitRepository;
+        this.conceptRepository = conceptRepository;
+        this.ontologyRepository = ontologyRepository;
+        this.semanticServiceGateway = semanticServiceGateway;
+    }
+
+    @Override
+    public TableColumnConcept findConcept(String uri) throws ConceptNotFoundException {
+        final Optional<TableColumnConcept> optional = conceptRepository.findById(uri);
+        if (optional.isEmpty()) {
+            log.error("Failed to find column concept with uri {}", uri);
+            throw new ConceptNotFoundException("Failed to find concept with uri " + uri);
+        }
+        return optional.get();
+    }
+
+    @Override
+    public TableColumnUnit findUnit(String uri) throws UnitNotFoundException {
+        final Optional<TableColumnUnit> optional = unitRepository.findById(uri);
+        if (optional.isEmpty()) {
+            log.error("Failed to find unit with uri {}", uri);
+            throw new UnitNotFoundException("Failed to find unit");
+        }
+        return optional.get();
+    }
+
+    @Override
+    public TableColumnConcept saveConcept(String uri, String authorization) throws SemanticEntityNotFoundException {
+        /* check compatible ontologies */
+        final Ontology ontology = getCompatibleOntology(uri);
+        if (ontology == null) {
+            return TableColumnConcept.builder()
+                    .uri(uri)
+                    .build();
+        }
+        final TableColumnConcept entity = tableMapper.entityDtoToTableColumnConcept(semanticServiceGateway.getEntity(ontology.getId(), uri, authorization));
+        final TableColumnConcept concept = conceptRepository.save(entity);
+        log.debug("saved concept with uri {}", concept.getUri());
+        log.trace("saved concept {}", concept.getUri());
+        return concept;
+    }
+
+    @Override
+    public TableColumnUnit saveUnit(String uri, String authorization) throws SemanticEntityNotFoundException {
+        final Ontology ontology = getCompatibleOntology(uri);
+        if (ontology == null) {
+            return TableColumnUnit.builder()
+                    .uri(uri)
+                    .build();
+        }
+        final TableColumnUnit entity = tableMapper.entityDtoToTableColumnUnit(semanticServiceGateway.getEntity(ontology.getId(), uri, authorization));
+        final TableColumnUnit unit = unitRepository.save(entity);
+        log.debug("saved unit with uri {}", unit.getUri());
+        log.trace("saved unit {}", unit.getUri());
+        return unit;
+    }
+
+    private Ontology getCompatibleOntology(String uri) {
+        final List<Ontology> ontologies = ontologyRepository.findAll()
+                .stream()
+                .filter(o -> uri.startsWith(o.getUri()))
+                .toList();
+        if (ontologies.size() != 1) {
+            log.warn("Failed to find registered ontology for entity with uri {}", uri);
+            return null;
+        }
+        final Ontology ontology = ontologies.get(0);
+        log.debug("found available compatible ontology with id {}", ontology.getId());
+        return ontology;
+    }
+
+}
diff --git a/dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java b/dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java
index 9f46855a19..c0b1681e8b 100644
--- a/dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java
+++ b/dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java
@@ -7,21 +7,14 @@ import at.tuwien.entities.container.Container;
 import at.tuwien.entities.database.Database;
 import at.tuwien.entities.database.table.Table;
 import at.tuwien.entities.database.table.columns.TableColumn;
-import at.tuwien.entities.database.table.columns.TableColumnConcept;
-import at.tuwien.entities.database.table.columns.TableColumnUnit;
 import at.tuwien.entities.user.User;
 import at.tuwien.exception.*;
 import at.tuwien.mapper.TableMapper;
 import at.tuwien.repository.elastic.TableColumnIdxRepository;
 import at.tuwien.repository.elastic.TableIdxRepository;
-import at.tuwien.repository.jpa.ConceptRepository;
 import at.tuwien.repository.jpa.TableColumnRepository;
 import at.tuwien.repository.jpa.TableRepository;
-import at.tuwien.repository.jpa.UnitRepository;
-import at.tuwien.service.ContainerService;
-import at.tuwien.service.DatabaseService;
-import at.tuwien.service.TableService;
-import at.tuwien.service.UserService;
+import at.tuwien.service.*;
 import com.mchange.v2.c3p0.ComboPooledDataSource;
 import lombok.extern.log4j.Log4j2;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -41,28 +34,26 @@ public class TableServiceImpl extends HibernateConnector implements TableService
 
     private final TableMapper tableMapper;
     private final UserService userService;
-    private final UnitRepository unitRepository;
-    private final TableRepository tableRepository;
     private final DatabaseService databaseService;
+    private final SemanticService semanticService;
+    private final TableRepository tableRepository;
     private final ContainerService containerService;
-    private final ConceptRepository conceptRepository;
     private final TableIdxRepository tableIdxRepository;
     private final TableColumnRepository tableColumnRepository;
     private final TableColumnIdxRepository tableColumnIdxRepository;
 
     @Autowired
-    public TableServiceImpl(TableMapper tableMapper, UserService userService, UnitRepository unitRepository,
+    public TableServiceImpl(TableMapper tableMapper, UserService userService, SemanticService semanticService,
                             TableRepository tableRepository, DatabaseService databaseService,
-                            ContainerService containerService, ConceptRepository conceptRepository,
-                            TableIdxRepository tableIdxRepository, TableColumnRepository tableColumnRepository,
+                            ContainerService containerService, TableIdxRepository tableIdxRepository,
+                            TableColumnRepository tableColumnRepository,
                             TableColumnIdxRepository tableColumnIdxRepository) {
         this.tableMapper = tableMapper;
         this.userService = userService;
-        this.unitRepository = unitRepository;
+        this.semanticService = semanticService;
         this.tableRepository = tableRepository;
         this.databaseService = databaseService;
         this.containerService = containerService;
-        this.conceptRepository = conceptRepository;
         this.tableIdxRepository = tableIdxRepository;
         this.tableColumnRepository = tableColumnRepository;
         this.tableColumnIdxRepository = tableColumnIdxRepository;
@@ -214,24 +205,28 @@ public class TableServiceImpl extends HibernateConnector implements TableService
     public TableColumn update(Long containerId, Long databaseId, Long tableId, Long columnId,
                               ColumnSemanticsUpdateDto updateDto, String authorization)
             throws TableNotFoundException, DatabaseNotFoundException, ContainerNotFoundException,
-            TableMalformedException, UnitNotFoundException, ConceptNotFoundException {
+            TableMalformedException, SemanticEntityNotFoundException {
         final Table table = findById(containerId, databaseId, tableId);
         final TableColumn column = findColumn(table, columnId);
         /* assign */
         if (updateDto.getUnitUri() != null) {
-            unitRepository.save(tableMapper.columnSemanticsUpdateDtoToTableColumnUnit(updateDto));
-            final TableColumnUnit unit = findUnit(updateDto.getUnitUri());
-            column.setUnit(unit);
-            log.debug("update unit of column, unit={}, column={}", unit, column);
+            try {
+                column.setUnit(semanticService.findUnit(updateDto.getUnitUri()));
+            } catch (UnitNotFoundException e) {
+                log.warn("Unit with uri {} not found in metadata database", updateDto.getUnitUri());
+                column.setUnit(semanticService.saveUnit(updateDto.getUnitUri(), authorization));
+            }
         } else {
             column.setUnit(null);
             log.debug("remove unit of column, column={}", column);
         }
         if (updateDto.getConceptUri() != null) {
-            conceptRepository.save(tableMapper.columnSemanticsUpdateDtoToTableColumnConcept(updateDto));
-            final TableColumnConcept concept = findConcept(updateDto.getConceptUri());
-            column.setConcept(concept);
-            log.debug("update ColumnConcept of column, concept={}, column={}", concept, column);
+            try {
+                column.setConcept(semanticService.findConcept(updateDto.getConceptUri()));
+            } catch (ConceptNotFoundException e) {
+                log.warn("Concept with uri {} not found in metadata database", updateDto.getConceptUri());
+                column.setConcept(semanticService.saveConcept(updateDto.getConceptUri(), authorization));
+            }
         } else {
             column.setConcept(null);
             log.debug("remove ColumnConcept of column, column={}", column);
@@ -267,36 +262,4 @@ public class TableServiceImpl extends HibernateConnector implements TableService
         return optional.get();
     }
 
-    /**
-     * Finds a ColumnConcept with given uri
-     *
-     * @param uri The uri.
-     * @return The concept, if successful.
-     * @throws ConceptNotFoundException The ColumnConcept was not found in the metadata database.
-     */
-    protected TableColumnConcept findConcept(String uri) throws ConceptNotFoundException {
-        final Optional<TableColumnConcept> optional = conceptRepository.findById(uri);
-        if (optional.isEmpty()) {
-            log.error("Failed to find column concept with uri {}", uri);
-            throw new ConceptNotFoundException("Failed to find concept with uri " + uri);
-        }
-        return optional.get();
-    }
-
-    /**
-     * Finds a unit with given uri
-     *
-     * @param uri The uri.
-     * @return The unit, if successful.
-     * @throws UnitNotFoundException The unit was not found in the metadata database.
-     */
-    protected TableColumnUnit findUnit(String uri) throws UnitNotFoundException {
-        final Optional<TableColumnUnit> optional = unitRepository.findById(uri);
-        if (optional.isEmpty()) {
-            log.error("Failed to find unit with uri {}", uri);
-            throw new UnitNotFoundException("Failed to find unit");
-        }
-        return optional.get();
-    }
-
 }
diff --git a/dbrepo-ui/.env.example b/dbrepo-ui/.env.example
index 53a7f375f5..c6d247fe18 100644
--- a/dbrepo-ui/.env.example
+++ b/dbrepo-ui/.env.example
@@ -13,3 +13,4 @@ SANDBOX=false
 SHARED_FILESYSTEM="/tmp"
 TITLE="Database Repository"
 VERSION="latest"
+GIT_HASH="deadbeef"
diff --git a/dbrepo-ui/Dockerfile b/dbrepo-ui/Dockerfile
index 582126fc3e..f3b107c8d8 100644
--- a/dbrepo-ui/Dockerfile
+++ b/dbrepo-ui/Dockerfile
@@ -57,6 +57,7 @@ ENV VERSION="${TAG}"
 ENV TITLE="Database Repository"
 ENV ICON="/favicon.ico"
 ENV DBREPO_CLIENT_SECRET="client-secret"
+ENV GIT_HASH="deadbeef"
 
 WORKDIR /app
 
diff --git a/dbrepo-ui/api/semantic.service.js b/dbrepo-ui/api/semantic.service.js
index 0c9f01b7db..01b8b021a3 100644
--- a/dbrepo-ui/api/semantic.service.js
+++ b/dbrepo-ui/api/semantic.service.js
@@ -36,6 +36,23 @@ class SemanticService {
     })
   }
 
+  updateConcept (data) {
+    return new Promise((resolve, reject) => {
+      api.put('/api/semantic/concept', data, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const concept = response.data
+          console.debug('response concept', concept)
+          resolve(concept)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to update concept', error)
+          Vue.$toast.error(`[${code}] Failed to update concept: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
   findAllUnits () {
     return new Promise((resolve, reject) => {
       api.get('/api/semantic/unit', { headers: { Accept: 'application/json' } })
@@ -53,6 +70,23 @@ class SemanticService {
     })
   }
 
+  updateUnit (data) {
+    return new Promise((resolve, reject) => {
+      api.put('/api/semantic/unit', data, { headers: { Accept: 'application/json' } })
+        .then((response) => {
+          const unit = response.data
+          console.debug('response unit', unit)
+          resolve(unit)
+        })
+        .catch((error) => {
+          const { code, message } = error
+          console.error('Failed to update unit', error)
+          Vue.$toast.error(`[${code}] Failed to update unit: ${message}`)
+          reject(error)
+        })
+    })
+  }
+
   findOntology (id) {
     return new Promise((resolve, reject) => {
       api.get(`/api/semantic/ontology/${id}`, { headers: { Accept: 'application/json' } })
diff --git a/dbrepo-ui/components/dialogs/EditSemantics.vue b/dbrepo-ui/components/dialogs/EditSemantics.vue
deleted file mode 100644
index 5debbc5840..0000000000
--- a/dbrepo-ui/components/dialogs/EditSemantics.vue
+++ /dev/null
@@ -1,96 +0,0 @@
-<template>
-  <div>
-    <v-card>
-      <v-card-title>Edit</v-card-title>
-      <v-card-text>
-        <v-form ref="form" v-model="valid" autocomplete="off" @submit.prevent="submit">
-          <v-row>
-            <v-col>
-              <v-text-field
-                v-model="semanticDto.name"
-                clearable
-                label="Name" />
-            </v-col>
-          </v-row>
-          <v-row>
-            <v-col>
-              <v-text-field
-                v-model="semanticDto.uri"
-                clearable
-                label="URI"
-                :rules="[v => !!v || $t('Required')]"
-                required />
-            </v-col>
-          </v-row>
-        </v-form>
-      </v-card-text>
-      <v-card-actions>
-        <v-spacer />
-        <v-btn
-          class="mb-2"
-          @click="cancel">
-          Cancel
-        </v-btn>
-        <v-btn
-          color="primary"
-          class="mb-2 mr-2"
-          :disabled="!valid"
-          :loading="loadingSave"
-          @click="save">
-          Save
-        </v-btn>
-      </v-card-actions>
-    </v-card>
-  </div>
-</template>
-
-<script>
-import SemanticService from '@/api/semantic.service'
-
-export default {
-  props: {
-    entity: {
-      type: Object,
-      default () {
-        return {}
-      }
-    }
-  },
-  data () {
-    return {
-      valid: false,
-      localEntity: null,
-      loadingSave: false,
-      semanticDto: {
-        name: null,
-        uri: null
-      }
-    }
-  },
-  computed: {
-  },
-  watch: {
-    entity () {
-      this.semanticDto.name = this.entity.name
-      this.semanticDto.uri = this.entity.uri
-    }
-  },
-  mounted () {
-    this.semanticDto.name = this.entity.name
-    this.semanticDto.uri = this.entity.uri
-  },
-  methods: {
-    cancel () {
-      this.$emit('close', { success: false, action: 'cancel' })
-    },
-    save () {
-      SemanticService.u
-    },
-    submit () {
-      this.$refs.form.validate()
-    }
-  }
-}
-</script>
-<style scoped>
-</style>
diff --git a/dbrepo-ui/components/dialogs/Semantics.vue b/dbrepo-ui/components/dialogs/Semantics.vue
index 97910f2690..caf5908002 100644
--- a/dbrepo-ui/components/dialogs/Semantics.vue
+++ b/dbrepo-ui/components/dialogs/Semantics.vue
@@ -1,37 +1,61 @@
 <template>
   <div>
     <v-card>
-      <v-card-title>Assign Semantic Information</v-card-title>
-      <v-card-subtitle v-if="!loadingSemantics">
-        Recommend <strong v-text="recommendations.length" /> {{ recommendations.length === 1 ? 'entity' : 'entities' }} that matches the column name
-      </v-card-subtitle>
+      <v-progress-linear
+        v-if="loadingSemantics"
+        color="primary"
+        indeterminate />
+      <v-card-title v-text="column.name" />
+      <v-card-subtitle v-if="loadingSemantics && !semanticEntity">Loading semantic recommendations ...</v-card-subtitle>
+      <v-card-text>
+        <v-alert
+          v-if="!entity"
+          border="left"
+          color="info"
+          dark
+          icon="mdi-share-variant"
+          class="pl-6">
+          <p>
+            The following ontologies automatically will query the fields <code>rdfs:label</code> and
+            <code>schema:description</code> and store it for this column. You can still use other URIs that are not
+            matching these ontologies, the URI will be displayed instead.
+          </p>
+          <div v-for="(item,idx) in ontologies" :key="idx">
+            <a :href="item.uri" target="_blank" v-text="item.uri" />
+          </div>
+        </v-alert>
+        <v-alert
+          v-if="entity"
+          border="left"
+          color="primary"
+          dark
+          icon="mdi-share-variant"
+          class="pl-6">
+          <div>
+            <a :href="entity.uri" class="white--text" target="_blank" v-text="entity.name ? entity.name : entity.uri" />
+          </div>
+          <div v-text="entity.description" />
+        </v-alert>
+      </v-card-text>
       <v-card-text>
         <v-form ref="form" v-model="valid" autocomplete="off" @submit.prevent="submit">
-          <v-row>
-            <v-col>
-              <v-progress-linear
-                v-if="loadingSemantics"
-                color="secondary"
-                indeterminate />
-              <v-list-item-group v-if="!loadingSemantics" v-model="recommendation">
-                <v-list-item v-for="(item,idx) in recommendations" :key="idx" three-line>
-                  <template v-slot:default="{ active, }">
-                    <v-list-item-action>
-                      <v-checkbox
-                        :input-value="active"
-                        color="primary" />
-                    </v-list-item-action>
-                    <v-list-item-content>
-                      <v-list-item-title v-text="item.label" />
-                      <v-list-item-subtitle v-text="item.uri" />
-                      <v-list-item-subtitle class="mt-1" v-text="item.comment" />
-                    </v-list-item-content>
-                  </template>
-                </v-list-item>
-              </v-list-item-group>
-            </v-col>
-          </v-row>
-          <v-row>
+          <v-list-item-group v-if="!loadingSemantics" v-model="recommendation">
+            <v-list-item v-for="(item,idx) in recommendations" :key="idx" three-line>
+              <template v-slot:default="{ active, }">
+                <v-list-item-action>
+                  <v-checkbox
+                    :input-value="active"
+                    color="primary" />
+                </v-list-item-action>
+                <v-list-item-content>
+                  <v-list-item-title v-text="item.label" />
+                  <v-list-item-subtitle v-text="item.uri" />
+                  <v-list-item-subtitle class="mt-1" v-text="item.description" />
+                </v-list-item-content>
+              </template>
+            </v-list-item>
+          </v-list-item-group>
+          <v-row dense>
             <v-col>
               <v-text-field
                 v-model="uri"
@@ -99,24 +123,26 @@ export default {
       valid: false,
       loading: false,
       loadingOntologies: false,
-      loadingSemantics: false,
-      ontologies: []
+      loadingSemantics: false
     }
   },
   computed: {
+    ontologies () {
+      return this.$store.state.ontologies
+    },
+    entity () {
+      if (!this.column[this.mode]) {
+        return null
+      }
+      return this.column[this.mode]
+    }
   },
   watch: {
     column () {
-      this.loadSemantics()
-      if (this.column.unit && this.mode === 'unit') {
-        this.uri = this.column.unit.uri
-        return
-      }
-      if (this.column.concept && this.mode === 'concept') {
-        this.uri = this.column.concept.uri
-        return
+      if (!this.column[this.mode]) {
+        this.recommendSemantics()
       }
-      this.uri = null
+      this.init()
     },
     recommendation (index) {
       if (!this.recommendations[index] || !('uri' in this.recommendations[index])) {
@@ -127,8 +153,10 @@ export default {
     }
   },
   mounted () {
-    this.loadOntologies()
-    this.loadSemantics()
+    this.init()
+    if (!this.column[this.mode]) {
+      this.recommendSemantics()
+    }
   },
   methods: {
     cancel () {
@@ -153,17 +181,7 @@ export default {
           this.loadingSave = false
         })
     },
-    loadOntologies () {
-      this.loadingOntologies = true
-      SemanticService.findAllOntologies()
-        .then((ontologies) => {
-          this.ontologies = ontologies
-        })
-        .finally(() => {
-          this.loadingOntologies = false
-        })
-    },
-    loadSemantics () {
+    recommendSemantics () {
       this.loadingSemantics = true
       SemanticService.suggestTableColumn(this.database.id, this.tableId, this.column.id)
         .then((recommendations) => {
@@ -179,6 +197,17 @@ export default {
       }
       return str.match(/https?:\/\//g)
     },
+    init () {
+      if (this.column.unit && this.mode === 'unit') {
+        this.uri = this.column.unit.uri
+        return
+      }
+      if (this.column.concept && this.mode === 'concept') {
+        this.uri = this.column.concept.uri
+        return
+      }
+      this.uri = null
+    },
     submit () {
       this.$refs.form.validate()
     }
diff --git a/dbrepo-ui/components/dialogs/ViewSemanticEntity.vue b/dbrepo-ui/components/dialogs/ViewSemanticEntity.vue
new file mode 100644
index 0000000000..6830ae76f4
--- /dev/null
+++ b/dbrepo-ui/components/dialogs/ViewSemanticEntity.vue
@@ -0,0 +1,72 @@
+<template>
+  <div>
+    <v-card>
+      <v-card-title v-text="entity.name" />
+      <v-card-subtitle>
+        <a :href="entity.uri" target="_blank" v-text="entity.uri" />
+      </v-card-subtitle>
+      <v-card-text>
+        <p v-text="description" />
+      </v-card-text>
+      <div v-for="(item,idx) in entity.columns" :key="idx">
+        <v-list-item-group>
+          <v-list-item two-line :to="link(item)">
+            <v-list-item-content>
+              <v-list-item-title v-text="item.name" />
+              <v-list-item-subtitle class="mt-2" v-text="link(item)" />
+            </v-list-item-content>
+          </v-list-item>
+        </v-list-item-group>
+      </div>
+      <v-card-actions class="mt-2">
+        <v-spacer />
+        <v-btn
+          class="mb-2"
+          @click="cancel">
+          Cancel
+        </v-btn>
+      </v-card-actions>
+    </v-card>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    mode: {
+      type: String,
+      default () {
+        return 'unit'
+      }
+    },
+    entity: {
+      type: Object,
+      default () {
+        return {}
+      }
+    }
+  },
+  data () {
+    return {
+    }
+  },
+  computed: {
+    description () {
+      if (!this.entity.description) {
+        return '(no description)'
+      }
+      return this.entity.description
+    }
+  },
+  methods: {
+    cancel () {
+      this.$emit('close', { success: false, action: 'cancel' })
+    },
+    link (item) {
+      return `/container/${item.database_id}/database/${item.database_id}/table/${item.table_id}/schema`
+    }
+  }
+}
+</script>
+<style scoped>
+</style>
diff --git a/dbrepo-ui/config.js b/dbrepo-ui/config.js
index d0222abf07..bc199503b7 100644
--- a/dbrepo-ui/config.js
+++ b/dbrepo-ui/config.js
@@ -17,5 +17,6 @@ config.elasticPassword = process.env.ELASTIC_PASSWORD || 'elastic'
 config.clientSecret = process.env.DBREPO_CLIENT_SECRET || 'MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG'
 config.defaultPublisher = process.env.DEFAULT_PID_PUBLISHER || 'Technische Universität Wien'
 config.doiUrl = process.env.DOI_URL || 'https://doi.org'
+config.gitHash = process.env.GIT_HASH || 'deadbeef'
 
 module.exports = config
diff --git a/dbrepo-ui/layouts/default.vue b/dbrepo-ui/layouts/default.vue
index 75eaccafae..30596e71b7 100644
--- a/dbrepo-ui/layouts/default.vue
+++ b/dbrepo-ui/layouts/default.vue
@@ -10,7 +10,7 @@
       <v-list-item class="mt-2">
         <v-list-item-content>
           <v-list-item-subtitle>
-            {{ version }}
+            {{ version }} ({{ gitHash }})
           </v-list-item-subtitle>
           <v-list-item-title class="text-h6">
             Database Repository
@@ -183,6 +183,9 @@ export default {
     version () {
       return this.$config.version
     },
+    gitHash () {
+      return this.$config.gitHash
+    },
     canListOntologies () {
       if (!this.roles) {
         return false
diff --git a/dbrepo-ui/nuxt.config.js b/dbrepo-ui/nuxt.config.js
index 9db2e6dc80..fee9bcc801 100644
--- a/dbrepo-ui/nuxt.config.js
+++ b/dbrepo-ui/nuxt.config.js
@@ -1,6 +1,6 @@
 import path from 'path'
 import colors from 'vuetify/es5/util/colors'
-import { api, icon, search, clientSecret, title, sandbox, logo, version, defaultPublisher, doiUrl, baseUrl } from './config'
+import { api, icon, search, clientSecret, title, sandbox, logo, version, defaultPublisher, doiUrl, baseUrl, gitHash } from './config'
 
 const proxy = {}
 
@@ -94,7 +94,8 @@ export default {
     clientSecret,
     defaultPublisher,
     doiUrl,
-    baseUrl
+    baseUrl,
+    gitHash
   },
 
   serverMiddleware: [
diff --git a/dbrepo-ui/pages/semantic/index.vue b/dbrepo-ui/pages/semantic/index.vue
index a9362fb6d3..63353bb5ab 100644
--- a/dbrepo-ui/pages/semantic/index.vue
+++ b/dbrepo-ui/pages/semantic/index.vue
@@ -25,39 +25,39 @@
             <a :href="item.uri" target="_blank" v-text="item.uri" />
           </template>
           <template v-slot:item.action="{ item }">
-            <v-btn small @click="edit(item)">
-              Edit
+            <v-btn small :disabled="disabled(item)" @click="view(item)">
+              Usages
             </v-btn>
           </template>
         </v-data-table>
       </v-card-text>
     </v-card>
     <v-dialog
-      v-model="editSemanticDialog"
-      persistent
+      v-model="viewSemanticEntityDialog"
       max-width="640">
-      <EditSemantics :entity="entity" @close="close" />
+      <ViewSemanticEntity :mode="mode" :entity="entity" @close="close" />
     </v-dialog>
     <v-breadcrumbs :items="items" class="pa-0 mt-2" />
   </div>
 </template>
 <script>
 import SemanticService from '@/api/semantic.service'
-import EditSemantics from '@/components/dialogs/EditSemantics.vue'
+import ViewSemanticEntity from '@/components/dialogs/ViewSemanticEntity.vue'
 
 export default {
   components: {
-    EditSemantics
+    ViewSemanticEntity
   },
   data () {
     return {
       loadingConcepts: false,
       loadingUnits: false,
       entity: null,
-      editSemanticDialog: false,
+      viewSemanticEntityDialog: false,
       headers: [
-        { text: 'Name', value: 'name' },
         { text: 'URI', value: 'uri' },
+        { text: 'Name', value: 'name' },
+        { text: 'Description', value: 'description' },
         { text: 'Usages', value: 'usages' },
         { text: null, value: 'action' }
       ],
@@ -97,6 +97,9 @@ export default {
     rows () {
       return this.tab === 0 ? this.concepts : this.units
     },
+    mode () {
+      return this.tab === 0 ? 'concept' : 'unit'
+    },
     canListOntologies () {
       if (!this.roles) {
         return false
@@ -137,12 +140,20 @@ export default {
           this.loadingUnits = false
         })
     },
-    edit (entity) {
+    disabled (item) {
+      return !item.usages || this.usages === 0
+    },
+    view (entity) {
       this.entity = entity
-      this.editSemanticDialog = true
+      this.viewSemanticEntityDialog = true
     },
     close (event) {
-      this.editSemanticDialog = false
+      if (this.mode === 'unit') {
+        this.loadUnits()
+      } else if (this.mode === 'concept') {
+        this.loadConcepts()
+      }
+      this.viewSemanticEntityDialog = false
     }
   }
 }
diff --git a/dbrepo-ui/pages/semantic/ontology/_ontology_id/index.vue b/dbrepo-ui/pages/semantic/ontology/_ontology_id/index.vue
index f8e486420b..a9b4422d3a 100644
--- a/dbrepo-ui/pages/semantic/ontology/_ontology_id/index.vue
+++ b/dbrepo-ui/pages/semantic/ontology/_ontology_id/index.vue
@@ -1,9 +1,16 @@
 <template>
   <div v-if="canListOntologies">
     <v-toolbar flat>
+      <v-toolbar-title>
+        <v-btn id="back-btn" plain class="mr-2" to="/semantic/ontology">
+          <v-icon left>mdi-arrow-left</v-icon>
+        </v-btn>
+      </v-toolbar-title>
       <v-toolbar-title>
         <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" />
-        <span v-if="!loading" v-text="title" />
+        <span v-if="!loading">
+          Ontology <a v-if="ontology" :href="ontology.uri" target="_blank" v-text="ontology.uri" />
+        </span>
       </v-toolbar-title>
       <v-spacer />
       <v-toolbar-title>
@@ -117,12 +124,6 @@ export default {
     ontologies () {
       return this.$store.state.ontologies
     },
-    title () {
-      if (!this.ontology) {
-        return 'Ontology'
-      }
-      return `Ontology ${this.ontology.prefix}`
-    },
     canListOntologies () {
       if (!this.roles) {
         return false
@@ -156,7 +157,7 @@ export default {
       SemanticService.unregisterOntology(this.$route.params.ontology_id)
         .then(async () => {
           await this.$store.dispatch('reloadOntologies')
-          await this.$router.push('/ontology')
+          await this.$router.push('/semantic/ontology')
         })
         .finally(() => {
           this.loadingDelete = false
diff --git a/dbrepo-ui/pages/semantic/ontology/index.vue b/dbrepo-ui/pages/semantic/ontology/index.vue
index dcb79ccc92..b39800ec41 100644
--- a/dbrepo-ui/pages/semantic/ontology/index.vue
+++ b/dbrepo-ui/pages/semantic/ontology/index.vue
@@ -1,6 +1,11 @@
 <template>
   <div v-if="canListOntologies">
     <v-toolbar flat>
+      <v-toolbar-title>
+        <v-btn id="back-btn" plain class="mr-2" to="/semantic">
+          <v-icon left>mdi-arrow-left</v-icon>
+        </v-btn>
+      </v-toolbar-title>
       <v-toolbar-title>{{ ontologies.length }} {{ $t('layout.ontologies', { name: 'vue-i18n' }) }}</v-toolbar-title>
       <v-spacer />
       <v-toolbar-title>
-- 
GitLab