Skip to content

Commit

Permalink
feat: add support for VocabProperty (#1154)
Browse files Browse the repository at this point in the history
  • Loading branch information
bobeal authored May 14, 2024
1 parent 1b37225 commit 0023f9f
Show file tree
Hide file tree
Showing 21 changed files with 612 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ enum class AttributeType(val key: String) {
Relationship("Relationship"),
GeoProperty("GeoProperty"),
JsonProperty("JsonProperty"),
LanguageProperty("LanguageProperty");
LanguageProperty("LanguageProperty"),
VocabProperty("VocabProperty");

companion object {
fun forKey(key: String): AttributeType =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_VALUES
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_OBJECTS
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_VALUES
import io.r2dbc.postgresql.codec.Json
import org.springframework.data.annotation.Id
import java.net.URI
Expand Down Expand Up @@ -48,7 +50,8 @@ data class TemporalEntityAttribute(
Relationship,
GeoProperty,
JsonProperty,
LanguageProperty;
LanguageProperty,
VocabProperty;

fun toExpandedName(): String =
when (this) {
Expand All @@ -57,6 +60,7 @@ data class TemporalEntityAttribute(
GeoProperty -> NGSILD_GEOPROPERTY_TYPE.uri
JsonProperty -> NGSILD_JSONPROPERTY_TYPE.uri
LanguageProperty -> NGSILD_LANGUAGEPROPERTY_TYPE.uri
VocabProperty -> NGSILD_VOCABPROPERTY_TYPE.uri
}

/**
Expand All @@ -69,6 +73,7 @@ data class TemporalEntityAttribute(
GeoProperty -> NGSILD_GEOPROPERTY_VALUES
JsonProperty -> NGSILD_JSONPROPERTY_VALUES
LanguageProperty -> NGSILD_LANGUAGEPROPERTY_VALUES
VocabProperty -> NGSILD_VOCABPROPERTY_VALUES
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_OBSERVED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PREFIX
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_RELATIONSHIP_OBJECT
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.buildNonReifiedTemporalValue
import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdEntity
import com.egm.stellio.shared.util.JsonUtils.serializeObject
Expand Down Expand Up @@ -874,6 +875,12 @@ class TemporalEntityAttributeService(
null,
null
)
TemporalEntityAttribute.AttributeType.VocabProperty ->
Triple(
serializeObject(attributePayload.getMemberValue(NGSILD_VOCABPROPERTY_VALUE)!!),
null,
null
)
}

private fun createContextualAttributeInstance(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_LANGUAGE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.logger
import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap
import com.egm.stellio.shared.util.JsonUtils.serializeObject
Expand Down Expand Up @@ -78,6 +79,12 @@ fun NgsiLdAttributeInstance.toTemporalAttributeMetadata(): Either<APIException,
AttributeValueType.ARRAY,
Triple(serializeObject(this.languageMap), null, null)
)
is NgsiLdVocabPropertyInstance ->
Triple(
TemporalEntityAttribute.AttributeType.VocabProperty,
AttributeValueType.ARRAY,
Triple(serializeObject(this.vocab), null, null)
)
}
if (attributeValue == Triple(null, null, null)) {
logger.warn("Unable to get a value from attribute: $this")
Expand Down Expand Up @@ -106,6 +113,7 @@ fun guessAttributeValueType(
TemporalEntityAttribute.AttributeType.GeoProperty -> AttributeValueType.GEOMETRY
TemporalEntityAttribute.AttributeType.JsonProperty -> AttributeValueType.JSON
TemporalEntityAttribute.AttributeType.LanguageProperty -> AttributeValueType.ARRAY
TemporalEntityAttribute.AttributeType.VocabProperty -> AttributeValueType.ARRAY
}

fun guessPropertyValueType(
Expand Down Expand Up @@ -148,7 +156,9 @@ fun mergePatch(
update.forEach { (attrName, attrValue) ->
if (!source.containsKey(attrName)) {
target[attrName] = attrValue
} else if (listOf(NGSILD_JSONPROPERTY_VALUE, NGSILD_PROPERTY_VALUE).contains(attrName)) {
} else if (
listOf(NGSILD_JSONPROPERTY_VALUE, NGSILD_VOCABPROPERTY_VALUE, NGSILD_PROPERTY_VALUE).contains(attrName)
) {
if (attrValue.size > 1) {
// a Property holding an array of value or a JsonPropery holding an array of JSON objects
// cannot be safely merged patch, so copy the whole value from the update
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_GEOPROPERTY_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PREFIX
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.buildExpandedPropertyValue
import com.egm.stellio.shared.util.JsonLdUtils.buildExpandedTemporalValue
import com.egm.stellio.shared.util.JsonLdUtils.buildNonReifiedPropertyValue
Expand Down Expand Up @@ -124,36 +125,49 @@ object TemporalEntityBuilder {
attributeInstance[valuesKey] =
buildExpandedTemporalValue(it.value) { attributeInstanceResult ->
attributeInstanceResult as SimplifiedAttributeInstanceResult
if (it.key.attributeType == TemporalEntityAttribute.AttributeType.JsonProperty) {
// flaky way to know if the serialized value is a JSON object or an array of JSON objects
val deserializedJsonValue: Any =
if ((attributeInstanceResult.value as String).startsWith("["))
deserializeListOfObjects(attributeInstanceResult.value)
else deserializeObject(attributeInstanceResult.value)
listOf(
mapOf(
NGSILD_JSONPROPERTY_VALUE to listOf(
mapOf(
JSONLD_TYPE to JSONLD_JSON,
JSONLD_VALUE to deserializedJsonValue
when (it.key.attributeType) {
TemporalEntityAttribute.AttributeType.JsonProperty -> {
// flaky way to know if the serialized value is a JSON object or an array of JSON objects
val deserializedJsonValue: Any =
if ((attributeInstanceResult.value as String).startsWith("["))
deserializeListOfObjects(attributeInstanceResult.value)
else deserializeObject(attributeInstanceResult.value)
listOf(
mapOf(
NGSILD_JSONPROPERTY_VALUE to listOf(
mapOf(
JSONLD_TYPE to JSONLD_JSON,
JSONLD_VALUE to deserializedJsonValue
)
)
)
),
mapOf(JSONLD_VALUE to attributeInstanceResult.time)
)
} else if (it.key.attributeType == TemporalEntityAttribute.AttributeType.LanguageProperty) {
listOf(
mapOf(
NGSILD_LANGUAGEPROPERTY_VALUE to
deserializeListOfObjects(attributeInstanceResult.value as String)
),
mapOf(JSONLD_VALUE to attributeInstanceResult.time)
)
} else {
listOf(
mapOf(JSONLD_VALUE to attributeInstanceResult.value),
mapOf(JSONLD_VALUE to attributeInstanceResult.time)
)
),
mapOf(JSONLD_VALUE to attributeInstanceResult.time)
)
}
TemporalEntityAttribute.AttributeType.LanguageProperty -> {
listOf(
mapOf(
NGSILD_LANGUAGEPROPERTY_VALUE to
deserializeListOfObjects(attributeInstanceResult.value as String)
),
mapOf(JSONLD_VALUE to attributeInstanceResult.time)
)
}
TemporalEntityAttribute.AttributeType.VocabProperty -> {
listOf(
mapOf(
NGSILD_VOCABPROPERTY_VALUE to
deserializeListOfObjects(attributeInstanceResult.value as String)
),
mapOf(JSONLD_VALUE to attributeInstanceResult.time)
)
}
else -> {
listOf(
mapOf(JSONLD_VALUE to attributeInstanceResult.value),
mapOf(JSONLD_VALUE to attributeInstanceResult.time)
)
}
}
}
attributeInstance.toMap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ class V0_29__JsonLd_migration : BaseJavaMigration() {
is NgsiLdGeoPropertyInstance -> TemporalEntityAttribute.AttributeValueType.GEOMETRY
is NgsiLdJsonPropertyInstance -> TemporalEntityAttribute.AttributeValueType.OBJECT
is NgsiLdLanguagePropertyInstance -> TemporalEntityAttribute.AttributeValueType.ARRAY
is NgsiLdVocabPropertyInstance -> TemporalEntityAttribute.AttributeValueType.ARRAY
}

jdbcTemplate.execute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ import com.egm.stellio.shared.model.ResourceNotFoundException
import com.egm.stellio.shared.model.addNonReifiedTemporalProperty
import com.egm.stellio.shared.model.getSingleEntry
import com.egm.stellio.shared.util.*
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_ID
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_LANGUAGE
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DATE_TIME_TYPE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_DEFAULT_VOCAB
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_INSTANCE_ID_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_JSONPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_LANGUAGEPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_MODIFIED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_OBSERVED_AT_PROPERTY
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_PROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_VOCABPROPERTY_VALUE
import com.egm.stellio.shared.util.JsonLdUtils.buildExpandedPropertyValue
import com.egm.stellio.shared.util.JsonUtils.deserializeAsMap
import io.mockk.spyk
Expand Down Expand Up @@ -60,6 +63,7 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer
private lateinit var outgoingTemporalEntityAttribute: TemporalEntityAttribute
private lateinit var jsonTemporalEntityAttribute: TemporalEntityAttribute
private lateinit var languageTemporalEntityAttribute: TemporalEntityAttribute
private lateinit var vocabTemporalEntityAttribute: TemporalEntityAttribute

val entityId = "urn:ngsi-ld:BeeHive:TESTC".toUri()

Expand Down Expand Up @@ -112,6 +116,18 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer
runBlocking {
temporalEntityAttributeService.create(languageTemporalEntityAttribute)
}

vocabTemporalEntityAttribute = TemporalEntityAttribute(
entityId = entityId,
attributeName = CATEGORY_VOCAPPROPERTY,
attributeValueType = TemporalEntityAttribute.AttributeValueType.ARRAY,
createdAt = now,
payload = SAMPLE_VOCAB_PROPERTY_PAYLOAD
)

runBlocking {
temporalEntityAttributeService.create(vocabTemporalEntityAttribute)
}
}

@AfterEach
Expand Down Expand Up @@ -689,6 +705,51 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer
}
}

@Test
fun `it should modify attribute instance for a VocabProperty property`() = runTest {
val attributeInstance = gimmeVocabPropertyAttributeInstance(vocabTemporalEntityAttribute.id)
attributeInstanceService.create(attributeInstance)

val instanceTemporalFragment =
loadSampleData("fragments/temporal_instance_vocab_fragment.jsonld")
val attributeInstancePayload =
mapOf(CATEGORY_COMPACT_VOCABPROPERTY to instanceTemporalFragment.deserializeAsMap())
val jsonLdAttribute = JsonLdUtils.expandJsonLdFragment(
attributeInstancePayload,
APIC_COMPOUND_CONTEXTS
) as ExpandedAttributes

val temporalEntitiesQuery = gimmeTemporalEntitiesQuery(
TemporalQuery(
timerel = TemporalQuery.Timerel.AFTER,
timeAt = ZonedDateTime.parse("1970-01-01T00:00:00Z")
)
)

attributeInstanceService.modifyAttributeInstance(
entityId,
CATEGORY_VOCAPPROPERTY,
attributeInstance.instanceId,
jsonLdAttribute.entries.first().value
).shouldSucceed()

attributeInstanceService.search(temporalEntitiesQuery, vocabTemporalEntityAttribute)
.shouldSucceedWith {
(it as List<FullAttributeInstanceResult>).single { result ->
val deserializedPayload = result.payload.deserializeAsMap()
result.time == ZonedDateTime.parse("2023-03-13T12:33:06Z") &&
deserializedPayload.containsKey(NGSILD_MODIFIED_AT_PROPERTY) &&
deserializedPayload.containsKey(NGSILD_INSTANCE_ID_PROPERTY) &&
deserializedPayload.containsKey(NGSILD_VOCABPROPERTY_VALUE) &&
(deserializedPayload[NGSILD_VOCABPROPERTY_VALUE] as List<Map<String, String>>)
.all { entry ->
entry[JSONLD_ID] == "${NGSILD_DEFAULT_VOCAB}stellio" ||
entry[JSONLD_ID] == "${NGSILD_DEFAULT_VOCAB}egm"
}
}
}
}

@Test
fun `it should delete attribute instance`() = runTest {
val attributeInstance = gimmeNumericPropertyAttributeInstance(incomingTemporalEntityAttribute.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ class EntityTypeServiceTests : WithTimescaleContainer, WithKafkaContainer {
TemporalEntityAttribute.AttributeType.LanguageProperty,
TemporalEntityAttribute.AttributeValueType.OBJECT
)
private val categoryVocabProperty = newTemporalEntityAttribute(
"urn:ngsi-ld:Apiary:TESTC",
CATEGORY_VOCAPPROPERTY,
TemporalEntityAttribute.AttributeType.VocabProperty,
TemporalEntityAttribute.AttributeValueType.ARRAY
)

@AfterEach
fun clearPreviousTemporalEntityAttributesAndObservations() {
Expand All @@ -103,6 +109,7 @@ class EntityTypeServiceTests : WithTimescaleContainer, WithKafkaContainer {
createTemporalEntityAttribute(outgoingProperty)
createTemporalEntityAttribute(luminosityJsonProperty)
createTemporalEntityAttribute(friendlyNameLanguageProperty)
createTemporalEntityAttribute(categoryVocabProperty)
}

@Test
Expand Down Expand Up @@ -131,7 +138,10 @@ class EntityTypeServiceTests : WithTimescaleContainer, WithKafkaContainer {
EntityType(
id = toUri(APIARY_TYPE),
typeName = APIARY_COMPACT_TYPE,
attributeNames = listOf(NGSILD_LOCATION_TERM)
attributeNames = listOf(
CATEGORY_COMPACT_VOCABPROPERTY,
NGSILD_LOCATION_TERM
)
),
EntityType(
id = toUri(BEEHIVE_TYPE),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,32 @@ fun gimmeLanguagePropertyAttributeInstance(
)
}

fun gimmeVocabPropertyAttributeInstance(
teaUuid: UUID,
timeProperty: AttributeInstance.TemporalProperty = AttributeInstance.TemporalProperty.OBSERVED_AT
): AttributeInstance {
val attributeMetadata = AttributeMetadata(
measuredValue = null,
value = SAMPLE_VOCAB_PROPERTY_PAYLOAD.asString(),
geoValue = null,
valueType = TemporalEntityAttribute.AttributeValueType.ARRAY,
datasetId = null,
type = TemporalEntityAttribute.AttributeType.VocabProperty,
observedAt = ngsiLdDateTime()
)
val payload = JsonLdUtils.buildExpandedPropertyValue(attributeMetadata.value!!)
.addNonReifiedTemporalProperty(JsonLdUtils.NGSILD_OBSERVED_AT_PROPERTY, attributeMetadata.observedAt!!)
.getSingleEntry()

return AttributeInstance(
temporalEntityAttribute = teaUuid,
time = attributeMetadata.observedAt!!,
attributeMetadata = attributeMetadata,
timeProperty = timeProperty,
payload = payload
)
}

fun gimmeTemporalEntitiesQuery(
temporalQuery: TemporalQuery,
withTemporalValues: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ const val EMPTY_PAYLOAD = "{}"
val EMPTY_JSON_PAYLOAD = Json.of(EMPTY_PAYLOAD)
val SAMPLE_JSON_PROPERTY_PAYLOAD = Json.of(
"""
{ "id": "123", "stringValue": "value", "nullValue": null }
{
"id": "123",
"stringValue": "value",
"nullValue": null
}
""".trimIndent()
)
val SAMPLE_LANGUAGE_PROPERTY_PAYLOAD = Json.of(
Expand All @@ -74,3 +78,20 @@ val SAMPLE_LANGUAGE_PROPERTY_PAYLOAD = Json.of(
}
""".trimIndent()
)
val SAMPLE_VOCAB_PROPERTY_PAYLOAD = Json.of(
"""
{
"https://uri.etsi.org/ngsi-ld/hasVocab": [
{
"@id": "https://uri.etsi.org/ngsi-ld/default-context/stellio"
},
{
"@id": "https://uri.etsi.org/ngsi-ld/default-context/egm"
}
],
"@type": [
"https://uri.etsi.org/ngsi-ld/VocabProperty"
]
}
""".trimIndent()
)
Loading

0 comments on commit 0023f9f

Please sign in to comment.