diff --git a/src/main/java/cz/trask/apioperator/ApiSync.java b/src/main/java/cz/trask/apioperator/ApiSync.java index 350efd6..53025af 100644 --- a/src/main/java/cz/trask/apioperator/ApiSync.java +++ b/src/main/java/cz/trask/apioperator/ApiSync.java @@ -3,7 +3,8 @@ package cz.trask.apioperator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import cz.trask.apioperator.impl.Import; +import cz.trask.apioperator.impl.ExportToWso2; +import cz.trask.apioperator.impl.ImportToApicurio; import cz.trask.apioperator.model.StartParameters; public class ApiSync { @@ -22,10 +23,18 @@ public class ApiSync { StartParameters sp = StartParameters.parse(commandLine); log.info("Parsed parameters: " + sp); - if (sp.getCommand().equalsIgnoreCase("import")) { + if (sp.getCommand().equals("import")) { log.info("Import command selected."); - Import imp = new Import(); + ImportToApicurio imp = new ImportToApicurio(); imp.process(); + } else if (sp.getCommand().equals("export")) { + log.info("Export command selected."); + ExportToWso2 exp = new ExportToWso2(); + exp.process(); + log.error("Export command not implemented yet."); + } else { + log.error("Unknown command: " + sp.getCommand()); + printHelp(); } } } diff --git a/src/main/java/cz/trask/apioperator/impl/Import.java b/src/main/java/cz/trask/apioperator/impl/ExportToWso2.java similarity index 98% rename from src/main/java/cz/trask/apioperator/impl/Import.java rename to src/main/java/cz/trask/apioperator/impl/ExportToWso2.java index 957de05..d585aef 100644 --- a/src/main/java/cz/trask/apioperator/impl/Import.java +++ b/src/main/java/cz/trask/apioperator/impl/ExportToWso2.java @@ -39,16 +39,16 @@ import io.apicurio.registry.rest.v2.beans.Rule; import io.apicurio.registry.rest.v2.beans.VersionSearchResults; import io.apicurio.registry.types.RuleType; -public class Import extends AbstractProcess { +public class ExportToWso2 extends AbstractProcess { - private static final Logger log = LogManager.getLogger(Import.class); + private static final Logger log = LogManager.getLogger(ExportToWso2.class); private final AtomicInteger apiCounter = new AtomicInteger(1); private final Gson gson = new Gson(); private final RegistryClient client; - public Import() throws Exception { + public ExportToWso2() throws Exception { this.client = RegistryClientFactory.create(config.getApicurioApiUrl()); } @@ -59,7 +59,7 @@ public class Import extends AbstractProcess { */ public void process() { try { - log.info("Starting API export…"); + log.info("Starting API export to WSO2 from Apicurio..."); RegisterResponse register = register(config.getSourceRegistrationApiUrl(), config.getSourceWso2User()); diff --git a/src/main/java/cz/trask/apioperator/impl/ImportToApicurio.java b/src/main/java/cz/trask/apioperator/impl/ImportToApicurio.java new file mode 100644 index 0000000..a8fb14c --- /dev/null +++ b/src/main/java/cz/trask/apioperator/impl/ImportToApicurio.java @@ -0,0 +1,358 @@ +package cz.trask.apioperator.impl; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; + +import cz.trask.apioperator.AbstractProcess; +import cz.trask.apioperator.model.APIInfo; +import cz.trask.apioperator.model.APIList; +import cz.trask.apioperator.model.FileType; +import cz.trask.apioperator.model.HttpResponse; +import cz.trask.apioperator.model.RegisterResponse; +import cz.trask.apioperator.model.TokenResponse; +import cz.trask.apioperator.model.ZipEntryData; +import cz.trask.apioperator.utils.ZipExtractor; +import io.apicurio.registry.rest.client.RegistryClient; +import io.apicurio.registry.rest.client.RegistryClientFactory; +import io.apicurio.registry.rest.client.exception.VersionAlreadyExistsException; +import io.apicurio.registry.rest.v2.beans.ArtifactMetaData; +import io.apicurio.registry.rest.v2.beans.ArtifactReference; +import io.apicurio.registry.rest.v2.beans.EditableMetaData; +import io.apicurio.registry.rest.v2.beans.Rule; +import io.apicurio.registry.rest.v2.beans.VersionSearchResults; +import io.apicurio.registry.types.RuleType; + +public class ImportToApicurio extends AbstractProcess { + + private static final Logger log = LogManager.getLogger(ImportToApicurio.class); + + private final AtomicInteger apiCounter = new AtomicInteger(1); + private final Gson gson = new Gson(); + + private final RegistryClient client; + + public ImportToApicurio() throws Exception { + this.client = RegistryClientFactory.create(config.getApicurioApiUrl()); + } + + /** + * Main entry point for the import process. + * + * @throws RuntimeException if any error occurs + */ + public void process() { + try { + log.info("Starting API import to Apicurio from WSO2..."); + + RegisterResponse register = register(config.getSourceRegistrationApiUrl(), config.getSourceWso2User()); + + String clientId = register.getClientId(); + log.info("Registered with clientId: {}", clientId); + + TokenResponse token = getToken(config.getSourcePublisherTokenUrl(), config.getSourceWso2User(), register, + "apim:api_view apim:api_create apim:api_manage apim:api_delete apim:api_publish " + + "apim:subscription_view apim:subscription_block apim:subscription_manage apim:external_services_discover " + + "apim:threat_protection_policy_create apim:threat_protection_policy_manage apim:document_create apim:document_manage " + + "apim:mediation_policy_view apim:mediation_policy_create apim:mediation_policy_manage apim:client_certificates_view " + + "apim:client_certificates_add apim:client_certificates_update apim:ep_certificates_view apim:ep_certificates_add " + + "apim:ep_certificates_update apim:publisher_settings apim:pub_alert_manage apim:shared_scope_manage apim:app_import_export " + + "apim:api_import_export apim:api_product_import_export apim:api_generate_key apim:common_operation_policy_view " + + "apim:common_operation_policy_manage apim:comment_write apim:comment_view apim:admin"); + + log.debug("Access token received – {}", token.getAccess_token()); + + APIList apis = getList(config.getSourcePublisherApiUrl(), token); + if (apis == null || apis.getList() == null || apis.getList().length == 0) { + throw new IllegalStateException( + "No APIs to export that match your criteria! Check the name of the API you want to export."); + } + + log.info("Found {} APIs", apis.getCount()); + + int maxThreads = config.getMaxThreads(); + ExecutorService executor = Executors.newFixedThreadPool(maxThreads); + + for (APIInfo api : apis.getList()) { + final int index = apiCounter.getAndIncrement(); + executor.submit(() -> processApi(api, token, index, apis.getCount())); + } + + executor.shutdown(); + if (!executor.awaitTermination(10, TimeUnit.MINUTES)) { + log.warn("Timeout waiting for API import tasks to finish"); + } + log.info("Finished processing APIs."); + } catch (Exception e) { + log.error("Error while exporting APIs.", e); + throw new RuntimeException("Export failed", e); + } + } + + /** + * Process a single API – fetches the data, creates or updates the corresponding + * artifact in Apicurio. + */ + private void processApi(APIInfo api, TokenResponse tokenResponse, int index, int total) { + long start = System.currentTimeMillis(); + String status = api.getLifeCycleStatus(); + + if (!status.contains("PUBLISHED") && !status.contains("DEPRECATED")) { + log.info("Skipping API {} of {} – not published (ID={})", index, total, api.getId()); + return; + } + + try { + log.info("Processing API {} of {}", index, total); + + Map httpHeaders = Collections.singletonMap("Authorization", + "Bearer " + tokenResponse.getAccess_token()); + + // 1) Retrieve basic information + HttpResponse apiInfoResp = makeRequest("GET", config.getSourceDevportalApiUrl() + "/apis/" + api.getId(), + httpHeaders, Collections.emptyMap()); + + HttpResponse subsResp = makeRequest("GET", + config.getSourcePublisherApiUrl() + "/subscriptions?apiId=" + api.getId(), httpHeaders, + Collections.emptyMap()); + + // 2) Export the API as a zip + HttpResponse exportedZip = makeRequest("GET", + config.getSourcePublisherApiUrl() + "/apis/export?apiId=" + api.getId(), httpHeaders, + Collections.emptyMap(), true); + + List zipEntries = ZipExtractor.extractFilesFromZip(exportedZip.getResponseBytes()); + + String swagger = null; + + for (ZipEntryData e : zipEntries) { + if (e.getType().toString().equals(FileType.OPENAPI.toString())) { + log.debug("Found main API definition file: {}", e.getName()); + swagger = new String(e.getContent()); + break; + } + } + + // 3) Deserialize JSON responses + TypeToken> mapType = new TypeToken<>() { + }; + Map apiMap = gson.fromJson(apiInfoResp.getResponse(), mapType.getType()); + Map subsMap = gson.fromJson(subsResp.getResponse(), mapType.getType()); + + @SuppressWarnings("unchecked") + List tagsList = (List) apiMap.get("tags"); + + // 4) Build the properties map + Map props = new LinkedHashMap<>(); + props.put("version", api.getVersion()); + props.put("status", status); + addSubscriptionsToProps(props, subsMap); + addEndpointsToProps(props, apiMap); + addTagsToProps(props, tagsList); + + // 5) Build the description that contains the publisher & devportal URLs + String baseDesc = api.getDescription() != null ? api.getDescription() : ""; + String pubUrl = config.getPublisherUrlPattern().replace("{API_ID}", api.getId()); + String devPortUrl = config.getDevportalUrlPattern().replace("{API_ID}", api.getId()); + + String fullDesc = baseDesc + " ***** PUBLISHER URL ***** " + pubUrl + " ***** DEVPORTAL URL ***** " + + devPortUrl; + + // 6) Update the swagger with the description and servers + Map swaggerMap = yaml.load(swagger); + JsonObject swaggerObj = gson.toJsonTree(swaggerMap).getAsJsonObject(); + updateSwagger(swaggerObj, apiMap, fullDesc); + + // 7) Prepare artifact creation/update + String group = config.getDefaultApiGroup(); + String mainArtifactId = api.getName() + api.getContext(); + + VersionSearchResults existingArtifacts; + try { + existingArtifacts = client.listArtifactVersions(group, mainArtifactId, 0, Integer.MAX_VALUE); + } catch (Exception e) { + log.debug("No API {} exists – will create it", api.getContext()); + existingArtifacts = null; + } + + if (existingArtifacts == null) { + // Create new artifact + List references = createReferencesFromZip(zipEntries, group, api); + + ArtifactMetaData meta = client.createArtifact(group, mainArtifactId, api.getVersion(), null, null, null, + api.getName(), fullDesc, null, null, null, + new ByteArrayInputStream(swaggerObj.toString().getBytes()), references); + + setMetaAndRules(meta, props, tagsList); + // Create the three required rules + createRule(meta, "NONE", RuleType.COMPATIBILITY); + createRule(meta, "NONE", RuleType.VALIDITY); + createRule(meta, "NONE", RuleType.INTEGRITY); + + } else { + // Artifact exists – check if the version exists + boolean versionExists = false; + try { + client.getArtifactVersionMetaData(group, mainArtifactId, api.getVersion()); + versionExists = true; + } catch (Exception e) { + // Version missing – will create it below + } + + List references = createReferencesFromZip(zipEntries, group, api); + + if (!versionExists) { + ArtifactMetaData meta = client.updateArtifact(group, mainArtifactId, api.getVersion(), + api.getName(), fullDesc, new ByteArrayInputStream(swaggerObj.toString().getBytes()), + references); + setMetaAndRules(meta, props, tagsList); + } else { + // Version already exists – no action needed + log.warn("API {} with version {} already exists. Skipping import.", api.getContext(), + api.getVersion()); + } + } + + log.info("Successfully imported API '{}' ({}). Took {} ms", api.getName(), api.getVersion(), + System.currentTimeMillis() - start); + } catch (IOException e) { + log.error("IO error while importing API {}: {}", api.getId(), e.getMessage(), e); + } catch (VersionAlreadyExistsException e) { + log.warn("API version already exists for {}: {}. Skipping.", api.getName(), api.getVersion()); + } catch (Exception e) { + log.error("Cannot export API '{}':{}", api.getName(), api.getVersion(), e); + } + } + + /* --------------------------------------------------------------------- */ + /* Helper methods */ + /* --------------------------------------------------------------------- */ + + private void updateSwagger(JsonObject swagger, Map apiMap, String description) { + JsonObject info = swagger.getAsJsonObject("info"); + if (info != null) { + info.addProperty("description", description); + } + + // Build servers array + JsonArray servers = new JsonArray(); + @SuppressWarnings("unchecked") + List> endpoints = (List>) apiMap.get("endpointURLs"); + if (endpoints != null) { + for (Map env : endpoints) { + @SuppressWarnings("unchecked") + Map urls = (Map) env.get("URLs"); + if (urls == null || urls.isEmpty()) + continue; + + JsonObject server = new JsonObject(); + urls.forEach((k, v) -> { + if (v != null && !v.isBlank()) { + if (k.equals("https")) { + server.addProperty("url", v); + } else if (k.equals("wss")) { + server.addProperty("url", v); + } + } + }); + server.addProperty("description", "Gateway: " + env.getOrDefault("environmentName", "")); + servers.add(server); + } + } + + swagger.remove("servers"); + swagger.add("servers", servers); + } + + private void addSubscriptionsToProps(Map props, Map subsMap) { + if (subsMap == null || !subsMap.containsKey("list")) + return; + @SuppressWarnings("unchecked") + List> list = (List>) subsMap.get("list"); + int i = 1; + for (Map sub : list) { + @SuppressWarnings("unchecked") + Map appInfo = (Map) sub.get("applicationInfo"); + if (appInfo == null) + continue; + props.put("subscription" + i, + appInfo.getOrDefault("name", "") + " (Owner: " + appInfo.getOrDefault("subscriber", "") + ")"); + i++; + } + } + + private void addEndpointsToProps(Map props, Map apiMap) { + if (apiMap == null || !apiMap.containsKey("endpointURLs")) + return; + @SuppressWarnings("unchecked") + List> envs = (List>) apiMap.get("endpointURLs"); + for (Map env : envs) { + @SuppressWarnings("unchecked") + Map urls = (Map) env.get("URLs"); + if (urls == null) + continue; + urls.forEach((k, v) -> props.put(k + " Endpoint", v)); + } + } + + private void addTagsToProps(Map props, List tags) { + if (tags != null && !tags.isEmpty()) { + props.put("tags", String.join(", ", tags)); + } + } + + private List createReferencesFromZip(List zipEntries, String group, APIInfo api) + throws IOException { + + List references = new ArrayList<>(); + for (ZipEntryData entry : zipEntries) { + String artifactId = api.getName() + "/" + api.getVersion() + "/" + entry.getName(); + + // Create the artifact (versioned) + try (ByteArrayInputStream is = new ByteArrayInputStream(entry.getContent())) { + client.createArtifactWithVersion(entry.getType().toString(), artifactId, api.getVersion(), is); + } + + ArtifactReference ref = new ArtifactReference(); + ref.setName(entry.getName()); + ref.setGroupId(entry.getType().toString()); + ref.setArtifactId(artifactId); + ref.setVersion(api.getVersion()); + references.add(ref); + } + return references; + } + + private void setMetaAndRules(ArtifactMetaData meta, Map props, List tags) { + EditableMetaData metaData = new EditableMetaData(); + metaData.setName(meta.getName()); + metaData.setDescription(meta.getDescription()); + metaData.setProperties(props); + metaData.setLabels(tags); + + client.updateArtifactMetaData(meta.getGroupId(), meta.getId(), metaData); + } + + private void createRule(ArtifactMetaData meta, String config, RuleType type) { + Rule rule = new Rule(); + rule.setConfig(config); + rule.setType(type); + client.createArtifactRule(meta.getGroupId(), meta.getId(), rule); + } +} diff --git a/src/main/java/cz/trask/apioperator/model/StartParameters.java b/src/main/java/cz/trask/apioperator/model/StartParameters.java index caa065e..7286210 100644 --- a/src/main/java/cz/trask/apioperator/model/StartParameters.java +++ b/src/main/java/cz/trask/apioperator/model/StartParameters.java @@ -14,7 +14,7 @@ public class StartParameters { } public void setCommand(String command) { - this.command = command; + this.command = command.toLowerCase(); } public static StartParameters parse(String commandLine) throws Exception {