diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/AddNetworkApiController.java b/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/AddNetworkApiController.java index 274f16e6..6bfd58a3 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/AddNetworkApiController.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/AddNetworkApiController.java @@ -1,5 +1,11 @@ package edu.asu.diging.quadriga.api.v1; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import org.bson.types.ObjectId; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -8,6 +14,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @@ -19,6 +26,7 @@ import edu.asu.diging.quadriga.core.exception.NodeNotFoundException; import edu.asu.diging.quadriga.core.exceptions.CollectionNotFoundException; import edu.asu.diging.quadriga.core.exceptions.InvalidObjectIdException; +import edu.asu.diging.quadriga.core.exceptions.MappedTripleGroupNotFoundException; import edu.asu.diging.quadriga.core.model.MappedTripleGroup; import edu.asu.diging.quadriga.core.model.MappedTripleType; import edu.asu.diging.quadriga.core.service.EventGraphService; @@ -60,23 +68,24 @@ public HttpStatus processJson(@RequestBody Quadruple quadruple, @PathVariable St logger.error("Quadruple not present in network submission request for collectionId: " + collectionId); return HttpStatus.BAD_REQUEST; } - + // Next, we check whether a collection and mappedTripleGroup is present // Every time a new network is submitted, the triple in that network has to be added as the // default MappedTripleGroup for given collectionId MappedTripleGroup mappedTripleGroup; try { mappedTripleGroup = mappedTripleGroupService.get(collectionId, MappedTripleType.DEFAULT_MAPPING); + logger.info("mapped triple found " + mappedTripleGroup); if(mappedTripleGroup == null) { return HttpStatus.NOT_FOUND; } - } catch(InvalidObjectIdException | CollectionNotFoundException e) { + } catch(InvalidObjectIdException | CollectionNotFoundException | MappedTripleGroupNotFoundException e) { logger.error("Couldn't submit network", e); return HttpStatus.NOT_FOUND; } eventGraphService.mapNetworkAndSave(quadruple.getGraph(), collectionId); - + try { // The new MappedTripleGroup's Id has to be added to Concepts and Predicates mappedTripleService.storeMappedGraph(quadruple.getGraph(), mappedTripleGroup); diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/model/Metadata.java b/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/model/Metadata.java index 77d16419..74e6f6e8 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/model/Metadata.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/model/Metadata.java @@ -1,5 +1,6 @@ package edu.asu.diging.quadriga.api.v1.model; +import edu.asu.diging.quadriga.core.model.Context; import edu.asu.diging.quadriga.core.model.DefaultMapping; public class Metadata { diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/model/NodeData.java b/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/model/NodeData.java index 18bce053..29c9d3a3 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/model/NodeData.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/model/NodeData.java @@ -1,5 +1,7 @@ package edu.asu.diging.quadriga.api.v1.model; +import edu.asu.diging.quadriga.core.model.Context; + public class NodeData { private String label; diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/config/PersistenceConfig.java b/quadriga/src/main/java/edu/asu/diging/quadriga/config/PersistenceConfig.java index 4083b72a..31445274 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/config/PersistenceConfig.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/config/PersistenceConfig.java @@ -24,7 +24,11 @@ @Configuration @PropertySource("classpath:config.properties") @EnableTransactionManagement -@EnableJpaRepositories(basePackages = { "edu.asu.diging.quadriga.core.data", "edu.asu.diging.simpleusers.core.data" }) +@EnableJpaRepositories(basePackages = { + "edu.asu.diging.quadriga.core.data", + "edu.asu.diging.quadriga.core.conceptpower.data", + "edu.asu.diging.simpleusers.core.data" +}) public class PersistenceConfig { @Autowired @@ -45,7 +49,10 @@ public DataSource dataSource() { public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource()); - em.setPackagesToScan(new String[] { "edu.asu.diging.quadriga.core.model", "edu.asu.diging.simpleusers.core.model" }); + em.setPackagesToScan(new String[] { "edu.asu.diging.quadriga.core.model", + "edu.asu.diging.quadriga.core.conceptpower.model", + "edu.asu.diging.simpleusers.core.model" + }); JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); em.setJpaVendorAdapter(vendorAdapter); diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/data/ConceptCacheRepository.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/data/ConceptCacheRepository.java new file mode 100644 index 00000000..7baa4317 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/data/ConceptCacheRepository.java @@ -0,0 +1,25 @@ +package edu.asu.diging.quadriga.core.conceptpower.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.stereotype.Repository; + +import edu.asu.diging.quadriga.core.conceptpower.model.ConceptCache; + +/** + * + * An interface used to retrieve data from conceptpower_concept_entry table that + * stores data extracted from ConceptPower + * + * @author poojakulkarni + * + */ +@Repository +public interface ConceptCacheRepository extends PagingAndSortingRepository { + + @Query("SELECT c from ConceptCache c WHERE ?1 in elements(c.alternativeUris)") + public List findConceptByAlternativeURI(String uri); + +} \ No newline at end of file diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/data/ConceptTypeRepository.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/data/ConceptTypeRepository.java new file mode 100644 index 00000000..17c8b7a9 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/data/ConceptTypeRepository.java @@ -0,0 +1,18 @@ +package edu.asu.diging.quadriga.core.conceptpower.data; + +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.stereotype.Repository; + +import edu.asu.diging.quadriga.core.conceptpower.model.ConceptType; + +/** + * An interface used to retrieve data from conceptpower_concept_type table that + * stores information for a concept's type extracted from ConceptPower + * + * @author poojakulkarni + * + */ +@Repository +public interface ConceptTypeRepository extends PagingAndSortingRepository { + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/model/ConceptCache.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/model/ConceptCache.java new file mode 100644 index 00000000..008c7f53 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/model/ConceptCache.java @@ -0,0 +1,250 @@ +package edu.asu.diging.quadriga.core.conceptpower.model; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +import javax.persistence.CollectionTable; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; +import javax.persistence.Transient; + +import org.apache.commons.lang3.StringUtils; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; + +/** + * This class represents the 'conceptpower_concept_cache' table present in the + * database that stores cached concept data + * + * @author Digital Innovation Group + * + */ +@Entity +@Table(name = "conceptpower_concept_cache") +public class ConceptCache implements Serializable, Comparable { + + private static final long serialVersionUID = 1L; + + @Id + private String uri; + private String id; + private String word; + private String pos; + + @Lob + private String description; + + private String conceptList; + private String typeId; + private boolean deleted; + private LocalDateTime lastUpdated; + + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "conceptpower_alternative_uris") + private List alternativeUris; + + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "conceptpower_equal_to") + private List equalTo; + + @ElementCollection + @LazyCollection(LazyCollectionOption.FALSE) + @CollectionTable(name = "conceptpower_wordnetids") + private List wordNetIds; + + private String creatorId; + + @Transient + private ConceptType conceptType; + + public ConceptCache() { + this.lastUpdated = LocalDateTime.now(); + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getWord() { + return word; + } + + public void setWord(String word) { + this.word = word; + } + + public String getPos() { + return pos; + } + + public void setPos(String pos) { + this.pos = pos; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getConceptList() { + return conceptList; + } + + public void setConceptList(String conceptList) { + this.conceptList = conceptList; + } + + public String getTypeId() { + return typeId; + } + + public void setTypeId(String typeId) { + this.typeId = typeId; + } + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } + + public LocalDateTime getLastUpdated() { + return lastUpdated; + } + + public void setLastUpdated(LocalDateTime lastUpdated) { + this.lastUpdated = lastUpdated; + } + + public List getAlternativeUris() { + return alternativeUris; + } + + public void setAlternativeUris(List alternativeUris) { + this.alternativeUris = alternativeUris; + } + + public List getEqualTo() { + return equalTo; + } + + public void setEqualTo(List equalTo) { + this.equalTo = equalTo; + } + + public List getWordNetIds() { + return wordNetIds; + } + + public void setWordNetIds(List wordNetIds) { + this.wordNetIds = wordNetIds; + } + + public String getCreatorId() { + return creatorId; + } + + public void setCreatorId(String creatorId) { + this.creatorId = creatorId; + } + + public ConceptType getConceptType() { + return conceptType; + } + + public void setConceptType(ConceptType conceptType) { + this.conceptType = conceptType; + } + + /** + * Compares the old and new cache values to determine if a difference exists. + * + *If both old and new cache values are {@code null}, blank, or empty, no difference is present. + *If the old value is {@code null}, blank, or empty, and the new value is not {@code null}, blank, or empty, a difference is present. + *If the old value is not {@code null}, blank, or empty, and the new value is {@code null}, blank, or empty, a difference is present. + *If both values are not {@code null}, blank, or empty, their content must be compared to determine if a difference exists. + * + */ + @Override + public int compareTo(ConceptCache conceptCache) { + + if(isDifferentList(conceptCache.getAlternativeUris(), this.getAlternativeUris())) { + return -1; + } + if(isDifferentList(conceptCache.getEqualTo(), this.getEqualTo())) { + return -1; + } + if(isDifferentList(conceptCache.getWordNetIds(), this.getWordNetIds())) { + return -1; + } + if (isDifferentString(conceptCache.getConceptList(), this.getConceptList())) { + return -1; + } + if (isDifferentString(conceptCache.getCreatorId(), this.getCreatorId())) { + return -1; + } + if (isDifferentString(conceptCache.getDescription(), this.getDescription())) { + return -1; + } + if (isDifferentString(conceptCache.getId(), this.getId())) { + return -1; + } + if (isDifferentString(conceptCache.getPos(), this.getPos())) { + return -1; + } + if (isDifferentString(conceptCache.getTypeId(), this.getTypeId())) { + return -1; + } + if (isDifferentString(conceptCache.getUri(), this.getUri())) { + return -1; + } + if (isDifferentString(conceptCache.getWord(), this.getWord())) { + return -1; + } + return 0; + } + + private static boolean isDifferentString(String str1, String str2) { + if(!(StringUtils.isEmpty(str1) && StringUtils.isEmpty(str2)) + && (StringUtils.isEmpty(str1) || !str1.equals(str2))) { + return true; + } + return false; + } + + private static boolean isDifferentList(List list1, List list2) { + if (!(isNullOrEmpty(list1) && isNullOrEmpty(list2) + && (isNullOrEmpty(list1) || !list1.equals(list2)))) { + return true; + } + return false; + } + + private static boolean isNullOrEmpty(List list) { + return list == null || list.isEmpty(); + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/model/ConceptType.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/model/ConceptType.java new file mode 100644 index 00000000..0b0ecf2d --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/model/ConceptType.java @@ -0,0 +1,94 @@ +package edu.asu.diging.quadriga.core.conceptpower.model; + +import java.io.Serializable; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.Table; + +import org.apache.commons.lang3.StringUtils; + +@Entity +@Table(name = "conceptpower_concept_type_cache") +public class ConceptType implements Serializable, Comparable { + + private static final long serialVersionUID = 1L; + + @Id + private String id; + private String name; + private String uri; + + @Lob + private String description; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + /* + * If both old and new types are null/blank, nothing has changed + * If old value is null/blank, new value is not null/blank, difference present + * If old value is not null/blank, new value is null/blank, difference present + * If both are not null/blank, we need to check difference + * + */ + @Override + public int compareTo(ConceptType type) { + + if (type == null ) { + return -1; + } + if(isDifferentString(type.getId(), this.getId())) { + return -1; + } + if(isDifferentString(type.getDescription(), this.getDescription())) { + return -1; + } + if(isDifferentString(type.getName(), this.getName())) { + return -1; + } + if(isDifferentString(type.getUri(), this.getUri())) { + return -1; + } + return 0; + } + + private static boolean isDifferentString(String str1, String str2) { + if(!(StringUtils.isEmpty(str1) && StringUtils.isEmpty(str2)) + && (StringUtils.isEmpty(str1) || !str1.equals(str2))) { + return true; + } + return false; + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/AlternativeId.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/AlternativeId.java new file mode 100644 index 00000000..6a5bac54 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/AlternativeId.java @@ -0,0 +1,36 @@ +package edu.asu.diging.quadriga.core.conceptpower.reply.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ "concept_id", "concept_uri" }) +public class AlternativeId { + + @JsonProperty("concept_id") + private String conceptId; + @JsonProperty("concept_uri") + private String conceptUri; + + @JsonProperty("concept_id") + public String getConceptId() { + return conceptId; + } + + @JsonProperty("concept_id") + public void setConceptId(String conceptId) { + this.conceptId = conceptId; + } + + @JsonProperty("concept_uri") + public String getConceptUri() { + return conceptUri; + } + + @JsonProperty("concept_uri") + public void setConceptUri(String conceptUri) { + this.conceptUri = conceptUri; + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/ConceptEntry.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/ConceptEntry.java new file mode 100644 index 00000000..34dbacda --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/ConceptEntry.java @@ -0,0 +1,203 @@ + +package edu.asu.diging.quadriga.core.conceptpower.reply.model; + +import java.util.List; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +/** + * + * This class represents the JSON response received from ConceptPower, with key + * 'conceptPowerEntry' under conceptPowerReply + * + * @author poojakulkarni + * + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ "id", "lemma", "pos", "description", "conceptList", "type", "deleted", "concept_uri", "creator_id", + "equal_to", "modified_by", "similar_to", "synonym_ids", "wordnet_id", "alternativeIds" }) +public class ConceptEntry { + + @JsonProperty("id") + private String id; + @JsonProperty("lemma") + private String lemma; + @JsonProperty("pos") + private String pos; + @JsonProperty("description") + private String description; + @JsonProperty("conceptList") + private String conceptList; + @JsonProperty("type") + private Type type; + @JsonProperty("deleted") + private Boolean deleted; + @JsonProperty("concept_uri") + private String conceptUri; + @JsonProperty("creator_id") + private String creatorId; + @JsonProperty("equal_to") + private String equalTo; + @JsonProperty("modified_by") + private String modifiedBy; + @JsonProperty("similar_to") + private String similarTo; + @JsonProperty("synonym_ids") + private String synonymIds; + @JsonProperty("wordnet_id") + private String wordnetId; + @JsonProperty("alternativeIds") + private List alternativeIds = null; + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("id") + public void setId(String id) { + this.id = id; + } + + @JsonProperty("lemma") + public String getLemma() { + return lemma; + } + + @JsonProperty("lemma") + public void setLemma(String lemma) { + this.lemma = lemma; + } + + @JsonProperty("pos") + public String getPos() { + return pos; + } + + @JsonProperty("pos") + public void setPos(String pos) { + this.pos = pos; + } + + @JsonProperty("description") + public String getDescription() { + return description; + } + + @JsonProperty("description") + public void setDescription(String description) { + this.description = description; + } + + @JsonProperty("conceptList") + public String getConceptList() { + return conceptList; + } + + @JsonProperty("type") + public Type getType() { + return type; + } + + @JsonProperty("type") + public void setType(Type type) { + this.type = type; + } + + @JsonProperty("conceptList") + public void setConceptList(String conceptList) { + this.conceptList = conceptList; + } + + @JsonProperty("deleted") + public Boolean getDeleted() { + return deleted; + } + + @JsonProperty("deleted") + public void setDeleted(Boolean deleted) { + this.deleted = deleted; + } + + @JsonProperty("concept_uri") + public String getConceptUri() { + return conceptUri; + } + + @JsonProperty("concept_uri") + public void setConceptUri(String conceptUri) { + this.conceptUri = conceptUri; + } + + @JsonProperty("creator_id") + public String getCreatorId() { + return creatorId; + } + + @JsonProperty("creator_id") + public void setCreatorId(String creatorId) { + this.creatorId = creatorId; + } + + @JsonProperty("equal_to") + public String getEqualTo() { + return equalTo; + } + + @JsonProperty("equal_to") + public void setEqualTo(String equalTo) { + this.equalTo = equalTo; + } + + @JsonProperty("modified_by") + public String getModifiedBy() { + return modifiedBy; + } + + @JsonProperty("modified_by") + public void setModifiedBy(String modifiedBy) { + this.modifiedBy = modifiedBy; + } + + @JsonProperty("similar_to") + public String getSimilarTo() { + return similarTo; + } + + @JsonProperty("similar_to") + public void setSimilarTo(String similarTo) { + this.similarTo = similarTo; + } + + @JsonProperty("synonym_ids") + public String getSynonymIds() { + return synonymIds; + } + + @JsonProperty("synonym_ids") + public void setSynonymIds(String synonymIds) { + this.synonymIds = synonymIds; + } + + @JsonProperty("wordnet_id") + public String getWordnetId() { + return wordnetId; + } + + @JsonProperty("wordnet_id") + public void setWordnetId(String wordnetId) { + this.wordnetId = wordnetId; + } + + @JsonProperty("alternativeIds") + public List getAlternativeIds() { + return alternativeIds; + } + + @JsonProperty("alternativeIds") + public void setAlternativeIds(List alternativeIds) { + this.alternativeIds = alternativeIds; + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/ConceptPowerReply.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/ConceptPowerReply.java new file mode 100644 index 00000000..17f92115 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/ConceptPowerReply.java @@ -0,0 +1,46 @@ + +package edu.asu.diging.quadriga.core.conceptpower.reply.model; + +import java.util.List; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +/** + * + * This class represents the JSON response received from ConceptPower, with key + * 'conceptPowerReply' + * + * @author poojakulkarni + * + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ "conceptEntries", "pagination" }) +public class ConceptPowerReply { + + @JsonProperty("conceptEntries") + private List conceptEntries = null; + @JsonProperty("pagination") + private Object pagination; + + @JsonProperty("conceptEntries") + public List getConceptEntries() { + return conceptEntries; + } + + @JsonProperty("conceptEntries") + public void setConceptEntries(List conceptEntries) { + this.conceptEntries = conceptEntries; + } + + @JsonProperty("pagination") + public Object getPagination() { + return pagination; + } + + @JsonProperty("pagination") + public void setPagination(Object pagination) { + this.pagination = pagination; + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/Type.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/Type.java new file mode 100644 index 00000000..bb3b6712 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/reply/model/Type.java @@ -0,0 +1,53 @@ +package edu.asu.diging.quadriga.core.conceptpower.reply.model; + +import java.util.HashMap; +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ "type_id", "type_uri", "type_name" }) +public class Type { + + @JsonProperty("type_id") + private String typeId; + @JsonProperty("type_uri") + private String typeUri; + @JsonProperty("type_name") + private String typeName; + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("type_id") + public String getTypeId() { + return typeId; + } + + @JsonProperty("type_id") + public void setTypeId(String typeId) { + this.typeId = typeId; + } + + @JsonProperty("type_uri") + public String getTypeUri() { + return typeUri; + } + + @JsonProperty("type_uri") + public void setTypeUri(String typeUri) { + this.typeUri = typeUri; + } + + @JsonProperty("type_name") + public String getTypeName() { + return typeName; + } + + @JsonProperty("type_name") + public void setTypeName(String typeName) { + this.typeName = typeName; + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptCacheService.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptCacheService.java new file mode 100644 index 00000000..d2141e6f --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptCacheService.java @@ -0,0 +1,46 @@ +package edu.asu.diging.quadriga.core.conceptpower.service; + +import edu.asu.diging.quadriga.core.conceptpower.model.ConceptCache; + +/** + * A service for working with the ConceptCache entries in the database + * + * @author poojakulkarni + * + */ +public interface ConceptCacheService { + + /** + * This method gets a ConceptCache entry from the database If it is not present, + * it checks the alternative URIs to get a conceptCache entry + * + * @param uri used to check in alternativeUris + * @return a conceptCache entry + */ + public ConceptCache getConceptByUri(String uri); + + /** + * This method checks the AlternativeURIs ElementCollection of ConceptCache The + * Concept which has listed the provided URI as an alternativeURI would be + * returned + * + * @param uri to be checked as an alternativeURI + * @return a ConceptCache object from ConceptCache table + */ + public ConceptCache getConceptByAlternativeUri(String uri); + + /** + * This method saves the provided ConceptCache entity in the database + * + * @param conceptCache is the entity to be saved + */ + public void saveConceptCache(ConceptCache conceptCache); + + /** + * This method deleted the ConceptCache entry that matches the provided URI + * + * @param uri used to delete the ConceptCache entry + */ + public void deleteConceptCacheByUri(String uri); + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptPowerConnectorService.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptPowerConnectorService.java new file mode 100644 index 00000000..ef9a1b9c --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptPowerConnectorService.java @@ -0,0 +1,17 @@ +package edu.asu.diging.quadriga.core.conceptpower.service; + +import edu.asu.diging.quadriga.core.conceptpower.reply.model.ConceptPowerReply; +import edu.asu.diging.quadriga.core.exceptions.ConceptpowerNoResponseException; + +/** + * A service that extracts XML data from ConceptPower using REST calls and + * parses the result into Java objects using JAXB model classes + * + * @author poojakulkarni + * + */ +public interface ConceptPowerConnectorService { + + public ConceptPowerReply getConceptPowerReply(String conceptURI) throws ConceptpowerNoResponseException; + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptPowerService.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptPowerService.java new file mode 100644 index 00000000..e8313707 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptPowerService.java @@ -0,0 +1,40 @@ +package edu.asu.diging.quadriga.core.conceptpower.service; + +import edu.asu.diging.quadriga.core.conceptpower.model.ConceptCache; +import edu.asu.diging.quadriga.core.conceptpower.reply.model.ConceptPowerReply; +import edu.asu.diging.quadriga.core.exceptions.ConceptpowerNoResponseException; + +/** + * This service is used to get concept data from ConceptPower + * + * @author poojakulkarni + * + */ +public interface ConceptPowerService { + + /** + * This method checks if Concept is present in DB If concept is present in DB & + * updated within last 2 days then it just returns the concept If concept is + * present in DB & is not updated within last 2 days, it calls conceptpower, + * gets latest concept data, updates the concept in the database and then + * returns the concept If concept is not present in DB, it calls conceptpower + * gets latest concept data, creates an entry for the concept in the database + * and returns the concept + * + * @param uri used to search entry in database or make a REST call to + * conceptpower + * @return the conceptCache database entry + * @throws ConceptpowerNoResponseException + */ + public ConceptCache getConceptByUri(String uri) throws ConceptpowerNoResponseException; + + /** + * This method maps the ConceptPowerReply object returned from ConceptPower to a + * ConceptCache object that would be stored in the database + * + * @param conceptPowerReply is the object used to generate a ConceptCache object + * @return the generated ConceptCache object + */ + public ConceptCache mapConceptPowerReplyToConceptCache(ConceptPowerReply conceptPowerReply); + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptTypeService.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptTypeService.java new file mode 100644 index 00000000..103ae153 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/ConceptTypeService.java @@ -0,0 +1,9 @@ +package edu.asu.diging.quadriga.core.conceptpower.service; + +import edu.asu.diging.quadriga.core.conceptpower.model.ConceptType; + +public interface ConceptTypeService { + + public void saveConceptType(ConceptType conceptType); + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptCacheServiceImpl.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptCacheServiceImpl.java new file mode 100644 index 00000000..a17bf468 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptCacheServiceImpl.java @@ -0,0 +1,48 @@ +package edu.asu.diging.quadriga.core.conceptpower.service.impl; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import edu.asu.diging.quadriga.core.conceptpower.data.ConceptCacheRepository; +import edu.asu.diging.quadriga.core.conceptpower.model.ConceptCache; +import edu.asu.diging.quadriga.core.conceptpower.service.ConceptCacheService; + +@Service +public class ConceptCacheServiceImpl implements ConceptCacheService { + + @Autowired + private ConceptCacheRepository conceptCacheRepository; + + @Override + public ConceptCache getConceptByUri(String uri) { + ConceptCache conceptCache = conceptCacheRepository.findById(uri).orElse(null); + if (conceptCache == null) { + conceptCache = getConceptByAlternativeUri(uri); + } + return conceptCache; + } + + @Override + public ConceptCache getConceptByAlternativeUri(String uri) { + List conceptCacheList = conceptCacheRepository.findConceptByAlternativeURI(uri); + if (conceptCacheList != null && !conceptCacheList.isEmpty()) { + return conceptCacheList.get(0); + } + return null; + } + + @Override + public void saveConceptCache(ConceptCache conceptCache) { + conceptCacheRepository.save(conceptCache); + } + + @Override + public void deleteConceptCacheByUri(String uri) { + if (uri != null && !uri.isEmpty()) { + conceptCacheRepository.deleteById(uri); + } + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptPowerConnectorServiceImpl.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptPowerConnectorServiceImpl.java new file mode 100644 index 00000000..019c32b9 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptPowerConnectorServiceImpl.java @@ -0,0 +1,70 @@ +package edu.asu.diging.quadriga.core.conceptpower.service.impl; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import edu.asu.diging.quadriga.core.conceptpower.reply.model.ConceptPowerReply; +import edu.asu.diging.quadriga.core.conceptpower.service.ConceptPowerConnectorService; +import edu.asu.diging.quadriga.core.exceptions.ConceptpowerNoResponseException; + +@Service +@PropertySource({ "classpath:config.properties" }) +public class ConceptPowerConnectorServiceImpl implements ConceptPowerConnectorService { + + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Value("${conceptpower_base_url}") + private String conceptPowerBaseURL; + + @Value("${conceptpower_id_endpoint}") + private String conceptPowerIdEndpoint; + + private RestTemplate restTemplate; + + public ConceptPowerConnectorServiceImpl() { + restTemplate = new RestTemplate(); + } + + @Override + public ConceptPowerReply getConceptPowerReply(String conceptURI) throws ConceptpowerNoResponseException { + Map parameters = new HashMap<>(); + parameters.put("concept_uri", conceptURI); + String conceptPowerURL = conceptPowerBaseURL + conceptPowerIdEndpoint; + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + HttpEntity httpEntity = new HttpEntity<>(httpHeaders); + ResponseEntity response; + + try { + response = restTemplate.exchange(conceptPowerURL, + HttpMethod.GET, httpEntity, ConceptPowerReply.class, parameters); + if(response == null) { + throw new ConceptpowerNoResponseException("ConceptPower returned a null response for URI: " + conceptURI); + } + } + catch (HttpClientErrorException e) { + throw new ConceptpowerNoResponseException("Error occurred while contacting ConceptPower for URI: " + conceptURI,e); + } + catch(Exception e) { + throw new ConceptpowerNoResponseException("Unexpected error occurred while processing ConceptPower response for URI: " + conceptURI,e); + } + return response.getBody(); + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptPowerServiceImpl.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptPowerServiceImpl.java new file mode 100644 index 00000000..4e238b80 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptPowerServiceImpl.java @@ -0,0 +1,209 @@ +package edu.asu.diging.quadriga.core.conceptpower.service.impl; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import edu.asu.diging.quadriga.core.conceptpower.model.ConceptCache; +import edu.asu.diging.quadriga.core.conceptpower.model.ConceptType; +import edu.asu.diging.quadriga.core.conceptpower.reply.model.ConceptEntry; +import edu.asu.diging.quadriga.core.conceptpower.reply.model.ConceptPowerReply; +import edu.asu.diging.quadriga.core.conceptpower.reply.model.Type; +import edu.asu.diging.quadriga.core.conceptpower.service.ConceptCacheService; +import edu.asu.diging.quadriga.core.conceptpower.service.ConceptPowerConnectorService; +import edu.asu.diging.quadriga.core.conceptpower.service.ConceptPowerService; +import edu.asu.diging.quadriga.core.conceptpower.service.ConceptTypeService; +import edu.asu.diging.quadriga.core.exceptions.ConceptpowerNoResponseException; + +@Service +public class ConceptPowerServiceImpl implements ConceptPowerService { + + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Autowired + private ConceptCacheService conceptCacheService; + + @Autowired + private ConceptTypeService conceptTypeService; + + @Autowired + private ConceptPowerConnectorService conceptPowerConnectorService; + + @Value("${conceptCacheUpdateInterval}") + private Integer conceptCacheUpdateInterval; + + @Override + public ConceptCache getConceptByUri(String uri) throws ConceptpowerNoResponseException { + + ConceptCache conceptCache = conceptCacheService.getConceptByUri(uri); + + conceptCacheUpdateInterval=0; + if (conceptCache == null || ChronoUnit.HOURS.between(conceptCache.getLastUpdated(), LocalDateTime.now()) >= conceptCacheUpdateInterval) { + conceptCache = saveConceptCacheFromConceptPowerReply(conceptCache, conceptPowerConnectorService.getConceptPowerReply(uri), uri); + } + return conceptCache; + } + + /** + * This method first saves the ConceptCache entry in the database which is + * mapped from ConceptPowerReply + * + * Then it saves the corresponding ConceptType entry in the database, if one + * exists + * + * After that, it checks if there are any concepts in the database which have + * the same URI as this concept's alternativeURIs. If such concepts exist, they + * will be deleted + * + * E.g.: Consider => Concepts => {AlternativeURIs} + * + * Existing Concepts in the database: C2 => {C2, C4}, C3 => {C3, C5} + * + * New Concept to be added to the database: C1 => {C1, C2, C3, C5} + * + * In this case, delete C2 and C3 from the database as C1 has listed C2 and C3 + * as alternatives If in the future, the concepts C2 or C3 are searched in the + * database, C1 will be returned + * + * @param conceptPowerReply is the object used to generate a ConceptCache object + * @param uri to be used for logging in case no ConceptCache entry + * was generated + */ + private ConceptCache saveConceptCacheFromConceptPowerReply(ConceptCache conceptCacheOld, ConceptPowerReply conceptPowerReply, String uri) { + ConceptCache conceptCache = mapConceptPowerReplyToConceptCache(conceptPowerReply); + + // Before returning, we need to check if we've updated ConceptCache or not + // If no diff was found, conceptCache won't be updated and 'lastUpdated' would stay the same + boolean updated = updateConceptCache(conceptCache, conceptCacheOld, uri); + + updated = updated || updateConceptType(conceptCache,conceptCacheOld); + + return updated ? conceptCache : conceptCacheOld; + } + + /** + * Updates Concept Type + * @param conceptCache + * @param conceptCacheOld + * @return + */ + private boolean updateConceptType(ConceptCache conceptCache, ConceptCache conceptCacheOld) { + if(conceptCache != null && conceptCache.getConceptType() != null) { + + ConceptType conceptType = conceptCache.getConceptType(); + ConceptType conceptTypeOld = null; + + if(conceptCacheOld != null) { + conceptTypeOld = conceptCacheOld.getConceptType(); + } + + // ConceptPower returned a concept type and either no conceptType entry exists in the DB + // or if one exists, it is different from the current conceptType entry + if (conceptType != null && (conceptTypeOld == null || conceptTypeOld.compareTo(conceptType) != 0)) { + + conceptCache.setConceptType(conceptType); + conceptTypeService.saveConceptType(conceptType); + return true; + } + } + return false; + } + + /** + * This method is to update concept cache. + * + * It checks if a concept is returned by conceptpower + * and no conceptcache exists in db or if it does, it is different from the current conceptCache entry + * and updates the conceptcache + * + * @param conceptCache + * @param conceptCacheOld + * @param uri + * + * @return + */ + private boolean updateConceptCache(ConceptCache conceptCache, ConceptCache conceptCacheOld, String uri) { + if (conceptCache != null && (conceptCacheOld == null || conceptCacheOld.compareTo(conceptCache) != 0)) { + conceptCacheService.saveConceptCache(conceptCache); + + Optional.ofNullable(conceptCache.getAlternativeUris()) + .ifPresent(nonNullAlternativeUris -> nonNullAlternativeUris + .stream() + .filter(alternativeUri -> alternativeUri != null) + .filter(nonNullAlternativeUri -> !nonNullAlternativeUri.equals(conceptCache.getUri())) + .forEach(alternativeUriValue -> conceptCacheService.deleteConceptCacheByUri(alternativeUriValue))); + return true; + + } else if(conceptCache == null) { + logger.error("ConceptPower did not return any concept entries for uri: " + uri); + + } + return false; + } + + @Override + public ConceptCache mapConceptPowerReplyToConceptCache(ConceptPowerReply conceptPowerReply) { + // If we get multiple ConceptPower entries in the reply, we use the first one + List conceptEntries = conceptPowerReply.getConceptEntries(); + ConceptCache conceptCache = null; + + if (conceptEntries == null || conceptEntries.isEmpty()) { + return conceptCache; + } + ConceptEntry conceptEntry = conceptEntries.get(0); + conceptCache = new ConceptCache(); + conceptCache.setUri(conceptEntry.getConceptUri()); + conceptCache.setConceptList(conceptEntry.getConceptList()); + conceptCache.setDescription(conceptEntry.getDescription()); + conceptCache.setPos(conceptEntry.getPos()); + conceptCache.setDeleted(conceptEntry.getDeleted() == null ? false : conceptEntry.getDeleted()); + conceptCache.setCreatorId(conceptEntry.getCreatorId()); + conceptCache.setWord(conceptEntry.getLemma()); + conceptCache.setId(conceptEntry.getId()); + + if (conceptEntry.getWordnetId() != null && !conceptEntry.getWordnetId().trim().equals("")) { + conceptCache.setWordNetIds(Arrays.asList(conceptEntry.getWordnetId().split(","))); + } else { + conceptCache.setWordNetIds(new ArrayList<>()); + } + + if (conceptEntry.getEqualTo() != null && !conceptEntry.getEqualTo().trim().equals("")) { + conceptCache.setEqualTo(Arrays.asList(conceptEntry.getEqualTo().split(","))); + } else { + conceptCache.setEqualTo(new ArrayList<>()); + } + + if(conceptEntry.getAlternativeIds() != null && !conceptEntry.getAlternativeIds().isEmpty()) { + conceptCache.setAlternativeUris( + conceptEntry.getAlternativeIds() + .stream() + .map(alternativeId -> alternativeId.getConceptUri()) + .filter(nullableAltUri -> nullableAltUri != null) + .filter(alternativeUri -> !alternativeUri.equals("")) + .collect(Collectors.toList())); + } + + if (conceptEntry.getType() != null) { + Type type = conceptEntry.getType(); + ConceptType conceptType = new ConceptType(); + conceptType.setUri(type.getTypeUri()); + conceptType.setId(type.getTypeId()); + conceptType.setName(type.getTypeName()); + conceptType.setDescription(""); + conceptCache.setConceptType(conceptType); + conceptCache.setTypeId(conceptEntry.getType().getTypeUri()); + } + return conceptCache; + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptTypeServiceImpl.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptTypeServiceImpl.java new file mode 100644 index 00000000..b2563431 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/conceptpower/service/impl/ConceptTypeServiceImpl.java @@ -0,0 +1,21 @@ +package edu.asu.diging.quadriga.core.conceptpower.service.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import edu.asu.diging.quadriga.core.conceptpower.data.ConceptTypeRepository; +import edu.asu.diging.quadriga.core.conceptpower.model.ConceptType; +import edu.asu.diging.quadriga.core.conceptpower.service.ConceptTypeService; + +@Service +public class ConceptTypeServiceImpl implements ConceptTypeService { + + @Autowired + private ConceptTypeRepository conceptTypeRepository; + + @Override + public void saveConceptType(ConceptType conceptType) { + conceptTypeRepository.save(conceptType); + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/data/EventGraphRepository.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/data/EventGraphRepository.java index 3614428d..c11812df 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/core/data/EventGraphRepository.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/data/EventGraphRepository.java @@ -4,12 +4,26 @@ import java.util.Optional; import org.bson.types.ObjectId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; import edu.asu.diging.quadriga.core.model.EventGraph; public interface EventGraphRepository extends MongoRepository { + public Optional> findByCollectionIdOrderByCreationTimeDesc(ObjectId collectionId); + + @Query(value = "{'context.sourceUri': ?0}") + public Optional> findByContextSourceUri(String sourceURI); + public Optional findFirstByCollectionIdOrderByCreationTimeDesc(ObjectId collectionId); + + public Optional> findByCollectionId(ObjectId collectionId); + + public Optional> findByCollectionIdOrderByCreationTimeAsc(ObjectId collectionId, Pageable Pageable); + + public Optional> findByCollectionIdOrderByCreationTimeAsc(ObjectId collectionId); } diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/data/neo4j/PredicateRepository.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/data/neo4j/PredicateRepository.java index 16ea2d6a..721a634e 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/core/data/neo4j/PredicateRepository.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/data/neo4j/PredicateRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; + import org.springframework.data.neo4j.annotation.Query; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.repository.query.Param; @@ -14,7 +15,7 @@ public interface PredicateRepository extends Neo4jRepository { public Optional> findByMappedTripleGroupId(String mappedTripleGroupId); - + public Optional> findByMappedTripleGroupId(String mappedTripleGroupId, Pageable paging); @Query("MATCH (p{mappedTripleGroupId:$mappedTripleGroupId})-[r:PREDICATE]->() RETURN COUNT(p)") diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/exceptions/ConceptpowerNoResponseException.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/exceptions/ConceptpowerNoResponseException.java new file mode 100644 index 00000000..ba38a7ac --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/exceptions/ConceptpowerNoResponseException.java @@ -0,0 +1,26 @@ +package edu.asu.diging.quadriga.core.exceptions; + +public class ConceptpowerNoResponseException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public ConceptpowerNoResponseException() { + super(); + } + + public ConceptpowerNoResponseException(String message, Throwable cause) { + super(message, cause); + } + + public ConceptpowerNoResponseException(String message) { + super(message); + } + + public ConceptpowerNoResponseException(Throwable cause) { + super(cause); + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/model/Collection.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/model/Collection.java index 6f6fe7f1..dc886711 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/core/model/Collection.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/model/Collection.java @@ -17,10 +17,13 @@ public class Collection { private ObjectId _id; private String name; + private String description; + private String owner; private OffsetDateTime creationTime; + private boolean archived; /** @@ -51,6 +54,7 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + public String getOwner() { return owner; } diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/model/Context.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/model/Context.java similarity index 80% rename from quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/model/Context.java rename to quadriga/src/main/java/edu/asu/diging/quadriga/core/model/Context.java index 12bbd452..c373bff3 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/api/v1/model/Context.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/model/Context.java @@ -1,14 +1,14 @@ -package edu.asu.diging.quadriga.api.v1.model; +package edu.asu.diging.quadriga.core.model; public class Context { - private String creator; + private String creator; - private String creationTime; + private String creationTime; - private String creationPlace; + private String creationPlace; - private String sourceUri; + private String sourceUri; public String getCreator() { return creator; diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/model/EventGraph.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/model/EventGraph.java index e1ca81d3..9d89b3e4 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/core/model/EventGraph.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/model/EventGraph.java @@ -13,12 +13,18 @@ public class EventGraph { private DefaultMapping defaultMapping; private OffsetDateTime creationTime; private ObjectId collectionId; + private String appName; + private Context context; + private String submittingApp; - public EventGraph() {} + public EventGraph() { + this.creationTime = OffsetDateTime.now(); + } public EventGraph(CreationEvent root) { this.rootEvent = root; + this.creationTime = OffsetDateTime.now(); } public ObjectId getId() { @@ -61,6 +67,14 @@ public void setCollectionId(ObjectId collectionId) { this.collectionId = collectionId; } + public Context getContext() { + return context; + } + + public void setContext(Context context) { + this.context = context; + } + public String getSubmittingApp() { return submittingApp; } @@ -68,5 +82,13 @@ public String getSubmittingApp() { public void setSubmittingApp(String submittingApp) { this.submittingApp = submittingApp; } + + public String getAppName() { + return appName; + } + + public void setAppName(String appName) { + this.appName = appName; + } } diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/CollectionManager.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/CollectionManager.java index e43497fa..fad2fc5f 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/CollectionManager.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/CollectionManager.java @@ -23,7 +23,10 @@ public interface CollectionManager { /** * Finds a collection from the collection table by id + * * @param id used to look up the collection in mongodb + * @throws InvalidObjectIdException if the id passed is not a hex string + * * @return Collection Instance that is found from the database * **/ @@ -38,6 +41,7 @@ public interface CollectionManager { /** * + * Edits an existing Collection and updates it in the db * @param id of the collection that needs to be updated * @param name will be the updated name value @@ -62,10 +66,26 @@ public interface CollectionManager { /** * Deletes a collection from collection table by id * @param id used to look up the collection in database + * * @return collection details if it is archived + * @throws CollectionNotFoundException in case the collection for the given id is missing * @throws InvalidObjectIdException if collectionId couldn't be converted to ObjectId */ public Collection deleteCollection(String id) throws CollectionNotFoundException, InvalidObjectIdException; + + /** + * This method checks whether a collection with given collectionId exists and + * returns the collection if it exists. + * If it doesn't exist, it simply throws an exception + * + * @param collectionId is the id of the collection to be checked + * @return the Collection entry found in the database + * @throws InvalidObjectIdException if collectionId couldn't be conveted to + * ObjectId + * @throws CollectionNotFoundException if collection with given collectionId + * does't exist + */ + public Collection getCollection(String collectionId) throws InvalidObjectIdException, CollectionNotFoundException; /** * This method returns the number of default mappings present in the collection diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/EventGraphService.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/EventGraphService.java index 8683c821..5bebed60 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/EventGraphService.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/EventGraphService.java @@ -3,8 +3,11 @@ import java.util.List; import org.bson.types.ObjectId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import edu.asu.diging.quadriga.api.v1.model.Graph; + import edu.asu.diging.quadriga.core.exceptions.InvalidObjectIdException; import edu.asu.diging.quadriga.core.model.EventGraph; @@ -18,14 +21,15 @@ public interface EventGraphService { public void saveEventGraphs(List graphs); /** - * Finds all eventGraphs mapped to a collection in the descending order of creation time + * Finds the latest eventGraph mapped to a collection in the descending order of creation time * - * @param collectionId is the id used to finds all eventGraphs + * @param collectionId is the id used to finds the latest eventGraph + * @return the latest eventGraph * @return a list of eventGraphs in descending order * @throws InvalidObjectIdException if the collectionId contains non-hexadecimal characters */ public EventGraph findLatestEventGraphByCollectionId(ObjectId collectionId); - + /** * Groups the event graphs mapped to a collection by source uri and returns the total count. * @param collectionId @@ -33,10 +37,30 @@ public interface EventGraphService { */ public long getNumberOfSubmittedNetworks(ObjectId collectionId); + /** + * Finds all eventGraphs mapped to a collection in the descending order of creation time + * + * @param collectionId is the id used to finds all eventGraphs + * + * @param pageable used to specify page number and size per page + * @return a list of eventGraphs in descending order + */ + public Page findAllEventGraphsByCollectionId(ObjectId collectionId, Pageable pageable); + + public List findAllEventGraphsByCollectionId(ObjectId collectionId); + /** * Maps the network to events and saves it in the database * @param graph * @param collectionId */ public void mapNetworkAndSave(Graph graph, String collectionId); + + /** + * Finds event graphs by sourceURI + * + * @param sourceURI used for searching eventGraph + */ + public List findEventGraphsBySourceURI(String sourceURI); + } diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/MappedTripleGroupService.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/MappedTripleGroupService.java index 0784eb66..3ab83ff8 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/MappedTripleGroupService.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/MappedTripleGroupService.java @@ -30,6 +30,6 @@ public MappedTripleGroup updateName(String mappedTripleGroupId, String name) throws InvalidObjectIdException, MappedTripleGroupNotFoundException; public MappedTripleGroup get(String collectionId, MappedTripleType mappedTripleType) - throws InvalidObjectIdException, CollectionNotFoundException; + throws InvalidObjectIdException, CollectionNotFoundException, MappedTripleGroupNotFoundException; } diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/CollectionManagerImpl.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/CollectionManagerImpl.java index 1f4c53e7..1132ec32 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/CollectionManagerImpl.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/CollectionManagerImpl.java @@ -1,9 +1,11 @@ package edu.asu.diging.quadriga.core.service.impl; import java.time.OffsetDateTime; + +import java.util.Objects; + import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; import org.bson.types.ObjectId; @@ -66,7 +68,7 @@ public Collection addCollection(String name, String description, String owner, L } /* (non-Javadoc) - * @see edu.asu.diging.quadriga.core.service.CollectionManager#findCollection(java.lang.String) + * @see edu.asu.diging.quadriga.core.service.ICollectionManager#findCollection(java.lang.String) */ @Override public Collection findCollection(String id) throws InvalidObjectIdException { @@ -124,6 +126,18 @@ public Collection deleteCollection(String id) throws CollectionNotFoundException throw new CollectionNotFoundException("CollectionId: " + id); } } + + /* (non-Javadoc) + * @see edu.asu.diging.quadriga.core.service.ICollectionManager#getCollection(java.lang.String) + */ + @Override + public Collection getCollection(String collectionId) throws InvalidObjectIdException, CollectionNotFoundException { + Collection collection = findCollection(collectionId); + if (collection == null) { + throw new CollectionNotFoundException("CollectionId: " + collectionId); + } + return collection; + } /* (non-Javadoc) * @see edu.asu.diging.quadriga.core.service.CollectionManager#findCollections(java.lang.String, java.util.List, org.springframework.data.domain.Pageable) @@ -183,5 +197,4 @@ public Page findByArchived(boolean archived,int pageInt,int sizeInt) return collectionRepo.findByArchived(archived, PageRequest.of(pageInt, sizeInt)); } - } diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/EventGraphServiceImpl.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/EventGraphServiceImpl.java index 629bd61d..cb2e51a6 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/EventGraphServiceImpl.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/EventGraphServiceImpl.java @@ -6,12 +6,16 @@ import org.bson.types.ObjectId; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import edu.asu.diging.quadriga.api.v1.model.Graph; import edu.asu.diging.quadriga.core.data.EventGraphRepository; import edu.asu.diging.quadriga.core.model.EventGraph; + import edu.asu.diging.quadriga.core.model.events.CreationEvent; + import edu.asu.diging.quadriga.core.mongo.EventGraphDao; import edu.asu.diging.quadriga.core.service.EventGraphService; import edu.asu.diging.quadriga.core.service.NetworkMapper; @@ -37,6 +41,16 @@ public void saveEventGraphs(List events) { } } + @Override + public List findAllEventGraphsByCollectionId(ObjectId collectionId) { + return repo.findByCollectionIdOrderByCreationTimeAsc(collectionId).orElse(null); + } + + @Override + public Page findAllEventGraphsByCollectionId(ObjectId collectionId, Pageable pageable) { + return repo.findByCollectionIdOrderByCreationTimeAsc(collectionId, pageable).orElse(null); + } + @Override public EventGraph findLatestEventGraphByCollectionId(ObjectId collectionId) { return repo.findFirstByCollectionIdOrderByCreationTimeDesc(collectionId).orElse(null); @@ -49,6 +63,11 @@ public long getNumberOfSubmittedNetworks(ObjectId collectionId) { return eventGraphDao.countEventGraphsByCollectionId(collectionId); } + + @Override + public List findEventGraphsBySourceURI(String sourceURI) { + return repo.findByContextSourceUri(sourceURI).orElse(null); + } @Override public void mapNetworkAndSave(Graph graph, String collectionId) { @@ -58,6 +77,7 @@ public void mapNetworkAndSave(Graph graph, String collectionId) { eventGraphs.forEach(e -> { e.setCollectionId(new ObjectId(collectionId)); e.setDefaultMapping(graph.getMetadata().getDefaultMapping()); + e.setContext(graph.getMetadata().getContext()); /* * FIXME: * diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/MappedTripleGroupServiceImpl.java b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/MappedTripleGroupServiceImpl.java index 845e7d33..c43d71d1 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/MappedTripleGroupServiceImpl.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/core/service/impl/MappedTripleGroupServiceImpl.java @@ -2,6 +2,9 @@ import org.bson.types.ObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -23,6 +26,9 @@ public class MappedTripleGroupServiceImpl implements MappedTripleGroupService { @Autowired private CollectionManager collectionManager; + + private Logger logger = LoggerFactory.getLogger(getClass()); + /** * Converts the given collectionId into an ObjectId and persists a @@ -135,17 +141,24 @@ public MappedTripleGroup updateName(String mappedTripleGroupId, String name) * ObjectId * @throws CollectionNotFoundException if a collection for given collectionId * doesn't exist + * @throws MappedTripleGroupNotFoundException */ @Override public MappedTripleGroup get(String collectionId, MappedTripleType mappedTripleType) - throws InvalidObjectIdException, CollectionNotFoundException { + throws InvalidObjectIdException, CollectionNotFoundException, MappedTripleGroupNotFoundException { MappedTripleGroup mappedTripleGroup = findByCollectionIdAndMappingType(collectionId, mappedTripleType); // In case this is a new collection, or existing collection but new mapping type for that collection // we create a new MappedTripleGroup entry if (mappedTripleGroup == null) { mappedTripleGroup = add(collectionId, mappedTripleType); + + // If mappedTripleGroup add fails + if (mappedTripleGroup == null) { + throw new MappedTripleGroupNotFoundException("Couldn't find or persist a new MappedTripleGroup entry for collectionId: " + collectionId); + } } + return mappedTripleGroup; } diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/legacy/service/impl/RepositoryManager.java b/quadriga/src/main/java/edu/asu/diging/quadriga/legacy/service/impl/RepositoryManager.java index ad754225..c492df7c 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/legacy/service/impl/RepositoryManager.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/legacy/service/impl/RepositoryManager.java @@ -40,6 +40,7 @@ public List processXMLandStoretoDb(String xml, String type) throws URISy creationEventList = xmlToObject.parseXML(xml); List flattenedlist = creationEventList.stream().flatMap(List::stream) .map(event ->new EventGraph(event)).collect(Collectors.toList()); + elementDao.saveEventGraphs(flattenedlist); diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/DisplayCollectionController.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/DisplayCollectionController.java index b6814130..a5a8a54a 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/web/DisplayCollectionController.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/DisplayCollectionController.java @@ -1,18 +1,48 @@ package edu.asu.diging.quadriga.web; +import javax.servlet.http.HttpServletRequest; + +import java.util.ArrayList; +import java.util.Collections; +import java.time.ZoneId; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import java.time.ZoneId; +import java.util.List; +import java.util.stream.Collectors; + +import org.bson.types.ObjectId; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; + +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.support.RequestContext; +import org.springframework.web.servlet.support.RequestContextUtils; + +import edu.asu.diging.quadriga.core.exceptions.CollectionNotFoundException; + import edu.asu.diging.quadriga.core.exceptions.InvalidObjectIdException; import edu.asu.diging.quadriga.core.model.Collection; import edu.asu.diging.quadriga.core.model.EventGraph; +import edu.asu.diging.quadriga.core.model.MappedTripleGroup; +import edu.asu.diging.quadriga.core.model.MappedTripleType; import edu.asu.diging.quadriga.core.service.CollectionManager; import edu.asu.diging.quadriga.core.service.EventGraphService; +import edu.asu.diging.quadriga.core.service.MappedTripleGroupService; +import edu.asu.diging.quadriga.core.service.PredicateManager; +import edu.asu.diging.quadriga.web.model.Network; @Controller public class DisplayCollectionController { @@ -22,34 +52,119 @@ public class DisplayCollectionController { @Autowired private EventGraphService eventGraphService; - + + @Autowired + private MappedTripleGroupService mappedTripleGroupService; + + @Autowired + private PredicateManager predicateManager; + private Logger logger = LoggerFactory.getLogger(getClass()); - @RequestMapping(value = "/auth/collections/{id}", method = RequestMethod.GET) - public String get(@PathVariable String id, Model model) { - + @RequestMapping(value = "/auth/collections/{collectionId}", method = RequestMethod.GET) + public String get( @RequestParam(required = false) Integer page , @RequestParam(required = false) Integer size, @PathVariable String collectionId, Model model) { + + // Get collection details Collection collection; try { - collection = collectionManager.findCollection(id); + collection = collectionManager.findCollection(collectionId); if(collection == null) { - logger.error("Couldn't find collection: ", id); + logger.error("Couldn't find collection: ", collectionId); return "error404Page"; } } catch (InvalidObjectIdException e) { logger.error("Couldn't find collection ", e); return "error404Page"; } - + + model.addAttribute("collection", collection); + + + //Determine page number and size for network pagination + page = (page == null || page < 0) ? 0 : page - 1; + size = (size == null || size < 1) ? 10 : size; + + model.addAttribute("size", size); + EventGraph latestNetwork = eventGraphService.findLatestEventGraphByCollectionId(collection.getId()); model.addAttribute("latestNetwork", latestNetwork); + + Pageable paging = PageRequest.of(page, size); + + // Get all EventGraphs for this collection + Page eventGraphsList = eventGraphService.findAllEventGraphsByCollectionId(collection.getId(), paging); + + if (!eventGraphsList.isEmpty()) { + // Latest EventGraph will be for the last network submitted + EventGraph lastNetwork = eventGraphService.findLatestEventGraphByCollectionId(collection.getId()); + model.addAttribute("lastNetworkSubmittedAt", lastNetwork.getCreationTime().atZoneSameInstant(ZoneId.systemDefault())); + model.addAttribute("lastNetworkSubmittedBy", lastNetwork.getAppName()); + + // Every EventGraph with same sourceURI belongs to the same network + // Group all EventGraphs with the same sourceURI together to get all networks + Map> groupedEventGraphs = groupEventGraphs(eventGraphsList); + List networks = new ArrayList<>(); + + groupedEventGraphs.forEach((sourceURI, eventGraphsInNetwork) -> { + Network network = new Network(); + network.setSourceURI(sourceURI); + // The EventGraph that has the oldest creation date & time will be the network's creation date & time + EventGraph firstEventGraph = Collections.min(eventGraphsInNetwork, (eventGraph1, eventGraph2) -> eventGraph1 + .getCreationTime().compareTo(eventGraph2.getCreationTime())); + network.setCreationTime(firstEventGraph.getCreationTime().atZoneSameInstant(ZoneId.systemDefault())); + network.setCreator(firstEventGraph.getContext().getCreator()); + network.setAppName(firstEventGraph.getAppName()); + networks.add(network); + }); + // Sort networks as per the creation time + networks.sort((network1, network2) -> network2.getCreationTime().compareTo(network1.getCreationTime())); + + // Add pagination to the networks as per determined page number and size + model.addAttribute("networks", networks.subList(page * size, Math.min(networks.size(), page * size + size))); + model.addAttribute("totalPages", networks.size() % 10 == 0 ? (networks.size()/size) : (networks.size()/size + 1)); + model.addAttribute("pageNumber", page); + model.addAttribute("numberOfSubmittedNetworks", networks.size()); + } + +// model.addAttribute("networks", eventGraphsList.getContent()); +// model.addAttribute("totalPages", eventGraphsList.getTotalPages()); +// model.addAttribute("pageNumber", page); model.addAttribute("collection", collection); - long numberOfSubmittedNetworks = eventGraphService.getNumberOfSubmittedNetworks(collection.getId()); - model.addAttribute("numberOfSubmittedNetworks", numberOfSubmittedNetworks); - +// model.addAttribute("numberOfSubmittedNetworks", numberOfSubmittedNetworks); +// model.addAttribute("collection", collection); + // Get default mappings from Concepts model.addAttribute("defaultMappings", collectionManager.getNumberOfDefaultMappings(collection.getId().toString())); return "auth/displayCollection"; - + } + /** + * This method returns the number of default mappings present in the collection + * One MappedTripleGroup will exist for the "DefaultMappings" for this collection + * To get this number of default mappings, this method will check how many 'Predicates' have + * this mappedTripleGroupId linked to them + * This is because every default mapping has one predicate + * So, if the MappedTripleGroupId is present on n predicates, this collection + * must have n defaultMappings + * + * @param collectionId used to find mappedTripleGroupId + * @return the number of default mappings + */ + private int getNumberOfDefaultMappings(String collectionId) { + try { + MappedTripleGroup mappedTripleGroup = mappedTripleGroupService.findByCollectionIdAndMappingType(collectionId, MappedTripleType.DEFAULT_MAPPING); + if(mappedTripleGroup != null) { + return predicateManager.countPredicatesByMappedTripleGroup(mappedTripleGroup.get_id().toString()); + } + + } catch (InvalidObjectIdException | CollectionNotFoundException e) { + logger.error("Couldn't find number of default mappings for collection ",e); + } + return 0; + } + + private Map> groupEventGraphs(Page eventGraphsList) { + return eventGraphsList.stream().collect(Collectors.groupingBy(eventGraph -> eventGraph.getContext().getSourceUri())); + } } diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/ListCollectionController.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/ListCollectionController.java index 0d29af49..e712251c 100644 --- a/quadriga/src/main/java/edu/asu/diging/quadriga/web/ListCollectionController.java +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/ListCollectionController.java @@ -56,6 +56,7 @@ public String list(@RequestParam(defaultValue = "1", required = false, value = " PageRequest.of(pageInt, sizeInt))); model.addAttribute("username", simpleUser.getUsername()); } + return "auth/showcollections"; } diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/NetworkController.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/NetworkController.java new file mode 100644 index 00000000..72dc18fe --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/NetworkController.java @@ -0,0 +1,61 @@ +package edu.asu.diging.quadriga.web; + +import java.util.List; + +import org.apache.commons.validator.routines.UrlValidator; +import org.bson.types.ObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import edu.asu.diging.quadriga.core.model.EventGraph; +import edu.asu.diging.quadriga.core.service.EventGraphService; +import edu.asu.diging.quadriga.web.service.GraphCreationService; + +@Controller +public class NetworkController { + + @Autowired + private EventGraphService eventGraphService; + + @Autowired + private GraphCreationService graphCreationService; + + private Logger logger = LoggerFactory.getLogger(getClass()); + + @RequestMapping(value = "/auth/collections/{collectionId}/network/") + public String get(@PathVariable String collectionId, + @RequestParam(value = "sourceUri", required = true) String sourceUri, Model model) { + + if (!isSourceUriValid(sourceUri)) { + logger.error("Invalid sourceUri: " + sourceUri); + return "error404Page"; + } + + List eventGraphs = eventGraphService.findAllEventGraphsByCollectionId(new ObjectId(collectionId)); + + if (eventGraphs == null) { + logger.error("No network found for collectionId: " + collectionId); + return "error404Page"; + } + + model.addAttribute("elements", graphCreationService.createGraph(eventGraphs)); + model.addAttribute("sourceURI", sourceUri); + + model.addAttribute("creator", eventGraphs.get(0).getContext().getCreator()); + model.addAttribute("appName", eventGraphs.get(0).getSubmittingApp()); + model.addAttribute("creationTime", eventGraphs.get(0).getCreationTime()); + return "auth/displayNetwork"; + } + + private boolean isSourceUriValid(String sourceURI) { + UrlValidator urlValidator = new UrlValidator(new String[] { "http", "https" }); + return urlValidator.isValid(sourceURI); + } + +} \ No newline at end of file diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphData.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphData.java new file mode 100644 index 00000000..cf3adc99 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphData.java @@ -0,0 +1,15 @@ +package edu.asu.diging.quadriga.web.model; + +public class GraphData { + + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + +} \ No newline at end of file diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphEdgeData.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphEdgeData.java new file mode 100644 index 00000000..254ad5c2 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphEdgeData.java @@ -0,0 +1,28 @@ +package edu.asu.diging.quadriga.web.model; + +public class GraphEdgeData extends GraphData { + + private String source; + private String target; + private String eventGraphId; + + public String getSource() { + return source; + } + public void setSource(String source) { + this.source = source; + } + public String getTarget() { + return target; + } + public void setTarget(String target) { + this.target = target; + } + public String getEventGraphId() { + return eventGraphId; + } + public void setEventGraphId(String eventGraphId) { + this.eventGraphId = eventGraphId; + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphElement.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphElement.java new file mode 100644 index 00000000..1d745e6b --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphElement.java @@ -0,0 +1,15 @@ +package edu.asu.diging.quadriga.web.model; + +public class GraphElement { + + private GraphData data; + + public GraphData getData() { + return data; + } + + public void setData(GraphData data) { + this.data = data; + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphElements.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphElements.java new file mode 100644 index 00000000..c0a21a78 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphElements.java @@ -0,0 +1,23 @@ +package edu.asu.diging.quadriga.web.model; + +import java.util.List; + +public class GraphElements { + + private List nodes; + private List edges; + + public List getNodes() { + return nodes; + } + public void setNodes(List nodes) { + this.nodes = nodes; + } + public List getEdges() { + return edges; + } + public void setEdges(List edges) { + this.edges = edges; + } + +} \ No newline at end of file diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphNodeData.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphNodeData.java new file mode 100644 index 00000000..ad18407a --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphNodeData.java @@ -0,0 +1,51 @@ +package edu.asu.diging.quadriga.web.model; + +import java.util.List; + +public class GraphNodeData extends GraphData { + + private int group; + private String label; + private String description; + private String uri; + private List eventGraphIds; + private List alternativeUris; + + public int getGroup() { + return group; + } + public void setGroup(int group) { + this.group = group; + } + public String getLabel() { + return label; + } + public void setLabel(String label) { + this.label = label; + } + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + public String getUri() { + return uri; + } + public void setUri(String uri) { + this.uri = uri; + } + public List getEventGraphIds() { + return eventGraphIds; + } + public void setEventGraphIds(List eventGraphIds) { + this.eventGraphIds = eventGraphIds; + } + public List getAlternativeUris() { + return alternativeUris; + } + public void setAlternativeUris(List alternativeUris) { + this.alternativeUris = alternativeUris; + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphNodeType.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphNodeType.java new file mode 100644 index 00000000..0b9ad30c --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/GraphNodeType.java @@ -0,0 +1,22 @@ +package edu.asu.diging.quadriga.web.model; + +public enum GraphNodeType { + + PREDICATE(0), + SUBJECT(1), + OBJECT(1); + + /** + * Corresponds to the group in GraphNodeData. + */ + private int groupId; + + private GraphNodeType(int groupId) { + this.groupId = groupId; + } + + public int getGroupId() { + return groupId; + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/Network.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/Network.java new file mode 100644 index 00000000..b6c41696 --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/model/Network.java @@ -0,0 +1,42 @@ +package edu.asu.diging.quadriga.web.model; + +import java.time.ZonedDateTime; + +/** + * A model to display networks on the collection page + * @author poojakulkarni + * + */ +public class Network { + + private String sourceURI; + private ZonedDateTime creationTime; + private String creator; + private String appName; + + public String getSourceURI() { + return sourceURI; + } + public void setSourceURI(String sourceURI) { + this.sourceURI = sourceURI; + } + public ZonedDateTime getCreationTime() { + return creationTime; + } + public void setCreationTime(ZonedDateTime zonedDateTime) { + this.creationTime = zonedDateTime; + } + public String getCreator() { + return creator; + } + public void setCreator(String creator) { + this.creator = creator; + } + public String getAppName() { + return appName; + } + public void setAppName(String appName) { + this.appName = appName; + } + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/service/GraphCreationService.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/service/GraphCreationService.java new file mode 100644 index 00000000..802cd44f --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/service/GraphCreationService.java @@ -0,0 +1,122 @@ +package edu.asu.diging.quadriga.web.service; + +import java.util.List; +import java.util.Map; + +import edu.asu.diging.quadriga.core.exceptions.ConceptpowerNoResponseException; +import edu.asu.diging.quadriga.core.model.EventGraph; +import edu.asu.diging.quadriga.core.model.events.AppellationEvent; +import edu.asu.diging.quadriga.core.model.events.RelationEvent; +import edu.asu.diging.quadriga.web.model.GraphData; +import edu.asu.diging.quadriga.web.model.GraphElements; +import edu.asu.diging.quadriga.web.model.GraphNodeData; +import edu.asu.diging.quadriga.web.model.GraphNodeType; + +/** + * This class is used as a service to create a network graph for the Cytoscape JS + * library. Any class that wants to have a custom implementation of graph creation + * for CytoscapeJS should implement this service. + * + * @author poojakulkarni + * + */ +public interface GraphCreationService { + + /** + * Creates a graph in the form that could be understood by Cytoscape: + * + * elements: { + * nodes: { + * [ + * {data: {id: "", group: "", label: ""}}, + * {data: {id: "", group: "", label: ""}} + * ] + * }, + * edges: { + * [ + * {data: {id: "", source: "", target: ""}}, + * {data: {id: "", source: "", target: ""}}, + * {data: {id: "", source: "", target: ""}} + * ] + * } + * } + * + * @param eventGraph is the source of data for creating the graph + * @return a GraphElements object that resembles the above JSON structure + */ + public GraphElements createGraph(List eventGraph); + + /** + * This method creates nodes and edges for subject, object and predicate nodes + * For a predicate node, the event type is always appellation event and the predicate node is created + * + * For subject and object, if the event type is appellation then the nodes that are created are leaf + * nodes + * But if event type is relation event, this method is called recursively to because every relation event + * itself has subject, object and predicate + * + * After the nodes are created, both subject and object are linked to the predicate node by creating edges + * One edge goes from subject to predicate, another one from object to predicate + * + * The predicate node's id is returned to the parent of the subtree, so that it could + * be linked to its parent's predicate + * + * @param event is the relation event for which the method createes subject, object and predicate nodes + * @param graphNodes maintain the list of nodes created + * @param graphEdges maintain the list of edges created + * @param uniqueNodes maintains a map that links every unique sourceURI to its corresponding node + * @param eventGraphId is the id of the current EventGraph + * @return + * @throws ConceptpowerNoResponseException + */ + public String createNodesAndEdges(RelationEvent event, List graphNodes, List graphEdges, + Map uniqueNodes, String eventGraphId) throws ConceptpowerNoResponseException; + + /** + * Creates a predicate node using the event data and adds it to list of graph nodes + * + * @param graphNodes list to maintain all current graph nodes + * @param event is an appellation event that contains data to be set to the node + * @return the id of the created predicate node + * @throws ConceptpowerNoResponseException + */ + public String createPredicateNode(List graphNodes, AppellationEvent event, String eventGraphId) throws ConceptpowerNoResponseException; + + /** + * Checks if the unique nodes map contains the same sourceURI as the node to be created + * If yes, returns this node + * Else it creates a subject or object node using the event data and adds it to the list of graph nodes + * and the map of unique nodes with key as sourceURI and value as the node object itself + * + * @param graphNodes list to maintain all current graph nodes + * @param event is an appellation event that contains data to be set to the node + * @param uniqueNodes map to maintain nodes with unique sourceURI that can be reused + * @param graphNodeType indicates whether node to be created is subject or object node to accordingly set group id + * @return the id of an existing node from unique nodes or the newly created node + * @throws ConceptpowerNoResponseException + */ + + public String createSubjectOrObjectNode(List graphNodes, AppellationEvent event, Map uniqueNodes, GraphNodeType graphNodeType, String eventGraphId) throws ConceptpowerNoResponseException; + + /** + * Creates a GraphNodeData object and sets details such as id, label, group + * + * @param event is the appellation event for which node is being created + * @param graphNodeType used to set group id + * @return the created GraphNodeData object + * @throws ConceptpowerNoResponseException + */ + public GraphNodeData createNode(AppellationEvent event, GraphNodeType graphNodeType, String eventGraphId) throws ConceptpowerNoResponseException; + + /** + * Creates that edge that links the provided source and target using their IDs and it to + * the list of graphEdges + * + * @param graphEdges is the list that maintains all edges created for the graph + * @param sourceId is the source node's id to be linked to the target + * @param targetId is the target node's id to be linked to the source + */ + public void createEdge(List graphEdges, String sourceId, String targetId, String eventGraphId); + + +} diff --git a/quadriga/src/main/java/edu/asu/diging/quadriga/web/service/impl/GraphCreationServiceImpl.java b/quadriga/src/main/java/edu/asu/diging/quadriga/web/service/impl/GraphCreationServiceImpl.java new file mode 100644 index 00000000..4132c97d --- /dev/null +++ b/quadriga/src/main/java/edu/asu/diging/quadriga/web/service/impl/GraphCreationServiceImpl.java @@ -0,0 +1,195 @@ +package edu.asu.diging.quadriga.web.service.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.bson.types.ObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import edu.asu.diging.quadriga.core.conceptpower.model.ConceptCache; +import edu.asu.diging.quadriga.core.conceptpower.service.ConceptPowerService; +import edu.asu.diging.quadriga.core.exceptions.ConceptpowerNoResponseException; +import edu.asu.diging.quadriga.core.model.EventGraph; +import edu.asu.diging.quadriga.core.model.elements.Relation; +import edu.asu.diging.quadriga.core.model.events.AppellationEvent; +import edu.asu.diging.quadriga.core.model.events.RelationEvent; +import edu.asu.diging.quadriga.web.model.GraphData; +import edu.asu.diging.quadriga.web.model.GraphEdgeData; +import edu.asu.diging.quadriga.web.model.GraphElement; +import edu.asu.diging.quadriga.web.model.GraphElements; +import edu.asu.diging.quadriga.web.model.GraphNodeData; +import edu.asu.diging.quadriga.web.model.GraphNodeType; +import edu.asu.diging.quadriga.web.service.GraphCreationService; + +@Service +public class GraphCreationServiceImpl implements GraphCreationService { + + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Autowired + private ConceptPowerService conceptPowerService; + + @Override + public GraphElements createGraph(List eventGraphs){ + + List graphEdges = new ArrayList<>(); + List graphNodes = new ArrayList<>(); + Map uniqueNodes = new HashMap(); + + eventGraphs.stream() + .filter(eventGraph -> eventGraph.getRootEvent() instanceof RelationEvent) + .forEach(validEventGraph -> { + try { + createNodesAndEdges((RelationEvent) (validEventGraph.getRootEvent()), + graphNodes, graphEdges, uniqueNodes, validEventGraph.getId().toString()); + } + catch (ConceptpowerNoResponseException e) { + logger.error("Could not create graph for id : "+validEventGraph.getId(), e); + } + }); + + GraphElements graphElements = new GraphElements(); + graphElements.setNodes(wrapInGraphElements(graphNodes)); + graphElements.setEdges(wrapInGraphElements(graphEdges)); + + return graphElements; + } + + @Override + public String createNodesAndEdges(RelationEvent event, List graphNodes, List graphEdges, + Map uniqueNodes, String eventGraphId) throws ConceptpowerNoResponseException { + Relation relation = event.getRelation(); + String predicateNodeId = null; + String subjectNodeId = null; + String objectNodeId = null; + + if (relation.getPredicate() != null) { + predicateNodeId = createPredicateNode(graphNodes, relation.getPredicate(), eventGraphId); + } else { + logger.error("A predicate is missing in one of the relations for EventGraph: " + eventGraphId); + } + + if (relation.getSubject() != null) { + if (relation.getSubject() instanceof RelationEvent) { + subjectNodeId = createNodesAndEdges((RelationEvent) relation.getSubject(), graphNodes, graphEdges, + uniqueNodes, eventGraphId); + } else { + subjectNodeId = createSubjectOrObjectNode(graphNodes, (AppellationEvent) relation.getSubject(), + uniqueNodes, GraphNodeType.SUBJECT, eventGraphId); + } + } else { + logger.error("A subject is missing in one of the relations for EventGraph: " + eventGraphId); + } + + if (relation.getObject() != null) { + if (relation.getObject() instanceof RelationEvent) { + objectNodeId = createNodesAndEdges((RelationEvent) relation.getObject(), graphNodes, graphEdges, + uniqueNodes, eventGraphId); + } else { + objectNodeId = createSubjectOrObjectNode(graphNodes, (AppellationEvent) relation.getObject(), + uniqueNodes, GraphNodeType.OBJECT, eventGraphId); + } + } else { + logger.error("An object is missing in one of the relations for EventGraph: " + eventGraphId); + } + + if(subjectNodeId != null && predicateNodeId != null) { + createEdge(graphEdges, subjectNodeId, predicateNodeId, eventGraphId); + } + + if(objectNodeId != null && predicateNodeId != null) { + createEdge(graphEdges, predicateNodeId, objectNodeId, eventGraphId); + } + + return predicateNodeId; + } + + @Override + public String createPredicateNode(List graphNodes, AppellationEvent event, String eventGraphId) throws ConceptpowerNoResponseException { + GraphNodeData predicateNode = createNode(event, GraphNodeType.PREDICATE, eventGraphId); + graphNodes.add(predicateNode); + return predicateNode.getId(); + } + + @Override + public String createSubjectOrObjectNode(List graphNodes, AppellationEvent event, + Map uniqueNodes, GraphNodeType graphNodeType, String eventGraphId) throws ConceptpowerNoResponseException { + String sourceUri = event.getTerm().getInterpretation().getSourceURI(); + GraphNodeData node; + + if (sourceUri != null && uniqueNodes.containsKey(sourceUri)) { + node = uniqueNodes.get(sourceUri); + node.getEventGraphIds().add(eventGraphId); + return node.getId(); + } + + node = createNode(event, graphNodeType, eventGraphId); + graphNodes.add(node); + uniqueNodes.put(sourceUri, node); + + if(sourceUri != null && !sourceUri.equals("") && node.getAlternativeUris() != null) { + node.getAlternativeUris() + .stream() + .filter(nullableAltUri -> nullableAltUri != null) + .filter(alternativeUri -> !alternativeUri.equals("") && !alternativeUri.equals(sourceUri)) + .forEach(alternativeUriValue -> uniqueNodes.put(alternativeUriValue, node)); + } + + return node.getId(); + } + + @Override + public GraphNodeData createNode(AppellationEvent event, GraphNodeType graphNodeType, String eventGraphId) throws ConceptpowerNoResponseException { + + GraphNodeData node = new GraphNodeData(); + node.setId(new ObjectId().toString()); + node.setGroup(graphNodeType.getGroupId()); + node.setEventGraphIds(new ArrayList(Collections.singletonList(eventGraphId))); + + String sourceURI = event.getTerm().getInterpretation().getSourceURI(); + + if (sourceURI != null && sourceURI.contains("www.digitalhps.org")) { + + node.setUri(sourceURI); + ConceptCache conceptCache = conceptPowerService.getConceptByUri(sourceURI); + + if (conceptCache != null) { + node.setLabel(conceptCache.getWord()); + node.setDescription(conceptCache.getDescription()); + node.setAlternativeUris(conceptCache.getAlternativeUris()); + } + } else { + // Need to get data from viaf if viaf URL is present, but doesn't seem to be part of story Q20-19 + node.setLabel(event.getTerm().getPrintedRepresentation().getTermParts().iterator().next().getExpression()); + } + + return node; + } + + @Override + public void createEdge(List graphEdges, String sourceId, String targetId, String eventGraphId) { + GraphEdgeData edge = new GraphEdgeData(); + edge.setId(new ObjectId().toString()); + edge.setSource(sourceId); + edge.setTarget(targetId); + edge.setEventGraphId(eventGraphId); + graphEdges.add(edge); + } + + private List wrapInGraphElements(List dataList) { + List elements = new ArrayList<>(); + dataList.forEach(data -> { + GraphElement element = new GraphElement(); + element.setData(data); + elements.add(element); + }); + return elements; + } + +} diff --git a/quadriga/src/main/resources/config.properties b/quadriga/src/main/resources/config.properties index 220dc200..0a0b26a9 100644 --- a/quadriga/src/main/resources/config.properties +++ b/quadriga/src/main/resources/config.properties @@ -6,9 +6,11 @@ db.password=${db.password} neo4j.url=${neo4j.url} neo4j.database=${neo4j.database} +conceptpower_base_url=https://chps.asu.edu/conceptpower +conceptpower_id_url=/rest/Concept?id={concept_uri} citesphere_client_id=${citesphere.client.id} citesphere_client_secret=${citesphere.client.secret} -citesphere_base_url=${citesphere.base.url} +citesphere_base_url=https://diging-dev.asu.edu/citesphere-review citesphere_token_endpoint=/api/oauth/token citesphere_check_token_endpoint=/api/oauth/check_token citesphere_get_apps_endpoint=/api/v1/admin/apps diff --git a/quadriga/src/main/webapp/WEB-INF/views/auth/displayCollection.html b/quadriga/src/main/webapp/WEB-INF/views/auth/displayCollection.html index c880ade4..1d0a7f86 100644 --- a/quadriga/src/main/webapp/WEB-INF/views/auth/displayCollection.html +++ b/quadriga/src/main/webapp/WEB-INF/views/auth/displayCollection.html @@ -21,6 +21,7 @@

