Skip to content

Commit

Permalink
Merge pull request #286 from kbss-cvut/kbss-cvut/termit-ui#449-excel-…
Browse files Browse the repository at this point in the history
…import

TermIt UI#449 excel import
  • Loading branch information
ledsoft authored Aug 12, 2024
2 parents 47817b5 + 94468a1 commit c95ab19
Show file tree
Hide file tree
Showing 46 changed files with 1,830 additions and 226 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@
<goal>copy-resources</goal>
</goals>
<configuration>
<propertiesEncoding>ISO-8859-1</propertiesEncoding>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>xlsx</nonFilteredFileExtension>
</nonFilteredFileExtensions>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package cz.cvut.kbss.termit.exception.importing;

/**
* Indicates that an existing vocabulary was expected for import but none was found.
*/
public class VocabularyDoesNotExistException extends VocabularyImportException {
public VocabularyDoesNotExistException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public VocabularyImportException(String message, String messageId) {
this.messageId = messageId;
}

public VocabularyImportException(String message, Throwable cause) {
super(message, cause);
this.messageId = null;
}

public String getMessageId() {
return messageId;
}
Expand Down
60 changes: 44 additions & 16 deletions src/main/java/cz/cvut/kbss/termit/persistence/dao/DataDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@
import cz.cvut.kbss.jopa.vocabulary.DC;
import cz.cvut.kbss.jopa.vocabulary.RDF;
import cz.cvut.kbss.jopa.vocabulary.RDFS;
import cz.cvut.kbss.ontodriver.rdf4j.util.Rdf4jUtils;
import cz.cvut.kbss.termit.dto.RdfsResource;
import cz.cvut.kbss.termit.exception.PersistenceException;
import cz.cvut.kbss.termit.persistence.dao.util.Quad;
import cz.cvut.kbss.termit.service.export.ExportFormat;
import cz.cvut.kbss.termit.service.export.util.TypeAwareByteArrayResource;
import cz.cvut.kbss.termit.util.Configuration;
import cz.cvut.kbss.termit.util.Configuration.Persistence;
import cz.cvut.kbss.termit.util.TypeAwareResource;
import jakarta.annotation.Nullable;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.rio.RDFFormat;
Expand All @@ -44,6 +47,7 @@
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -73,14 +77,14 @@ public DataDao(EntityManager em, Configuration config) {
*/
public List<RdfsResource> findAllProperties() {
final List<RdfsResource> result = em.createNativeQuery("SELECT ?x ?label ?comment ?type WHERE {" +
"BIND (?property as ?type)" +
"?x a ?type ." +
"OPTIONAL { ?x ?has-label ?label . }" +
"OPTIONAL { ?x ?has-comment ?comment . }" +
"}", "RdfsResource")
.setParameter("property", URI.create(RDF.PROPERTY))
.setParameter("has-label", RDFS_LABEL)
.setParameter("has-comment", URI.create(RDFS.COMMENT)).getResultList();
"BIND (?property as ?type)" +
"?x a ?type ." +
"OPTIONAL { ?x ?has-label ?label . }" +
"OPTIONAL { ?x ?has-comment ?comment . }" +
"}", "RdfsResource")
.setParameter("property", URI.create(RDF.PROPERTY))
.setParameter("has-label", RDFS_LABEL)
.setParameter("has-comment", URI.create(RDFS.COMMENT)).getResultList();
return consolidateTranslations(result);
}

Expand Down Expand Up @@ -127,14 +131,15 @@ public void persist(RdfsResource instance) {
*/
public Optional<RdfsResource> find(URI id) {
Objects.requireNonNull(id);
final List<RdfsResource> resources = consolidateTranslations(em.createNativeQuery("SELECT ?x ?label ?comment ?type WHERE {" +
"BIND (?id AS ?x)" +
"?x a ?type ." +
"OPTIONAL { ?x ?has-label ?label .}" +
"OPTIONAL { ?x ?has-comment ?comment . }" +
"}", "RdfsResource").setParameter("id", id)
.setParameter("has-label", RDFS_LABEL)
.setParameter("has-comment", URI.create(RDFS.COMMENT)).getResultList());
final List<RdfsResource> resources = consolidateTranslations(
em.createNativeQuery("SELECT ?x ?label ?comment ?type WHERE {" +
"BIND (?id AS ?x)" +
"?x a ?type ." +
"OPTIONAL { ?x ?has-label ?label .}" +
"OPTIONAL { ?x ?has-comment ?comment . }" +
"}", "RdfsResource").setParameter("id", id)
.setParameter("has-label", RDFS_LABEL)
.setParameter("has-comment", URI.create(RDFS.COMMENT)).getResultList());
if (resources.isEmpty()) {
return Optional.empty();
}
Expand Down Expand Up @@ -207,4 +212,27 @@ public TypeAwareResource exportDataAsTurtle(URI... contexts) {
ExportFormat.TURTLE.getFileExtension());
}
}

/**
* Inserts the specified raw data into the repository.
* <p>
* This method allows bypassing the JOPA-based persistence layer and thus should be used very carefully and
* sparsely.
*
* @param data Data to insert
*/
public void insertRawData(Collection<Quad> data) {
Objects.requireNonNull(data);
final org.eclipse.rdf4j.repository.Repository repo = em.unwrap(org.eclipse.rdf4j.repository.Repository.class);
try (final RepositoryConnection con = repo.getConnection()) {
final ValueFactory vf = con.getValueFactory();
data.forEach(quad -> {
Value v = quad.object() instanceof URI ? vf.createIRI(quad.object().toString()) :
Rdf4jUtils.createLiteral(quad.object(), config.getLanguage(), vf);

con.add(vf.createIRI(quad.subject().toString()), vf.createIRI(quad.predicate().toString()), v,
quad.context() != null ? vf.createIRI(quad.context().toString()) : null);
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
import cz.cvut.kbss.termit.model.Glossary;
import cz.cvut.kbss.termit.model.Vocabulary;
import cz.cvut.kbss.termit.persistence.dao.VocabularyDao;
import cz.cvut.kbss.termit.service.importer.VocabularyImporter;
import cz.cvut.kbss.termit.util.Configuration;
import cz.cvut.kbss.termit.util.Utils;
import jakarta.validation.constraints.NotNull;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Model;
Expand Down Expand Up @@ -71,7 +73,7 @@
*/
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class SKOSImporter {
public class SKOSImporter implements VocabularyImporter {

private static final Logger LOG = LoggerFactory.getLogger(SKOSImporter.class);

Expand Down Expand Up @@ -102,49 +104,17 @@ public SKOSImporter(Configuration config, VocabularyDao vocabularyDao, EntityMan
this.em = em;
}

/**
* Imports a new vocabulary from the specified streams representing the vocabulary in SKOS format.
*
* @param rename Whether to change vocabulary, glossary and term IRIs in case of a conflict with existing
* data
* @param mediaType Input data media type
* @param persist Consumer of the imported vocabulary, used to save the imported data
* @param inputStreams Streams containing the imported SKOS data
* @return The imported vocabulary
* @throws VocabularyExistsException If a vocabulary/glossary with the same identifier already exists and
* {@code rename} is set to {@code false}
* @throws IllegalArgumentException Indicates invalid input data, e.g., no input streams, missing language tags
* etc.
*/
public Vocabulary importVocabulary(boolean rename, String mediaType, final Consumer<Vocabulary> persist,
final InputStream... inputStreams) {
return importVocabulary(rename, null, mediaType, persist, inputStreams);
}

/**
* Imports a SKOS vocabulary from the specified streams, possibly replacing an existing one.
* <p>
* If the specified {@code vocabularyIri} identifies an existing vocabulary, its content is replaced with the
* imported data.
*
* @param vocabularyIri Target vocabulary identifier
* @param mediaType Input data media type
* @param persist Consumer of the imported vocabulary, used to save the imported data
* @param inputStreams Streams containing the imported SKOS data
* @return The imported vocabulary
* @throws IllegalArgumentException Indicates invalid input data, e.g., no input streams, missing language tags
* etc.
*/
public Vocabulary importVocabulary(URI vocabularyIri, String mediaType, final Consumer<Vocabulary> persist,
final InputStream... inputStreams) {
Objects.requireNonNull(vocabularyIri);
return importVocabulary(false, vocabularyIri, mediaType, persist, inputStreams);
@Override
public Vocabulary importVocabulary(ImportConfiguration config, ImportInput data) {
Objects.requireNonNull(config);
Objects.requireNonNull(data);
return importVocabulary(config.allowReIdentify(), config.vocabularyIri(), data.mediaType(), config.prePersist(), data.data());
}

private Vocabulary importVocabulary(final boolean rename,
final URI vocabularyIri,
final String mediaType,
final Consumer<Vocabulary> persist,
final Consumer<Vocabulary> prePersist,
final InputStream... inputStreams) {
if (inputStreams.length == 0) {
throw new IllegalArgumentException("No input provided for importing vocabulary.");
Expand Down Expand Up @@ -184,7 +154,8 @@ private Vocabulary importVocabulary(final boolean rename,
em.flush();
em.clear();

persist.accept(vocabulary);
prePersist.accept(vocabulary);
vocabularyDao.persist(vocabulary);
addDataIntoRepository(vocabulary.getUri());
LOG.debug("Vocabulary import successfully finished.");
return vocabulary;
Expand Down Expand Up @@ -391,4 +362,14 @@ private void handleGlossaryStringProperty(IRI property, Consumer<MultilingualStr
private void setVocabularyDescriptionFromGlossary(final Vocabulary vocabulary) {
handleGlossaryStringProperty(DCTERMS.DESCRIPTION, vocabulary::setDescription);
}

/**
* Checks whether this importer supports the specified media type.
*
* @param mediaType Media type to check
* @return {@code true} when media type is supported, {@code false} otherwise
*/
public static boolean supportsMediaType(@NotNull String mediaType) {
return Rio.getParserFormatForMIMEType(mediaType).isPresent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package cz.cvut.kbss.termit.persistence.dao.util;

import java.net.URI;

public record Quad(URI subject, URI predicate, Object object, URI context) {
}
16 changes: 16 additions & 0 deletions src/main/java/cz/cvut/kbss/termit/rest/VocabularyController.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import cz.cvut.kbss.termit.service.business.VocabularyService;
import cz.cvut.kbss.termit.util.Configuration;
import cz.cvut.kbss.termit.util.Constants.QueryParams;
import cz.cvut.kbss.termit.util.TypeAwareResource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
Expand All @@ -44,6 +45,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -193,6 +195,20 @@ public ResponseEntity<Void> createVocabulary(
return ResponseEntity.created(locationWithout(generateLocation(vocabulary.getUri()), "/import")).build();
}

@Operation(description = "Gets a template Excel file that can be used to import terms into TermIt")
@ApiResponse(responseCode = "200", description = "Template Excel file is returned as attachment")
@GetMapping("/import/template")
@PreAuthorize("permitAll()")
public ResponseEntity<TypeAwareResource> getExcelTemplateFile() {
final TypeAwareResource template = vocabularyService.getExcelTemplateFile();
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(
template.getMediaType().orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE)))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + template.getFilename() + "\"")
.body(template);
}

URI locationWithout(URI location, String toRemove) {
return URI.create(location.toString().replace(toRemove, ""));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ public ResponseEntity<ErrorInfo> invalidLanguageConstantException(HttpServletReq
}

@ExceptionHandler
public ResponseEntity<ErrorInfo> vocabularyImportException(HttpServletRequest request,
public ResponseEntity<ErrorInfo> invalidTermStateException(HttpServletRequest request,
InvalidTermStateException e) {
logException(e, request);
return new ResponseEntity<>(
Expand Down
26 changes: 15 additions & 11 deletions src/main/java/cz/cvut/kbss/termit/service/business/TermService.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.lang.NonNull;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -307,8 +308,8 @@ public Term findRequired(URI id) {
/**
* Gets a reference to a Term with the specified identifier.
* <p>
* Note that this method is not protected by ACL-based authorization and should thus not be used without some
* other type of authorization.
* Note that this method is not protected by ACL-based authorization and should thus not be used without some other
* type of authorization.
*
* @param id Term identifier
* @return Matching Term reference wrapped in an {@code Optional}
Expand All @@ -327,12 +328,12 @@ public Term getReference(URI id) {
public List<Term> findSubTerms(Term parent) {
Objects.requireNonNull(parent);
return parent.getSubTerms() == null ? Collections.emptyList() :
parent.getSubTerms().stream().map(u -> repositoryService.find(u.getUri()).orElseThrow(
() -> new NotFoundException(
"Child of term " + parent + " with id " + u.getUri() + " not found!")))
.sorted(Comparator.comparing((Term t) -> t.getLabel().get(config.getPersistence().getLanguage()),
Comparator.nullsLast(Comparator.naturalOrder())))
.collect(Collectors.toList());
parent.getSubTerms().stream().map(u -> repositoryService.find(u.getUri()).orElseThrow(
() -> new NotFoundException(
"Child of term " + parent + " with id " + u.getUri() + " not found!")))
.sorted(Comparator.comparing((Term t) -> t.getLabel().get(config.getPersistence().getLanguage()),
Comparator.nullsLast(Comparator.naturalOrder())))
.collect(Collectors.toList());
}

/**
Expand Down Expand Up @@ -422,7 +423,7 @@ public Term update(Term term) {
* @param term Term to remove
*/
@PreAuthorize("@termAuthorizationService.canRemove(#term)")
public void remove(Term term) {
public void remove(@NonNull Term term) {
Objects.requireNonNull(term);
repositoryService.remove(term);
}
Expand Down Expand Up @@ -522,14 +523,17 @@ public void setState(Term term, URI state) {
private void checkForInvalidTerminalStateAssignment(Term term, URI state) {
final List<RdfsResource> states = languageService.getTermStates();
final Predicate<URI> isStateTerminal = (URI s) -> states.stream().filter(r -> r.getUri().equals(s)).findFirst()
.map(r -> r.hasType(cz.cvut.kbss.termit.util.Vocabulary.s_c_koncovy_stav_pojmu))
.map(r -> r.hasType(
cz.cvut.kbss.termit.util.Vocabulary.s_c_koncovy_stav_pojmu))
.orElse(false);
if (!isStateTerminal.test(state)) {
return;
}
if (Utils.emptyIfNull(term.getSubTerms()).stream()
.anyMatch(Predicate.not(ti -> isStateTerminal.test(ti.getState())))) {
throw new InvalidTermStateException("Cannot set state of term " + term + " to terminal when at least one of its sub-terms is not in terminal state.", "error.term.state.terminal.liveChildren");
throw new InvalidTermStateException(
"Cannot set state of term " + term + " to terminal when at least one of its sub-terms is not in terminal state.",
"error.term.state.terminal.liveChildren");
}
}

Expand Down
Loading

0 comments on commit c95ab19

Please sign in to comment.