Skip to content

Commit

Permalink
Adds a new package with functionality to compute the delta of gbfs files
Browse files Browse the repository at this point in the history
  • Loading branch information
testower committed Sep 26, 2024
1 parent 3aadc3d commit 530f16e
Show file tree
Hide file tree
Showing 8 changed files with 482 additions and 0 deletions.
184 changes: 184 additions & 0 deletions src/main/java/org/entur/lamassu/delta/BaseGBFSFileDeltaCalculator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
*
*
* * 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 org.jetbrains.annotations.NotNull;

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.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

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(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<>(
getLastUpdated(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 a, T b) {
T delta = createEntity();
Method[] methods = a.getClass().getDeclaredMethods();
for (Method method : methods) {
try {
if (!EXCLUDE_METHODS.contains(method.getName()) && method.getParameterCount() == 0 && (method.invoke(a) == null || !method.invoke(a).equals(method.invoke(b)))) {
getSetter(methods, method.getName()).ifPresent(setter -> {
try {
setter.invoke(delta, method.invoke(b));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
});

}
} catch (InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return delta;
}

private @NotNull Optional<Method> getSetter(Method[] methods, String getterName) {
String setterName = getterName.replace("get", "set");
return Arrays.stream(methods).filter(method1 -> method1.getName().equals(setterName)).findFirst();
}

/**
* 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();
}
40 changes: 40 additions & 0 deletions src/main/java/org/entur/lamassu/delta/DeltaType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
*
*
* * 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
}
33 changes: 33 additions & 0 deletions src/main/java/org/entur/lamassu/delta/GBFSEntityDelta.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
*
*
* * 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;

/**
* A record representing the difference (delta) between two enumerable entities of a GBFS file
* @param entityId The unique ID of the entity
* @param type The type of the delta (create, update or delete)
* @param entity An instance of the entity itself, containing the changed fields.
* Note: this field is non-null only if the delta type is "UPDATE"
* @param <E> The type of the enumerable entity that was compared
*/
public record GBFSEntityDelta<E>(
String entityId,
DeltaType type,
E entity
) {}
37 changes: 37 additions & 0 deletions src/main/java/org/entur/lamassu/delta/GBFSFileDelta.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
*
*
* * 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.util.List;

/**
* Record representing the difference between two instances of a GBFS file
*
* @param base Numerical identifier of the base of the comparison
* @param compare Numerical identifier of the compare side
* @param fileName The file name of the GBFS files being compared
* @param entityDelta A list of entity deltas for the enumerable entity of type E in the GBFS file
* @param <E> The type of the enumerable entity in the GBFS file
*/
public record GBFSFileDelta<E>(
Long base,
Long compare,
String fileName,
List<GBFSEntityDelta<E>> entityDelta
) {}
43 changes: 43 additions & 0 deletions src/main/java/org/entur/lamassu/delta/GBFSFileDeltaCalculator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
*
*
* * 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 org.jetbrains.annotations.NotNull;

/**
* Represents a calculator that can compute the delta (difference) between to instances
* of a GBFS file.
*
* @param <S> The type of the GBFS file instances to compare
* @param <T> The type of the enumerable entity inside the GBFS file being compared
*/
public interface GBFSFileDeltaCalculator<S, T> {

/**
* Calculate the delta (difference) between to instances of a GBFS file of type S
*
* @param base The base of the comparison
* Note: This parameter can be null, in it is then interpreted as a full update
*
* @param compare The instance to compare with the base
* Note: This parameter can't be null
* @return An instance of GBFSFileDelta containing deltas of the enumerable entity of type T
*/
GBFSFileDelta<T> calculateDelta(S base, @NotNull S compare);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
*
*
* * 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 org.mobilitydata.gbfs.v3_0.station_status.GBFSStation;
import org.mobilitydata.gbfs.v3_0.station_status.GBFSStationStatus;

import java.util.List;

public class GBFSStationStatusDeltaCalculator extends BaseGBFSFileDeltaCalculator<GBFSStationStatus, GBFSStation> {

public static final String FILE_NAME = "station_status";

@Override
protected List<GBFSStation> getEntities(GBFSStationStatus instance) {
return instance.getData().getStations();
}

@Override
protected String getEntityId(GBFSStation entity) {
return entity.getStationId();
}

@Override
protected GBFSStation createEntity() {
return new GBFSStation();
}

@Override
protected long getLastUpdated(GBFSStationStatus instance) {
return instance.getLastUpdated().getTime();
}

@Override
protected String getFileName() {
return FILE_NAME;
}
}
Loading

0 comments on commit 530f16e

Please sign in to comment.