-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #543 from entur/feature/gbfs-delta
Compute delta of GBFS files and refactor vehicles and stations updaters
- Loading branch information
Showing
31 changed files
with
2,671 additions
and
258 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
src/main/java/org/entur/lamassu/cache/UpdateContinuityCache.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package org.entur.lamassu.cache; | ||
|
||
import java.util.Date; | ||
|
||
/** | ||
* Interface for tracking GBFS update continuity. | ||
* Used to store timestamps of last successful updates to detect missed updates. | ||
*/ | ||
public interface UpdateContinuityCache { | ||
/** | ||
* Get the timestamp of the last successful update. | ||
* | ||
* @param systemId ID of the system to check | ||
* @return Timestamp of last update or null if no previous update exists | ||
*/ | ||
Date getLastUpdateTime(String systemId); | ||
|
||
/** | ||
* Store the timestamp of a successful update. If timestamp is null, entry | ||
* is removed from cache. | ||
* | ||
* @param systemId ID of the system being updated | ||
* @param timestamp Timestamp of the successful update | ||
*/ | ||
void setLastUpdateTime(String systemId, Date timestamp); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
src/main/java/org/entur/lamassu/cache/impl/RedisUpdateContinuityCache.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package org.entur.lamassu.cache.impl; | ||
|
||
import java.util.Date; | ||
import org.entur.lamassu.cache.UpdateContinuityCache; | ||
import org.redisson.api.RMapCache; | ||
|
||
/** | ||
* Redis-backed implementation of UpdateContinuityCache using Redisson. | ||
*/ | ||
public class RedisUpdateContinuityCache implements UpdateContinuityCache { | ||
|
||
private final RMapCache<String, Date> cache; | ||
|
||
public RedisUpdateContinuityCache(RMapCache<String, Date> cache) { | ||
this.cache = cache; | ||
} | ||
|
||
@Override | ||
public Date getLastUpdateTime(String systemId) { | ||
return cache.get(systemId); | ||
} | ||
|
||
@Override | ||
public void setLastUpdateTime(String systemId, Date timestamp) { | ||
if (timestamp == null) { | ||
cache.remove(systemId); | ||
} else { | ||
cache.put(systemId, timestamp); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
213 changes: 213 additions & 0 deletions
213
src/main/java/org/entur/lamassu/delta/BaseGBFSFileDeltaCalculator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
/* | ||
* | ||
* | ||
* * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by | ||
* * the European Commission - subsequent versions of the EUPL (the "Licence"); | ||
* * You may not use this work except in compliance with the Licence. | ||
* * You may obtain a copy of the Licence at: | ||
* * | ||
* * https://joinup.ec.europa.eu/software/page/eupl | ||
* * | ||
* * Unless required by applicable law or agreed to in writing, software | ||
* * distributed under the Licence is distributed on an "AS IS" basis, | ||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* * See the Licence for the specific language governing permissions and | ||
* * limitations under the Licence. | ||
* | ||
*/ | ||
|
||
package org.entur.lamassu.delta; | ||
|
||
import java.lang.reflect.InvocationTargetException; | ||
import java.lang.reflect.Method; | ||
import java.util.Arrays; | ||
import java.util.Collection; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
public abstract class BaseGBFSFileDeltaCalculator<S, T> | ||
implements GBFSFileDeltaCalculator<S, T> { | ||
|
||
private static final List<String> EXCLUDE_METHODS = List.of( | ||
"toString", | ||
"hashCode", | ||
"equals" | ||
); | ||
|
||
@Override | ||
public final GBFSFileDelta<T> calculateDelta(@Nullable S base, @NotNull S compare) { | ||
List<GBFSEntityDelta<T>> entityDeltas = getEntityDeltas(base, compare); | ||
return getGBFSFileDelta(base, compare, entityDeltas); | ||
} | ||
|
||
private @NotNull GBFSFileDelta<T> getGBFSFileDelta( | ||
S base, | ||
@NotNull S compare, | ||
List<GBFSEntityDelta<T>> entityDeltas | ||
) { | ||
return new GBFSFileDelta<>( | ||
getNullableLastUpdated(base), | ||
getLastUpdated(compare), | ||
getFileName(), | ||
entityDeltas | ||
); | ||
} | ||
|
||
private @NotNull List<GBFSEntityDelta<T>> getEntityDeltas(S base, @NotNull S compare) { | ||
List<T> baseEntities = getBaseEntities(base); | ||
Map<String, T> baseEntityMap = getBaseEntityMap(baseEntities); | ||
List<String> baseEntityIds = getEntityIds(baseEntityMap); | ||
List<T> compareEntities = getEntities(compare); | ||
List<String> compareEntityIds = getEntityIds(compareEntities); | ||
|
||
return Stream | ||
.of( | ||
getDeletedEntityDeltas(baseEntities, compareEntityIds), | ||
getKeptEntityDeltas(compareEntities, baseEntityMap, baseEntityIds) | ||
) | ||
.flatMap(Collection::stream) | ||
.toList(); | ||
} | ||
|
||
private @NotNull List<String> getEntityIds(Map<String, ?> entityMap) { | ||
return entityMap.keySet().stream().toList(); | ||
} | ||
|
||
private @NotNull List<String> getEntityIds(List<T> entities) { | ||
return entities.stream().map(this::getEntityId).toList(); | ||
} | ||
|
||
private @NotNull List<T> getBaseEntities(S base) { | ||
return base != null ? getEntities(base) : List.of(); | ||
} | ||
|
||
private @NotNull Map<String, T> getBaseEntityMap(List<T> baseEntities) { | ||
return baseEntities.stream().collect(Collectors.toMap(this::getEntityId, v -> v)); | ||
} | ||
|
||
private @NotNull List<GBFSEntityDelta<T>> getDeletedEntityDeltas( | ||
List<T> baseEntities, | ||
List<String> compareEntityIds | ||
) { | ||
return baseEntities | ||
.stream() | ||
.map(this::getEntityId) | ||
.filter(id -> !compareEntityIds.contains(id)) | ||
.map(id -> new GBFSEntityDelta<T>(id, DeltaType.DELETE, null)) | ||
.toList(); | ||
} | ||
|
||
private @NotNull List<GBFSEntityDelta<T>> getKeptEntityDeltas( | ||
List<T> compareEntities, | ||
Map<String, T> baseEntityMap, | ||
List<String> baseEntityIds | ||
) { | ||
return compareEntities | ||
.stream() | ||
// We do not need to return a delta for entities that haven't changed. We trust the implementation | ||
// of equals from the gbfs model here. | ||
.filter(entity -> !entity.equals(baseEntityMap.get(getEntityId(entity)))) | ||
.map(entity -> { | ||
var entityId = getEntityId(entity); | ||
// If the entity exists in the base, then this delta is an update, and we can compute | ||
// the entity delta | ||
if (baseEntityIds.contains(entityId)) { | ||
return new GBFSEntityDelta<>( | ||
entityId, | ||
DeltaType.UPDATE, | ||
getEntityDelta(baseEntityMap.get(entityId), entity) | ||
); | ||
// Otherwise, this is a new entity, and the "delta" contains the entire entity | ||
} else { | ||
return new GBFSEntityDelta<>(entityId, DeltaType.CREATE, entity); | ||
} | ||
}) | ||
.toList(); | ||
} | ||
|
||
private T getEntityDelta(T base, T compare) { | ||
T delta = createEntity(); | ||
Method[] methods = base.getClass().getDeclaredMethods(); | ||
for (Method method : methods) { | ||
if (isMethodEligibleForDelta(method)) { | ||
Method setter = getSetter(methods, method.getName()); | ||
if (setter != null) { | ||
copyValueToDelta(method, setter, compare, delta); | ||
} | ||
} | ||
} | ||
return delta; | ||
} | ||
|
||
private boolean isMethodEligibleForDelta(Method method) { | ||
return !EXCLUDE_METHODS.contains(method.getName()) && isGetter(method); | ||
} | ||
|
||
private boolean isGetter(Method method) { | ||
return method.getParameterCount() == 0; | ||
} | ||
|
||
private void copyValueToDelta(Method getter, Method setter, T source, T target) { | ||
try { | ||
setter.invoke(target, getter.invoke(source)); | ||
} catch (IllegalAccessException | InvocationTargetException e) { | ||
throw new GBFSDeltaException( | ||
"Failed to set value for field " + getter.getName(), | ||
e | ||
); | ||
} | ||
} | ||
|
||
private @Nullable Method getSetter(Method[] methods, String getterName) { | ||
String setterName = getterName.replace("get", "set"); | ||
return Arrays | ||
.stream(methods) | ||
.filter(method1 -> method1.getName().equals(setterName)) | ||
.findFirst() | ||
.orElse(null); | ||
} | ||
|
||
private Long getNullableLastUpdated(S instance) { | ||
if (instance == null) { | ||
return null; | ||
} | ||
return getLastUpdated(instance); | ||
} | ||
|
||
/** | ||
* Get a list of enumerable entities from the GBFS file instance | ||
* @param instance The GBFS file instance | ||
* @return List of enumerable entities of type T | ||
*/ | ||
protected abstract List<T> getEntities(S instance); | ||
|
||
/** | ||
* Get the id of the entity | ||
* @param entity The entity | ||
* @return The entity's id | ||
*/ | ||
protected abstract String getEntityId(T entity); | ||
|
||
/** | ||
* Create a new instance of the entity of type T | ||
* @return An instance of T | ||
*/ | ||
protected abstract T createEntity(); | ||
|
||
/** | ||
* Get the last updated time of the GBFS file instance | ||
* @param instance The GBFS file instance | ||
* @return The last updated time | ||
*/ | ||
protected abstract long getLastUpdated(S instance); | ||
|
||
/** | ||
* Get the file name of the GBFS file | ||
* @return The file name | ||
*/ | ||
protected abstract String getFileName(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/* | ||
* | ||
* | ||
* * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by | ||
* * the European Commission - subsequent versions of the EUPL (the "Licence"); | ||
* * You may not use this work except in compliance with the Licence. | ||
* * You may obtain a copy of the Licence at: | ||
* * | ||
* * https://joinup.ec.europa.eu/software/page/eupl | ||
* * | ||
* * Unless required by applicable law or agreed to in writing, software | ||
* * distributed under the Licence is distributed on an "AS IS" basis, | ||
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* * See the Licence for the specific language governing permissions and | ||
* * limitations under the Licence. | ||
* | ||
*/ | ||
|
||
package org.entur.lamassu.delta; | ||
|
||
/** | ||
* Enum representing the type of delta. | ||
*/ | ||
public enum DeltaType { | ||
/** | ||
* A new entity was created | ||
*/ | ||
CREATE, | ||
|
||
/** | ||
* En existing entity was updated | ||
*/ | ||
UPDATE, | ||
|
||
/** | ||
* An existing entity was deleted | ||
*/ | ||
DELETE, | ||
} |
Oops, something went wrong.