This is an automated email from the ASF dual-hosted git repository.

dominikriemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new 6543af8fad refactor: Change data stream update event to only publish 
schema changes (#5163)
6543af8fad is described below

commit 6543af8fad6331dd70c3820f4072083e4df1df92
Author: Sven Oehler <[email protected]>
AuthorDate: Fri Oct 30 12:25:35 2026 +0100

    refactor: Change data stream update event to only publish schema changes 
(#5163)
---
 .../management/AdapterUpdateManagement.java        |  23 +---
 .../management/AdapterSchemaUpdateTest.java        |  94 ++++++++++++++++
 .../management/AdapterUpdateAuditTest.java         |   8 +-
 ...vent.java => DataStreamSchemaUpdatedEvent.java} |   4 +-
 .../update/DataStreamUpdateManagement.java         |  14 ++-
 .../pipeline/update/EventSchemaChangeDetector.java |  84 ++++++++++++++
 .../update/DataStreamUpdateManagementTest.java     | 115 +++++++++++++++++++
 .../update/EventSchemaChangeDetectorTest.java      | 122 +++++++++++++++++++++
 .../rest/impl/connect/AdapterResource.java         |  13 +--
 .../rest/impl/connect/CompactAdapterResource.java  |   7 +-
 .../rest/impl/pe/DataStreamResource.java           |  10 +-
 .../core/PipelineManagementConfiguration.java      |  11 ++
 .../core/storage/StorageApiConfigurationTest.java  |   5 +
 13 files changed, 460 insertions(+), 50 deletions(-)

diff --git 
a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterUpdateManagement.java
 
b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterUpdateManagement.java
index e5d8eb53be..8bdf0b62fd 100644
--- 
a/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterUpdateManagement.java
+++ 
b/streampipes-connect-management/src/main/java/org/apache/streampipes/connect/management/management/AdapterUpdateManagement.java
@@ -22,9 +22,7 @@ import org.apache.streampipes.audit.api.AuditOutcome;
 import org.apache.streampipes.audit.events.adapter.AdapterAuditRecorder;
 import org.apache.streampipes.audit.events.adapter.AdapterEditedDetails;
 import org.apache.streampipes.commons.exceptions.connect.AdapterException;
-import 
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
 import 
org.apache.streampipes.manager.pipeline.update.DataStreamUpdateManagement;
-import org.apache.streampipes.manager.pipeline.update.DataStreamUpdatedEvent;
 import org.apache.streampipes.model.SpDataStream;
 import org.apache.streampipes.model.connect.adapter.AdapterDescription;
 import org.apache.streampipes.model.connect.adapter.PipelineUpdateInfo;
@@ -32,8 +30,6 @@ import 
org.apache.streampipes.resource.management.AdapterResourceManager;
 import org.apache.streampipes.resource.management.DataStreamResourceManager;
 import org.apache.streampipes.resource.management.SpResourceManager;
 
-import org.springframework.context.ApplicationEventPublisher;
-
 import java.util.List;
 
 public class AdapterUpdateManagement {
@@ -44,21 +40,15 @@ public class AdapterUpdateManagement {
   private final AdapterResourceManager adapterResourceManager;
   private final DataStreamResourceManager dataStreamResourceManager;
   private final DataStreamUpdateManagement dataStreamUpdateManagement;
-  private final ApplicationEventPublisher eventPublisher;
 
   public AdapterUpdateManagement(AdapterMasterManagement 
adapterMasterManagement,
-                                 ExtensionServiceRequestManager requestManager,
                                  SpResourceManager resourceManager,
-                                 ApplicationEventPublisher eventPublisher) {
+                                 DataStreamUpdateManagement 
dataStreamUpdateManagement) {
     this.adapterAudit = new 
AdapterAuditRecorder(resourceManager.getAuditService());
     this.adapterMasterManagement = adapterMasterManagement;
     this.adapterResourceManager = resourceManager.manageAdapters();
     this.dataStreamResourceManager = resourceManager.manageDataStreams();
-    this.dataStreamUpdateManagement = new DataStreamUpdateManagement(
-        requestManager,
-        resourceManager
-    );
-    this.eventPublisher = eventPublisher;
+    this.dataStreamUpdateManagement = dataStreamUpdateManagement;
   }
 
   public void updateAdapter(AdapterDescription ad)
@@ -84,7 +74,6 @@ public class AdapterUpdateManagement {
       // update data source
       var updatedDataStream = this.updateDataSource(ad);
       dataStreamUpdateManagement.updateDataStream(updatedDataStream);
-      publishEvent(new DataStreamUpdatedEvent(updatedDataStream));
 
       if (shouldRestart) {
         this.adapterMasterManagement.startAdapter(ad.getElementId(), actor);
@@ -104,7 +93,7 @@ public class AdapterUpdateManagement {
     // get data source
     SpDataStream dataStream = 
this.dataStreamResourceManager.find(ad.getCorrespondingDataStreamElementId());
 
-    return SourcesManagement.updateDataStream(ad, dataStream);
+    return SourcesManagement.updateDataStream(ad, new 
SpDataStream(dataStream));
   }
 
   private SpDataStream toDataStreamUpdate(AdapterDescription 
adapterDescription) {
@@ -115,10 +104,4 @@ public class AdapterUpdateManagement {
     dataStream.setEventSchema(adapterDescription.getEventSchema());
     return dataStream;
   }
-
-  private void publishEvent(Object event) {
-    if (eventPublisher != null) {
-      eventPublisher.publishEvent(event);
-    }
-  }
 }
diff --git 
a/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/AdapterSchemaUpdateTest.java
 
b/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/AdapterSchemaUpdateTest.java
new file mode 100644
index 0000000000..347affff1f
--- /dev/null
+++ 
b/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/AdapterSchemaUpdateTest.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.connect.management.management;
+
+import org.apache.streampipes.audit.api.AuditService;
+import 
org.apache.streampipes.manager.pipeline.update.DataStreamSchemaUpdatedEvent;
+import 
org.apache.streampipes.manager.pipeline.update.DataStreamUpdateManagement;
+import 
org.apache.streampipes.manager.pipeline.update.PipelineUpdateCoordinator;
+import org.apache.streampipes.model.SpDataStream;
+import org.apache.streampipes.model.connect.adapter.AdapterDescription;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+import org.apache.streampipes.resource.management.AdapterResourceManager;
+import org.apache.streampipes.resource.management.DataStreamResourceManager;
+import org.apache.streampipes.resource.management.SpResourceManager;
+import org.apache.streampipes.storage.api.connect.IAdapterStorage;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.context.ApplicationEventPublisher;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+class AdapterSchemaUpdateTest {
+
+  @Test
+  void publishesSchemaEventWhenAdapterFieldAdded() throws Exception {
+    verifyUpdate(true);
+  }
+
+  @Test
+  void doesNotPublishSchemaEventWhenAdapterRenamed() throws Exception {
+    verifyUpdate(false);
+  }
+
+  private void verifyUpdate(boolean schemaChanged) throws Exception {
+    var resources = mock(SpResourceManager.class);
+    var adapters = mock(AdapterResourceManager.class);
+    var streams = mock(DataStreamResourceManager.class);
+    var storage = mock(IAdapterStorage.class);
+    var publisher = mock(ApplicationEventPublisher.class);
+    when(resources.getAuditService()).thenReturn(mock(AuditService.class));
+    when(resources.manageAdapters()).thenReturn(adapters);
+    when(resources.manageDataStreams()).thenReturn(streams);
+    when(adapters.getDb()).thenReturn(storage);
+    when(storage.getElementById("adapter-1")).thenReturn(new 
AdapterDescription());
+    var previous = new SpDataStream();
+    previous.setElementId("stream-1");
+    when(streams.find("stream-1")).thenReturn(previous);
+    var updated = new AdapterDescription();
+    updated.setElementId("adapter-1");
+    updated.setCorrespondingDataStreamElementId("stream-1");
+    updated.setName("Renamed adapter");
+    var adapterStream = new SpDataStream(previous);
+    if (schemaChanged) {
+      adapterStream.getEventSchema().addEventProperty(new 
EventPropertyPrimitive("xs:double", "value", null, null));
+    }
+    updated.setDataStream(adapterStream);
+
+    try (var ignored = mockConstruction(PipelineUpdateCoordinator.class)) {
+      new AdapterUpdateManagement(mock(AdapterMasterManagement.class), 
resources,
+          new DataStreamUpdateManagement(null, resources, publisher))
+          .updateAdapter(updated, "user-1");
+
+      verify(streams).update(any(SpDataStream.class));
+      assertEquals(0, previous.getEventSchema().getEventProperties().size());
+      if (schemaChanged) {
+        
verify(publisher).publishEvent(any(DataStreamSchemaUpdatedEvent.class));
+      } else {
+        verifyNoInteractions(publisher);
+      }
+    }
+  }
+}
diff --git 
a/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/AdapterUpdateAuditTest.java
 
b/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/AdapterUpdateAuditTest.java
index fde3170441..dc254c7233 100644
--- 
a/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/AdapterUpdateAuditTest.java
+++ 
b/streampipes-connect-management/src/test/java/org/apache/streampipes/connect/management/management/AdapterUpdateAuditTest.java
@@ -40,7 +40,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.mockConstruction;
 import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
@@ -84,6 +83,8 @@ class AdapterUpdateAuditTest {
     before.setName("old");
     var after = new AdapterDescription();
     after.setElementId("adapter-1");
+    after.setCorrespondingDataStreamElementId("stream-1");
+    when(streams.find("stream-1")).thenReturn(new SpDataStream());
     after.setName(changed ? "new" : "old");
     after.setRunning(true);
     when(storage.getElementById("adapter-1")).thenReturn(before);
@@ -93,10 +94,9 @@ class AdapterUpdateAuditTest {
     if (failRestart) {
       doThrow(new 
AdapterException("private")).when(master).startAdapter("adapter-1", "user-1");
     }
-    try (var updates = mockConstruction(DataStreamUpdateManagement.class);
-         var sources = mockStatic(SourcesManagement.class)) {
+    try (var sources = mockStatic(SourcesManagement.class)) {
       sources.when(() -> SourcesManagement.updateDataStream(any(), 
any())).thenReturn(new SpDataStream());
-      var manager = new AdapterUpdateManagement(master, null, resources, null);
+      var manager = new AdapterUpdateManagement(master, resources, 
mock(DataStreamUpdateManagement.class));
       if (failPersistence || failRestart) {
         assertThrows(AdapterException.class, () -> 
manager.updateAdapter(after, "user-1"));
       } else {
diff --git 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/DataStreamUpdatedEvent.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/DataStreamSchemaUpdatedEvent.java
similarity index 88%
rename from 
streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/DataStreamUpdatedEvent.java
rename to 
streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/DataStreamSchemaUpdatedEvent.java
index 76bb4622c6..abdf023a75 100644
--- 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/DataStreamUpdatedEvent.java
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/DataStreamSchemaUpdatedEvent.java
@@ -18,7 +18,5 @@
 
 package org.apache.streampipes.manager.pipeline.update;
 
-import org.apache.streampipes.model.SpDataStream;
-
-public record DataStreamUpdatedEvent(SpDataStream dataStream) {
+public record DataStreamSchemaUpdatedEvent(String elementId, String name) {
 }
diff --git 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/DataStreamUpdateManagement.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/DataStreamUpdateManagement.java
index 9fe004577c..5de6578114 100644
--- 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/DataStreamUpdateManagement.java
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/DataStreamUpdateManagement.java
@@ -24,22 +24,34 @@ import 
org.apache.streampipes.model.connect.adapter.PipelineUpdateInfo;
 import org.apache.streampipes.resource.management.DataStreamResourceManager;
 import org.apache.streampipes.resource.management.SpResourceManager;
 
+import org.springframework.context.ApplicationEventPublisher;
+
 import java.util.List;
+import java.util.Objects;
 
 public class DataStreamUpdateManagement {
 
   private final DataStreamResourceManager dataStreamResourceManager;
   private final PipelineUpdateCoordinator pipelineUpdateCoordinator;
+  private final ApplicationEventPublisher eventPublisher;
 
   public DataStreamUpdateManagement(ExtensionServiceRequestManager 
requestManager,
-                                    SpResourceManager resourceManager) {
+                                    SpResourceManager resourceManager,
+                                    ApplicationEventPublisher eventPublisher) {
+    this.eventPublisher = Objects.requireNonNull(eventPublisher);
     this.pipelineUpdateCoordinator = new 
PipelineUpdateCoordinator(requestManager, resourceManager);
     this.dataStreamResourceManager = resourceManager.manageDataStreams();
   }
 
   public void updateDataStream(SpDataStream dataStream) {
+    var previous = dataStreamResourceManager.find(dataStream.getElementId());
+    boolean schemaChanged = EventSchemaChangeDetector.hasChanged(
+        previous == null ? null : previous.getEventSchema(), 
dataStream.getEventSchema());
     dataStreamResourceManager.update(dataStream);
     pipelineUpdateCoordinator.updatePipelines(dataStream);
+    if (schemaChanged) {
+      eventPublisher.publishEvent(new 
DataStreamSchemaUpdatedEvent(dataStream.getElementId(), dataStream.getName()));
+    }
   }
 
   public List<PipelineUpdateInfo> checkPipelineMigrations(SpDataStream 
dataStream) {
diff --git 
a/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/EventSchemaChangeDetector.java
 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/EventSchemaChangeDetector.java
new file mode 100644
index 0000000000..d300b2b396
--- /dev/null
+++ 
b/streampipes-pipeline-management/src/main/java/org/apache/streampipes/manager/pipeline/update/EventSchemaChangeDetector.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.manager.pipeline.update;
+
+import org.apache.streampipes.model.schema.EventProperty;
+import org.apache.streampipes.model.schema.EventPropertyList;
+import org.apache.streampipes.model.schema.EventPropertyNested;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+import org.apache.streampipes.model.schema.EventSchema;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+final class EventSchemaChangeDetector {
+
+  private EventSchemaChangeDetector() {
+  }
+
+  static boolean hasChanged(EventSchema previous, EventSchema updated) {
+    return !sameProperties(previous == null ? null : 
previous.getEventProperties(),
+        updated == null ? null : updated.getEventProperties());
+  }
+
+  private static boolean sameProperties(List<EventProperty> previous, 
List<EventProperty> updated) {
+    var previousProperties = previous == null ? List.<EventProperty>of() : 
previous;
+    var unmatched = new ArrayList<>(updated == null ? List.<EventProperty>of() 
: updated);
+    if (previousProperties.size() != unmatched.size()) {
+      return false;
+    }
+    // Match fields independently of order, including fields within nested 
objects.
+    for (var property : previousProperties) {
+      boolean matched = false;
+      for (int i = 0; i < unmatched.size(); i++) {
+        if (sameProperty(property, unmatched.get(i))) {
+          unmatched.remove(i);
+          matched = true;
+          break;
+        }
+      }
+      if (!matched) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static boolean sameProperty(EventProperty previous, EventProperty 
updated) {
+    if (previous == null || updated == null) {
+      return previous == updated;
+    }
+    if (previous.getClass() != updated.getClass()
+        || !Objects.equals(previous.getRuntimeName(), updated.getRuntimeName())
+        || !Objects.equals(previous.getPropertyScope(), 
updated.getPropertyScope())) {
+      return false;
+    }
+    if (previous instanceof EventPropertyPrimitive primitive) {
+      return Objects.equals(primitive.getRuntimeType(), 
((EventPropertyPrimitive) updated).getRuntimeType());
+    }
+    if (previous instanceof EventPropertyNested nested) {
+      return sameProperties(nested.getEventProperties(), 
((EventPropertyNested) updated).getEventProperties());
+    }
+    if (previous instanceof EventPropertyList list) {
+      return sameProperty(list.getEventProperty(), ((EventPropertyList) 
updated).getEventProperty());
+    }
+    return true;
+  }
+}
diff --git 
a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/update/DataStreamUpdateManagementTest.java
 
b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/update/DataStreamUpdateManagementTest.java
new file mode 100644
index 0000000000..97e16c7f0e
--- /dev/null
+++ 
b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/update/DataStreamUpdateManagementTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.manager.pipeline.update;
+
+import org.apache.streampipes.model.SpDataStream;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+import org.apache.streampipes.resource.management.DataStreamResourceManager;
+import org.apache.streampipes.resource.management.SpResourceManager;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.context.ApplicationEventPublisher;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+class DataStreamUpdateManagementTest {
+
+  @Test
+  void publishesSchemaEventAfterSuccessfulUpdate() {
+    var previous = stream();
+    var updated = new SpDataStream(previous);
+    updated.getEventSchema().addEventProperty(new 
EventPropertyPrimitive("xs:string", "new-field", null, null));
+    var resources = mock(SpResourceManager.class);
+    var streams = mock(DataStreamResourceManager.class);
+    var publisher = mock(ApplicationEventPublisher.class);
+    when(resources.manageDataStreams()).thenReturn(streams);
+    when(streams.find("stream-1")).thenReturn(previous);
+
+    try (var coordinators = mockConstruction(PipelineUpdateCoordinator.class)) 
{
+      var management = new DataStreamUpdateManagement(null, resources, 
publisher);
+      management.updateDataStream(updated);
+
+      var order = inOrder(streams, coordinators.constructed().getFirst(), 
publisher);
+      order.verify(streams).find("stream-1");
+      order.verify(streams).update(updated);
+      
order.verify(coordinators.constructed().getFirst()).updatePipelines(updated);
+      order.verify(publisher).publishEvent(new 
DataStreamSchemaUpdatedEvent(updated.getElementId(), updated.getName()));
+    }
+  }
+
+  @Test
+  void persistsStreamRenameWithoutSchemaEvent() {
+    var previous = stream();
+    var updated = new SpDataStream(previous);
+    updated.setName("Renamed stream");
+    updated.setDescription("New description");
+    var resources = mock(SpResourceManager.class);
+    var streams = mock(DataStreamResourceManager.class);
+    var publisher = mock(ApplicationEventPublisher.class);
+    when(resources.manageDataStreams()).thenReturn(streams);
+    when(streams.find("stream-1")).thenReturn(previous);
+
+    try (var coordinators = mockConstruction(PipelineUpdateCoordinator.class)) 
{
+      new DataStreamUpdateManagement(null, resources, 
publisher).updateDataStream(updated);
+
+      verify(streams).update(updated);
+      verify(coordinators.constructed().getFirst()).updatePipelines(updated);
+      verifyNoInteractions(publisher);
+    }
+  }
+
+  @Test
+  void doesNotPublishWhenPersistenceFails() {
+    var updated = stream();
+    updated.getEventSchema().addEventProperty(new 
EventPropertyPrimitive("xs:string", "new-field", null, null));
+    var resources = mock(SpResourceManager.class);
+    var streams = mock(DataStreamResourceManager.class);
+    var publisher = mock(ApplicationEventPublisher.class);
+    when(resources.manageDataStreams()).thenReturn(streams);
+    when(streams.find("stream-1")).thenReturn(stream());
+    doThrow(new IllegalStateException("Persistence 
failed")).when(streams).update(updated);
+
+    try (var coordinators = mockConstruction(PipelineUpdateCoordinator.class)) 
{
+      var management = new DataStreamUpdateManagement(null, resources, 
publisher);
+      assertThrows(IllegalStateException.class, () -> 
management.updateDataStream(updated));
+
+      verifyNoInteractions(publisher, coordinators.constructed().getFirst());
+    }
+  }
+
+  @Test
+  void requiresEventPublisher() {
+    assertThrows(NullPointerException.class,
+        () -> new DataStreamUpdateManagement(null, 
mock(SpResourceManager.class), null));
+  }
+
+  private SpDataStream stream() {
+    var stream = new SpDataStream();
+    stream.setElementId("stream-1");
+    stream.setName("Stream");
+    return stream;
+  }
+}
diff --git 
a/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/update/EventSchemaChangeDetectorTest.java
 
b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/update/EventSchemaChangeDetectorTest.java
new file mode 100644
index 0000000000..81d0b6086f
--- /dev/null
+++ 
b/streampipes-pipeline-management/src/test/java/org/apache/streampipes/manager/pipeline/update/EventSchemaChangeDetectorTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.streampipes.manager.pipeline.update;
+
+import org.apache.streampipes.model.schema.EventProperty;
+import org.apache.streampipes.model.schema.EventPropertyList;
+import org.apache.streampipes.model.schema.EventPropertyNested;
+import org.apache.streampipes.model.schema.EventPropertyPrimitive;
+import org.apache.streampipes.model.schema.EventSchema;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.net.URI;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class EventSchemaChangeDetectorTest {
+
+  @ParameterizedTest
+  @MethodSource("schemaChanges")
+  void detectsFieldChanges(EventSchema previous, EventSchema updated) {
+    assertTrue(EventSchemaChangeDetector.hasChanged(previous, updated));
+    assertTrue(EventSchemaChangeDetector.hasChanged(updated, previous));
+  }
+
+  private static Stream<Arguments> schemaChanges() {
+    var original = schema(field("value", "xs:string", "measurement"));
+    return Stream.of(
+        Arguments.of(original, schema()),
+        Arguments.of(original, schema(field("value", "xs:string", 
"measurement"), field("extra"))),
+        Arguments.of(original, schema(field("renamed", "xs:string", 
"measurement"))),
+        Arguments.of(original, schema(field("value", "xs:double", 
"measurement"))),
+        Arguments.of(original, schema(field("value", "xs:string", 
"dimension"))),
+        Arguments.of(original, schema(field("value", "xs:string", null))),
+        Arguments.of(schema(new EventPropertyNested("object", 
List.of(field("child")))),
+            schema(new EventPropertyNested("object", 
List.of(field("renamed"))))),
+        Arguments.of(schema(new EventPropertyNested("object", 
List.of(field("child")))),
+            schema(new EventPropertyNested("object", List.of()))),
+        Arguments.of(schema(new EventPropertyList("values", field("item"))),
+            schema(new EventPropertyList("values", field("item", "xs:double", 
null)))),
+        Arguments.of(schema(new EventPropertyList("values", new 
EventPropertyNested("item", List.of(field("child"))))),
+            schema(new EventPropertyList("values", new 
EventPropertyNested("item", List.of(field("new")))))),
+        Arguments.of(schema(field("value")), schema(new 
EventPropertyNested("value", List.of()))),
+        Arguments.of(schema(field("value")), schema(new 
EventPropertyList("value", field("item")))),
+        Arguments.of(null, schema(field("value")))
+    );
+  }
+
+  @Test
+  void ignoresFieldOrderAtEveryLevel() {
+    var previous = schema(field("a"), new EventPropertyNested("object", 
List.of(field("b"), field("c"))),
+        new EventPropertyList("list", new EventPropertyNested("item", 
List.of(field("d"), field("e")))));
+    var updated = schema(
+        new EventPropertyList("list", new EventPropertyNested("item", 
List.of(field("e"), field("d")))),
+        new EventPropertyNested("object", List.of(field("c"), field("b"))), 
field("a"));
+
+    assertFalse(EventSchemaChangeDetector.hasChanged(previous, updated));
+  }
+
+  @Test
+  void ignoresDescriptiveAndGeneratedMetadata() {
+    var previous = field("value");
+    var updated = field("value");
+    updated.setLabel("New label");
+    updated.setDescription("New description");
+    updated.setElementId("new-id");
+    updated.setRuntimeId("new-runtime-id");
+    updated.setSemanticType("new-semantic-type");
+    updated.setMeasurementUnit(URI.create("urn:unit:new"));
+
+    assertFalse(EventSchemaChangeDetector.hasChanged(schema(previous), 
schema(updated)));
+  }
+
+  @Test
+  void treatsMissingAndEmptySchemasAsHavingNoFields() {
+    assertFalse(EventSchemaChangeDetector.hasChanged(null, schema()));
+    var missingFields = schema();
+    missingFields.setEventProperties(null);
+    assertFalse(EventSchemaChangeDetector.hasChanged(missingFields, schema()));
+  }
+
+  @Test
+  void detectsDuplicateFieldRemoval() {
+    assertTrue(EventSchemaChangeDetector.hasChanged(schema(field("value"), 
field("value")), schema(field("value"))));
+  }
+
+  private static EventSchema schema(EventProperty... properties) {
+    return new EventSchema(List.of(properties));
+  }
+
+  private static EventPropertyPrimitive field(String name) {
+    return field(name, "xs:string", null);
+  }
+
+  private static EventPropertyPrimitive field(String name, String type, String 
scope) {
+    var property = new EventPropertyPrimitive(type, name, null, null);
+    property.setPropertyScope(scope);
+    return property;
+  }
+}
diff --git 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java
 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java
index cbacb0482e..2245858c4c 100644
--- 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/AdapterResource.java
@@ -28,6 +28,7 @@ import 
org.apache.streampipes.connect.management.management.WorkerRestClient;
 import 
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
 import org.apache.streampipes.manager.pipeline.PipelineManager;
 import org.apache.streampipes.manager.pipeline.update.DataStreamDeletedEvent;
+import 
org.apache.streampipes.manager.pipeline.update.DataStreamUpdateManagement;
 import org.apache.streampipes.model.client.user.DefaultRole;
 import org.apache.streampipes.model.client.user.Permission;
 import org.apache.streampipes.model.connect.adapter.AdapterDescription;
@@ -86,18 +87,11 @@ public class AdapterResource extends 
AbstractAdapterResource<AdapterMasterManage
   private final SpResourceManager resourceManager;
   private final IAdapterStorage adapterDescriptionStorage;
 
-  public AdapterResource(WorkerRestClient workerRestClient,
-                         ExtensionServiceRequestManager requestManager,
-                         SpResourceManager resourceManager,
-                         @Qualifier("adapterDescriptionStorage") 
IAdapterStorage adapterDescriptionStorage,
-                         IExtensionsServiceStorage extensionsServiceStorage) {
-    this(workerRestClient, requestManager, null, resourceManager, 
adapterDescriptionStorage, extensionsServiceStorage);
-  }
-
   @Autowired
   public AdapterResource(WorkerRestClient workerRestClient,
                          ExtensionServiceRequestManager requestManager,
                          ApplicationEventPublisher eventPublisher,
+                         DataStreamUpdateManagement dataStreamUpdateManagement,
                          SpResourceManager resourceManager,
                          @Qualifier("adapterDescriptionStorage") 
IAdapterStorage adapterDescriptionStorage,
                          IExtensionsServiceStorage extensionsServiceStorage) {
@@ -118,9 +112,8 @@ public class AdapterResource extends 
AbstractAdapterResource<AdapterMasterManage
     );
     this.adapterUpdateManagement = new AdapterUpdateManagement(
         managementService,
-        requestManager,
         resourceManager,
-        eventPublisher
+        dataStreamUpdateManagement
     );
   }
 
diff --git 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/CompactAdapterResource.java
 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/CompactAdapterResource.java
index 9ad177a823..4ac3a71d1b 100644
--- 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/CompactAdapterResource.java
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/connect/CompactAdapterResource.java
@@ -32,6 +32,7 @@ import 
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestMana
 import 
org.apache.streampipes.manager.execution.endpoint.ExtensionsServiceEndpointGenerator;
 import org.apache.streampipes.manager.pipeline.PipelineManager;
 import 
org.apache.streampipes.manager.pipeline.compact.CompactPipelineManagement;
+import 
org.apache.streampipes.manager.pipeline.update.DataStreamUpdateManagement;
 import org.apache.streampipes.model.client.user.DefaultPrivilege;
 import org.apache.streampipes.model.connect.adapter.AdapterDescription;
 import org.apache.streampipes.model.connect.adapter.compact.CompactAdapter;
@@ -49,7 +50,6 @@ import 
org.apache.streampipes.svcdiscovery.api.ISpServiceDiscovery;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
@@ -76,7 +76,7 @@ public class CompactAdapterResource extends 
AbstractAdapterResource<AdapterMaste
 
   public CompactAdapterResource(WorkerRestClient workerRestClient,
                                 ExtensionServiceRequestManager requestManager,
-                                ApplicationEventPublisher eventPublisher,
+                                DataStreamUpdateManagement 
dataStreamUpdateManagement,
                                 SpResourceManager resourceManager,
                                 @Qualifier("adapterDescriptionStorage") 
IAdapterStorage adapterDescriptionStorage,
                                 IExtensionsServiceStorage 
extensionsServiceStorage,
@@ -108,9 +108,8 @@ public class CompactAdapterResource extends 
AbstractAdapterResource<AdapterMaste
     );
     this.adapterUpdateManagement = new AdapterUpdateManagement(
         managementService,
-        requestManager,
         resourceManager,
-        eventPublisher
+        dataStreamUpdateManagement
     );
   }
 
diff --git 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/pe/DataStreamResource.java
 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/pe/DataStreamResource.java
index f7063bff4e..947df373e4 100644
--- 
a/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/pe/DataStreamResource.java
+++ 
b/streampipes-rest/src/main/java/org/apache/streampipes/rest/impl/pe/DataStreamResource.java
@@ -18,10 +18,8 @@
 
 package org.apache.streampipes.rest.impl.pe;
 
-import 
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
 import org.apache.streampipes.manager.pipeline.update.DataStreamDeletedEvent;
 import 
org.apache.streampipes.manager.pipeline.update.DataStreamUpdateManagement;
-import org.apache.streampipes.manager.pipeline.update.DataStreamUpdatedEvent;
 import org.apache.streampipes.model.SpDataStream;
 import org.apache.streampipes.model.connect.adapter.PipelineUpdateInfo;
 import org.apache.streampipes.model.message.Message;
@@ -57,14 +55,11 @@ public class DataStreamResource extends 
AbstractAuthGuardedRestResource {
   private final ApplicationEventPublisher eventPublisher;
   private final DataStreamResourceManager dataStreamResourceManager;
 
-  public DataStreamResource(ExtensionServiceRequestManager requestManager,
+  public DataStreamResource(DataStreamUpdateManagement 
dataStreamUpdateManagement,
                             ApplicationEventPublisher eventPublisher,
                             SpResourceManager resourceManager) {
     this.dataStreamResourceManager = resourceManager.manageDataStreams();
-    this.dataStreamUpdateManagement = new DataStreamUpdateManagement(
-        requestManager,
-        resourceManager
-    );
+    this.dataStreamUpdateManagement = dataStreamUpdateManagement;
     this.eventPublisher = eventPublisher;
   }
 
@@ -112,7 +107,6 @@ public class DataStreamResource extends 
AbstractAuthGuardedRestResource {
           "Element ID in path variable does not match element ID in request 
body");
     } else {
       dataStreamUpdateManagement.updateDataStream(updatedElement);
-      publishEvent(new DataStreamUpdatedEvent(updatedElement));
       return 
constructSuccessMessage(NotificationType.STORAGE_SUCCESS.uiNotification());
     }
   }
diff --git 
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/PipelineManagementConfiguration.java
 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/PipelineManagementConfiguration.java
index 77e23708b8..f75163b1f2 100644
--- 
a/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/PipelineManagementConfiguration.java
+++ 
b/streampipes-service-core/src/main/java/org/apache/streampipes/service/core/PipelineManagementConfiguration.java
@@ -18,9 +18,12 @@
 
 package org.apache.streampipes.service.core;
 
+import 
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
 import org.apache.streampipes.manager.extensions.AvailableExtensionsProvider;
+import 
org.apache.streampipes.manager.pipeline.update.DataStreamUpdateManagement;
 import org.apache.streampipes.manager.setup.InstallationConfiguration;
 import org.apache.streampipes.resource.management.ResourceDeletionManager;
+import org.apache.streampipes.resource.management.SpResourceManager;
 import org.apache.streampipes.storage.api.connect.IAdapterStorage;
 import 
org.apache.streampipes.storage.api.pipeline.ICompactPipelineTemplateStorage;
 import 
org.apache.streampipes.storage.api.pipeline.IPipelineElementDescriptionStorage;
@@ -30,12 +33,20 @@ import 
org.apache.streampipes.svcdiscovery.SpServiceDiscoveryCore;
 import org.apache.streampipes.svcdiscovery.api.ISpServiceDiscovery;
 
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
 @Configuration
 public class PipelineManagementConfiguration {
 
+  @Bean
+  public DataStreamUpdateManagement 
dataStreamUpdateManagement(ExtensionServiceRequestManager requestManager,
+                                                              
SpResourceManager resourceManager,
+                                                              
ApplicationEventPublisher eventPublisher) {
+    return new DataStreamUpdateManagement(requestManager, resourceManager, 
eventPublisher);
+  }
+
   @Bean
   public ResourceDeletionManager resourceDeletionManager(IGenericStorage 
genericStorage) {
     return new ResourceDeletionManager(genericStorage);
diff --git 
a/streampipes-service-core/src/test/java/org/apache/streampipes/service/core/storage/StorageApiConfigurationTest.java
 
b/streampipes-service-core/src/test/java/org/apache/streampipes/service/core/storage/StorageApiConfigurationTest.java
index dc92169e97..17007cd8e0 100644
--- 
a/streampipes-service-core/src/test/java/org/apache/streampipes/service/core/storage/StorageApiConfigurationTest.java
+++ 
b/streampipes-service-core/src/test/java/org/apache/streampipes/service/core/storage/StorageApiConfigurationTest.java
@@ -17,9 +17,11 @@
  */
 package org.apache.streampipes.service.core.storage;
 
+import 
org.apache.streampipes.manager.api.extensions.ExtensionServiceRequestManager;
 import org.apache.streampipes.manager.extensions.AvailableExtensionsProvider;
 import org.apache.streampipes.manager.setup.InstallationConfiguration;
 import org.apache.streampipes.model.graph.DataProcessorDescription;
+import org.apache.streampipes.resource.management.SpResourceManager;
 import org.apache.streampipes.service.core.PipelineManagementConfiguration;
 import org.apache.streampipes.storage.api.connect.IAdapterStorage;
 import org.apache.streampipes.storage.api.function.IFunctionStateStorage;
@@ -81,6 +83,7 @@ import static 
org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.when;
@@ -106,6 +109,8 @@ class StorageApiConfigurationTest {
     try (var clients = mockStatic(Utils.class);
          var context = new AnnotationConfigApplicationContext()) {
       context.registerBean(CacheManager.class, () -> cacheManager);
+      context.registerBean(ExtensionServiceRequestManager.class, () -> 
mock(ExtensionServiceRequestManager.class));
+      context.registerBean(SpResourceManager.class, () -> 
mock(SpResourceManager.class, RETURNS_DEEP_STUBS));
       context.register(StorageApiConfiguration.class, 
PipelineManagementConfiguration.class, AdapterStorages.class);
       context.refresh();
 

Reply via email to