From f9d92bc327a4cc2ef31b6a40501e99ca46e892f6 Mon Sep 17 00:00:00 2001 From: zemion Date: Mon, 11 May 2026 21:30:32 +0200 Subject: [PATCH] First working prototype --- .gitignore | 3 + README.md | 18 +- src/App.java | 97 +++++ .../addideas/api/emission/EmissionEngine.java | 368 ++++++++++++++++++ src/de/addideas/api/emission/EnvLoader.java | 27 ++ .../api/emission/ProvidersConfigLoader.java | 184 +++++++++ .../api/emission/SimpleTemplateEngine.java | 191 +++++++++ .../api/emission/descriptors/AuthConfig.java | 26 ++ .../emission/descriptors/FieldMapping.java | 20 + .../api/emission/descriptors/ListMapping.java | 29 ++ .../emission/descriptors/ModelRegistry.java | 37 ++ .../descriptors/OperationDescriptor.java | 29 ++ .../descriptors/ProviderDescriptor.java | 31 ++ .../descriptors/QueryParamMapping.java | 7 + .../descriptors/RequestBodyTemplate.java | 12 + .../emission/descriptors/ResponseMapping.java | 31 ++ .../emission/descriptors/ResponseValue.java | 42 ++ .../api/emission/model/AirportInfo.java | 37 ++ .../api/emission/model/AirportSearch.java | 12 + .../api/emission/model/EmissionEstimate.java | 48 +++ .../api/emission/model/FlightLeg.java | 30 ++ .../api/emission/model/FlightRequest.java | 31 ++ .../api/emission/model/LegEstimate.java | 12 + src/providers.xml | 80 ++++ 24 files changed, 1401 insertions(+), 1 deletion(-) create mode 100644 src/App.java create mode 100644 src/de/addideas/api/emission/EmissionEngine.java create mode 100644 src/de/addideas/api/emission/EnvLoader.java create mode 100644 src/de/addideas/api/emission/ProvidersConfigLoader.java create mode 100644 src/de/addideas/api/emission/SimpleTemplateEngine.java create mode 100644 src/de/addideas/api/emission/descriptors/AuthConfig.java create mode 100644 src/de/addideas/api/emission/descriptors/FieldMapping.java create mode 100644 src/de/addideas/api/emission/descriptors/ListMapping.java create mode 100644 src/de/addideas/api/emission/descriptors/ModelRegistry.java create mode 100644 src/de/addideas/api/emission/descriptors/OperationDescriptor.java create mode 100644 src/de/addideas/api/emission/descriptors/ProviderDescriptor.java create mode 100644 src/de/addideas/api/emission/descriptors/QueryParamMapping.java create mode 100644 src/de/addideas/api/emission/descriptors/RequestBodyTemplate.java create mode 100644 src/de/addideas/api/emission/descriptors/ResponseMapping.java create mode 100644 src/de/addideas/api/emission/descriptors/ResponseValue.java create mode 100644 src/de/addideas/api/emission/model/AirportInfo.java create mode 100644 src/de/addideas/api/emission/model/AirportSearch.java create mode 100644 src/de/addideas/api/emission/model/EmissionEstimate.java create mode 100644 src/de/addideas/api/emission/model/FlightLeg.java create mode 100644 src/de/addideas/api/emission/model/FlightRequest.java create mode 100644 src/de/addideas/api/emission/model/LegEstimate.java create mode 100644 src/providers.xml diff --git a/.gitignore b/.gitignore index d6fa46b..f499334 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # ---> Java # Compiled class file +bin *.class # Log file @@ -25,6 +26,7 @@ hs_err_pid* replay_pid* # ---> VisualStudioCode +.vscode .vscode/* !.vscode/settings.json !.vscode/tasks.json @@ -38,3 +40,4 @@ replay_pid* # Built Visual Studio Code Extensions *.vsix +.env \ No newline at end of file diff --git a/README.md b/README.md index b2a0809..7c03a53 100644 --- a/README.md +++ b/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). diff --git a/src/App.java b/src/App.java new file mode 100644 index 0000000..fada35f --- /dev/null +++ b/src/App.java @@ -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 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 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 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 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()); + } + } +} diff --git a/src/de/addideas/api/emission/EmissionEngine.java b/src/de/addideas/api/emission/EmissionEngine.java new file mode 100644 index 0000000..3d6a024 --- /dev/null +++ b/src/de/addideas/api/emission/EmissionEngine.java @@ -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 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 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 ResponseValue callOperation(String providerId, + String operationId, + Object request, + String apiKey, + Class 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 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 list = mapList(rm, json, resultClass); + return ResponseValue.list(list); + } + default -> throw new IllegalStateException("Unsupported response mode: " + rm.getMode()); + } + } + + private HttpResponse 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 estimateFlight(String providerId, FlightRequest request, String apiKey) throws Exception { + return callOperation(providerId, OP_FLIGHT_ESTIMATE, request, apiKey, EmissionEstimate.class); + } + + public ResponseValue 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 mapSingle(ResponseMapping rm, String json, Class 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 list = new ArrayList<>(); + List 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 List mapList(ResponseMapping rm, String json, Class expectedClass) throws Exception { + JsonNode root = objectMapper.readTree(json); + + String typeName = rm.getRootType(); + if (typeName == null || typeName.isEmpty()) { + typeName = modelRegistry.getNameForClass(expectedClass); + } + + List result = new ArrayList<>(); + List 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 list = new ArrayList<>(); + List 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 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 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; + } + +} diff --git a/src/de/addideas/api/emission/EnvLoader.java b/src/de/addideas/api/emission/EnvLoader.java new file mode 100644 index 0000000..2c8eaa5 --- /dev/null +++ b/src/de/addideas/api/emission/EnvLoader.java @@ -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 loadEnv(String filename) { + Map 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; + } +} \ No newline at end of file diff --git a/src/de/addideas/api/emission/ProvidersConfigLoader.java b/src/de/addideas/api/emission/ProvidersConfigLoader.java new file mode 100644 index 0000000..ae14e40 --- /dev/null +++ b/src/de/addideas/api/emission/ProvidersConfigLoader.java @@ -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 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 + 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 – 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 getProviders() { + return providers; + } + + public ProviderDescriptor getProvider(String id) { + return providers.get(id); + } +} diff --git a/src/de/addideas/api/emission/SimpleTemplateEngine.java b/src/de/addideas/api/emission/SimpleTemplateEngine.java new file mode 100644 index 0000000..2030647 --- /dev/null +++ b/src/de/addideas/api/emission/SimpleTemplateEngine.java @@ -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; + } +} diff --git a/src/de/addideas/api/emission/descriptors/AuthConfig.java b/src/de/addideas/api/emission/descriptors/AuthConfig.java new file mode 100644 index 0000000..0041437 --- /dev/null +++ b/src/de/addideas/api/emission/descriptors/AuthConfig.java @@ -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; } +} \ No newline at end of file diff --git a/src/de/addideas/api/emission/descriptors/FieldMapping.java b/src/de/addideas/api/emission/descriptors/FieldMapping.java new file mode 100644 index 0000000..f7b52c3 --- /dev/null +++ b/src/de/addideas/api/emission/descriptors/FieldMapping.java @@ -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; } +} diff --git a/src/de/addideas/api/emission/descriptors/ListMapping.java b/src/de/addideas/api/emission/descriptors/ListMapping.java new file mode 100644 index 0000000..5f908d7 --- /dev/null +++ b/src/de/addideas/api/emission/descriptors/ListMapping.java @@ -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 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 getItemFieldMappings() { return itemFieldMappings; } + public void addItemFieldMapping(FieldMapping fm) { itemFieldMappings.add(fm); } +} diff --git a/src/de/addideas/api/emission/descriptors/ModelRegistry.java b/src/de/addideas/api/emission/descriptors/ModelRegistry.java new file mode 100644 index 0000000..96cba55 --- /dev/null +++ b/src/de/addideas/api/emission/descriptors/ModelRegistry.java @@ -0,0 +1,37 @@ +package de.addideas.api.emission.descriptors; + +import java.util.HashMap; +import java.util.Map; + +public class ModelRegistry { + + private final Map> nameToClass = new HashMap<>(); + private final Map, String> classToName = new HashMap<>(); + + public void register(String name, Class 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 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); + } + } +} + diff --git a/src/de/addideas/api/emission/descriptors/OperationDescriptor.java b/src/de/addideas/api/emission/descriptors/OperationDescriptor.java new file mode 100644 index 0000000..1782af5 --- /dev/null +++ b/src/de/addideas/api/emission/descriptors/OperationDescriptor.java @@ -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 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 getQueryParams() { return queryParams; } + + public ResponseMapping getResponseMapping() { return responseMapping; } + public void setResponseMapping(ResponseMapping responseMapping) { this.responseMapping = responseMapping; } +} diff --git a/src/de/addideas/api/emission/descriptors/ProviderDescriptor.java b/src/de/addideas/api/emission/descriptors/ProviderDescriptor.java new file mode 100644 index 0000000..98beab0 --- /dev/null +++ b/src/de/addideas/api/emission/descriptors/ProviderDescriptor.java @@ -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 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 getOperations() { return operations; } + public void setOperations(Map operations) { this.operations = operations; } + + public OperationDescriptor getOperation(String opId) { + return operations.get(opId); + } +} \ No newline at end of file diff --git a/src/de/addideas/api/emission/descriptors/QueryParamMapping.java b/src/de/addideas/api/emission/descriptors/QueryParamMapping.java new file mode 100644 index 0000000..8b3f26a --- /dev/null +++ b/src/de/addideas/api/emission/descriptors/QueryParamMapping.java @@ -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... +} diff --git a/src/de/addideas/api/emission/descriptors/RequestBodyTemplate.java b/src/de/addideas/api/emission/descriptors/RequestBodyTemplate.java new file mode 100644 index 0000000..3c60a28 --- /dev/null +++ b/src/de/addideas/api/emission/descriptors/RequestBodyTemplate.java @@ -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; } +} \ No newline at end of file diff --git a/src/de/addideas/api/emission/descriptors/ResponseMapping.java b/src/de/addideas/api/emission/descriptors/ResponseMapping.java new file mode 100644 index 0000000..fd29f5a --- /dev/null +++ b/src/de/addideas/api/emission/descriptors/ResponseMapping.java @@ -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 fieldMappings = new ArrayList<>(); + private final List 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 getFieldMappings() { return fieldMappings; } + public void addFieldMapping(FieldMapping fm) { fieldMappings.add(fm); } + + public List getListMappings() { return listMappings; } + public void addListMapping(ListMapping lm) { listMappings.add(lm); } +} + diff --git a/src/de/addideas/api/emission/descriptors/ResponseValue.java b/src/de/addideas/api/emission/descriptors/ResponseValue.java new file mode 100644 index 0000000..d670000 --- /dev/null +++ b/src/de/addideas/api/emission/descriptors/ResponseValue.java @@ -0,0 +1,42 @@ +package de.addideas.api.emission.descriptors; + +import java.util.List; + +public class ResponseValue { + + private final T single; + private final List list; + private final boolean listMode; + + private ResponseValue(T single, List list) { + this.single = single; + this.list = list; + this.listMode = (single == null && list != null); + } + + public static ResponseValue single(T value) { + return new ResponseValue<>(value, null); + } + + public static ResponseValue list(List 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 asList() { + if (!listMode) { + throw new IllegalStateException("Response is a single value, not a list"); + } + return list; + } +} \ No newline at end of file diff --git a/src/de/addideas/api/emission/model/AirportInfo.java b/src/de/addideas/api/emission/model/AirportInfo.java new file mode 100644 index 0000000..ceba735 --- /dev/null +++ b/src/de/addideas/api/emission/model/AirportInfo.java @@ -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; } +} + diff --git a/src/de/addideas/api/emission/model/AirportSearch.java b/src/de/addideas/api/emission/model/AirportSearch.java new file mode 100644 index 0000000..a9d202d --- /dev/null +++ b/src/de/addideas/api/emission/model/AirportSearch.java @@ -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; } +} diff --git a/src/de/addideas/api/emission/model/EmissionEstimate.java b/src/de/addideas/api/emission/model/EmissionEstimate.java new file mode 100644 index 0000000..d3f42d3 --- /dev/null +++ b/src/de/addideas/api/emission/model/EmissionEstimate.java @@ -0,0 +1,48 @@ +package de.addideas.api.emission.model; + +import java.util.ArrayList; +import java.util.List; + +public class EmissionEstimate { + private List 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 getLegs() { return legs; } + public void setLegs(List 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; } +} diff --git a/src/de/addideas/api/emission/model/FlightLeg.java b/src/de/addideas/api/emission/model/FlightLeg.java new file mode 100644 index 0000000..870963e --- /dev/null +++ b/src/de/addideas/api/emission/model/FlightLeg.java @@ -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; } +} diff --git a/src/de/addideas/api/emission/model/FlightRequest.java b/src/de/addideas/api/emission/model/FlightRequest.java new file mode 100644 index 0000000..e525107 --- /dev/null +++ b/src/de/addideas/api/emission/model/FlightRequest.java @@ -0,0 +1,31 @@ +package de.addideas.api.emission.model; + +import java.util.ArrayList; +import java.util.List; + +public class FlightRequest { + private List legs = new ArrayList<>(); + private String cabinClass = "economy"; + private int passengers = 1; + private boolean roundtrip = false; + private boolean includeNonCo2 = true; + private String currency; + + public List getLegs() { return legs; } + public void setLegs(List 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; } +} diff --git a/src/de/addideas/api/emission/model/LegEstimate.java b/src/de/addideas/api/emission/model/LegEstimate.java new file mode 100644 index 0000000..2457901 --- /dev/null +++ b/src/de/addideas/api/emission/model/LegEstimate.java @@ -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; } +} diff --git a/src/providers.xml b/src/providers.xml new file mode 100644 index 0000000..298d6e6 --- /dev/null +++ b/src/providers.xml @@ -0,0 +1,80 @@ + + + + + calco2la.to + https://api.calco2la.to + + +
Authorization
+ Bearer ${API_KEY} + CALCO2LATO_API_KEY +
+ + + + + + { + "flights": [ + #for leg in request.legs + { + "departure": "${leg.originIata}", + "arrival": "${leg.destinationIata}", + "travelClass": "${request.cabinClass}", + "passengerCount": ${request.passengers} + }#sep, + #end + ] + } + + + + + + + + + + + + + + + + + + + + + + { + "iata": "${request.query}", + "per_page": ${request.limit} + } + + + + + + + + + + + + + + + + + +
+ +
\ No newline at end of file