Description:

No networks submitted to this collection yet!

Number of submitted networks:

+

Last network submitted time:

@@ -28,20 +29,84 @@

No networks submitted to this coll Last network submitted by:

- +

Mappings

+ +
+ +
# Mappings Triples
1 Default Mappings
+ +

Networks

+ + + + + + + + + + + + + + + + + + +
#NameDate and TimeAction
+ + Graph for text submitted by + from + + + + Visualize + +
+ + diff --git a/quadriga/src/main/webapp/WEB-INF/views/auth/displayNetwork.html b/quadriga/src/main/webapp/WEB-INF/views/auth/displayNetwork.html new file mode 100644 index 00000000..55bc97fe --- /dev/null +++ b/quadriga/src/main/webapp/WEB-INF/views/auth/displayNetwork.html @@ -0,0 +1,252 @@ + + +Quadriga 2.0 + + + + + + + + + + + + + +
+
+

Network page

+
+ +
+ +
+ +
+
+
+
+ +
+
+ +
+
+
+ +
+
+

+ Source URI: +

+

+

+ Network name: +

+

+ Graph submitted by from +

+

+ Submitted on: +

+

+
+
+
+
+ +
+
+ + Select a node to view concept inforrmation + +
+
+
+
+ +
+
+ Select a node to view text occurences +
+
+
+
+ +
+
+ + \ No newline at end of file diff --git a/quadriga/src/main/webapp/WEB-INF/views/auth/editCollection.html b/quadriga/src/main/webapp/WEB-INF/views/auth/editCollection.html index 1d1b419e..9d49f5c2 100644 --- a/quadriga/src/main/webapp/WEB-INF/views/auth/editCollection.html +++ b/quadriga/src/main/webapp/WEB-INF/views/auth/editCollection.html @@ -34,9 +34,9 @@ var spinner = $('#spinner'); var selectedApps = [[${collectionForm.getApps()}]]; $.ajax({ - 'url': [[@{|/citesphere/apps|}]], - 'type': "GET", - async: true, + 'url' : [[@{|/citesphere/apps|}]], + 'type' : "GET", + 'async' : true, 'success': function(data) { spinner.hide(); data.forEach(function(app) { diff --git a/quadriga/src/main/webapp/WEB-INF/views/layouts/main.html b/quadriga/src/main/webapp/WEB-INF/views/layouts/main.html index b3cbd98a..bd8b911b 100644 --- a/quadriga/src/main/webapp/WEB-INF/views/layouts/main.html +++ b/quadriga/src/main/webapp/WEB-INF/views/layouts/main.html @@ -28,6 +28,9 @@ + + @@ -46,50 +49,59 @@ - +
-