From 85754d419d8c7448e31e27c0178eda91d763dce9 Mon Sep 17 00:00:00 2001 From: Hylke van der Schaaf Date: Tue, 14 May 2024 21:42:03 +0200 Subject: [PATCH] Reworked special handling of tables into hooks --- .../ilt/frostserver/model/EntityType.java | 16 ++ .../model/loader/DefEntityType.java | 4 +- .../pgjooq/factories/HookPostUpdate.java | 3 +- .../pgjooq/factories/HookPreUpdate.java | 3 +- .../pgjooq/tables/StaTableAbstract.java | 8 +- .../pgjooq/utils/validator/ValidatorCUD.java | 2 +- .../coremodel/HookPostDeleteLocation.java | 48 +++++ .../coremodel/HookPostInsertHistLoc.java | 87 ++++++++ .../HookPrePostInsertUpdateThing.java | 203 ++++++++++++++++++ .../coremodel/TableImpHistLocations.java | 61 +----- .../plugin/coremodel/TableImpLocations.java | 20 +- .../plugin/coremodel/TableImpThings.java | 85 +------- .../model/HistoricalLocation.json | 135 ++++++------ .../plugincoremodelv2/model/Location.json | 7 + .../TableImpMultiDatastreams.java | 4 +- Tools/ModelEditor/pom.xml | 5 + 16 files changed, 459 insertions(+), 232 deletions(-) create mode 100644 Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPostDeleteLocation.java create mode 100644 Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPostInsertHistLoc.java create mode 100644 Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPrePostInsertUpdateThing.java diff --git a/FROST-Server.Core.Model/src/main/java/de/fraunhofer/iosb/ilt/frostserver/model/EntityType.java b/FROST-Server.Core.Model/src/main/java/de/fraunhofer/iosb/ilt/frostserver/model/EntityType.java index 1e46d13b2..4a0031104 100644 --- a/FROST-Server.Core.Model/src/main/java/de/fraunhofer/iosb/ilt/frostserver/model/EntityType.java +++ b/FROST-Server.Core.Model/src/main/java/de/fraunhofer/iosb/ilt/frostserver/model/EntityType.java @@ -304,6 +304,22 @@ public NavigationPropertyMain getNavigationProperty(String name) { return null; } + public NavigationPropertyEntity getNavigationPropertyEntity(String name) { + Property property = propertiesByName.get(name); + if (property instanceof NavigationPropertyEntity npe) { + return npe; + } + return null; + } + + public NavigationPropertyEntitySet getNavigationPropertyEntitySet(String name) { + Property property = propertiesByName.get(name); + if (property instanceof NavigationPropertyEntitySet npes) { + return npes; + } + return null; + } + /** * The Set of PROPERTIES that Entities of this type have. * diff --git a/FROST-Server.Core.Model/src/main/java/de/fraunhofer/iosb/ilt/frostserver/model/loader/DefEntityType.java b/FROST-Server.Core.Model/src/main/java/de/fraunhofer/iosb/ilt/frostserver/model/loader/DefEntityType.java index bb8a28b24..47e2e889f 100644 --- a/FROST-Server.Core.Model/src/main/java/de/fraunhofer/iosb/ilt/frostserver/model/loader/DefEntityType.java +++ b/FROST-Server.Core.Model/src/main/java/de/fraunhofer/iosb/ilt/frostserver/model/loader/DefEntityType.java @@ -122,8 +122,8 @@ public class DefEntityType implements AnnotatedConfigurable { */ @ConfigurableField(editor = EditorList.class, optional = true, label = "PM Hooks", description = "Persistence Manager Hooks") - @EditorList.EdOptsList(editor = EditorSubclass.class) - @EditorSubclass.EdOptsSubclass(iface = DefPmHook.class, merge = true, nameField = "@class", shortenClassNames = true) + @EditorList.EdOptsList(editor = EditorClass.class) + @EditorClass.EdOptsClass(clazz = DefPmHook.class) private List hooks = new ArrayList<>(); /** diff --git a/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/factories/HookPostUpdate.java b/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/factories/HookPostUpdate.java index 16da64944..bace6ae98 100644 --- a/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/factories/HookPostUpdate.java +++ b/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/factories/HookPostUpdate.java @@ -20,6 +20,7 @@ import de.fraunhofer.iosb.ilt.frostserver.model.core.Entity; import de.fraunhofer.iosb.ilt.frostserver.model.core.PkValue; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.JooqPersistenceManager; +import de.fraunhofer.iosb.ilt.frostserver.service.UpdateMode; import de.fraunhofer.iosb.ilt.frostserver.util.exception.IncompleteEntityException; import de.fraunhofer.iosb.ilt.frostserver.util.exception.NoSuchEntityException; @@ -31,5 +32,5 @@ */ public interface HookPostUpdate extends JooqPmHook { - public void postUpdateInDatabase(JooqPersistenceManager pm, Entity entity, PkValue entityId) throws NoSuchEntityException, IncompleteEntityException; + public void postUpdateInDatabase(JooqPersistenceManager pm, Entity entity, PkValue entityId, UpdateMode updateMode) throws NoSuchEntityException, IncompleteEntityException; } diff --git a/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/factories/HookPreUpdate.java b/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/factories/HookPreUpdate.java index 32eed0b9e..07dc9ad28 100644 --- a/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/factories/HookPreUpdate.java +++ b/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/factories/HookPreUpdate.java @@ -20,6 +20,7 @@ import de.fraunhofer.iosb.ilt.frostserver.model.core.Entity; import de.fraunhofer.iosb.ilt.frostserver.model.core.PkValue; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.JooqPersistenceManager; +import de.fraunhofer.iosb.ilt.frostserver.service.UpdateMode; import de.fraunhofer.iosb.ilt.frostserver.util.exception.IncompleteEntityException; import de.fraunhofer.iosb.ilt.frostserver.util.exception.NoSuchEntityException; @@ -31,5 +32,5 @@ */ public interface HookPreUpdate extends JooqPmHook { - public void preUpdateInDatabase(JooqPersistenceManager pm, Entity entity, PkValue entityId) throws NoSuchEntityException, IncompleteEntityException; + public void preUpdateInDatabase(JooqPersistenceManager pm, Entity entity, PkValue entityId, UpdateMode updateMode) throws NoSuchEntityException, IncompleteEntityException; } diff --git a/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/tables/StaTableAbstract.java b/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/tables/StaTableAbstract.java index 6f50bf9d7..7ffb19e2b 100644 --- a/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/tables/StaTableAbstract.java +++ b/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/tables/StaTableAbstract.java @@ -378,6 +378,10 @@ protected void updateNavigationPropertySet(Entity entity, EntitySet linkedSet, J relation.link(pm, entity, linkedSet, navProp); } else { for (Entity child : linkedSet) { + EntityFactories ef = pm.getEntityFactories(); + if (!ef.entityExists(pm, child, false)) { + throw new NoSuchEntityException("Can not link " + child.getEntityType() + " with no id."); + } relation.link(pm, entity, child, navProp); } } @@ -392,7 +396,7 @@ public EntityChangedMessage updateInDatabase(JooqPersistenceManager pm, Entity e final EntityChangedMessage message = new EntityChangedMessage(); for (SortingWrapper hookWrapper : hooksPreUpdate) { - hookWrapper.getObject().preUpdateInDatabase(pm, entity, entityId); + hookWrapper.getObject().preUpdateInDatabase(pm, entity, entityId, updateMode); } for (NavigationPropertyMain np : entityType.getNavigationEntities()) { @@ -445,7 +449,7 @@ public EntityChangedMessage updateInDatabase(JooqPersistenceManager pm, Entity e } for (SortingWrapper hookWrapper : hooksPostUpdate) { - hookWrapper.getObject().postUpdateInDatabase(pm, entity, entityId); + hookWrapper.getObject().postUpdateInDatabase(pm, entity, entityId, updateMode); } return message; diff --git a/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/utils/validator/ValidatorCUD.java b/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/utils/validator/ValidatorCUD.java index 7e5319ec4..8a2993255 100644 --- a/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/utils/validator/ValidatorCUD.java +++ b/FROST-Server.SQLjooq/src/main/java/de/fraunhofer/iosb/ilt/frostserver/persistence/pgjooq/utils/validator/ValidatorCUD.java @@ -89,7 +89,7 @@ public void registerHooks(StaMainTable mainTable, JooqPersistenceManager ppm) { } if (getCheckUpdate() != null) { LOGGER.info(" - update: {}", getCheckUpdate()); - mainTable.registerHookPreUpdate(-10, (pm, entity, id) -> { + mainTable.registerHookPreUpdate(-10, (pm, entity, id, updateMode) -> { if (PrincipalExtended.getLocalPrincipal().isAdmin()) { return; } diff --git a/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPostDeleteLocation.java b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPostDeleteLocation.java new file mode 100644 index 000000000..8af4b60ee --- /dev/null +++ b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPostDeleteLocation.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 + * Karlsruhe, Germany. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package de.fraunhofer.iosb.ilt.frostserver.plugin.coremodel; + +import de.fraunhofer.iosb.ilt.frostserver.model.core.PkValue; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.JooqPersistenceManager; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.factories.HookPostDelete; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.tables.TableCollection; +import de.fraunhofer.iosb.ilt.frostserver.util.exception.NoSuchEntityException; +import org.jooq.TableField; +import org.jooq.impl.DSL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author hylke + */ +class HookPostDeleteLocation implements HookPostDelete { + + private static final Logger LOGGER = LoggerFactory.getLogger(HookPostDeleteLocation.class.getName()); + + @Override + public void postDelete(JooqPersistenceManager pm, PkValue entityId) throws NoSuchEntityException { + final TableCollection tables = pm.getTableCollection(); + // Also postDelete all historicalLocations that no longer reference any location + TableImpHistLocations thl = tables.getTableForClass(TableImpHistLocations.class); + TableImpLocationsHistLocations tlhl = tables.getTableForClass(TableImpLocationsHistLocations.class); + int count = pm.getDslContext().delete(thl).where(((TableField) thl.getId()).in(DSL.select(thl.getId()).from(thl).leftJoin(tlhl).on(((TableField) thl.getId()).eq(tlhl.getHistLocationId())).where(tlhl.getLocationId().isNull()))).execute(); + LOGGER.debug("Deleted {} HistoricalLocations", count); + } + +} diff --git a/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPostInsertHistLoc.java b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPostInsertHistLoc.java new file mode 100644 index 000000000..740c35c0f --- /dev/null +++ b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPostInsertHistLoc.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 + * Karlsruhe, Germany. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package de.fraunhofer.iosb.ilt.frostserver.plugin.coremodel; + +import de.fraunhofer.iosb.ilt.frostserver.model.EntityType; +import de.fraunhofer.iosb.ilt.frostserver.model.core.Entity; +import de.fraunhofer.iosb.ilt.frostserver.model.ext.TimeInstant; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.JooqPersistenceManager; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.factories.EntityFactories; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.factories.HookPostInsert; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.tables.TableCollection; +import de.fraunhofer.iosb.ilt.frostserver.property.EntityPropertyMain; +import de.fraunhofer.iosb.ilt.frostserver.property.NavigationPropertyMain.NavigationPropertyEntity; +import de.fraunhofer.iosb.ilt.frostserver.property.NavigationPropertyMain.NavigationPropertyEntitySet; +import de.fraunhofer.iosb.ilt.frostserver.util.exception.IncompleteEntityException; +import de.fraunhofer.iosb.ilt.frostserver.util.exception.NoSuchEntityException; +import java.util.Collections; +import java.util.Map; +import net.time4j.Moment; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.Record; +import org.jooq.TableField; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author hylke + */ +class HookPostInsertHistLoc implements HookPostInsert { + + private static final Logger LOGGER = LoggerFactory.getLogger(HookPostInsertHistLoc.class.getName()); + + @Override + public boolean postInsertIntoDatabase(JooqPersistenceManager pm, Entity histLoc, Map insertFields) throws NoSuchEntityException, IncompleteEntityException { + final EntityFactories ef = pm.getEntityFactories(); + final TableCollection tc = pm.getTableCollection(); + final EntityType etHistLoc = histLoc.getEntityType(); + final NavigationPropertyEntity npThing = (NavigationPropertyEntity) etHistLoc.getNavigationProperty("Thing"); + final NavigationPropertyEntitySet npLocations = (NavigationPropertyEntitySet) etHistLoc.getNavigationProperty("Locations"); + final EntityPropertyMain epTime = etHistLoc.getEntityProperty("time"); + Entity thing = histLoc.getProperty(npThing); + Object thingId = thing.getPrimaryKeyValues().get(0); + DSLContext dslContext = pm.getDslContext(); + TableImpHistLocations thl = tc.getTableForClass(TableImpHistLocations.class); + final TimeInstant hlTime = histLoc.getProperty(epTime); + Moment newTime = hlTime.getDateTime(); + // https://github.com/opengeospatial/sensorthings/issues/30 + // Check the time of the latest HistoricalLocation of our thing. + // If this time is earlier than our time, set the Locations of our Thing to our Locations. + Record lastHistLocation = dslContext.select(Collections.emptyList()).from(thl).where(((TableField) thl.getThingId()).eq(thingId).and(thl.time.gt(newTime))).orderBy(thl.time.desc()).limit(1).fetchOne(); + if (lastHistLocation == null) { + // We are the newest. + // Unlink old Locations from Thing. + TableImpThingsLocations qtl = tc.getTableForClass(TableImpThingsLocations.class); + long count = dslContext.delete(qtl).where(((TableField) qtl.getThingId()).eq(thingId)).execute(); + LOGGER.debug(EntityFactories.UNLINKED_L_FROM_T, count, thingId); + // Link new locations to Thing. + for (Entity l : histLoc.getProperty(npLocations)) { + if (!l.getPrimaryKeyValues().isFullySet() || !ef.entityExists(pm, l, true)) { + throw new NoSuchEntityException("Location with no id."); + } + Object locationId = l.getPrimaryKeyValues().get(0); + dslContext.insertInto(qtl).set((TableField) qtl.getThingId(), thingId).set(qtl.getLocationId(), locationId).execute(); + LOGGER.debug(EntityFactories.LINKED_L_TO_T, locationId, thingId); + } + } + return true; + } + +} diff --git a/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPrePostInsertUpdateThing.java b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPrePostInsertUpdateThing.java new file mode 100644 index 000000000..5126010f1 --- /dev/null +++ b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/HookPrePostInsertUpdateThing.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2023 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131 + * Karlsruhe, Germany. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package de.fraunhofer.iosb.ilt.frostserver.plugin.coremodel; + +import de.fraunhofer.iosb.ilt.configurable.annotations.ConfigurableField; +import de.fraunhofer.iosb.ilt.configurable.editor.EditorString; +import de.fraunhofer.iosb.ilt.frostserver.model.EntityChangedMessage; +import de.fraunhofer.iosb.ilt.frostserver.model.EntityType; +import de.fraunhofer.iosb.ilt.frostserver.model.ModelRegistry; +import de.fraunhofer.iosb.ilt.frostserver.model.core.Entity; +import de.fraunhofer.iosb.ilt.frostserver.model.core.EntitySet; +import de.fraunhofer.iosb.ilt.frostserver.model.core.PkValue; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.JooqPersistenceManager; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.factories.EntityFactories; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.factories.HookPostInsert; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.factories.HookPostUpdate; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.factories.HookPreInsert; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.factories.HookPreUpdate; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.tables.StaMainTable; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.tables.StaTable; +import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.tables.TableCollection; +import de.fraunhofer.iosb.ilt.frostserver.property.NavigationPropertyMain.NavigationPropertyEntitySet; +import de.fraunhofer.iosb.ilt.frostserver.service.UpdateMode; +import de.fraunhofer.iosb.ilt.frostserver.util.exception.IncompleteEntityException; +import de.fraunhofer.iosb.ilt.frostserver.util.exception.NoSuchEntityException; +import java.util.Map; +import net.time4j.Moment; +import org.jooq.DSLContext; +import org.jooq.Field; +import org.jooq.TableField; +import org.jooq.exception.DataAccessException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author hylke + */ +public class HookPrePostInsertUpdateThing implements HookPreInsert, HookPostInsert, HookPreUpdate, HookPostUpdate { + + private static final Logger LOGGER = LoggerFactory.getLogger(HookPrePostInsertUpdateThing.class); + + @ConfigurableField(editor = EditorString.class, + label = "Things Locations LinkTable") + @EditorString.EdOptsString(dflt = "THINGS_LOCATIONS") + private String ttlName = "THINGS_LOCATIONS"; + + @ConfigurableField(editor = EditorString.class, + label = "LinkTable ThingId") + @EditorString.EdOptsString(dflt = "THING_ID") + private String ttlThingIdName = "THING_ID"; + + @ConfigurableField(editor = EditorString.class, + label = "LinkTable LocationId") + @EditorString.EdOptsString(dflt = "LOCATION_ID") + private String ttlLocationIdName = "LOCATION_ID"; + + @ConfigurableField(editor = EditorString.class, + label = "Locations-HistLocations LinkTable") + @EditorString.EdOptsString(dflt = "LOCATIONS_HIST_LOCATIONS") + private String tlhlName = "LOCATIONS_HIST_LOCATIONS"; + + @ConfigurableField(editor = EditorString.class, + label = "LinkTable HistLocationId") + @EditorString.EdOptsString(dflt = "HIST_LOCATION_ID") + private String tlhlHistLocationIdName = "HIST_LOCATION_ID"; + + @ConfigurableField(editor = EditorString.class, + label = "Time Field") + @EditorString.EdOptsString(dflt = "TIME") + private String thlTimeName = "TIME"; + + @Override + public boolean preInsertIntoDatabase(Phase fase, JooqPersistenceManager pm, Entity entity, Map insertFields) throws NoSuchEntityException, IncompleteEntityException { + return true; + } + + @Override + public boolean postInsertIntoDatabase(JooqPersistenceManager pm, Entity entity, Map insertFields) throws NoSuchEntityException, IncompleteEntityException { + PkValue entityId = entity.getPrimaryKeyValues(); + createHistLocationLinkLocations(pm, entity, entityId); + return true; + } + + @Override + public void preUpdateInDatabase(JooqPersistenceManager pm, Entity entity, PkValue entityId, UpdateMode updateMode) throws NoSuchEntityException, IncompleteEntityException { + TableCollection tables = pm.getTableCollection(); + EntityType et = entity.getEntityType(); + NavigationPropertyEntitySet npThingLocations = et.getNavigationPropertyEntitySet("Locations"); + if (npThingLocations == null) { + LOGGER.error("Given Entity of type {} has no navigationPropertySet Locations", et); + return; + } + EntitySet locations = entity.getProperty(npThingLocations); + if (locations != null && !locations.isEmpty()) { + // New locations to be set, delete old locations. + Object thingId = entityId.get(0); + DSLContext dslContext = pm.getDslContext(); + StaTable ttl = tables.getTableForName(ttlName); + // Unlink old Locations from Thing. + long count = dslContext.delete(ttl).where(((TableField) ttl.field(ttlThingIdName)).eq(thingId)).execute(); + LOGGER.debug(EntityFactories.UNLINKED_L_FROM_T, count, thingId); + } + } + + @Override + public void postUpdateInDatabase(JooqPersistenceManager pm, Entity entity, PkValue entityId, UpdateMode updateMode) throws NoSuchEntityException, IncompleteEntityException { + createHistLocationLinkLocations(pm, entity, entityId); + } + + public void createHistLocationLinkLocations(JooqPersistenceManager pm, Entity entity, PkValue entityId) throws DataAccessException { + TableCollection tables = pm.getTableCollection(); + EntityType et = entity.getEntityType(); + NavigationPropertyEntitySet npThingLocations = et.getNavigationPropertyEntitySet("Locations"); + if (npThingLocations == null) { + LOGGER.error("EntityType {} has no navigationPropertySet Locations", et); + return; + } + NavigationPropertyEntitySet npThingHistLocs = et.getNavigationPropertyEntitySet("HistoricalLocations"); + if (npThingHistLocs == null) { + LOGGER.error("EntityType {} has no navigationPropertySet HistoricalLocations", et); + return; + } + EntityType etHistLoc = npThingHistLocs.getEntityType(); + EntitySet locations = entity.getProperty(npThingLocations); + DSLContext dslContext = pm.getDslContext(); + Object thingId = entityId.get(0); + // Now link the new locations also to a historicalLocation. + if (locations != null && !locations.isEmpty()) { + // Insert a new HL into the DB + StaMainTable thl = tables.getTableForType(etHistLoc); + Object histLocationId = dslContext.insertInto(thl) + .set((TableField) thl.field(thlTimeName), Moment.nowInSystemTime()) + .set((TableField) thl.field(ttlThingIdName), thingId) + .returningResult(thl.getPkFields().get(0)) + .fetchOne(0); + LOGGER.debug(EntityFactories.CREATED_HL, histLocationId); + + // Link the locations to the new HL + StaTable tlhl = tables.getTableForName(tlhlName); + for (Entity location : locations) { + Object locationId = location.getPrimaryKeyValues().get(0); + if (locationId == null) { + LOGGER.error("Location with no ID"); + } + dslContext.insertInto(tlhl) + .set(((TableField) tlhl.field(tlhlHistLocationIdName)), histLocationId) + .set((tlhl.field(ttlLocationIdName)), locationId) + .execute(); + LOGGER.debug(EntityFactories.LINKED_L_TO_HL, locationId, histLocationId); + } + + // Send a message about the creation of a new HL + Entity newHl = pm.get(etHistLoc, PkValue.of(histLocationId)); + ModelRegistry modelRegistry = pm.getCoreSettings().getModelRegistry(); + newHl.setQuery(modelRegistry.getMessageQueryGenerator().getQueryFor(newHl.getEntityType())); + pm.getEntityChangedMessages().add( + new EntityChangedMessage() + .setEventType(EntityChangedMessage.Type.CREATE) + .setEntity(newHl)); + } + } + + public void setTtlName(String ttlName) { + this.ttlName = ttlName; + } + + public void setTtlThingIdName(String ttlThingIdName) { + this.ttlThingIdName = ttlThingIdName; + } + + public void setTtlLocationIdName(String ttlLocationIdName) { + this.ttlLocationIdName = ttlLocationIdName; + } + + public void setTlhlName(String tlhlName) { + this.tlhlName = tlhlName; + } + + public void setTlhlHistLocationIdName(String tlhlHistLocationIdName) { + this.tlhlHistLocationIdName = tlhlHistLocationIdName; + } + + public void setThlTimeName(String thlTimeName) { + this.thlTimeName = thlTimeName; + } + +} diff --git a/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpHistLocations.java b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpHistLocations.java index d50796dfc..09a1b00e7 100644 --- a/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpHistLocations.java +++ b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpHistLocations.java @@ -18,8 +18,6 @@ package de.fraunhofer.iosb.ilt.frostserver.plugin.coremodel; import de.fraunhofer.iosb.ilt.frostserver.model.EntityType; -import de.fraunhofer.iosb.ilt.frostserver.model.core.Entity; -import de.fraunhofer.iosb.ilt.frostserver.model.ext.TimeInstant; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.JooqPersistenceManager; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.bindings.MomentBinding; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.factories.EntityFactories; @@ -29,15 +27,10 @@ import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.tables.TableCollection; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.PropertyFieldRegistry; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.validator.SecurityTableWrapper; -import de.fraunhofer.iosb.ilt.frostserver.service.UpdateMode; -import de.fraunhofer.iosb.ilt.frostserver.util.exception.IncompleteEntityException; -import de.fraunhofer.iosb.ilt.frostserver.util.exception.NoSuchEntityException; import de.fraunhofer.iosb.ilt.frostserver.util.user.PrincipalExtended; import java.util.Arrays; -import java.util.Collections; import java.util.List; import net.time4j.Moment; -import org.jooq.DSLContext; import org.jooq.DataType; import org.jooq.Field; import org.jooq.Name; @@ -46,8 +39,6 @@ import org.jooq.TableField; import org.jooq.impl.DSL; import org.jooq.impl.SQLDataType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class TableImpHistLocations extends StaTableAbstract { @@ -56,7 +47,6 @@ public class TableImpHistLocations extends StaTableAbstract(pluginCoreModel.epTime, table -> table.time)); pfReg.addEntry(pluginCoreModel.npThingHistLoc, TableImpHistLocations::getThingId); pfReg.addEntry(pluginCoreModel.npLocationsHistLoc, TableImpHistLocations::getId); - } - - @Override - public boolean insertIntoDatabase(JooqPersistenceManager pm, Entity histLoc, UpdateMode updateMode) throws NoSuchEntityException, IncompleteEntityException { - super.insertIntoDatabase(pm, histLoc, updateMode); - EntityFactories entityFactories = pm.getEntityFactories(); - Entity thing = histLoc.getProperty(pluginCoreModel.npThingHistLoc); - Object thingId = thing.getPrimaryKeyValues().get(0); - DSLContext dslContext = pm.getDslContext(); - TableImpHistLocations thl = getTables().getTableForClass(TableImpHistLocations.class); - - final TimeInstant hlTime = histLoc.getProperty(pluginCoreModel.epTime); - Moment newTime = hlTime.getDateTime(); - // https://github.com/opengeospatial/sensorthings/issues/30 - // Check the time of the latest HistoricalLocation of our thing. - // If this time is earlier than our time, set the Locations of our Thing to our Locations. - Record lastHistLocation = dslContext.select(Collections.emptyList()) - .from(thl) - .where( - ((TableField) thl.getThingId()).eq(thingId) - .and(thl.time.gt(newTime))) - .orderBy(thl.time.desc()) - .limit(1) - .fetchOne(); - if (lastHistLocation == null) { - // We are the newest. - // Unlink old Locations from Thing. - TableImpThingsLocations qtl = getTables().getTableForClass(TableImpThingsLocations.class); - long count = dslContext - .delete(qtl) - .where(((TableField) qtl.getThingId()).eq(thingId)) - .execute(); - LOGGER.debug(EntityFactories.UNLINKED_L_FROM_T, count, thingId); - - // Link new locations to Thing. - for (Entity l : histLoc.getProperty(pluginCoreModel.npLocationsHistLoc)) { - if (!l.getPrimaryKeyValues().isFullySet() || !entityFactories.entityExists(pm, l, true)) { - throw new NoSuchEntityException("Location with no id."); - } - Object locationId = l.getPrimaryKeyValues().get(0); - - dslContext.insertInto(qtl) - .set(((TableField) qtl.getThingId()), thingId) - .set((qtl.getLocationId()), locationId) - .execute(); - LOGGER.debug(EntityFactories.LINKED_L_TO_T, locationId, thingId); - } - } - - return true; + registerHookPostInsert(0, new HookPostInsertHistLoc()); } @Override diff --git a/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpLocations.java b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpLocations.java index 0e00e249e..78fc2e6e8 100644 --- a/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpLocations.java +++ b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpLocations.java @@ -188,6 +188,7 @@ public void initProperties(final EntityFactories entityFactories) { pfReg.addEntryMap(ModelRegistry.EP_PROPERTIES, table -> table.colProperties); pfReg.addEntry(pluginCoreModel.npThingsLocation, TableImpLocations::getId); pfReg.addEntry(pluginCoreModel.npHistoricalLocationsLocation, TableImpLocations::getId); + registerHookPostDelete(0, new HookPostDeleteLocation()); } @Override @@ -255,25 +256,6 @@ protected void updateNavigationPropertySet(Entity location, EntitySet linkedSet, super.updateNavigationPropertySet(location, linkedSet, pm, updateMode); } - @Override - public void delete(JooqPersistenceManager pm, PkValue entityId) throws NoSuchEntityException { - super.delete(pm, entityId); - final TableCollection tables = getTables(); - // Also delete all historicalLocations that no longer reference any location - TableImpHistLocations thl = tables.getTableForClass(TableImpHistLocations.class); - TableImpLocationsHistLocations tlhl = tables.getTableForClass(TableImpLocationsHistLocations.class); - int count = pm.getDslContext() - .delete(thl) - .where(((TableField) thl.getId()).in( - DSL.select(thl.getId()) - .from(thl) - .leftJoin(tlhl).on(((TableField) thl.getId()).eq(tlhl.getHistLocationId())) - .where(tlhl.getLocationId().isNull()))) - .execute(); - LOGGER.debug("Deleted {} HistoricalLocations", count); - - } - @Override public EntityType getEntityType() { return pluginCoreModel.etLocation; diff --git a/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpThings.java b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpThings.java index 026fe6c3d..db56b9c42 100644 --- a/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpThings.java +++ b/Plugins/CoreModel/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/coremodel/TableImpThings.java @@ -17,12 +17,8 @@ */ package de.fraunhofer.iosb.ilt.frostserver.plugin.coremodel; -import de.fraunhofer.iosb.ilt.frostserver.model.EntityChangedMessage; import de.fraunhofer.iosb.ilt.frostserver.model.EntityType; import de.fraunhofer.iosb.ilt.frostserver.model.ModelRegistry; -import de.fraunhofer.iosb.ilt.frostserver.model.core.Entity; -import de.fraunhofer.iosb.ilt.frostserver.model.core.EntitySet; -import de.fraunhofer.iosb.ilt.frostserver.model.core.PkValue; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.JooqPersistenceManager; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.bindings.JsonBinding; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.bindings.JsonValue; @@ -32,15 +28,9 @@ import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.tables.StaTableAbstract; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.tables.TableCollection; import de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.validator.SecurityTableWrapper; -import de.fraunhofer.iosb.ilt.frostserver.service.UpdateMode; -import de.fraunhofer.iosb.ilt.frostserver.util.exception.IncompleteEntityException; -import de.fraunhofer.iosb.ilt.frostserver.util.exception.NoSuchEntityException; import de.fraunhofer.iosb.ilt.frostserver.util.user.PrincipalExtended; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import net.time4j.Moment; -import org.jooq.DSLContext; import org.jooq.DataType; import org.jooq.Field; import org.jooq.Name; @@ -136,76 +126,11 @@ public void initProperties(final EntityFactories entityFactories) { pfReg.addEntry(pluginCoreModel.npDatastreamsThing, TableImpThings::getId); pfReg.addEntry(pluginCoreModel.npHistoricalLocationsThing, TableImpThings::getId); pfReg.addEntry(pluginCoreModel.npLocationsThing, TableImpThings::getId); - } - - @Override - protected void updateNavigationPropertySet(Entity thing, EntitySet linkedSet, JooqPersistenceManager pm, UpdateMode updateMode) throws IncompleteEntityException, NoSuchEntityException { - final ModelRegistry modelRegistry = getModelRegistry(); - EntityType linkedEntityType = linkedSet.getEntityType(); - if (linkedEntityType.equals(pluginCoreModel.etLocation)) { - final TableCollection tables = getTables(); - // We know a Thing has a single-valued PK. - Object thingId = thing.getPrimaryKeyValues().get(0); - DSLContext dslContext = pm.getDslContext(); - EntityFactories entityFactories = pm.getEntityFactories(); - TableImpThingsLocations ttl = tables.getTableForClass(TableImpThingsLocations.class); - - // Unlink old Locations from Thing. - long count = dslContext.delete(ttl).where(((TableField) ttl.getThingId()).eq(thingId)).execute(); - LOGGER.debug(EntityFactories.UNLINKED_L_FROM_T, count, thingId); - - // Maybe Create new Locations and link them to this Thing. - List locationIds = new ArrayList<>(); - boolean admin = PrincipalExtended.getLocalPrincipal().isAdmin(); - for (Entity l : linkedSet) { - if (updateMode.createAndLinkNew) { - entityFactories.entityExistsOrCreate(pm, l, updateMode); - } else if (!entityFactories.entityExists(pm, l, admin)) { - throw new NoSuchEntityException("Linked Location with no id."); - } - PkValue lPk = l.getPrimaryKeyValues(); - Object lId = lPk.get(0); - - dslContext.insertInto(ttl) - .set((TableField) ttl.getThingId(), thingId) - .set(ttl.getLocationId(), lId) - .execute(); - LOGGER.debug(EntityFactories.LINKED_L_TO_T, lId, thingId); - locationIds.add(lId); - } - - // Now link the new locations also to a historicalLocation. - if (!locationIds.isEmpty()) { - // Insert a new HL into the DB - TableImpHistLocations qhl = tables.getTableForClass(TableImpHistLocations.class); - Object histLocationId = dslContext.insertInto(qhl) - .set((TableField) qhl.getThingId(), thingId) - .set(qhl.time, Moment.nowInSystemTime()) - .returningResult(qhl.getId()) - .fetchOne(0); - LOGGER.debug(EntityFactories.CREATED_HL, histLocationId); - - // Link the locations to the new HL - TableImpLocationsHistLocations qlhl = tables.getTableForClass(TableImpLocationsHistLocations.class); - for (Object locId : locationIds) { - dslContext.insertInto(qlhl) - .set(((TableField) qlhl.getHistLocationId()), histLocationId) - .set((qlhl.getLocationId()), locId) - .execute(); - LOGGER.debug(EntityFactories.LINKED_L_TO_HL, locId, histLocationId); - } - - // Send a message about the creation of a new HL - Entity newHl = pm.get(pluginCoreModel.etHistoricalLocation, PkValue.of(histLocationId)); - newHl.setQuery(modelRegistry.getMessageQueryGenerator().getQueryFor(newHl.getEntityType())); - pm.getEntityChangedMessages().add( - new EntityChangedMessage() - .setEventType(EntityChangedMessage.Type.CREATE) - .setEntity(newHl)); - } - return; - } - super.updateNavigationPropertySet(thing, linkedSet, pm, updateMode); + HookPrePostInsertUpdateThing hook = new HookPrePostInsertUpdateThing(); + registerHookPreInsert(0, hook); + registerHookPostInsert(0, hook); + registerHookPreUpdate(0, hook); + registerHookPostUpdate(0, hook); } @Override diff --git a/Plugins/CoreModelV2/src/main/resources/plugincoremodelv2/model/HistoricalLocation.json b/Plugins/CoreModelV2/src/main/resources/plugincoremodelv2/model/HistoricalLocation.json index 5d6b2878a..3ff6636ca 100644 --- a/Plugins/CoreModelV2/src/main/resources/plugincoremodelv2/model/HistoricalLocation.json +++ b/Plugins/CoreModelV2/src/main/resources/plugincoremodelv2/model/HistoricalLocation.json @@ -1,69 +1,76 @@ { - "conformance": [], - "simplePropertyTypes": [], - "entityTypes": [ - { - "name": "HistoricalLocation", - "plural": "HistoricalLocations", - "adminOnly": false, - "table": "hist_locations", - "orderByDflt": [], - "entityProperties": [ + "conformance": [], + "simplePropertyTypes": [], + "entityTypes": [ { - "name": "id", - "aliases": [ - "@iot.id" - ], - "type": "Id", - "handlers": [ - { - "@class": "de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.fieldmapper.FieldMapperId", - "field": "id" - } - ], - "annotations": [] - }, - { - "name": "time", - "type": "Edm.DateTimeOffset", - "required": true, - "nullable": false, - "handlers": [ - { - "@class": "de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.fieldmapper.FieldMapperTimeInstant", - "field": "time" - } - ], - "annotations": [] - } - ], - "navigationProperties": [ - { - "name": "Thing", - "entitySet": false, - "entityType": "Thing", - "required": true, - "symmetrical": false, - "priority": 0, - "inverse": { - "name": "HistoricalLocations", - "entitySet": true, - "required": false, - "priority": 0, + "name": "HistoricalLocation", + "plural": "HistoricalLocations", + "adminOnly": false, + "table": "hist_locations", + "orderByDflt": [], + "entityProperties": [ + { + "name": "id", + "aliases": [ + "@iot.id" + ], + "type": "Id", + "handlers": [ + { + "@class": "de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.fieldmapper.FieldMapperId", + "field": "id" + } + ], + "annotations": [] + }, + { + "name": "time", + "type": "Edm.DateTimeOffset", + "required": true, + "nullable": false, + "handlers": [ + { + "@class": "de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.fieldmapper.FieldMapperTimeInstant", + "field": "time" + } + ], + "annotations": [] + } + ], + "navigationProperties": [ + { + "name": "Thing", + "entitySet": false, + "entityType": "Thing", + "required": true, + "symmetrical": false, + "priority": 0, + "inverse": { + "name": "HistoricalLocations", + "entitySet": true, + "required": false, + "priority": 0, + "annotations": [] + }, + "handlers": [ + { + "@class": "de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.fieldmapper.FieldMapperOneToMany", + "field": "thing_id", + "otherTable": "things", + "otherField": "id" + } + ], + "annotations": [] + } + ], + "hooks": [ + { + "hook": { + "@class": "de.fraunhofer.iosb.ilt.frostserver.plugin.coremodel.HookPostInsertHistLoc" + } + } + ], "annotations": [] - }, - "handlers": [ - { - "@class": "de.fraunhofer.iosb.ilt.frostserver.persistence.pgjooq.utils.fieldmapper.FieldMapperOneToMany", - "field": "thing_id", - "otherTable": "things", - "otherField": "id" - } - ], - "annotations": [] } - ], - "annotations": [] - } - ] + ] } \ No newline at end of file diff --git a/Plugins/CoreModelV2/src/main/resources/plugincoremodelv2/model/Location.json b/Plugins/CoreModelV2/src/main/resources/plugincoremodelv2/model/Location.json index 81bc02d2f..add04ba6b 100644 --- a/Plugins/CoreModelV2/src/main/resources/plugincoremodelv2/model/Location.json +++ b/Plugins/CoreModelV2/src/main/resources/plugincoremodelv2/model/Location.json @@ -113,6 +113,13 @@ "annotations": [] } ], + "hooks": [ + { + "hook": { + "@class": "de.fraunhofer.iosb.ilt.frostserver.plugin.coremodel.HookPostDeleteLocation" + } + } + ], "annotations": [] } ] diff --git a/Plugins/MultiDatastream/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/multidatastream/TableImpMultiDatastreams.java b/Plugins/MultiDatastream/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/multidatastream/TableImpMultiDatastreams.java index fc8758185..51c4f9a33 100644 --- a/Plugins/MultiDatastream/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/multidatastream/TableImpMultiDatastreams.java +++ b/Plugins/MultiDatastream/src/main/java/de/fraunhofer/iosb/ilt/frostserver/plugin/multidatastream/TableImpMultiDatastreams.java @@ -326,7 +326,7 @@ public void initProperties(final EntityFactories entityFactories) { return true; }); obsPropsTable.registerHookPreUpdate(-1, - (pm, entity, entityId) -> { + (pm, entity, entityId, updateMode) -> { EntitySet mds = entity.getProperty(pluginMultiDatastream.npMultiDatastreamsObsProp); if (mds != null && !mds.isEmpty()) { throw new IllegalArgumentException("Adding a MultiDatastream to an ObservedProperty is not allowed."); @@ -385,7 +385,7 @@ public void initProperties(final EntityFactories entityFactories) { return true; }); // On update, make sure we still have either a DS or MDS, but not both. - observationsTable.registerHookPreUpdate(-1, (pm, entity, entityId) -> { + observationsTable.registerHookPreUpdate(-1, (pm, entity, entityId, updateMode) -> { Entity oldObservation = pm.get(pluginCoreModel.etObservation, entityId); boolean newHasDatastream = checkDatastreamSet(oldObservation, entity, pm); boolean newHasMultiDatastream = checkMultiDatastreamSet(oldObservation, entity, pm); diff --git a/Tools/ModelEditor/pom.xml b/Tools/ModelEditor/pom.xml index 96e326e12..9ca5248d6 100644 --- a/Tools/ModelEditor/pom.xml +++ b/Tools/ModelEditor/pom.xml @@ -29,6 +29,11 @@ FROST-Server.Core.Model ${project.version} + + ${project.groupId} + FROST-Server.Plugin.CoreModel + ${project.version} + ${project.groupId} FROST-Server.SQLjooq