Skip to content
Snippets Groups Projects
Verified Commit 6e08b4a2 authored by Martin Weise's avatar Martin Weise
Browse files

Added more functionality

parent ba03ad28
No related branches found
No related tags found
2 merge requests!163Relase 1.3.0,!162Resolve "Improve Semantic Service handling"
Showing
with 423 additions and 12 deletions
package at.tuwien.api.database.table.columns;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
......@@ -20,6 +21,14 @@ public class ColumnBriefDto {
@NotNull(message = "id is required")
private Long id;
@JsonProperty("database_id")
@NotNull(message = "database id is required")
private Long databaseId;
@JsonProperty("table_id")
@NotNull(message = "table id is required")
private Long tableId;
@NotBlank(message = "name is required")
@Schema(example = "date")
private String name;
......
package at.tuwien.api.database.table.columns.concepts;
import at.tuwien.api.database.table.columns.ColumnBriefDto;
import at.tuwien.api.database.table.columns.ColumnDto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
......@@ -9,6 +11,7 @@ import jakarta.validation.constraints.NotNull;
import lombok.extern.jackson.Jacksonized;
import java.time.Instant;
import java.util.List;
@Getter
@Setter
......@@ -26,7 +29,9 @@ public class ConceptDto {
private String name;
@NotNull
@Schema(required = true)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
private Instant created;
@NotNull
private List<ColumnBriefDto> columns;
}
package at.tuwien.api.database.table.columns.concepts;
import at.tuwien.api.database.table.columns.ColumnBriefDto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import jakarta.validation.constraints.NotBlank;
......@@ -9,6 +9,7 @@ import jakarta.validation.constraints.NotNull;
import lombok.extern.jackson.Jacksonized;
import java.time.Instant;
import java.util.List;
@Getter
@Setter
......@@ -26,7 +27,9 @@ public class UnitDto {
private String name;
@NotNull
@Schema(required = true)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
private Instant created;
@NotNull
private List<ColumnBriefDto> columns;
}
......@@ -36,4 +36,15 @@ public class TableColumnConcept {
@Column(nullable = false, updatable = false, columnDefinition = "TIMESTAMP")
@CreatedDate
private Instant created;
@ToString.Exclude
@OneToMany(fetch = FetchType.LAZY)
@JoinTable(name = "mdb_columns_concepts",
inverseJoinColumns = {
@JoinColumn(name = "cid", referencedColumnName = "id", insertable = false, updatable = false),
@JoinColumn(name = "tid", referencedColumnName = "tid", insertable = false, updatable = false),
@JoinColumn(name = "cdbid", referencedColumnName = "cdbid", insertable = false, updatable = false)
},
joinColumns = @JoinColumn(name = "uri", referencedColumnName = "uri"))
private List<TableColumn> columns;
}
......@@ -36,4 +36,15 @@ public class TableColumnUnit {
@Column(nullable = false, updatable = false, columnDefinition = "TIMESTAMP")
@CreatedDate
private Instant created;
@ToString.Exclude
@OneToMany(fetch = FetchType.LAZY)
@JoinTable(name = "mdb_columns_concepts",
inverseJoinColumns = {
@JoinColumn(name = "cid", referencedColumnName = "id", insertable = false, updatable = false),
@JoinColumn(name = "tid", referencedColumnName = "tid", insertable = false, updatable = false),
@JoinColumn(name = "cdbid", referencedColumnName = "cdbid", insertable = false, updatable = false)
},
joinColumns = @JoinColumn(name = "uri", referencedColumnName = "uri"))
private List<TableColumn> columns;
}
......@@ -29,10 +29,10 @@ public class Ontology {
@Column(updatable = false, nullable = false)
private Long id;
@Column(nullable = false)
@Column(nullable = false, unique = true)
private String uri;
@Column(nullable = false, columnDefinition = "VARCHAR(8)")
@Column(nullable = false, unique = true, columnDefinition = "VARCHAR(8)")
private String prefix;
@Column
......
......@@ -353,6 +353,8 @@ CREATE TABLE IF NOT EXISTS `fda`.`mdb_ontologies`
last_modified timestamp,
created timestamp NOT NULL DEFAULT NOW(),
created_by character varying(255) NULL,
UNIQUE (prefix),
UNIQUE (uri),
PRIMARY KEY (id)
) WITH SYSTEM VERSIONING;
......
......@@ -5,6 +5,7 @@ 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.mapper.OntologyMapper;
import at.tuwien.mapper.SemanticMapper;
import at.tuwien.service.SemanticService;
import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.Operation;
......@@ -12,33 +13,62 @@ import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Log4j2
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api/semantic")
public class SemanticsEndpoint {
private final SemanticMapper semanticMapper;
private final OntologyMapper ontologyMapper;
private final SemanticService semanticService;
@Autowired
public SemanticsEndpoint(OntologyMapper ontologyMapper, SemanticService semanticService) {
public SemanticsEndpoint(SemanticMapper semanticMapper, OntologyMapper ontologyMapper, SemanticService semanticService) {
this.semanticMapper = semanticMapper;
this.ontologyMapper = ontologyMapper;
this.semanticService = semanticService;
}
@GetMapping("/concept")
@Transactional(readOnly = true)
@Timed(value = "semantics.concept.list", description = "Time needed to find all semantic concepts")
@Operation(summary = "List semantic concepts")
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "Find all semantic concepts",
content = {@Content(
mediaType = "application/json",
schema = @Schema(implementation = ConceptDto[].class))}),
})
public ResponseEntity<List<ConceptDto>> findAllConcepts() {
log.debug("endpoint list concepts");
final List<ConceptDto> dtos = semanticService.findAllConcepts()
.stream()
.map(semanticMapper::tableColumnConceptToConceptDto)
.toList();
log.trace("Find all concepts resulted in dtos {}", dtos);
return ResponseEntity.ok()
.body(dtos);
}
@PostMapping("/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")
@Operation(summary = "Create or update a semantic concept", security = @SecurityRequirement(name = "bearerAuth"))
@ApiResponses(value = {
@ApiResponse(responseCode = "202",
description = "Created or updated a semantic concept",
......@@ -50,14 +80,57 @@ public class SemanticsEndpoint {
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);
return ResponseEntity.accepted()
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);
return ResponseEntity.ok()
.body(dto);
}
@GetMapping("/unit")
@Transactional(readOnly = true)
@Timed(value = "semantics.concept.list", description = "Time needed to find all semantic units")
@Operation(summary = "List semantic units")
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "Find all semantic units",
content = {@Content(
mediaType = "application/json",
schema = @Schema(implementation = UnitDto[].class))}),
})
public ResponseEntity<List<UnitDto>> findAllUnits() {
log.debug("endpoint list units");
final List<UnitDto> dtos = semanticService.findAllUnits()
.stream()
.map(semanticMapper::tableColumnUnitToUnitDto)
.toList();
log.trace("Find all units resulted in dtos {}", dtos);
return ResponseEntity.ok()
.body(dtos);
}
@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")
@Operation(summary = "Create or update a semantic unit", security = @SecurityRequirement(name = "bearerAuth"))
@ApiResponses(value = {
@ApiResponse(responseCode = "202",
description = "Created or updated a semantic unit",
......
......@@ -45,7 +45,9 @@ public class WebSecurityConfig {
new AntPathRequestMatcher("/swagger-ui.html")
);
final OrRequestMatcher publicEndpoints = new OrRequestMatcher(
new AntPathRequestMatcher("/api/semantic/ontology/**", "GET")
new AntPathRequestMatcher("/api/semantic/ontology/**", "GET"),
new AntPathRequestMatcher("/api/semantic/concept", "GET"),
new AntPathRequestMatcher("/api/semantic/unit", "GET")
);
/* enable CORS and disable CSRF */
http = http.cors().and().csrf().disable();
......
package at.tuwien.mapper;
import at.tuwien.api.database.table.columns.concepts.ConceptDto;
import at.tuwien.api.database.table.columns.concepts.UnitDto;
import at.tuwien.entities.database.table.columns.TableColumnConcept;
import at.tuwien.entities.database.table.columns.TableColumnUnit;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring", uses = {TableMapper.class})
public interface SemanticMapper {
org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SemanticMapper.class);
ConceptDto tableColumnConceptToConceptDto(TableColumnConcept data);
UnitDto tableColumnUnitToUnitDto(TableColumnUnit data);
}
package at.tuwien.mapper;
import at.tuwien.api.database.table.columns.ColumnBriefDto;
import at.tuwien.api.database.table.columns.ColumnDto;
import at.tuwien.entities.database.table.columns.TableColumn;
import at.tuwien.entities.database.table.columns.TableColumnKey;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
@Mapper(componentModel = "spring")
......@@ -9,6 +14,18 @@ public interface TableMapper {
org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TableMapper.class);
@Mappings({
@Mapping(source = "tid", target = "tableId"),
@Mapping(source = "cdbid", target = "databaseId"),
})
ColumnBriefDto tableColumnToColumnBriefDto(TableColumn data);
@Mappings({
@Mapping(source = "tid", target = "tableId"),
@Mapping(source = "cdbid", target = "databaseId"),
})
ColumnDto tableColumnToColumnDto(TableColumn data);
default TableColumnKey toTableColumnKey(Long databaseId, Long tableId, Long columnId) {
return TableColumnKey.builder()
.cdbid(databaseId)
......
......@@ -5,7 +5,13 @@ import at.tuwien.api.database.table.columns.concepts.UnitSaveDto;
import at.tuwien.entities.database.table.columns.TableColumnConcept;
import at.tuwien.entities.database.table.columns.TableColumnUnit;
import java.util.List;
public interface SemanticService {
List<TableColumnConcept> findAllConcepts();
List<TableColumnUnit> findAllUnits();
TableColumnConcept saveConcept(ConceptSaveDto data);
TableColumnUnit saveUnit(UnitSaveDto data);
......
......@@ -11,6 +11,9 @@ import at.tuwien.service.SemanticService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Log4j2
@Service
......@@ -29,6 +32,21 @@ public class SemanticServiceImpl implements SemanticService {
}
@Override
@Transactional(readOnly = true)
public List<TableColumnConcept> findAllConcepts() {
final List<TableColumnConcept> concepts = tableColumnConceptRepository.findAll();
return concepts;
}
@Override
@Transactional(readOnly = true)
public List<TableColumnUnit> findAllUnits() {
final List<TableColumnUnit> units = tableColumnUnitRepository.findAll();
return units;
}
@Override
@Transactional
public TableColumnConcept saveConcept(ConceptSaveDto data) {
final TableColumnConcept entity = ontologyMapper.conceptSaveDtoToTableColumnConcept(data);
final TableColumnConcept concept = tableColumnConceptRepository.save(entity);
......@@ -37,6 +55,7 @@ public class SemanticServiceImpl implements SemanticService {
}
@Override
@Transactional
public TableColumnUnit saveUnit(UnitSaveDto data) {
final TableColumnUnit entity = ontologyMapper.unitSaveDtoToTableColumnUnit(data);
final TableColumnUnit unit = tableColumnUnitRepository.save(entity);
......
......@@ -19,6 +19,40 @@ class SemanticService {
})
}
findAllConcepts () {
return new Promise((resolve, reject) => {
api.get('/api/semantic/concept', { headers: { Accept: 'application/json' } })
.then((response) => {
const concepts = response.data
console.debug('response concepts', concepts)
resolve(concepts)
})
.catch((error) => {
const { code, message } = error
console.error('Failed to load concepts', error)
Vue.$toast.error(`[${code}] Failed to load concepts: ${message}`)
reject(error)
})
})
}
findAllUnits () {
return new Promise((resolve, reject) => {
api.get('/api/semantic/unit', { headers: { Accept: 'application/json' } })
.then((response) => {
const units = response.data
console.debug('response units', units)
resolve(units)
})
.catch((error) => {
const { code, message } = error
console.error('Failed to load units', error)
Vue.$toast.error(`[${code}] Failed to load units: ${message}`)
reject(error)
})
})
}
findOntology (id) {
return new Promise((resolve, reject) => {
api.get(`/api/semantic/ontology/${id}`, { headers: { Accept: 'application/json' } })
......
<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>
......@@ -5,19 +5,76 @@
<v-spacer />
<v-toolbar-title>
<v-btn v-if="canListOntologies" to="/semantic/ontology" color="secondary">
Ontologies
{{ ontologies.length }} Ontologies
</v-btn>
</v-toolbar-title>
</v-toolbar>
<v-tabs v-model="tab">
<v-tab>Concepts</v-tab>
<v-tab>Units</v-tab>
</v-tabs>
<v-card flat>
<v-card-text>
<v-data-table
:headers="headers"
:items="rows"
:options.sync="options"
:server-items-length="total"
:footer-props="footerProps">
<template v-slot:item.uri="{ item }">
<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>
</template>
</v-data-table>
</v-card-text>
</v-card>
<v-dialog
v-model="editSemanticDialog"
persistent
max-width="640">
<EditSemantics :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'
export default {
components: {
EditSemantics
},
data () {
return {
loadingConcepts: false,
loadingUnits: false,
entity: null,
editSemanticDialog: false,
headers: [
{ text: 'Name', value: 'name' },
{ text: 'URI', value: 'uri' },
{ text: 'Usages', value: 'usages' },
{ text: null, value: 'action' }
],
options: {
page: 1,
itemsPerPage: 10
},
total: -1,
footerProps: {
'items-per-page-options': [10, 20, 30, 40, 50]
},
tab: 0,
tabs: [
'concepts', 'units'
],
concepts: [],
units: [],
createOntologyDialog: false,
items: [
{ text: `${this.$t('layout.semantics', { name: 'vue-i18n' })}`, to: '/semantic', activeClass: '' }
......@@ -34,6 +91,12 @@ export default {
roles () {
return this.$store.state.roles
},
ontologies () {
return this.$store.state.ontologies
},
rows () {
return this.tab === 0 ? this.concepts : this.units
},
canListOntologies () {
if (!this.roles) {
return false
......@@ -41,7 +104,46 @@ export default {
return this.roles.includes('list-ontologies')
}
},
mounted () {
this.loadUnits()
this.loadConcepts()
},
methods: {
loadConcepts () {
this.loadingConcepts = true
SemanticService.findAllConcepts()
.then((concepts) => {
concepts = concepts.map((column) => {
column.usages = column.columns.length
return column
})
this.concepts = concepts
})
.finally(() => {
this.loadingConcepts = false
})
},
loadUnits () {
this.loadingUnits = true
SemanticService.findAllUnits()
.then((units) => {
units = units.map((unit) => {
unit.usages = unit.columns.length
return unit
})
this.units = units
})
.finally(() => {
this.loadingUnits = false
})
},
edit (entity) {
this.entity = entity
this.editSemanticDialog = true
},
close (event) {
this.editSemanticDialog = false
}
}
}
</script>
<template>
<div v-if="canListOntologies">
<v-toolbar flat>
<v-toolbar-title>{{ $t('layout.ontologies', { name: 'vue-i18n' }) }}</v-toolbar-title>
<v-toolbar-title>{{ ontologies.length }} {{ $t('layout.ontologies', { name: 'vue-i18n' }) }}</v-toolbar-title>
<v-spacer />
<v-toolbar-title>
<v-btn v-if="canCreateOntology" color="primary" name="create-ontology" @click.stop="createOntologyDialog = true">
......@@ -47,6 +47,9 @@ export default {
roles () {
return this.$store.state.roles
},
ontologies () {
return this.$store.state.ontologies
},
canListOntologies () {
if (!this.roles) {
return false
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment