Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<properties>
<java.version>21</java.version>
<entur.helpers.version>5.50.0</entur.helpers.version>
<netex-parser-java.version>3.0.5</netex-parser-java.version>
<netex-parser-java.version>3.1.77</netex-parser-java.version>
<jakarta-xml-bind.version>4.0.5</jakarta-xml-bind.version>
<glassfish-jaxb.version>4.0.6</glassfish-jaxb.version>
</properties>
Expand Down
119 changes: 119 additions & 0 deletions src/main/java/no/entur/mummu/config/SwaggerConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.properties.SpringDocConfigProperties;
import org.springdoc.core.providers.ObjectMapperProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Configuration
public class SwaggerConfiguration {
Expand Down Expand Up @@ -82,6 +87,120 @@
));
}

@Bean
public OpenApiCustomizer netexModelCompatibilityCustomizer() {
return openApi -> {
Map<String, Schema> schemas = openApi.getComponents().getSchemas();
if (schemas == null) return;

// Fix TariffZoneRefs_RelStructure: rename tariffZoneRef_ back to tariffZoneRef
Schema tariffZoneRefs = schemas.get("TariffZoneRefs_RelStructure");
if (tariffZoneRefs != null && tariffZoneRefs.getProperties() != null) {
Map<String, Schema> props = tariffZoneRefs.getProperties();
if (props.containsKey("tariffZoneRef_")) {
Schema tariffZoneRefProp = props.remove("tariffZoneRef_");
// Restore original schema: array of TariffZoneRef with XML name
Schema restoredProp = new ArraySchema()
.items(new Schema<>().$ref("#/components/schemas/TariffZoneRef"));
restoredProp.xml(new io.swagger.v3.oas.models.media.XML().name("TariffZoneRef"));
props.put("tariffZoneRef", restoredProp);
// Re-sort properties alphabetically
Map<String, Schema> sorted = new LinkedHashMap<>();
props.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> sorted.put(e.getKey(), e.getValue()));
tariffZoneRefs.setProperties(sorted);
// Restore required field
tariffZoneRefs.setRequired(List.of("tariffZoneRef"));
}
}

// Fix Quays_RelStructure: restore quayRefOrQuay items to empty schema
Schema quaysRel = schemas.get("Quays_RelStructure");
if (quaysRel != null && quaysRel.getProperties() != null) {
Map<String, Schema> props = quaysRel.getProperties();
Schema quayProp = props.get("quayRefOrQuay");
if (quayProp != null && quayProp.getItems() != null) {
quayProp.setItems(new Schema<>());
}
}

// Fix ParkingAreas_RelStructure: rename parkingAreaRefOrParkingArea_ back
renameProperty(schemas, "ParkingAreas_RelStructure",
"parkingAreaRefOrParkingArea_", "parkingAreaRefOrParkingArea");

// Fix StopPlacesInFrame_RelStructure: rename stopPlace_ back to stopPlace
Schema stopPlacesInFrame = schemas.get("StopPlacesInFrame_RelStructure");
if (stopPlacesInFrame != null && stopPlacesInFrame.getProperties() != null) {
Map<String, Schema> spProps = stopPlacesInFrame.getProperties();
if (spProps.containsKey("stopPlace_")) {
spProps.remove("stopPlace_");
Schema stopPlaceProp = new ArraySchema()
.items(new Schema<>().$ref("#/components/schemas/StopPlace"));
stopPlaceProp.xml(new io.swagger.v3.oas.models.media.XML().name("StopPlace"));
spProps.put("stopPlace", stopPlaceProp);
// Re-sort alphabetically
Map<String, Schema> sorted = new LinkedHashMap<>();
spProps.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> sorted.put(e.getKey(), e.getValue()));
stopPlacesInFrame.setProperties(sorted);
stopPlacesInFrame.setRequired(List.of("stopPlace"));
}
}

// Fix StopPlaceRefs_RelStructure: restore items ref and required
restoreJaxbElementArrayProp(schemas, "StopPlaceRefs_RelStructure",
"stopPlaceRef", "StopPlaceRefStructure", "StopPlaceRef");

// Fix ParkingAreaRefs_RelStructure: restore items ref and required
restoreJaxbElementArrayProp(schemas, "ParkingAreaRefs_RelStructure",
"parkingAreaRef", "ParkingAreaRefStructure", "ParkingAreaRef");

// Restore TariffZoneRef schema (removed in new model, still referenced)
if (!schemas.containsKey("TariffZoneRef")) {
schemas.put("TariffZoneRef", schemas.getOrDefault("ZoneRefStructure", new Schema<>()));
}
};
}

private static void restoreJaxbElementArrayProp(Map<String, Schema> schemas,
String schemaName, String propName,
String itemsRef, String xmlName) {
Schema schema = schemas.get(schemaName);
if (schema != null && schema.getProperties() != null) {
Map<String, Schema> props = schema.getProperties();
Schema prop = props.get(propName);
if (prop != null) {
prop.setItems(new Schema<>().$ref("#/components/schemas/" + itemsRef));
prop.xml(new io.swagger.v3.oas.models.media.XML().name(xmlName));
schema.setRequired(List.of(propName));
}
}
}

private static void renameProperty(Map<String, Schema> schemas, String schemaName,
String oldPropName, String newPropName) {
Schema schema = schemas.get(schemaName);
if (schema != null && schema.getProperties() != null) {
Map<String, Schema> props = schema.getProperties();
if (props.containsKey(oldPropName)) {
Schema prop = props.remove(oldPropName);
// Reset items to empty schema (unwrapped JAXBElements)
if (prop.getItems() != null) {
prop.setItems(new Schema<>());
}
props.put(newPropName, prop);
// Re-sort alphabetically
Map<String, Schema> sorted = new LinkedHashMap<>();
props.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(e -> sorted.put(e.getKey(), e.getValue()));
schema.setProperties(sorted);
}
}
}

