1515
1616package org .rutebanken .tiamat .versioning .save ;
1717
18+ import com .google .common .collect .Sets ;
1819import org .rutebanken .tiamat .auth .StopPlaceAuthorizationService ;
1920import org .rutebanken .tiamat .auth .UsernameFetcher ;
2021import org .rutebanken .tiamat .changelog .EntityChangedListener ;
4748
4849import java .time .Instant ;
4950import java .util .HashSet ;
51+ import java .util .List ;
5052import java .util .Set ;
5153import java .util .stream .Collectors ;
5254
@@ -64,6 +66,17 @@ public class StopPlaceVersionedSaverService {
6466
6567 public static final InterchangeWeightingEnumeration DEFAULT_WEIGHTING = InterchangeWeightingEnumeration .INTERCHANGE_ALLOWED ;
6668
69+ private static final Set <String > QUAY_DIFF_IGNORE_FIELDS = Sets .newHashSet (
70+ "id" , "version" , "changed" , "changedBy" , "status" , "modification" , "envelope" , "polygon"
71+ );
72+
73+ private static final Set <String > STOP_PLACE_DIFF_IGNORE_FIELDS = Sets .newHashSet (
74+ "id" , "version" , "changed" , "changedBy" , "status" , "modification" , "envelope" , "polygon" , "quays" , "children" , "validBetween" , "accessibilityAssessment"
75+ );
76+
77+ private final org .rutebanken .tiamat .diff .generic .GenericDiffConfig quayDiffConfig ;
78+ private final org .rutebanken .tiamat .diff .generic .GenericDiffConfig stopPlaceDiffConfig ;
79+
6780 @ Value ("${tiamat.multimodal.allowSameNameForChild:false}" )
6881 private boolean allowSameNameForChild ;
6982
@@ -115,9 +128,25 @@ public class StopPlaceVersionedSaverService {
115128 @ Autowired
116129 private TiamatObjectDiffer tiamatObjectDiffer ;
117130
131+ @ Autowired
132+ private org .rutebanken .tiamat .diff .generic .GenericObjectDiffer genericObjectDiffer ;
133+
118134 @ Autowired
119135 private PrometheusMetricsService prometheusMetricsService ;
120136
137+ public StopPlaceVersionedSaverService () {
138+ this .quayDiffConfig = org .rutebanken .tiamat .diff .generic .GenericDiffConfig .builder ()
139+ .identifiers (Sets .newHashSet ("netexId" , "ref" ))
140+ .ignoreFields (QUAY_DIFF_IGNORE_FIELDS )
141+ .onlyDoEqualsCheck (Sets .newHashSet (org .locationtech .jts .geom .Geometry .class ))
142+ .build ();
143+ this .stopPlaceDiffConfig = org .rutebanken .tiamat .diff .generic .GenericDiffConfig .builder ()
144+ .identifiers (Sets .newHashSet ("netexId" , "ref" ))
145+ .ignoreFields (STOP_PLACE_DIFF_IGNORE_FIELDS )
146+ .onlyDoEqualsCheck (Sets .newHashSet (org .locationtech .jts .geom .Geometry .class ))
147+ .build ();
148+ }
149+
121150 public StopPlace saveNewVersion (StopPlace existingVersion , StopPlace newVersion , Instant defaultValidFrom ) {
122151 return saveNewVersion (existingVersion , newVersion , defaultValidFrom , new HashSet <>());
123152 }
@@ -133,7 +162,7 @@ public StopPlace saveNewVersion(StopPlace existingStopPlace, StopPlace newVersio
133162 public StopPlace saveNewVersion (StopPlace newVersion ) {
134163 return saveNewVersion (null , newVersion );
135164 }
136-
165+
137166 public StopPlace saveNewVersion (StopPlace existingVersion , StopPlace newVersion , Instant defaultValidFrom , Set <String > childStopsUpdated ) {
138167
139168 versionValidator .validate (existingVersion , newVersion );
@@ -158,11 +187,18 @@ public StopPlace saveNewVersion(StopPlace existingVersion, StopPlace newVersion,
158187 submodeValidator .validate (newVersion );
159188
160189 Instant changed = Instant .now ();
161- newVersion .setChanged (changed );
162190
163- logger .debug ("Rearrange accessibility assessments for: {}" , newVersion );
164191 accessibilityAssessmentOptimizer .optimizeAccessibilityAssessments (newVersion );
165192
193+ // Clean up empty/invalid fields before comparison
194+ cleanEmptyFields (newVersion );
195+ if (existingVersion != null ) {
196+ cleanEmptyFields (existingVersion );
197+ }
198+
199+ // Check if the stop place itself (excluding quays) has changed
200+ boolean stopPlaceChanged = hasStopPlaceChanged (existingVersion , newVersion );
201+
166202 Instant newVersionValidFrom = validityUpdater .updateValidBetween (existingVersion , newVersion , defaultValidFrom );
167203
168204 if (existingVersion == null ) {
@@ -177,9 +213,42 @@ public StopPlace saveNewVersion(StopPlace existingVersion, StopPlace newVersion,
177213 stopPlaceAuthorizationService .assertAuthorizedToEdit (existingVersionRefetched , newVersion , childStopsUpdated );
178214 }
179215
216+ // Identify which quays actually changed by comparing their content
217+ final Set <String > modifiedQuayNetexIds = newVersion .getQuays () == null ? new HashSet <>() :
218+ newVersion .getQuays ().stream ()
219+ .filter (quay -> {
220+ if (existingVersion == null || existingVersion .getQuays () == null ) {
221+ return true ;
222+ }
223+ var existingQuay = existingVersion .getQuays ().stream ()
224+ .filter (eq -> eq .getNetexId () != null && eq .getNetexId ().equals (quay .getNetexId ()))
225+ .findFirst ()
226+ .orElse (null );
227+ return hasQuayChanged (existingQuay , quay );
228+ })
229+ .map (quay -> quay .getNetexId ())
230+ .collect (Collectors .toSet ());
231+
180232 newVersion = versionIncrementor .initiateOrIncrementVersions (newVersion );
181233
182- newVersion .setChangedBy (usernameFetcher .getUserNameForAuthenticatedUser ());
234+ String changedByUser = usernameFetcher .getUserNameForAuthenticatedUser ();
235+
236+ // Only update stop place's changed/changedBy if stop place itself changed
237+ if (stopPlaceChanged ) {
238+ newVersion .setChanged (changed );
239+ newVersion .setChangedBy (changedByUser );
240+ }
241+
242+ // Set changed and changedBy only on quays that were actually modified
243+ if (newVersion .getQuays () != null ) {
244+ newVersion .getQuays ().forEach (quay -> {
245+ if (modifiedQuayNetexIds .contains (quay .getNetexId ())) {
246+ quay .setChanged (changed );
247+ quay .setChangedBy (changedByUser );
248+ }
249+ });
250+ }
251+
183252 logger .info ("StopPlace [{}], version {} changed by user [{}]. {}" , newVersion .getNetexId (), newVersion .getVersion (), newVersion .getChangedBy (), newVersion .getValidBetween ());
184253
185254 if (newVersion .getWeighting () == null ) {
@@ -194,6 +263,7 @@ public StopPlace saveNewVersion(StopPlace existingVersion, StopPlace newVersion,
194263 if (newVersion .getChildren () != null ) {
195264 newVersion .getChildren ().forEach (child -> {
196265 child .setChanged (changed );
266+ child .setChangedBy (changedByUser );
197267 tariffZonesLookupService .populateTariffZone (child );
198268 });
199269
@@ -307,4 +377,70 @@ private void updateParentSiteRefsForChildren(StopPlace parentStopPlace) {
307377 logger .info ("Updated {} childs with parent site refs" , count );
308378 }
309379
380+ /**
381+ * Check if a quay has actual changes by comparing with its previous version.
382+ * Ignores auto-generated fields like polygon, version, changed, etc.
383+ *
384+ * @param existingQuay the previous version of the quay (optional)
385+ * @param newQuay the new version of the quay
386+ * @return true if the quay has actual changes or is new
387+ */
388+ private boolean hasQuayChanged (org .rutebanken .tiamat .model .Quay existingQuay , org .rutebanken .tiamat .model .Quay newQuay ) {
389+ if (existingQuay == null ) {
390+ return true ;
391+ }
392+
393+ try {
394+ return !genericObjectDiffer .compareObjects (existingQuay , newQuay , quayDiffConfig ).isEmpty ();
395+ } catch (IllegalAccessException e ) {
396+ logger .warn ("Could not compare quay {}, marking as modified" , newQuay .getNetexId (), e );
397+ return true ;
398+ }
399+ }
400+
401+ /**
402+ * Check if a stop place has actual changes by comparing with its previous version.
403+ * Excludes quays and children from comparison.
404+ *
405+ * @param existingStopPlace the previous version of the stop place (optional)
406+ * @param newStopPlace the new version of the stop place
407+ * @return true if the stop place has actual changes or is new
408+ */
409+ private boolean hasStopPlaceChanged (StopPlace existingStopPlace , StopPlace newStopPlace ) {
410+ if (existingStopPlace == null ) {
411+ return true ;
412+ }
413+
414+ try {
415+ return !genericObjectDiffer .compareObjects (existingStopPlace , newStopPlace , stopPlaceDiffConfig ).isEmpty ();
416+ } catch (IllegalAccessException e ) {
417+ logger .warn ("Could not compare stop place {}, marking as modified" , newStopPlace .getNetexId (), e );
418+ return true ;
419+ }
420+ }
421+
422+ /**
423+ * Remove empty or invalid fields that shouldn't count as actual changes.
424+ */
425+ private void cleanEmptyFields (StopPlace stopPlace ) {
426+ // Clean empty alternative names
427+ if (stopPlace .getAlternativeNames () != null ) {
428+ stopPlace .getAlternativeNames ().removeIf (altName ->
429+ altName .getName () == null ||
430+ altName .getName ().getValue () == null ||
431+ altName .getName ().getValue ().trim ().isEmpty () ||
432+ "null" .equalsIgnoreCase (altName .getName ().getValue ().trim ())
433+ );
434+ }
435+
436+ // Clean empty keyValues entries
437+ if (stopPlace .getKeyValues () != null ) {
438+ stopPlace .getKeyValues ().entrySet ().removeIf (entry ->
439+ entry .getValue () == null ||
440+ entry .getValue ().getItems () == null ||
441+ entry .getValue ().getItems ().isEmpty ()
442+ );
443+ }
444+ }
445+
310446}
0 commit comments