First working prototype
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
# ---> Java
|
# ---> Java
|
||||||
# Compiled class file
|
# Compiled class file
|
||||||
|
bin
|
||||||
*.class
|
*.class
|
||||||
|
|
||||||
# Log file
|
# Log file
|
||||||
@@ -25,6 +26,7 @@ hs_err_pid*
|
|||||||
replay_pid*
|
replay_pid*
|
||||||
|
|
||||||
# ---> VisualStudioCode
|
# ---> VisualStudioCode
|
||||||
|
.vscode
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/settings.json
|
!.vscode/settings.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
@@ -38,3 +40,4 @@ replay_pid*
|
|||||||
# Built Visual Studio Code Extensions
|
# Built Visual Studio Code Extensions
|
||||||
*.vsix
|
*.vsix
|
||||||
|
|
||||||
|
.env
|
||||||
18
README.md
18
README.md
@@ -1,2 +1,18 @@
|
|||||||
# emission-api-lib-java
|
## Getting Started
|
||||||
|
|
||||||
|
Welcome to the VS Code Java world. Here is a guideline to help you get started to write Java code in Visual Studio Code.
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
The workspace contains two folders by default, where:
|
||||||
|
|
||||||
|
- `src`: the folder to maintain sources
|
||||||
|
- `lib`: the folder to maintain dependencies
|
||||||
|
|
||||||
|
Meanwhile, the compiled output files will be generated in the `bin` folder by default.
|
||||||
|
|
||||||
|
> If you want to customize the folder structure, open `.vscode/settings.json` and update the related settings there.
|
||||||
|
|
||||||
|
## Dependency Management
|
||||||
|
|
||||||
|
The `JAVA PROJECTS` view allows you to manage your dependencies. More details can be found [here](https://github.com/microsoft/vscode-java-dependency#manage-dependencies).
|
||||||
|
|||||||
97
src/App.java
Normal file
97
src/App.java
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import java.io.InputStream;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import de.addideas.api.emission.EmissionEngine;
|
||||||
|
import de.addideas.api.emission.EnvLoader;
|
||||||
|
import de.addideas.api.emission.ProvidersConfigLoader;
|
||||||
|
import de.addideas.api.emission.descriptors.ProviderDescriptor;
|
||||||
|
import de.addideas.api.emission.descriptors.ResponseValue;
|
||||||
|
import de.addideas.api.emission.model.AirportInfo;
|
||||||
|
import de.addideas.api.emission.model.AirportSearch;
|
||||||
|
import de.addideas.api.emission.model.EmissionEstimate;
|
||||||
|
import de.addideas.api.emission.model.FlightLeg;
|
||||||
|
import de.addideas.api.emission.model.FlightRequest;
|
||||||
|
|
||||||
|
public class App {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
// 1) Load XML
|
||||||
|
InputStream xml = App.class.getClassLoader().getResourceAsStream("providers.xml");
|
||||||
|
if (xml == null) {
|
||||||
|
throw new IllegalStateException("providers.xml not found on classpath");
|
||||||
|
}
|
||||||
|
ProvidersConfigLoader loader = new ProvidersConfigLoader(xml);
|
||||||
|
Map<String, ProviderDescriptor> providers = loader.getProviders();
|
||||||
|
|
||||||
|
// 2) Construct engine
|
||||||
|
EmissionEngine engine = new EmissionEngine(providers);
|
||||||
|
|
||||||
|
// 3) Check if calco2la.to supports flight estimation
|
||||||
|
String providerId = "calco2lato";
|
||||||
|
boolean supported = engine.isOperationSupported(
|
||||||
|
providerId, EmissionEngine.OP_FLIGHT_ESTIMATE);
|
||||||
|
System.out.println("Provider " + providerId + " supports flight estimate: " + supported);
|
||||||
|
if (!supported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Build a sample FlightRequest
|
||||||
|
FlightLeg leg = new FlightLeg();
|
||||||
|
leg.setOriginIata("FRA");
|
||||||
|
leg.setDestinationIata("JFK");
|
||||||
|
leg.setDepartureTime(ZonedDateTime.now()); // optional
|
||||||
|
|
||||||
|
FlightRequest request = new FlightRequest();
|
||||||
|
request.getLegs().add(leg);
|
||||||
|
request.setCabinClass("economy");
|
||||||
|
request.setPassengers(1);
|
||||||
|
request.setIncludeNonCo2(true);
|
||||||
|
|
||||||
|
// 5) API key – inject from env or config
|
||||||
|
Map<String, String> env = EnvLoader.loadEnv(".env");
|
||||||
|
String apiKeyName = engine.getProvider(providerId).getAuth().getEnvKey();
|
||||||
|
String apiKey = env.get(apiKeyName);
|
||||||
|
|
||||||
|
if (apiKey == null) {
|
||||||
|
System.err.println(apiKeyName + " missing in .env");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 6) Call engine
|
||||||
|
ResponseValue<EmissionEstimate> estimate = engine.estimateFlight(providerId, request, apiKey);
|
||||||
|
|
||||||
|
if (!estimate.isList()) {
|
||||||
|
EmissionEstimate est = estimate.asSingle();
|
||||||
|
System.out.println("Vendor: " + est.getVendor());
|
||||||
|
System.out.println("Raw response:");
|
||||||
|
System.out.println(est.getVendorRaw());
|
||||||
|
System.out.println("CO2: " + est.getCo2eKg());
|
||||||
|
} else {
|
||||||
|
// if some provider decides to return multiple estimates for some reason
|
||||||
|
for(EmissionEstimate est : estimate.asList()) {
|
||||||
|
System.out.println("Vendor: " + est.getVendor());
|
||||||
|
System.out.println("Raw response:");
|
||||||
|
System.out.println(est.getVendorRaw());
|
||||||
|
System.out.println("CO2: " + est.getCo2eKg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Airport search demo
|
||||||
|
AirportSearch asr = new AirportSearch();
|
||||||
|
asr.setQuery("FRA");
|
||||||
|
asr.setLimit(5);
|
||||||
|
|
||||||
|
ResponseValue<AirportInfo> airports = engine.airportSearch(providerId, asr, apiKey);
|
||||||
|
if(airports.isList()) {
|
||||||
|
for (AirportInfo ai : airports.asList()) {
|
||||||
|
System.out.printf("%s (%s) - %s; %s,%s%n",
|
||||||
|
ai.getName(), ai.getIataCode(), ai.getCity(), ai.getLatitude(), ai.getLongitude());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AirportInfo single = airports.asSingle();
|
||||||
|
System.out.printf("%s (%s) - %s; %s,%s%n",
|
||||||
|
single.getName(), single.getIataCode(), single.getCity(), single.getLatitude(), single.getLongitude());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
368
src/de/addideas/api/emission/EmissionEngine.java
Normal file
368
src/de/addideas/api/emission/EmissionEngine.java
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
package de.addideas.api.emission;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import de.addideas.api.emission.descriptors.AuthConfig;
|
||||||
|
import de.addideas.api.emission.descriptors.FieldMapping;
|
||||||
|
import de.addideas.api.emission.descriptors.ListMapping;
|
||||||
|
import de.addideas.api.emission.descriptors.ModelRegistry;
|
||||||
|
import de.addideas.api.emission.descriptors.OperationDescriptor;
|
||||||
|
import de.addideas.api.emission.descriptors.ProviderDescriptor;
|
||||||
|
import de.addideas.api.emission.descriptors.ResponseMapping;
|
||||||
|
import de.addideas.api.emission.descriptors.ResponseValue;
|
||||||
|
import de.addideas.api.emission.model.AirportInfo;
|
||||||
|
import de.addideas.api.emission.model.AirportSearch;
|
||||||
|
import de.addideas.api.emission.model.EmissionEstimate;
|
||||||
|
import de.addideas.api.emission.model.FlightRequest;
|
||||||
|
import de.addideas.api.emission.model.LegEstimate;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
public class EmissionEngine {
|
||||||
|
private final Map<String, ProviderDescriptor> providers;
|
||||||
|
private final ModelRegistry modelRegistry = new ModelRegistry();
|
||||||
|
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
public static final String OP_FLIGHT_ESTIMATE = "travel.flight.estimate_emissions";
|
||||||
|
public static final String OP_AIRPORT_SEARCH = "travel.airport.search";
|
||||||
|
|
||||||
|
public EmissionEngine(Map<String, ProviderDescriptor> providers) {
|
||||||
|
modelRegistry.register("EmissionEstimate", EmissionEstimate.class);
|
||||||
|
modelRegistry.register("LegEstimate", LegEstimate.class);
|
||||||
|
modelRegistry.register("AirportInfo", AirportInfo.class);
|
||||||
|
|
||||||
|
this.providers = providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOperationSupported(String providerId, String operationId) {
|
||||||
|
ProviderDescriptor p = providers.get(providerId);
|
||||||
|
if (p == null) return false;
|
||||||
|
return p.getOperation(operationId) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProviderDescriptor getProvider(String providerId) {
|
||||||
|
return providers.get(providerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> ResponseValue<T> callOperation(String providerId,
|
||||||
|
String operationId,
|
||||||
|
Object request,
|
||||||
|
String apiKey,
|
||||||
|
Class<T> resultClass) throws Exception {
|
||||||
|
ProviderDescriptor provider = providers.get(providerId);
|
||||||
|
if (provider == null) {
|
||||||
|
throw new IllegalArgumentException("Unknown provider: " + providerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
OperationDescriptor op = provider.getOperation(operationId);
|
||||||
|
if (op == null) {
|
||||||
|
throw new IllegalArgumentException("Provider " + providerId
|
||||||
|
+ " does not support " + operationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse<String> response = performHttpCall(provider, op, request, apiKey);
|
||||||
|
String json = response.body();
|
||||||
|
|
||||||
|
ResponseMapping rm = op.getResponseMapping();
|
||||||
|
if (rm == null) {
|
||||||
|
throw new IllegalStateException("No responseMapping defined for " + operationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (rm.getMode()) {
|
||||||
|
case SINGLE -> {
|
||||||
|
T obj = mapSingle(rm, json, resultClass);
|
||||||
|
return ResponseValue.single(obj);
|
||||||
|
}
|
||||||
|
case LIST -> {
|
||||||
|
List<T> list = mapList(rm, json, resultClass);
|
||||||
|
return ResponseValue.list(list);
|
||||||
|
}
|
||||||
|
default -> throw new IllegalStateException("Unsupported response mode: " + rm.getMode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponse<String> performHttpCall(ProviderDescriptor provider, OperationDescriptor op, Object request,
|
||||||
|
String apiKey) throws Exception {
|
||||||
|
String url = provider.getBaseUrl() + op.getPath();
|
||||||
|
HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(url));
|
||||||
|
applyAuth(builder, provider.getAuth(), apiKey);
|
||||||
|
|
||||||
|
// body from template (if defined)
|
||||||
|
String body = "";
|
||||||
|
if (op.getRequestBody() != null) {
|
||||||
|
body = SimpleTemplateEngine.render(op.getRequestBody().getTemplate(), request);
|
||||||
|
builder.header("Content-Type", "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
String method = op.getMethod() != null ? op.getMethod().toUpperCase() : "POST";
|
||||||
|
if ("POST".equals(method)) {
|
||||||
|
builder.POST(HttpRequest.BodyPublishers.ofString(body));
|
||||||
|
} else if ("GET".equals(method)) {
|
||||||
|
builder.GET();
|
||||||
|
} else {
|
||||||
|
builder.method(method, HttpRequest.BodyPublishers.ofString(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpRequest httpRequest = builder.build();
|
||||||
|
return httpClient.send(
|
||||||
|
httpRequest, HttpResponse.BodyHandlers.ofString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseValue<EmissionEstimate> estimateFlight(String providerId, FlightRequest request, String apiKey) throws Exception {
|
||||||
|
return callOperation(providerId, OP_FLIGHT_ESTIMATE, request, apiKey, EmissionEstimate.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseValue<AirportInfo> airportSearch(String providerId, AirportSearch request, String apiKey) throws Exception {
|
||||||
|
return callOperation(providerId, OP_AIRPORT_SEARCH, request, apiKey, AirportInfo.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyAuth(HttpRequest.Builder builder, AuthConfig auth, String apiKey) {
|
||||||
|
if (auth == null || auth.getType() == AuthConfig.Type.NONE) return;
|
||||||
|
|
||||||
|
switch (auth.getType()) {
|
||||||
|
case APIKEY -> {
|
||||||
|
String header = auth.getHeader() != null ? auth.getHeader() : "Authorization";
|
||||||
|
String format = auth.getFormat() != null ? auth.getFormat() : "${API_KEY}";
|
||||||
|
String value = format.replace("${API_KEY}", apiKey);
|
||||||
|
builder.header(header, value);
|
||||||
|
}
|
||||||
|
case BASIC -> {
|
||||||
|
if (auth.isUsernameIsApiKey()) {
|
||||||
|
String token = Base64.getEncoder().encodeToString((apiKey + ":")
|
||||||
|
.getBytes(StandardCharsets.UTF_8));
|
||||||
|
builder.header("Authorization", "Basic " + token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case OAUTH, NONE -> {
|
||||||
|
// not implemented yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object applyTransform(String transform, Object value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
switch (transform) {
|
||||||
|
case "divideBy1000" -> {
|
||||||
|
double d = Double.parseDouble(value.toString());
|
||||||
|
return d / 1000.0;
|
||||||
|
}
|
||||||
|
// you can add more transforms here
|
||||||
|
default -> {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T mapSingle(ResponseMapping rm, String json, Class<T> expectedClass) throws Exception {
|
||||||
|
JsonNode root = objectMapper.readTree(json);
|
||||||
|
|
||||||
|
// Choose the node for the root object
|
||||||
|
JsonNode rootNode = evaluateJsonNode(root, rm.getRootSource());
|
||||||
|
if (rootNode == null) rootNode = root;
|
||||||
|
|
||||||
|
// Create root object
|
||||||
|
String typeName = rm.getRootType();
|
||||||
|
if (typeName == null || typeName.isEmpty()) {
|
||||||
|
typeName = modelRegistry.getNameForClass(expectedClass);
|
||||||
|
}
|
||||||
|
T target = modelRegistry.newInstance(typeName);
|
||||||
|
|
||||||
|
// Apply simple field mappings
|
||||||
|
for (FieldMapping fm : rm.getFieldMappings()) {
|
||||||
|
Object value = resolveFieldValue(rootNode, fm);
|
||||||
|
setTargetFieldOnObject(target, fm.getTargetFieldPath(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply nested list mappings
|
||||||
|
for (ListMapping lm : rm.getListMappings()) {
|
||||||
|
List<Object> list = new ArrayList<>();
|
||||||
|
List<JsonNode> items = evaluateJsonPathList(rootNode, lm.getSourceJsonPath());
|
||||||
|
for (JsonNode itemNode : items) {
|
||||||
|
Object item = modelRegistry.newInstance(lm.getItemType());
|
||||||
|
for (FieldMapping fm : lm.getItemFieldMappings()) {
|
||||||
|
Object value = resolveFieldValue(itemNode, fm);
|
||||||
|
setTargetFieldOnObject(item, fm.getTargetFieldPath(), value);
|
||||||
|
}
|
||||||
|
list.add(item);
|
||||||
|
}
|
||||||
|
setTargetFieldOnObject(target, lm.getTargetFieldPath(), list);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> List<T> mapList(ResponseMapping rm, String json, Class<T> expectedClass) throws Exception {
|
||||||
|
JsonNode root = objectMapper.readTree(json);
|
||||||
|
|
||||||
|
String typeName = rm.getRootType();
|
||||||
|
if (typeName == null || typeName.isEmpty()) {
|
||||||
|
typeName = modelRegistry.getNameForClass(expectedClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<T> result = new ArrayList<>();
|
||||||
|
List<JsonNode> nodes = evaluateJsonPathList(root, rm.getRootSource());
|
||||||
|
|
||||||
|
for (JsonNode node : nodes) {
|
||||||
|
T item = modelRegistry.newInstance(typeName);
|
||||||
|
|
||||||
|
for (FieldMapping fm : rm.getFieldMappings()) {
|
||||||
|
Object value = resolveFieldValue(node, fm);
|
||||||
|
setTargetFieldOnObject(item, fm.getTargetFieldPath(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// nested lists *inside* each element, if any
|
||||||
|
for (ListMapping lm : rm.getListMappings()) {
|
||||||
|
List<Object> list = new ArrayList<>();
|
||||||
|
List<JsonNode> subItems = evaluateJsonPathList(node, lm.getSourceJsonPath());
|
||||||
|
for (JsonNode subNode : subItems) {
|
||||||
|
Object sub = modelRegistry.newInstance(lm.getItemType());
|
||||||
|
for (FieldMapping fm : lm.getItemFieldMappings()) {
|
||||||
|
Object value = resolveFieldValue(subNode, fm);
|
||||||
|
setTargetFieldOnObject(sub, fm.getTargetFieldPath(), value);
|
||||||
|
}
|
||||||
|
list.add(sub);
|
||||||
|
}
|
||||||
|
setTargetFieldOnObject(item, lm.getTargetFieldPath(), list);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object resolveFieldValue(JsonNode context, FieldMapping fm) {
|
||||||
|
Object value;
|
||||||
|
if (fm.getConstantValue() != null && !fm.getConstantValue().isEmpty()) {
|
||||||
|
value = fm.getConstantValue();
|
||||||
|
} else {
|
||||||
|
value = evaluateJsonPath(context, fm.getSourceJsonPath());
|
||||||
|
}
|
||||||
|
if (fm.getTransform() != null && value != null) {
|
||||||
|
value = applyTransform(fm.getTransform(), value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns JsonNode for JSONPath, for both scalar and array
|
||||||
|
private JsonNode evaluateJsonNode(JsonNode root, String path) {
|
||||||
|
if (path == null || path.isEmpty()) return null;
|
||||||
|
String p = path.trim();
|
||||||
|
if (p.equals("$")) return root;
|
||||||
|
if (!p.startsWith("$.")) return null;
|
||||||
|
|
||||||
|
String remainder = p.substring(2); // skip "$."
|
||||||
|
String[] tokens = remainder.split("\\.");
|
||||||
|
|
||||||
|
JsonNode current = root;
|
||||||
|
for (String token : tokens) {
|
||||||
|
if (current == null) return null;
|
||||||
|
|
||||||
|
int idxStart = token.indexOf('[');
|
||||||
|
if (idxStart != -1 && token.endsWith("]")) {
|
||||||
|
String fieldName = token.substring(0, idxStart);
|
||||||
|
String idxPart = token.substring(idxStart + 1, token.length() - 1); // inside []
|
||||||
|
if (idxPart.equals("*")) {
|
||||||
|
// wildcard array handled by caller
|
||||||
|
current = current.get(fieldName);
|
||||||
|
break; // caller will process array
|
||||||
|
} else {
|
||||||
|
int index = Integer.parseInt(idxPart);
|
||||||
|
current = current.get(fieldName);
|
||||||
|
if (current == null || !current.isArray()) return null;
|
||||||
|
if (index < 0 || index >= current.size()) return null;
|
||||||
|
current = current.get(index);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current = current.get(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object evaluateJsonPath(JsonNode root, String path) {
|
||||||
|
JsonNode node = evaluateJsonNode(root, path);
|
||||||
|
if (node == null) return null;
|
||||||
|
if (node.isNumber()) return node.numberValue();
|
||||||
|
if (node.isBoolean()) return node.booleanValue();
|
||||||
|
if (node.isTextual()) return node.textValue();
|
||||||
|
return node.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<JsonNode> evaluateJsonPathList(JsonNode root, String path) {
|
||||||
|
if (path == null || !path.endsWith("[*]")) return java.util.List.of();
|
||||||
|
String basePath = path.substring(0, path.length() - 3); // remove [*]
|
||||||
|
|
||||||
|
JsonNode arrNode = evaluateJsonNode(root, basePath);
|
||||||
|
if (arrNode == null || !arrNode.isArray()) return java.util.List.of();
|
||||||
|
|
||||||
|
List<JsonNode> items = new ArrayList<>();
|
||||||
|
arrNode.forEach(items::add);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTargetFieldOnObject(Object target,
|
||||||
|
String targetFieldPath,
|
||||||
|
Object value) {
|
||||||
|
if (targetFieldPath == null || targetFieldPath.isEmpty()) return;
|
||||||
|
|
||||||
|
String fieldName = targetFieldPath;
|
||||||
|
int dot = targetFieldPath.indexOf('.');
|
||||||
|
if (dot != -1) {
|
||||||
|
fieldName = targetFieldPath.substring(dot + 1); // strip "AirportInfo."
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Class<?> cls = target.getClass();
|
||||||
|
// try setter
|
||||||
|
String setterName = "set" + Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
|
||||||
|
for (var m : cls.getMethods()) {
|
||||||
|
if (m.getName().equals(setterName) && m.getParameterCount() == 1) {
|
||||||
|
Class<?> paramType = m.getParameterTypes()[0];
|
||||||
|
Object converted = convertValue(value, paramType);
|
||||||
|
m.invoke(target, converted);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// try field
|
||||||
|
var f = cls.getDeclaredField(fieldName);
|
||||||
|
f.setAccessible(true);
|
||||||
|
Object converted = convertValue(value, f.getType());
|
||||||
|
f.set(target, converted);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object convertValue(Object value, Class<?> targetType) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (targetType.isAssignableFrom(value.getClass())) return value;
|
||||||
|
|
||||||
|
String s = value.toString();
|
||||||
|
if (targetType == Double.class || targetType == double.class) {
|
||||||
|
return Double.parseDouble(s);
|
||||||
|
}
|
||||||
|
if (targetType == Integer.class || targetType == int.class) {
|
||||||
|
return Integer.parseInt(s);
|
||||||
|
}
|
||||||
|
if (targetType == Boolean.class || targetType == boolean.class) {
|
||||||
|
return Boolean.parseBoolean(s);
|
||||||
|
}
|
||||||
|
if (targetType == String.class) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
src/de/addideas/api/emission/EnvLoader.java
Normal file
27
src/de/addideas/api/emission/EnvLoader.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package de.addideas.api.emission;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class EnvLoader {
|
||||||
|
|
||||||
|
public static Map<String, String> loadEnv(String filename) {
|
||||||
|
Map<String, String> map = new HashMap<>();
|
||||||
|
try {
|
||||||
|
for (String line : Files.readAllLines(Paths.get(filename))) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
int eq = line.indexOf('=');
|
||||||
|
if (eq == -1) continue;
|
||||||
|
String key = line.substring(0, eq).trim();
|
||||||
|
String value = line.substring(eq + 1).trim();
|
||||||
|
map.put(key, value);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Failed to load .env file: " + filename, e);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
||||||
184
src/de/addideas/api/emission/ProvidersConfigLoader.java
Normal file
184
src/de/addideas/api/emission/ProvidersConfigLoader.java
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package de.addideas.api.emission;
|
||||||
|
|
||||||
|
import de.addideas.api.emission.descriptors.AuthConfig;
|
||||||
|
import de.addideas.api.emission.descriptors.FieldMapping;
|
||||||
|
import de.addideas.api.emission.descriptors.ListMapping;
|
||||||
|
import de.addideas.api.emission.descriptors.OperationDescriptor;
|
||||||
|
import de.addideas.api.emission.descriptors.ProviderDescriptor;
|
||||||
|
import de.addideas.api.emission.descriptors.RequestBodyTemplate;
|
||||||
|
import de.addideas.api.emission.descriptors.ResponseMapping;
|
||||||
|
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Element;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ProvidersConfigLoader {
|
||||||
|
|
||||||
|
private final Map<String, ProviderDescriptor> providers = new HashMap<>();
|
||||||
|
|
||||||
|
public ProvidersConfigLoader(InputStream xmlInput) throws Exception {
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
factory.setNamespaceAware(true);
|
||||||
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
|
Document doc = builder.parse(xmlInput);
|
||||||
|
|
||||||
|
Element root = doc.getDocumentElement();
|
||||||
|
NodeList providerNodes = root.getElementsByTagNameNS(root.getNamespaceURI(), "provider");
|
||||||
|
for (int i = 0; i < providerNodes.getLength(); i++) {
|
||||||
|
Element p = (Element) providerNodes.item(i);
|
||||||
|
ProviderDescriptor desc = parseProvider(p, root.getNamespaceURI());
|
||||||
|
providers.put(desc.getId(), desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProviderDescriptor parseProvider(Element p, String ns) {
|
||||||
|
ProviderDescriptor desc = new ProviderDescriptor();
|
||||||
|
desc.setId(p.getAttribute("id"));
|
||||||
|
|
||||||
|
desc.setName(getChildText(p, ns, "name"));
|
||||||
|
desc.setBaseUrl(getChildText(p, ns, "baseUrl"));
|
||||||
|
|
||||||
|
Element authElem = getChildElement(p, ns, "auth");
|
||||||
|
if (authElem != null) {
|
||||||
|
AuthConfig auth = new AuthConfig();
|
||||||
|
String typeStr = authElem.getAttribute("type");
|
||||||
|
AuthConfig.Type type = AuthConfig.Type.NONE;
|
||||||
|
if (typeStr != null && !typeStr.isEmpty()) {
|
||||||
|
type = AuthConfig.Type.valueOf(typeStr.toUpperCase());
|
||||||
|
}
|
||||||
|
auth.setType(type);
|
||||||
|
|
||||||
|
auth.setHeader(getChildText(authElem, ns, "header"));
|
||||||
|
auth.setFormat(getChildText(authElem, ns, "format"));
|
||||||
|
auth.setUsernameIsApiKey("true".equalsIgnoreCase(getChildText(authElem, ns, "usernameIsApiKey")));
|
||||||
|
|
||||||
|
// NEW: read <envKey>
|
||||||
|
auth.setEnvKey(getChildText(authElem, ns, "envKey"));
|
||||||
|
|
||||||
|
desc.setAuth(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Element opsElem = getChildElement(p, ns, "operations");
|
||||||
|
if (opsElem != null) {
|
||||||
|
NodeList opNodes = opsElem.getElementsByTagNameNS(ns, "operation");
|
||||||
|
for (int i = 0; i < opNodes.getLength(); i++) {
|
||||||
|
Element opElem = (Element) opNodes.item(i);
|
||||||
|
OperationDescriptor op = parseOperation(opElem, ns);
|
||||||
|
desc.getOperations().put(op.getId(), op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OperationDescriptor parseOperation(Element opElem, String ns) {
|
||||||
|
OperationDescriptor op = new OperationDescriptor();
|
||||||
|
op.setId(opElem.getAttribute("id"));
|
||||||
|
|
||||||
|
Element httpElem = getChildElement(opElem, ns, "http");
|
||||||
|
if (httpElem != null) {
|
||||||
|
op.setMethod(httpElem.getAttribute("method"));
|
||||||
|
op.setPath(httpElem.getAttribute("path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Element bodyElem = getChildElement(opElem, ns, "requestBody");
|
||||||
|
if (bodyElem != null) {
|
||||||
|
RequestBodyTemplate tmpl = new RequestBodyTemplate();
|
||||||
|
tmpl.setFormat(bodyElem.getAttribute("format"));
|
||||||
|
tmpl.setTemplate(bodyElem.getTextContent().trim());
|
||||||
|
op.setRequestBody(tmpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
Element respElem = getChildElement(opElem, ns, "responseMapping");
|
||||||
|
if (respElem != null) {
|
||||||
|
ResponseMapping rm = new ResponseMapping();
|
||||||
|
|
||||||
|
String modeStr = respElem.getAttribute("mode");
|
||||||
|
if (modeStr != null && !modeStr.isEmpty()) {
|
||||||
|
rm.setMode(ResponseMapping.Mode.valueOf(modeStr.toUpperCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
rm.setRootType(respElem.getAttribute("rootType"));
|
||||||
|
String rootSource = respElem.getAttribute("rootSource");
|
||||||
|
if (rootSource == null || rootSource.isEmpty()) {
|
||||||
|
rm.setRootSource("$");
|
||||||
|
} else {
|
||||||
|
rm.setRootSource(rootSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
// direct fields
|
||||||
|
NodeList fieldNodes = respElem.getElementsByTagNameNS(ns, "field");
|
||||||
|
for (int i = 0; i < fieldNodes.getLength(); i++) {
|
||||||
|
Element f = (Element) fieldNodes.item(i);
|
||||||
|
// skip fields that are inside <list> – handle those separately
|
||||||
|
if (!f.getParentNode().equals(respElem)) continue;
|
||||||
|
|
||||||
|
FieldMapping fm = new FieldMapping();
|
||||||
|
fm.setSourceJsonPath(f.getAttribute("source"));
|
||||||
|
fm.setTargetFieldPath(f.getAttribute("target"));
|
||||||
|
String tr = f.getAttribute("transform");
|
||||||
|
if (!tr.isEmpty()) fm.setTransform(tr);
|
||||||
|
String constant = f.getAttribute("value");
|
||||||
|
if (!constant.isEmpty()) fm.setConstantValue(constant);
|
||||||
|
|
||||||
|
rm.addFieldMapping(fm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// nested lists
|
||||||
|
NodeList listNodes = respElem.getElementsByTagNameNS(ns, "list");
|
||||||
|
for (int i = 0; i < listNodes.getLength(); i++) {
|
||||||
|
Element le = (Element) listNodes.item(i);
|
||||||
|
ListMapping lm = new ListMapping();
|
||||||
|
lm.setSourceJsonPath(le.getAttribute("source"));
|
||||||
|
lm.setTargetFieldPath(le.getAttribute("target"));
|
||||||
|
lm.setItemType(le.getAttribute("itemType"));
|
||||||
|
|
||||||
|
NodeList itemFields = le.getElementsByTagNameNS(ns, "field");
|
||||||
|
for (int j = 0; j < itemFields.getLength(); j++) {
|
||||||
|
Element f = (Element) itemFields.item(j);
|
||||||
|
FieldMapping fm = new FieldMapping();
|
||||||
|
fm.setSourceJsonPath(f.getAttribute("source"));
|
||||||
|
fm.setTargetFieldPath(f.getAttribute("target"));
|
||||||
|
String tr = f.getAttribute("transform");
|
||||||
|
if (!tr.isEmpty()) fm.setTransform(tr);
|
||||||
|
String constant = f.getAttribute("value");
|
||||||
|
if (!constant.isEmpty()) fm.setConstantValue(constant);
|
||||||
|
lm.addItemFieldMapping(fm);
|
||||||
|
}
|
||||||
|
rm.addListMapping(lm);
|
||||||
|
}
|
||||||
|
|
||||||
|
op.setResponseMapping(rm);
|
||||||
|
}
|
||||||
|
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static Element getChildElement(Element parent, String ns, String localName) {
|
||||||
|
NodeList children = parent.getElementsByTagNameNS(ns, localName);
|
||||||
|
if (children.getLength() == 0) return null;
|
||||||
|
return (Element) children.item(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getChildText(Element parent, String ns, String localName) {
|
||||||
|
Element child = getChildElement(parent, ns, localName);
|
||||||
|
return (child != null) ? child.getTextContent().trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, ProviderDescriptor> getProviders() {
|
||||||
|
return providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProviderDescriptor getProvider(String id) {
|
||||||
|
return providers.get(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
191
src/de/addideas/api/emission/SimpleTemplateEngine.java
Normal file
191
src/de/addideas/api/emission/SimpleTemplateEngine.java
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package de.addideas.api.emission;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class SimpleTemplateEngine {
|
||||||
|
|
||||||
|
private static final Pattern PLACEHOLDER = Pattern.compile("\\$\\{([^}]+)}");
|
||||||
|
|
||||||
|
public static String render(String template, Object request) {
|
||||||
|
// First handle loops, then scalar placeholders
|
||||||
|
String withLoops = renderLoops(template, request);
|
||||||
|
return renderScalars(withLoops, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Loops -----
|
||||||
|
|
||||||
|
private static String renderLoops(String template, Object request) {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
|
||||||
|
int pos = 0;
|
||||||
|
while (true) {
|
||||||
|
int forIndex = template.indexOf("#for", pos);
|
||||||
|
if (forIndex == -1) {
|
||||||
|
result.append(template.substring(pos));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy up to #for
|
||||||
|
result.append(template, pos, forIndex);
|
||||||
|
|
||||||
|
int lineEnd = template.indexOf('\n', forIndex);
|
||||||
|
if (lineEnd == -1) lineEnd = template.length();
|
||||||
|
String forLine = template.substring(forIndex, lineEnd).trim();
|
||||||
|
// expected: "#for leg in request.legs"
|
||||||
|
String[] parts = forLine.split("\\s+");
|
||||||
|
if (parts.length < 4 || !parts[0].equals("#for") || !parts[2].equals("in")) {
|
||||||
|
// Not a valid for-line, just copy literal
|
||||||
|
result.append(template.substring(forIndex, lineEnd));
|
||||||
|
pos = lineEnd;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String varName = parts[1]; // "leg"
|
||||||
|
String expr = parts[3]; // "request.legs"
|
||||||
|
|
||||||
|
// find #end corresponding
|
||||||
|
int endIndex = template.indexOf("#end", lineEnd);
|
||||||
|
if (endIndex == -1) {
|
||||||
|
throw new IllegalStateException("Missing #end for loop starting at: " + forLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop body between for-line end and "#end"
|
||||||
|
String loopBody = template.substring(lineEnd, endIndex);
|
||||||
|
|
||||||
|
// inside loop body, optional "#sep" line
|
||||||
|
String bodyWithoutSep;
|
||||||
|
String sep = "";
|
||||||
|
int sepIndex = loopBody.indexOf("#sep");
|
||||||
|
if (sepIndex != -1) {
|
||||||
|
int sepLineEnd = loopBody.indexOf('\n', sepIndex);
|
||||||
|
if (sepLineEnd == -1) sepLineEnd = loopBody.length();
|
||||||
|
// content before #sep line is body chunk
|
||||||
|
bodyWithoutSep = loopBody.substring(0, sepIndex);
|
||||||
|
// separator is substring after #sep line end
|
||||||
|
sep = loopBody.substring(sepLineEnd);
|
||||||
|
} else {
|
||||||
|
bodyWithoutSep = loopBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
// evaluate expr on request -> should be List
|
||||||
|
Object collectionObj = resolveExpression(expr, request);
|
||||||
|
if (collectionObj instanceof List<?> list) {
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
Object item = list.get(i);
|
||||||
|
String chunk = bodyWithoutSep;
|
||||||
|
// Render scalar placeholders in this chunk, with "varName" bound
|
||||||
|
chunk = renderScalarsWithVar(chunk, request, varName, item);
|
||||||
|
result.append(chunk);
|
||||||
|
if (i < list.size() - 1 && !sep.isEmpty()) {
|
||||||
|
result.append(sep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = endIndex + "#end".length();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Scalar rendering -----
|
||||||
|
|
||||||
|
private static String renderScalars(String template, Object request) {
|
||||||
|
return renderScalarsWithVar(template, request, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String renderScalarsWithVar(String template,
|
||||||
|
Object request,
|
||||||
|
String varName,
|
||||||
|
Object varValue) {
|
||||||
|
Matcher m = PLACEHOLDER.matcher(template);
|
||||||
|
StringBuffer sb = new StringBuffer();
|
||||||
|
|
||||||
|
while (m.find()) {
|
||||||
|
String expr = m.group(1).trim(); // e.g. "request.cabinClass" or "leg.originIata"
|
||||||
|
Object value;
|
||||||
|
|
||||||
|
if (varName != null && expr.startsWith(varName + ".")) {
|
||||||
|
// expression relative to loop variable, e.g. leg.originIata
|
||||||
|
String subPath = expr.substring(varName.length() + 1); // skip "leg."
|
||||||
|
value = resolveExpression(subPath, varValue);
|
||||||
|
} else {
|
||||||
|
// regular expression starting with "request."
|
||||||
|
value = resolveExpression(expr, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
String replacement = (value == null) ? "null" : value.toString();
|
||||||
|
replacement = replacement.replace("\\", "\\\\").replace("$", "\\$");
|
||||||
|
m.appendReplacement(sb, replacement);
|
||||||
|
}
|
||||||
|
m.appendTail(sb);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Expression resolution -----
|
||||||
|
// Supports:
|
||||||
|
// request.cabinClass
|
||||||
|
// request.legs[0].originIata
|
||||||
|
// And for loop var:
|
||||||
|
// originIata (when called with varValue)
|
||||||
|
|
||||||
|
private static Object resolveExpression(String expr, Object rootObj) {
|
||||||
|
if (rootObj == null || expr == null || expr.isEmpty()) return null;
|
||||||
|
|
||||||
|
String path = expr;
|
||||||
|
if (expr.startsWith("request.")) {
|
||||||
|
path = expr.substring("request.".length());
|
||||||
|
}
|
||||||
|
|
||||||
|
Object current = rootObj;
|
||||||
|
String[] parts = path.split("\\.");
|
||||||
|
for (String part : parts) {
|
||||||
|
if (current == null) return null;
|
||||||
|
|
||||||
|
int idxStart = part.indexOf('[');
|
||||||
|
if (idxStart != -1 && part.endsWith("]")) {
|
||||||
|
String fieldName = part.substring(0, idxStart);
|
||||||
|
int index = Integer.parseInt(part.substring(idxStart + 1, part.length() - 1));
|
||||||
|
current = getFieldValue(current, fieldName);
|
||||||
|
if (!(current instanceof List<?> list)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (index < 0 || index >= list.size()) return null;
|
||||||
|
current = list.get(index);
|
||||||
|
} else {
|
||||||
|
current = getFieldValue(current, part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object getFieldValue(Object obj, String name) {
|
||||||
|
Class<?> cls = obj.getClass();
|
||||||
|
// Try getter
|
||||||
|
String getterName = "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||||
|
try {
|
||||||
|
Method m = cls.getMethod(getterName);
|
||||||
|
return m.invoke(obj);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// Try boolean isX
|
||||||
|
String isName = "is" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
|
||||||
|
try {
|
||||||
|
Method m = cls.getMethod(isName);
|
||||||
|
return m.invoke(obj);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
// Try field
|
||||||
|
try {
|
||||||
|
Field f = cls.getDeclaredField(name);
|
||||||
|
f.setAccessible(true);
|
||||||
|
return f.get(obj);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/de/addideas/api/emission/descriptors/AuthConfig.java
Normal file
26
src/de/addideas/api/emission/descriptors/AuthConfig.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package de.addideas.api.emission.descriptors;
|
||||||
|
|
||||||
|
public class AuthConfig {
|
||||||
|
public enum Type { APIKEY, BASIC, OAUTH, NONE }
|
||||||
|
|
||||||
|
private Type type = Type.NONE;
|
||||||
|
private String header;
|
||||||
|
private String format;
|
||||||
|
private boolean usernameIsApiKey;
|
||||||
|
private String envKey;
|
||||||
|
|
||||||
|
public Type getType() { return type; }
|
||||||
|
public void setType(Type type) { this.type = type; }
|
||||||
|
|
||||||
|
public String getHeader() { return header; }
|
||||||
|
public void setHeader(String header) { this.header = header; }
|
||||||
|
|
||||||
|
public String getFormat() { return format; }
|
||||||
|
public void setFormat(String format) { this.format = format; }
|
||||||
|
|
||||||
|
public boolean isUsernameIsApiKey() { return usernameIsApiKey; }
|
||||||
|
public void setUsernameIsApiKey(boolean usernameIsApiKey) { this.usernameIsApiKey = usernameIsApiKey; }
|
||||||
|
|
||||||
|
public String getEnvKey() { return envKey; }
|
||||||
|
public void setEnvKey(String envKey) { this.envKey = envKey; }
|
||||||
|
}
|
||||||
20
src/de/addideas/api/emission/descriptors/FieldMapping.java
Normal file
20
src/de/addideas/api/emission/descriptors/FieldMapping.java
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package de.addideas.api.emission.descriptors;
|
||||||
|
|
||||||
|
public class FieldMapping {
|
||||||
|
private String sourceJsonPath; // e.g. "$.co2_kg"
|
||||||
|
private String targetFieldPath; // e.g. "EmissionEstimate.co2Kg"
|
||||||
|
private String transform; // optional, e.g. "divideBy1000"
|
||||||
|
private String constantValue; // optional for constant mappings
|
||||||
|
|
||||||
|
public String getSourceJsonPath() { return sourceJsonPath; }
|
||||||
|
public void setSourceJsonPath(String sourceJsonPath) { this.sourceJsonPath = sourceJsonPath; }
|
||||||
|
|
||||||
|
public String getTargetFieldPath() { return targetFieldPath; }
|
||||||
|
public void setTargetFieldPath(String targetFieldPath) { this.targetFieldPath = targetFieldPath; }
|
||||||
|
|
||||||
|
public String getTransform() { return transform; }
|
||||||
|
public void setTransform(String transform) { this.transform = transform; }
|
||||||
|
|
||||||
|
public String getConstantValue() { return constantValue; }
|
||||||
|
public void setConstantValue(String constantValue) { this.constantValue = constantValue; }
|
||||||
|
}
|
||||||
29
src/de/addideas/api/emission/descriptors/ListMapping.java
Normal file
29
src/de/addideas/api/emission/descriptors/ListMapping.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package de.addideas.api.emission.descriptors;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ListMapping {
|
||||||
|
// JSON array to pull items from, relative to the current JSON node
|
||||||
|
private String sourceJsonPath; // e.g. "$.legs[*]"
|
||||||
|
|
||||||
|
// Which field on the current object receives the list: "EmissionEstimate.legs" or "legs"
|
||||||
|
private String targetFieldPath;
|
||||||
|
|
||||||
|
// Type name for each list element, must be registered in ModelRegistry
|
||||||
|
private String itemType;
|
||||||
|
|
||||||
|
private final List<FieldMapping> itemFieldMappings = new ArrayList<>();
|
||||||
|
|
||||||
|
public String getSourceJsonPath() { return sourceJsonPath; }
|
||||||
|
public void setSourceJsonPath(String sourceJsonPath) { this.sourceJsonPath = sourceJsonPath; }
|
||||||
|
|
||||||
|
public String getTargetFieldPath() { return targetFieldPath; }
|
||||||
|
public void setTargetFieldPath(String targetFieldPath) { this.targetFieldPath = targetFieldPath; }
|
||||||
|
|
||||||
|
public String getItemType() { return itemType; }
|
||||||
|
public void setItemType(String itemType) { this.itemType = itemType; }
|
||||||
|
|
||||||
|
public List<FieldMapping> getItemFieldMappings() { return itemFieldMappings; }
|
||||||
|
public void addItemFieldMapping(FieldMapping fm) { itemFieldMappings.add(fm); }
|
||||||
|
}
|
||||||
37
src/de/addideas/api/emission/descriptors/ModelRegistry.java
Normal file
37
src/de/addideas/api/emission/descriptors/ModelRegistry.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package de.addideas.api.emission.descriptors;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ModelRegistry {
|
||||||
|
|
||||||
|
private final Map<String, Class<?>> nameToClass = new HashMap<>();
|
||||||
|
private final Map<Class<?>, String> classToName = new HashMap<>();
|
||||||
|
|
||||||
|
public <T> void register(String name, Class<T> clazz) {
|
||||||
|
nameToClass.put(name, clazz);
|
||||||
|
classToName.put(clazz, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Class<?> getClassForName(String name) {
|
||||||
|
return nameToClass.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getNameForClass(Class<?> clazz) {
|
||||||
|
return classToName.get(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T newInstance(String name) {
|
||||||
|
Class<?> cls = getClassForName(name);
|
||||||
|
if (cls == null) {
|
||||||
|
throw new IllegalArgumentException("Unknown model type: " + name);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return (T) cls.getDeclaredConstructor().newInstance();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to instantiate model type: " + name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package de.addideas.api.emission.descriptors;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class OperationDescriptor {
|
||||||
|
private String id; // e.g. "travel.flight.estimate_emissions"
|
||||||
|
private String method; // "GET", "POST"
|
||||||
|
private String path; // e.g. "/v1/flight/estimate"
|
||||||
|
private RequestBodyTemplate requestBody; // may be null
|
||||||
|
private List<QueryParamMapping> queryParams;
|
||||||
|
private ResponseMapping responseMapping;
|
||||||
|
|
||||||
|
public String getId() { return id; }
|
||||||
|
public void setId(String id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getMethod() { return method; }
|
||||||
|
public void setMethod(String method) { this.method = method; }
|
||||||
|
|
||||||
|
public String getPath() { return path; }
|
||||||
|
public void setPath(String path) { this.path = path; }
|
||||||
|
|
||||||
|
public RequestBodyTemplate getRequestBody() { return requestBody; }
|
||||||
|
public void setRequestBody(RequestBodyTemplate requestBody) { this.requestBody = requestBody; }
|
||||||
|
|
||||||
|
public List<QueryParamMapping> getQueryParams() { return queryParams; }
|
||||||
|
|
||||||
|
public ResponseMapping getResponseMapping() { return responseMapping; }
|
||||||
|
public void setResponseMapping(ResponseMapping responseMapping) { this.responseMapping = responseMapping; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package de.addideas.api.emission.descriptors;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ProviderDescriptor {
|
||||||
|
private String id;
|
||||||
|
private String name;
|
||||||
|
private String baseUrl;
|
||||||
|
private AuthConfig auth;
|
||||||
|
private Map<String, OperationDescriptor> operations = new HashMap<>();
|
||||||
|
|
||||||
|
public String getId() { return id; }
|
||||||
|
public void setId(String id) { this.id = id; }
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getBaseUrl() { return baseUrl; }
|
||||||
|
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
|
||||||
|
|
||||||
|
public AuthConfig getAuth() { return auth; }
|
||||||
|
public void setAuth(AuthConfig auth) { this.auth = auth; }
|
||||||
|
|
||||||
|
public Map<String, OperationDescriptor> getOperations() { return operations; }
|
||||||
|
public void setOperations(Map<String, OperationDescriptor> operations) { this.operations = operations; }
|
||||||
|
|
||||||
|
public OperationDescriptor getOperation(String opId) {
|
||||||
|
return operations.get(opId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.addideas.api.emission.descriptors;
|
||||||
|
|
||||||
|
public class QueryParamMapping {
|
||||||
|
private String name; // query param name
|
||||||
|
private String fromPath; // e.g. "AirportSearchRequest.query"
|
||||||
|
// getters/setters...
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.addideas.api.emission.descriptors;
|
||||||
|
|
||||||
|
public class RequestBodyTemplate {
|
||||||
|
private String format; // "json"
|
||||||
|
private String template; // templated JSON
|
||||||
|
|
||||||
|
public String getFormat() { return format; }
|
||||||
|
public void setFormat(String format) { this.format = format; }
|
||||||
|
|
||||||
|
public String getTemplate() { return template; }
|
||||||
|
public void setTemplate(String template) { this.template = template; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package de.addideas.api.emission.descriptors;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ResponseMapping {
|
||||||
|
public enum Mode { SINGLE, LIST }
|
||||||
|
|
||||||
|
private Mode mode = Mode.SINGLE;
|
||||||
|
private String rootType; // e.g. "EmissionEstimate", "AirportInfo"
|
||||||
|
private String rootSource; // e.g. "$" or "$.results[*]"
|
||||||
|
|
||||||
|
private final List<FieldMapping> fieldMappings = new ArrayList<>();
|
||||||
|
private final List<ListMapping> listMappings = new ArrayList<>();
|
||||||
|
|
||||||
|
public Mode getMode() { return mode; }
|
||||||
|
public void setMode(Mode mode) { this.mode = mode; }
|
||||||
|
|
||||||
|
public String getRootType() { return rootType; }
|
||||||
|
public void setRootType(String rootType) { this.rootType = rootType; }
|
||||||
|
|
||||||
|
public String getRootSource() { return rootSource; }
|
||||||
|
public void setRootSource(String rootSource) { this.rootSource = rootSource; }
|
||||||
|
|
||||||
|
public List<FieldMapping> getFieldMappings() { return fieldMappings; }
|
||||||
|
public void addFieldMapping(FieldMapping fm) { fieldMappings.add(fm); }
|
||||||
|
|
||||||
|
public List<ListMapping> getListMappings() { return listMappings; }
|
||||||
|
public void addListMapping(ListMapping lm) { listMappings.add(lm); }
|
||||||
|
}
|
||||||
|
|
||||||
42
src/de/addideas/api/emission/descriptors/ResponseValue.java
Normal file
42
src/de/addideas/api/emission/descriptors/ResponseValue.java
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package de.addideas.api.emission.descriptors;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ResponseValue<T> {
|
||||||
|
|
||||||
|
private final T single;
|
||||||
|
private final List<T> list;
|
||||||
|
private final boolean listMode;
|
||||||
|
|
||||||
|
private ResponseValue(T single, List<T> list) {
|
||||||
|
this.single = single;
|
||||||
|
this.list = list;
|
||||||
|
this.listMode = (single == null && list != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ResponseValue<T> single(T value) {
|
||||||
|
return new ResponseValue<>(value, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ResponseValue<T> list(List<T> values) {
|
||||||
|
return new ResponseValue<>(null, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isList() {
|
||||||
|
return listMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T asSingle() {
|
||||||
|
if (listMode) {
|
||||||
|
throw new IllegalStateException("Response is a list, not a single value");
|
||||||
|
}
|
||||||
|
return single;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> asList() {
|
||||||
|
if (!listMode) {
|
||||||
|
throw new IllegalStateException("Response is a single value, not a list");
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/de/addideas/api/emission/model/AirportInfo.java
Normal file
37
src/de/addideas/api/emission/model/AirportInfo.java
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package de.addideas.api.emission.model;
|
||||||
|
|
||||||
|
public class AirportInfo {
|
||||||
|
private String iataCode;
|
||||||
|
private String icaoCode;
|
||||||
|
private String name;
|
||||||
|
private String city;
|
||||||
|
private String country;
|
||||||
|
private Double latitude;
|
||||||
|
private Double longitude;
|
||||||
|
private String vendorRaw; // raw JSON as string for now
|
||||||
|
|
||||||
|
public String getIataCode() { return iataCode; }
|
||||||
|
public void setIataCode(String iataCode) { this.iataCode = iataCode; }
|
||||||
|
|
||||||
|
public String getIcaoCode() { return icaoCode; }
|
||||||
|
public void setIcaoCode(String icaoCode) { this.icaoCode = icaoCode; }
|
||||||
|
|
||||||
|
public String getName() { return name; }
|
||||||
|
public void setName(String name) { this.name = name; }
|
||||||
|
|
||||||
|
public String getCity() { return city; }
|
||||||
|
public void setCity(String city) { this.city = city; }
|
||||||
|
|
||||||
|
public String getCountry() { return country; }
|
||||||
|
public void setCountry(String country) { this.country = country; }
|
||||||
|
|
||||||
|
public Double getLatitude() { return latitude; }
|
||||||
|
public void setLatitude(Double latitude) { this.latitude = latitude; }
|
||||||
|
|
||||||
|
public Double getLongitude() { return longitude; }
|
||||||
|
public void setLongitude(Double longitude) { this.longitude = longitude; }
|
||||||
|
|
||||||
|
public String getVendorRaw() { return vendorRaw; }
|
||||||
|
public void setVendorRaw(String vendorRaw) { this.vendorRaw = vendorRaw; }
|
||||||
|
}
|
||||||
|
|
||||||
12
src/de/addideas/api/emission/model/AirportSearch.java
Normal file
12
src/de/addideas/api/emission/model/AirportSearch.java
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package de.addideas.api.emission.model;
|
||||||
|
|
||||||
|
public class AirportSearch {
|
||||||
|
private String query;
|
||||||
|
private int limit = 10;
|
||||||
|
|
||||||
|
public String getQuery() { return query; }
|
||||||
|
public void setQuery(String query) { this.query = query; }
|
||||||
|
|
||||||
|
public int getLimit() { return limit; }
|
||||||
|
public void setLimit(int limit) { this.limit = limit; }
|
||||||
|
}
|
||||||
48
src/de/addideas/api/emission/model/EmissionEstimate.java
Normal file
48
src/de/addideas/api/emission/model/EmissionEstimate.java
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package de.addideas.api.emission.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class EmissionEstimate {
|
||||||
|
private List<LegEstimate> legs = new ArrayList<>();
|
||||||
|
|
||||||
|
private Double co2Kg;
|
||||||
|
private Double co2eKg;
|
||||||
|
private Double nonCo2Multiplier;
|
||||||
|
private String vendor;
|
||||||
|
private String methodName;
|
||||||
|
private String methodVersion;
|
||||||
|
private String standard;
|
||||||
|
private String documentationUrl;
|
||||||
|
private String vendorRaw; // raw JSON as string for now
|
||||||
|
|
||||||
|
public List<LegEstimate> getLegs() { return legs; }
|
||||||
|
public void setLegs(List<LegEstimate> legs) { this.legs = legs; }
|
||||||
|
|
||||||
|
public Double getCo2Kg() { return co2Kg; }
|
||||||
|
public void setCo2Kg(Double co2Kg) { this.co2Kg = co2Kg; }
|
||||||
|
|
||||||
|
public Double getCo2eKg() { return co2eKg; }
|
||||||
|
public void setCo2eKg(Double co2eKg) { this.co2eKg = co2eKg; }
|
||||||
|
|
||||||
|
public Double getNonCo2Multiplier() { return nonCo2Multiplier; }
|
||||||
|
public void setNonCo2Multiplier(Double nonCo2Multiplier) { this.nonCo2Multiplier = nonCo2Multiplier; }
|
||||||
|
|
||||||
|
public String getVendor() { return vendor; }
|
||||||
|
public void setVendor(String vendor) { this.vendor = vendor; }
|
||||||
|
|
||||||
|
public String getMethodName() { return methodName; }
|
||||||
|
public void setMethodName(String methodName) { this.methodName = methodName; }
|
||||||
|
|
||||||
|
public String getMethodVersion() { return methodVersion; }
|
||||||
|
public void setMethodVersion(String methodVersion) { this.methodVersion = methodVersion; }
|
||||||
|
|
||||||
|
public String getStandard() { return standard; }
|
||||||
|
public void setStandard(String standard) { this.standard = standard; }
|
||||||
|
|
||||||
|
public String getDocumentationUrl() { return documentationUrl; }
|
||||||
|
public void setDocumentationUrl(String documentationUrl) { this.documentationUrl = documentationUrl; }
|
||||||
|
|
||||||
|
public String getVendorRaw() { return vendorRaw; }
|
||||||
|
public void setVendorRaw(String vendorRaw) { this.vendorRaw = vendorRaw; }
|
||||||
|
}
|
||||||
30
src/de/addideas/api/emission/model/FlightLeg.java
Normal file
30
src/de/addideas/api/emission/model/FlightLeg.java
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package de.addideas.api.emission.model;
|
||||||
|
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
|
public class FlightLeg {
|
||||||
|
private String originIata;
|
||||||
|
private String destinationIata;
|
||||||
|
private ZonedDateTime departureTime;
|
||||||
|
private String marketingCarrier;
|
||||||
|
private String operatingCarrier;
|
||||||
|
private String flightNumber;
|
||||||
|
|
||||||
|
public String getOriginIata() { return originIata; }
|
||||||
|
public void setOriginIata(String originIata) { this.originIata = originIata; }
|
||||||
|
|
||||||
|
public String getDestinationIata() { return destinationIata; }
|
||||||
|
public void setDestinationIata(String destinationIata) { this.destinationIata = destinationIata; }
|
||||||
|
|
||||||
|
public ZonedDateTime getDepartureTime() { return departureTime; }
|
||||||
|
public void setDepartureTime(ZonedDateTime departureTime) { this.departureTime = departureTime; }
|
||||||
|
|
||||||
|
public String getMarketingCarrier() { return marketingCarrier; }
|
||||||
|
public void setMarketingCarrier(String marketingCarrier) { this.marketingCarrier = marketingCarrier; }
|
||||||
|
|
||||||
|
public String getOperatingCarrier() { return operatingCarrier; }
|
||||||
|
public void setOperatingCarrier(String operatingCarrier) { this.operatingCarrier = operatingCarrier; }
|
||||||
|
|
||||||
|
public String getFlightNumber() { return flightNumber; }
|
||||||
|
public void setFlightNumber(String flightNumber) { this.flightNumber = flightNumber; }
|
||||||
|
}
|
||||||
31
src/de/addideas/api/emission/model/FlightRequest.java
Normal file
31
src/de/addideas/api/emission/model/FlightRequest.java
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package de.addideas.api.emission.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FlightRequest {
|
||||||
|
private List<FlightLeg> legs = new ArrayList<>();
|
||||||
|
private String cabinClass = "economy";
|
||||||
|
private int passengers = 1;
|
||||||
|
private boolean roundtrip = false;
|
||||||
|
private boolean includeNonCo2 = true;
|
||||||
|
private String currency;
|
||||||
|
|
||||||
|
public List<FlightLeg> getLegs() { return legs; }
|
||||||
|
public void setLegs(List<FlightLeg> legs) { this.legs = legs; }
|
||||||
|
|
||||||
|
public String getCabinClass() { return cabinClass; }
|
||||||
|
public void setCabinClass(String cabinClass) { this.cabinClass = cabinClass; }
|
||||||
|
|
||||||
|
public int getPassengers() { return passengers; }
|
||||||
|
public void setPassengers(int passengers) { this.passengers = passengers; }
|
||||||
|
|
||||||
|
public boolean isRoundtrip() { return roundtrip; }
|
||||||
|
public void setRoundtrip(boolean roundtrip) { this.roundtrip = roundtrip; }
|
||||||
|
|
||||||
|
public boolean isIncludeNonCo2() { return includeNonCo2; }
|
||||||
|
public void setIncludeNonCo2(boolean includeNonCo2) { this.includeNonCo2 = includeNonCo2; }
|
||||||
|
|
||||||
|
public String getCurrency() { return currency; }
|
||||||
|
public void setCurrency(String currency) { this.currency = currency; }
|
||||||
|
}
|
||||||
12
src/de/addideas/api/emission/model/LegEstimate.java
Normal file
12
src/de/addideas/api/emission/model/LegEstimate.java
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package de.addideas.api.emission.model;
|
||||||
|
|
||||||
|
public class LegEstimate {
|
||||||
|
private Double co2Kg;
|
||||||
|
private Double distanceKm;
|
||||||
|
|
||||||
|
public Double getCo2Kg() { return co2Kg; }
|
||||||
|
public void setCo2Kg(Double co2Kg) { this.co2Kg = co2Kg; }
|
||||||
|
|
||||||
|
public Double getDistanceKm() { return distanceKm; }
|
||||||
|
public void setDistanceKm(Double distanceKm) { this.distanceKm = distanceKm; }
|
||||||
|
}
|
||||||
80
src/providers.xml
Normal file
80
src/providers.xml
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<providers xmlns="https://calco2la.to/schema/providers/v1">
|
||||||
|
|
||||||
|
<provider id="calco2lato">
|
||||||
|
<name>calco2la.to</name>
|
||||||
|
<baseUrl>https://api.calco2la.to</baseUrl>
|
||||||
|
|
||||||
|
<auth type="apiKey">
|
||||||
|
<header>Authorization</header>
|
||||||
|
<format>Bearer ${API_KEY}</format>
|
||||||
|
<envKey>CALCO2LATO_API_KEY</envKey>
|
||||||
|
</auth>
|
||||||
|
|
||||||
|
<operations>
|
||||||
|
<operation id="travel.flight.estimate_emissions">
|
||||||
|
<http method="POST" path="/latest/transport/flight"/>
|
||||||
|
<requestBody format="json">
|
||||||
|
{
|
||||||
|
"flights": [
|
||||||
|
#for leg in request.legs
|
||||||
|
{
|
||||||
|
"departure": "${leg.originIata}",
|
||||||
|
"arrival": "${leg.destinationIata}",
|
||||||
|
"travelClass": "${request.cabinClass}",
|
||||||
|
"passengerCount": ${request.passengers}
|
||||||
|
}#sep,
|
||||||
|
#end
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</requestBody>
|
||||||
|
<responseMapping mode="single" rootType="EmissionEstimate">
|
||||||
|
<!-- adjust source paths to whatever calco2la.to actually returns -->
|
||||||
|
<field source="$.co2WithoutRfi" target="EmissionEstimate.co2Kg"/>
|
||||||
|
<field source="$.co2" target="EmissionEstimate.co2eKg"/>
|
||||||
|
<list source="$.flights[*]" target="EmissionEstimate.legs" itemType="LegEstimate">
|
||||||
|
<field source="$.co2" target="LegEstimate.co2Kg"/>
|
||||||
|
<field source="$.distance" target="LegEstimate.distanceKm"/>
|
||||||
|
<!-- etc. -->
|
||||||
|
</list>
|
||||||
|
<!--
|
||||||
|
<field source="$.rf_factor" target="EmissionEstimate.nonCo2Multiplier"/>
|
||||||
|
<field source="$.method.name" target="EmissionEstimate.methodName"/>
|
||||||
|
<field source="$.method.version" target="EmissionEstimate.methodVersion"/>
|
||||||
|
<field source="$.standard" target="EmissionEstimate.standard"/>
|
||||||
|
<field source="$.documentation_url" target="EmissionEstimate.documentationUrl"/>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- store full raw JSON -->
|
||||||
|
<field source="$" target="EmissionEstimate.vendorRaw"/>
|
||||||
|
</responseMapping>
|
||||||
|
</operation>
|
||||||
|
|
||||||
|
<operation id="travel.airport.search">
|
||||||
|
<http method="POST" path="/latest/transport/airports"/>
|
||||||
|
|
||||||
|
<requestBody format="json">
|
||||||
|
{
|
||||||
|
"iata": "${request.query}",
|
||||||
|
"per_page": ${request.limit}
|
||||||
|
}
|
||||||
|
</requestBody>
|
||||||
|
|
||||||
|
<responseMapping mode="list" rootType="AirportInfo" rootSource="$.results[*]">
|
||||||
|
<field source="$.iata" target="AirportInfo.iataCode"/>
|
||||||
|
<field source="$.icao" target="AirportInfo.icaoCode"/>
|
||||||
|
<field source="$.name" target="AirportInfo.name"/>
|
||||||
|
<field source="$.city" target="AirportInfo.city"/>
|
||||||
|
<!-- <field source="$.country" target="AirportInfo.country"/> -->
|
||||||
|
<field source="$.lat" target="AirportInfo.latitude"/>
|
||||||
|
<field source="$.lon" target="AirportInfo.longitude"/>
|
||||||
|
|
||||||
|
<!-- store full raw JSON -->
|
||||||
|
<field source="$" target="AirportInfo.vendorRaw"/>
|
||||||
|
</responseMapping>
|
||||||
|
</operation>
|
||||||
|
|
||||||
|
</operations>
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
</providers>
|
||||||
Reference in New Issue
Block a user