@Bean
public ObjectMapperProvider springDocObjectMapperProvider() {
return new ObjectMapperProvider(new SpringDocConfigProperties()) {
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/no/entur/mummu/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import no.entur.mummu.serializers.CustomSerializers;
import no.entur.mummu.serializers.NetexJsonMixins;
import org.rutebanken.netex.model.ParkingAreaRefs_RelStructure;
import org.rutebanken.netex.model.ParkingAreas_RelStructure;
import org.rutebanken.netex.model.Quays_RelStructure;
import org.rutebanken.netex.model.StopPlaceRefs_RelStructure;
import org.rutebanken.netex.model.TariffZoneRefs_RelStructure;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
Expand Down Expand Up @@ -67,6 +73,11 @@ public ObjectMapper jsonObjectMapper() {
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.featuresToEnable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS)
.modules(modules)
.mixIn(Quays_RelStructure.class, NetexJsonMixins.QuaysRelStructureMixin.class)
.mixIn(TariffZoneRefs_RelStructure.class, NetexJsonMixins.TariffZoneRefsRelStructureMixin.class)
.mixIn(ParkingAreas_RelStructure.class, NetexJsonMixins.ParkingAreasRelStructureMixin.class)
.mixIn(StopPlaceRefs_RelStructure.class, NetexJsonMixins.StopPlaceRefsRelStructureMixin.class)
.mixIn(ParkingAreaRefs_RelStructure.class, NetexJsonMixins.ParkingAreaRefsRelStructureMixin.class)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package no.entur.mummu.serializers;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import jakarta.xml.bind.JAXBElement;

import java.io.IOException;
import java.util.List;

public class JAXBElementUnwrappingSerializer extends JsonSerializer<List<JAXBElement<?>>> {

@Override
public void serialize(List<JAXBElement<?>> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartArray();
for (JAXBElement<?> element : value) {
gen.writeObject(element.getValue());
}
gen.writeEndArray();
}
}
44 changes: 44 additions & 0 deletions src/main/java/no/entur/mummu/serializers/NetexJsonMixins.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package no.entur.mummu.serializers;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import jakarta.xml.bind.JAXBElement;
import org.rutebanken.netex.model.ParkingAreaRefStructure;
import org.rutebanken.netex.model.StopPlaceRefStructure;
import org.rutebanken.netex.model.ZoneRefStructure;

import java.util.List;

public class NetexJsonMixins {

public abstract static class QuaysRelStructureMixin {
@JsonSerialize(using = JAXBElementUnwrappingSerializer.class)
@JsonProperty("quayRefOrQuay")
public abstract List<JAXBElement<?>> getQuayRefOrQuay();
}

@SuppressWarnings("rawtypes")
public abstract static class TariffZoneRefsRelStructureMixin {
@JsonSerialize(using = JAXBElementUnwrappingSerializer.class)
@JsonProperty("tariffZoneRef")
public abstract List getTariffZoneRef_();
}

public abstract static class ParkingAreasRelStructureMixin {
@JsonSerialize(using = JAXBElementUnwrappingSerializer.class)
@JsonProperty("parkingAreaRefOrParkingArea")
public abstract List<JAXBElement<?>> getParkingAreaRefOrParkingArea_();
}

public abstract static class StopPlaceRefsRelStructureMixin {
@JsonSerialize(using = JAXBElementUnwrappingSerializer.class)
@JsonProperty("stopPlaceRef")
public abstract List<JAXBElement<? extends StopPlaceRefStructure>> getStopPlaceRef();
}

public abstract static class ParkingAreaRefsRelStructureMixin {
@JsonSerialize(using = JAXBElementUnwrappingSerializer.class)
@JsonProperty("parkingAreaRef")
public abstract List<JAXBElement<? extends ParkingAreaRefStructure>> getParkingAreaRef();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ public List<ScheduledStopPoint> getScheduledStopPoints(
public Collection<ScheduledStopPoint> getScheduledStopPointsForStopPlaceWithId(String id) {
return netexEntitiesIndex.getPassengerStopAssignmentsByStopPointRefIndex().entries().stream().filter(entry -> {
var passengerStopAssignment = entry.getValue();
return passengerStopAssignment.getStopPlaceRef() != null && passengerStopAssignment.getStopPlaceRef().getRef().equals(id);
return passengerStopAssignment.getStopPlaceRef() != null && passengerStopAssignment.getStopPlaceRef().getValue().getRef().equals(id);
}).map(entry -> {
var stopPointRef = entry.getKey();
return netexEntitiesIndex.getScheduledStopPointIndex().getLatestVersion(stopPointRef);
Expand All @@ -347,10 +347,10 @@ public StopPlace getStopPlaceByScheduledStopPointId(String id) {
.map(
passengerStopAssignment ->
netexEntitiesIndex.getStopPlaceIndex().getVersion(
passengerStopAssignment.getStopPlaceRef().getRef(),
passengerStopAssignment.getStopPlaceRef().getVersion()
passengerStopAssignment.getStopPlaceRef().getValue().getRef(),
passengerStopAssignment.getStopPlaceRef().getValue().getVersion()
)

).findFirst().orElseThrow(NotFoundException::new);
).map(StopPlace.class::cast).findFirst().orElseThrow(NotFoundException::new);
}
}
11 changes: 9 additions & 2 deletions src/main/java/no/entur/mummu/services/NetexObjectFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.rutebanken.netex.model.Quays_RelStructure;
import org.rutebanken.netex.model.ScheduledStopPoint;
import org.rutebanken.netex.model.ScheduledStopPointsInFrame_RelStructure;
import org.rutebanken.netex.model.Site_VersionStructure;
import org.rutebanken.netex.model.StopPlace;
import org.rutebanken.netex.model.StopPlacesInFrame_RelStructure;
import org.rutebanken.netex.model.TariffZone;
Expand Down Expand Up @@ -46,7 +47,10 @@
}

public JAXBElement<StopPlacesInFrame_RelStructure> createStopPlaces(List<StopPlace> stopPlaces) {
var stopPlacesInFrame = createStopPlacesInFrame_RelStructure().withStopPlace(stopPlaces);
Collection<JAXBElement<? extends Site_VersionStructure>> elements = stopPlaces.stream()
.map(this::createStopPlace)
.collect(Collectors.toList());
var stopPlacesInFrame = createStopPlacesInFrame_RelStructure().withStopPlace_(elements);
return new JAXBElement<>(_stopPlaces_QNAME, StopPlacesInFrame_RelStructure.class, stopPlacesInFrame);
}

Expand Down Expand Up @@ -85,7 +89,10 @@
}

public JAXBElement<Quays_RelStructure> createQuays(Collection<Quay> quays) {
var quaysRelStructure = createQuays_RelStructure().withQuayRefOrQuay(quays);
Collection<JAXBElement<?>> elements = quays.stream()
.map(this::createQuay)
.collect(Collectors.toList());
var quaysRelStructure = createQuays_RelStructure().withQuayRefOrQuay(elements);
return new JAXBElement<>(_quays_QNAME, Quays_RelStructure.class, quaysRelStructure);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package no.entur.mummu.util;

import jakarta.xml.bind.JAXBElement;
import org.rutebanken.netex.model.Quay;
import org.rutebanken.netex.model.StopPlace;

Expand Down Expand Up @@ -27,6 +28,6 @@ public boolean test(StopPlace stopPlace) {

return stopPlace.getQuays().getQuayRefOrQuay().stream()
.filter(Objects::nonNull)
.anyMatch(v -> quayIds.contains(((Quay) v).getId()));
.anyMatch(v -> quayIds.contains(((Quay) ((JAXBElement<?>) v).getValue()).getId()));
}
}
9 changes: 8 additions & 1 deletion src/main/java/no/entur/mummu/util/TransportModesFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ public TransportModesFilter(List<VehicleModeEnumeration> transportModes) {

@Override
public boolean test(StopPlace stopPlace) {
return transportModes == null || transportModes.contains(stopPlace.getTransportMode());
if (transportModes == null) {
return true;
}
if (stopPlace.getTransportMode() == null) {
return false;
}
return transportModes.stream()
.anyMatch(mode -> mode.value().equals(stopPlace.getTransportMode().value()));
}
}
2 changes: 1 addition & 1 deletion src/main/resources/public/openapi.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package no.entur.mummu.util;

import jakarta.xml.bind.JAXBElement;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.rutebanken.netex.model.ObjectFactory;
import org.rutebanken.netex.model.Quay;
import org.rutebanken.netex.model.Quays_RelStructure;
import org.rutebanken.netex.model.StopPlace;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

Expand Down Expand Up @@ -83,7 +86,7 @@
@Test
void testReturnsFalse_WhenOnlyNullQuaysInStopPlace() {
var filter = new StopPlaceByQuayIdsFilter(List.of("NSR:Quay:1"));
var quays = new ArrayList<>();
var quays = new ArrayList<JAXBElement<?>>();
quays.add(null);
var stopPlace = new StopPlace()
.withQuays(new Quays_RelStructure()
Expand All @@ -110,10 +113,11 @@
Assertions.assertFalse(filter.test(stopPlace));
}

private static final ObjectFactory objectFactory = new ObjectFactory();

private StopPlace createStopPlaceWithQuays(List<String> quayIds) {
var quays = quayIds.stream()
.map(id -> id != null ? new Quay().withId(id) : null)
.map(Object.class::cast)
List<JAXBElement<?>> quays = quayIds.stream()
.<JAXBElement<?>>map(id -> id != null ? objectFactory.createQuay(new Quay().withId(id)) : null)
.toList();

return new StopPlace()
Expand Down