commit 0bda9455ca84aa61480cc513f1458eb40e3ef3b7 Author: Radek Davidek Date: Wed Oct 15 13:42:12 2025 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9081e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +target +bin +.settings +.classpath +.project +*.log +.idea +.vscode \ No newline at end of file diff --git a/client-truststore.jks b/client-truststore.jks new file mode 100644 index 0000000..9fe4b89 Binary files /dev/null and b/client-truststore.jks differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b5c2e7c --- /dev/null +++ b/pom.xml @@ -0,0 +1,93 @@ + + 4.0.0 + cz.trask.migration + apicurio-migration-tool + jar + 1.0.0 + apicurio-migration-tool + http://maven.apache.org + + + 11 + ${java.version} + ${java.version} + UTF-8 + + + + + + io.apicurio + apicurio-registry-client + 2.6.13.Final + + + org.apache.logging.log4j + log4j-core + 2.24.2 + + + com.google.code.gson + gson + 2.13.1 + + + org.yaml + snakeyaml + 2.4 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + + shade + + + false + + + cz.trask.apioperator.ApiSync + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.1 + + + + cz.trask.apioperator.ApiSync + + + + + + + + + + wso2.releases + WSO2 internal Repository + https://maven.wso2.org/nexus/content/repositories/releases/ + + true + daily + ignore + + + + \ No newline at end of file diff --git a/src/main/java/cz/trask/migration/AbstractProcess.java b/src/main/java/cz/trask/migration/AbstractProcess.java new file mode 100644 index 0000000..fa459e0 --- /dev/null +++ b/src/main/java/cz/trask/migration/AbstractProcess.java @@ -0,0 +1,388 @@ +package cz.trask.migration; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; + +import javax.net.ssl.HttpsURLConnection; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.yaml.snakeyaml.Yaml; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import cz.trask.migration.config.ConfigManager; +import cz.trask.migration.model.APIList; +import cz.trask.migration.model.HttpResponse; +import cz.trask.migration.model.RegisterResponse; +import cz.trask.migration.model.TokenResponse; + +public abstract class AbstractProcess { + + private static Logger log = LogManager.getLogger(AbstractProcess.class); + + protected Gson gson; + + protected Yaml yaml; + + protected ConfigManager config = ConfigManager.getInstance(); + + protected AbstractProcess() { + + gson = new GsonBuilder().create(); + + yaml = new Yaml(); + + setTrustStoreCredentials(); + + javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(new javax.net.ssl.HostnameVerifier() { + public boolean verify(String hostname, javax.net.ssl.SSLSession sslSession) { + return true; + } + }); + } + + protected String getTrustStorePath() { + String path = config.getTruststorePath(); + if (!new File(path).canRead()) { + path = System.getProperty("user.dir") + File.separatorChar + config.getTruststorePath(); + if (!new File(path).canRead()) { + return null; + } + } + return path; + } + + protected void setTrustStoreCredentials() { + log.info(getTrustStorePath()); + System.setProperty("javax.net.ssl.trustStore", getTrustStorePath()); + System.setProperty("javax.net.ssl.trustStorePassword", config.getTruststorePassword()); + } + + /** + * Retrieve access token based on clientId and clientSecret + * + * @param reg - client application object with clientId and clientSecret + * @param scope - requested OAuth2 scope + * @throws Exception + */ + protected TokenResponse getToken(String publisherurl, String wso2User, RegisterResponse reg, String scope) + throws Exception { + + byte[] decoded = Base64.getDecoder().decode(wso2User); + String decodedstring = new String(decoded); + String[] decodedstringparts = decodedstring.split(":"); + + String username = decodedstringparts[0]; + String password = decodedstringparts[1]; + + log.debug("Getting token with Username: '" + wso2User + "' URL: " + publisherurl); + + Map httpHeaders = new HashMap<>(); + + httpHeaders.put("Authorization", "Basic ".concat(Base64.getEncoder() + .encodeToString(reg.getClientId().concat(":").concat(reg.getClientSecret()).getBytes()))); + httpHeaders.put("Content-Type", "application/x-www-form-urlencoded"); + + String data = "grant_type=password&username=".concat(username).concat("&password=") + .concat(URLEncoder.encode(password, "UTF-8")).concat("&scope=").concat(scope); + HttpResponse response = makeDataRequest(publisherurl, httpHeaders, data); + + log.debug("Token response: HTTP Code " + response.getResponseCode() + " Json: " + response.getResponse()); + + TokenResponse resp = gson.fromJson(response.getResponse(), TokenResponse.class); + return resp; + } + + /** + * Register client application to get clientId and clientSecret + * + * @throws Exception + */ + protected RegisterResponse register(String publisherurl, String wso2User) throws Exception { + + log.debug("Registering with Username: '" + wso2User + "' URL: " + publisherurl); + + byte[] decodedUserBytes = Base64.getDecoder().decode(wso2User); + String decodeduserappkey = new String(decodedUserBytes); + String[] decodeduserparts = decodeduserappkey.split(":"); + String decodeduser = decodeduserparts[0]; + + Map httpHeaders = new HashMap<>(); + + httpHeaders.put("Authorization", "Basic ".concat(wso2User)); + httpHeaders.put("Content-Type", "application/json"); + + String data = "{\"callbackUrl\": \"www.google.lk\",\"clientName\": \"rest_api_publisher" + decodeduser + + "\",\"owner\": \"" + decodeduser + "\",\"grantType\": \"password refresh_token\",\"saasApp\": true}"; + HttpResponse response = makeDataRequest(publisherurl, httpHeaders, data); + + log.debug( + "Register API response: HTTP Code " + response.getResponseCode() + " Json: " + response.getResponse()); + + RegisterResponse resp = gson.fromJson(response.getResponse(), RegisterResponse.class); + return resp; + } + + /** + * Common function used for http request + * + * @param method - http method + * @param urlStr - url to dev poral + * @param httpHeaders + * @param params - currently is not used + * @throws Exception + */ + protected HttpResponse makeRequest(String method, String urlStr, Map httpHeaders, + Map params) throws Exception { + return makeRequest(method, urlStr, httpHeaders, params, false); + } + + /** + * Common function used for http request + * + * @param method - http method + * @param urlStr - url to dev poral + * @param httpHeaders + * @param data - request data + * @param binary - binary or text mode + * @throws Exception + */ + protected HttpResponse makeRequest(String method, String urlStr, Map httpHeaders, + Map params, boolean binary) throws Exception { + + log.info("Calling URL: " + urlStr); + String query = ""; + for (String key : params.keySet()) { + query = query.concat(URLEncoder.encode(key, "UTF-8")).concat("=") + .concat(URLEncoder.encode(params.get(key), "UTF-8")).concat("&"); + } + + if (query.length() > 1 && "GET".equals(method)) { + urlStr = urlStr.concat("?").concat(query); + } + + URL url = new URL(urlStr); + + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod(method); + con.setDoInput(true); + + for (String key : httpHeaders.keySet()) { + con.addRequestProperty(key, httpHeaders.get(key)); + } + + if (query.length() > 1 && "POST".equals(method)) { + con.setDoOutput(true); + OutputStream out = con.getOutputStream(); + out.write(query.getBytes("UTF-8")); + } + + InputStream in = con.getInputStream(); + + String res = ""; + byte[] buf = new byte[4096]; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + int read = in.read(buf); + + while (read != -1) { + if (binary) + baos.write(buf, 0, read); + else + res = res.concat(new String(buf, 0, read)); + + read = in.read(buf); + } + + baos.flush(); + + HttpResponse resp = new HttpResponse(); + resp.setHeaders(con.getHeaderFields()); + if (binary) + resp.setResponseBytes(baos.toByteArray()); + else + resp.setResponse(res); + resp.setResponseCode(con.getResponseCode()); + + con.disconnect(); + + log.info("Response code: " + resp.getResponseCode()); + // log.info("Response: " + resp.getResponse()); + + return resp; + } + + /** + * Common function used for http request + * + * @param urlStr - url to dev poral + * @param httpHeaders + * @param data - request data + * @throws Exception + */ + protected HttpResponse makeDataRequest(String urlStr, Map httpHeaders, String data) + throws Exception { + + byte[] json = data.getBytes(Charset.forName("UTF-8")); + + URL url = new URL(urlStr); + + HttpsURLConnection con = (HttpsURLConnection) url.openConnection(); + con.setRequestMethod("POST"); + con.setDoInput(true); + con.setDoOutput(true); + + for (String key : httpHeaders.keySet()) { + con.addRequestProperty(key, httpHeaders.get(key)); + } + + con.addRequestProperty("Content-Length", "" + json.length); + + OutputStream out = con.getOutputStream(); + out.write(json); + + InputStream in = con.getInputStream(); + + String res = ""; + byte[] buf = new byte[4096]; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + int read = in.read(buf); + + while (read != -1) { + res = res.concat(new String(buf, 0, read)); + read = in.read(buf); + } + + baos.flush(); + + HttpResponse resp = new HttpResponse(); + resp.setHeaders(con.getHeaderFields()); + resp.setResponse(res); + resp.setResponseCode(con.getResponseCode()); + + return resp; + } + + /** + * Retrieve the list of APIs by name. + * + * @param tokenResponse - WSO2 APIM access token + * @throws Exception + */ + protected APIList getList(String publisherurl, TokenResponse tokenResponse) throws Exception { + + APIList listOfApis = null; + + try { + String url = publisherurl.concat(String.format("/apis?limit=9999&offset=0")); + + log.debug("Getting APIs with token: '" + tokenResponse.getAccess_token() + "' URL: " + url); + + Map httpHeaders = new HashMap<>(); + Map params = new HashMap<>(); + + httpHeaders.put("Authorization", "Bearer ".concat(tokenResponse.getAccess_token())); + + HttpResponse response = makeRequest("GET", url, httpHeaders, params); + + log.debug("Listing APIs: HTTP Code " + response.getResponseCode() + " Data: " + response.getResponse()); + + listOfApis = gson.fromJson(response.getResponse(), APIList.class); + + if (response.getResponseCode() != 200) + log.error("Cannot list API. Something bad happened."); + } catch (Exception e) { + log.error("Cannot list API:" + e); + throw new Exception("Cannot list API:" + e.getMessage()); + } + return listOfApis; + } + + /** + * Common function used for upload API + * + * @param urlStr - url to dev poral + * @param httpHeaders + * @param params - currently is not used + * @param api - zip file to upload + * @throws Exception + */ + protected static HttpResponse makeFileRequest(String method, String urlStr, Map httpHeaders, + byte[] buff, String attachmentFileName) throws Exception { + + if (buff == null) { + log.error("Cannot send NULL payload to rest service."); + } + + String crlf = "\r\n"; + + String twoHyphens = "--"; + String boundary = "----" + System.currentTimeMillis() + "----"; + + URL url = new URL(urlStr); + + HttpsURLConnection con = (HttpsURLConnection) url.openConnection(); + con.setUseCaches(false); + con.setDoOutput(true); + + con.setRequestMethod(method); + con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + con.setRequestProperty("User-Agent", "curl/7.55.1"); + con.setRequestProperty("Accept", "*/*"); + + for (String key : httpHeaders.keySet()) { + con.addRequestProperty(key, httpHeaders.get(key)); + } + + DataOutputStream request = new DataOutputStream(con.getOutputStream()); + request.writeBytes(crlf); + request.writeBytes(twoHyphens + boundary + crlf); + request.writeBytes( + "Content-Disposition: form-data; name=\"file\"; filename=\"" + attachmentFileName + "\"" + crlf); + request.writeBytes("Content-Type: application/octet-stream" + crlf); + request.writeBytes("Content-Transfer-Encoding: binary" + crlf); + request.writeBytes(crlf); + request.write(buff); + request.writeBytes(crlf); + request.writeBytes(twoHyphens + boundary + twoHyphens + crlf); + request.flush(); + request.close(); + + String res = ""; + byte[] buf = new byte[4096]; + + HttpResponse resp = new HttpResponse(); + InputStream in; + int responseCode = con.getResponseCode(); + + if (responseCode == 200 || responseCode == 201) { + in = con.getInputStream(); + } else { + in = con.getErrorStream(); + } + + while (in.available() > 0) { + int read = in.read(buf); + res = res.concat(new String(buf, 0, read)); + } + + resp.setHeaders(con.getHeaderFields()); + resp.setResponse(res); + resp.setResponseCode(responseCode); + + return resp; + } +} diff --git a/src/main/java/cz/trask/migration/ApiSync.java b/src/main/java/cz/trask/migration/ApiSync.java new file mode 100644 index 0000000..842a99e --- /dev/null +++ b/src/main/java/cz/trask/migration/ApiSync.java @@ -0,0 +1,45 @@ +package cz.trask.migration; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import cz.trask.migration.impl.ExportToWso2; +import cz.trask.migration.impl.ImportToApicurio; +import cz.trask.migration.model.StartParameters; + +public class ApiSync { + + private static Logger log = LogManager.getLogger(ApiSync.class); + + protected final static String resourceName = "api-operator.properties"; + + public static void main(String[] args) throws Exception { + log.info("API Operator started."); + if (args == null) { + log.error("No parameters found."); + printHelp(); + } else { + String commandLine = String.join("", args).trim(); + StartParameters sp = StartParameters.parse(commandLine); + log.info("Parsed parameters: " + sp); + + if (sp.getCommand().equals("import")) { + log.info("Import command selected."); + 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(); + } + } + } + + private static void printHelp() { + System.out.println("Not enough parameters.\nRun command in this format:\njava -jar shaded.jar [import/export]"); + } +} diff --git a/src/main/java/cz/trask/migration/config/ConfigManager.java b/src/main/java/cz/trask/migration/config/ConfigManager.java new file mode 100644 index 0000000..4aae903 --- /dev/null +++ b/src/main/java/cz/trask/migration/config/ConfigManager.java @@ -0,0 +1,242 @@ +package cz.trask.migration.config; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Properties; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class ConfigManager { + + /* -------------------------------------------------------------------- */ + /* LOGIC & CONSTANTS */ + /* -------------------------------------------------------------------- */ + + private static final Logger log = LogManager.getLogger(ConfigManager.class); + + private static final String PROPERTY_FILENAME = "api-operator.properties"; + + // SOURCE + public static final String PROP_SOURCE_REGISTRATION_API_URL = "SOURCE_REGISTRATION_API_URL"; + public static final String PROP_SOURCE_PUBLISHER_API_URL = "SOURCE_PUBLISHER_API_URL"; + public static final String PROP_SOURCE_DEVPORTAL_API_URL = "SOURCE_DEVPORTAL_API_URL"; + public static final String PROP_SOURCE_PUBLISHER_TOKEN_URL = "SOURCE_PUBLISHER_TOKEN_URL"; + public static final String PROP_SOURCE_WSO2_USER = "SOURCE_WSO2_USER"; + + // TARGET + public static final String PROP_TARGET_REGISTRATION_API_URL = "TARGET_REGISTRATION_API_URL"; + public static final String PROP_TARGET_PUBLISHER_API_URL = "TARGET_PUBLISHER_API_URL"; + public static final String PROP_TARGET_DEVPORTAL_API_URL = "TARGET_DEVPORTAL_API_URL"; + public static final String PROP_TARGET_PUBLISHER_TOKEN_URL = "TARGET_PUBLISHER_TOKEN_URL"; + public static final String PROP_TARGET_WSO2_USER = "TARGET_WSO2_USER"; + + // TRUSTSTORE + public static final String PROP_TRUSTSTORE_PATH = "TRUSTSTORE_PATH"; + public static final String PROP_TRUSTSTORE_PASSWORD = "TRUSTSTORE_PASSWORD"; + + // URL PATTERNS + public static final String PROP_PUBLISHER_URL_PATTERN = "PUBLISHER_URL_PATTERN"; + public static final String PROP_DEVPORTAL_URL_PATTERN = "DEVPORTAL_URL_PATTERN"; + + // APICURIO & GROUP + public static final String PROP_APICURIO_API_URL = "APICURIO_API_URL"; + public static final String PROP_DEFAULT_API_GROUP = "DEFAULT_API_GROUP"; + + // MAX THREADS + public static final String PROP_MAX_THREADS = "MAX_THREADS"; + + /* -------------------------------------------------------------------- */ + /* CONFIGURATION FIELDS */ + /* -------------------------------------------------------------------- */ + + private final String sourceRegistrationApiUrl; + private final String sourcePublisherApiUrl; + private final String sourceDevportalApiUrl; + private final String sourcePublisherTokenUrl; + private final String sourceWso2User; + + private final String targetRegistrationApiUrl; + private final String targetPublisherApiUrl; + private final String targetDevportalApiUrl; + private final String targetPublisherTokenUrl; + private final String targetWso2User; + + private final String truststorePath; + private final String truststorePassword; + + private final String publisherUrlPattern; + private final String devportalUrlPattern; + + private final String apicurioApiUrl; + private final String defaultApiGroup; + + private final int maxThreads; + + private static volatile ConfigManager INSTANCE; + + /** Všechny načtené hodnoty. */ + private final Properties props = new Properties(); + + /* -------------------------------------------------------------------- */ + /* SINGLETON – lazy‑initialization‑on-demand holder */ + /* -------------------------------------------------------------------- */ + + private ConfigManager() { + + log.info("Loading property file '{}'", PROPERTY_FILENAME); + + InputStream in = null; + try { + File extFile = new File(PROPERTY_FILENAME); + if (extFile.exists() && extFile.isFile()) { + log.info("External property file found: {}", extFile.getAbsolutePath()); + in = Files.newInputStream(extFile.toPath()); + } else { + log.debug("External property file not found."); + } + } catch (Exception e) { + log.warn("Error while accessing external property file", e); + } + + if (in == null) { + try { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + in = cl.getResourceAsStream(PROPERTY_FILENAME); + if (in != null) { + log.info("Internal property file found on classpath."); + } else { + log.warn("Property file '{}' not found on classpath.", PROPERTY_FILENAME); + } + } catch (Exception e) { + log.error("Unable to load property file from classpath", e); + } + } + + try (InputStream input = in) { + if (input != null) { + props.load(input); + log.info("Property file loaded successfully."); + } else { + log.warn("No property source available; proceeding with defaults where possible."); + } + } catch (Exception e) { + log.error("Cannot load property file.", e); + } + + sourceRegistrationApiUrl = getRequired(PROP_SOURCE_REGISTRATION_API_URL); + sourcePublisherApiUrl = getRequired(PROP_SOURCE_PUBLISHER_API_URL); + sourceDevportalApiUrl = getRequired(PROP_SOURCE_DEVPORTAL_API_URL); + sourcePublisherTokenUrl = getRequired(PROP_SOURCE_PUBLISHER_TOKEN_URL); + sourceWso2User = getRequired(PROP_SOURCE_WSO2_USER); + + targetRegistrationApiUrl = getRequired(PROP_TARGET_REGISTRATION_API_URL); + targetPublisherApiUrl = getRequired(PROP_TARGET_PUBLISHER_API_URL); + targetDevportalApiUrl = getRequired(PROP_TARGET_DEVPORTAL_API_URL); + targetPublisherTokenUrl = getRequired(PROP_TARGET_PUBLISHER_TOKEN_URL); + targetWso2User = getRequired(PROP_TARGET_WSO2_USER); + + truststorePath = getRequired(PROP_TRUSTSTORE_PATH); + truststorePassword = getRequired(PROP_TRUSTSTORE_PASSWORD); + + publisherUrlPattern = getRequired(PROP_PUBLISHER_URL_PATTERN); + devportalUrlPattern = getRequired(PROP_DEVPORTAL_URL_PATTERN); + + apicurioApiUrl = getRequired(PROP_APICURIO_API_URL); + defaultApiGroup = getRequired(PROP_DEFAULT_API_GROUP); + + maxThreads = Integer.parseInt(props.getProperty(PROP_MAX_THREADS, "10")); + } + + public static ConfigManager getInstance() { + if (INSTANCE == null) { + synchronized (ConfigManager.class) { + if (INSTANCE == null) { + INSTANCE = new ConfigManager(); + } + } + } + return INSTANCE; + } + + private String getRequired(String key) { + String value = props.getProperty(key); + if (value == null) { + throw new IllegalStateException("Missing required property: " + key); + } + return value.trim(); + } + + /* -------------------------------------------------------------------- */ + /* PUBLIC GETTERS */ + /* -------------------------------------------------------------------- */ + + public String getSourceRegistrationApiUrl() { + return sourceRegistrationApiUrl; + } + + public String getSourcePublisherApiUrl() { + return sourcePublisherApiUrl; + } + + public String getSourceDevportalApiUrl() { + return sourceDevportalApiUrl; + } + + public String getSourcePublisherTokenUrl() { + return sourcePublisherTokenUrl; + } + + public String getSourceWso2User() { + return sourceWso2User; + } + + public String getTargetRegistrationApiUrl() { + return targetRegistrationApiUrl; + } + + public String getTargetPublisherApiUrl() { + return targetPublisherApiUrl; + } + + public String getTargetDevportalApiUrl() { + return targetDevportalApiUrl; + } + + public String getTargetPublisherTokenUrl() { + return targetPublisherTokenUrl; + } + + public String getTargetWso2User() { + return targetWso2User; + } + + public String getTruststorePath() { + return truststorePath; + } + + public String getTruststorePassword() { + return truststorePassword; + } + + public String getPublisherUrlPattern() { + return publisherUrlPattern; + } + + public String getDevportalUrlPattern() { + return devportalUrlPattern; + } + + public String getApicurioApiUrl() { + return apicurioApiUrl; + } + + public String getDefaultApiGroup() { + return defaultApiGroup; + } + + public int getMaxThreads() { + return maxThreads; + } +} diff --git a/src/main/java/cz/trask/migration/impl/ExportToWso2.java b/src/main/java/cz/trask/migration/impl/ExportToWso2.java new file mode 100644 index 0000000..2036369 --- /dev/null +++ b/src/main/java/cz/trask/migration/impl/ExportToWso2.java @@ -0,0 +1,152 @@ +package cz.trask.migration.impl; + +import java.util.HashMap; +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 cz.trask.migration.AbstractProcess; +import cz.trask.migration.model.HttpResponse; +import cz.trask.migration.model.RegisterResponse; +import cz.trask.migration.model.TokenResponse; +import cz.trask.migration.utils.ZipUtils; +import io.apicurio.registry.rest.client.RegistryClient; +import io.apicurio.registry.rest.client.RegistryClientFactory; +import io.apicurio.registry.rest.v2.beans.ArtifactReference; +import io.apicurio.registry.rest.v2.beans.ArtifactSearchResults; +import io.apicurio.registry.rest.v2.beans.SearchedArtifact; +import io.apicurio.registry.rest.v2.beans.SearchedVersion; +import io.apicurio.registry.rest.v2.beans.VersionSearchResults; + +public class ExportToWso2 extends AbstractProcess { + + private static final Logger log = LogManager.getLogger(ExportToWso2.class); + + private final AtomicInteger apiCounter = new AtomicInteger(1); + + private final RegistryClient client; + + public ExportToWso2() 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 export to WSO2 from Apicurio..."); + + 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()); + + ArtifactSearchResults apis = client.searchArtifacts(config.getDefaultApiGroup(), null, null, null, null, + null, null, null, null); + + log.info("Found {} APIs", apis.getCount()); + + int maxThreads = config.getMaxThreads(); + ExecutorService executor = Executors.newFixedThreadPool(maxThreads); + + for (SearchedArtifact api : apis.getArtifacts()) { + 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); + } + } + + private void processApi(SearchedArtifact api, TokenResponse tokenResponse, int index, int total) { + long start = System.currentTimeMillis(); + + try { + log.info("Processing API {} of {}", index, total); + + VersionSearchResults versions = client.listArtifactVersions(config.getDefaultApiGroup(), api.getId(), null, + null); + + for (SearchedVersion ver : versions.getVersions()) { + log.info(" - Found version: {}", ver.getVersion()); + List ref = client.getArtifactReferencesByCoordinates(config.getDefaultApiGroup(), + api.getId(), ver.getVersion()); + if (ref != null && !ref.isEmpty()) { + log.info("Artifact has {} references", ref.size()); + byte[] data = ZipUtils.prepareApiZipFile(client, ver, ref); + String fileName = api.getName() + "-" + ver.getVersion() + ".zip"; + if (data != null && data.length > 0 && fileName != null && !fileName.isEmpty()) { + int responseCode = publishApiToWso2(fileName, data, tokenResponse); + if (responseCode == 200 || responseCode == 201) { + log.info(" - API version {} imported successfully", ver.getVersion()); + } else { + log.warn(" - API version {} import failed with response code {}", ver.getVersion(), + responseCode); + } + } + } + } + long end = System.currentTimeMillis(); + log.info("Finished processing API {} of {} in {} ms", index, total, (end - start)); + } catch (Exception e) { + log.error("IO error while importing API {}: {}", api.getId(), e.getMessage(), e); + } + } + + /* --------------------------------------------------------------------- */ + /* Helper methods */ + /* --------------------------------------------------------------------- */ + + private int publishApiToWso2(String fileName, byte[] data, TokenResponse tokenResponse) { + int responseCode = -1; + try { + String url = config.getTargetPublisherApiUrl() + .concat(String.format("?preserveProvider=false&overwrite=true")); + + log.info("API Import URL: " + url); + + Map httpHeaders = new HashMap<>(); + + httpHeaders.put("Authorization", "Bearer " + tokenResponse.getAccess_token()); + + HttpResponse response = makeFileRequest("POST", url, httpHeaders, data, fileName); + + responseCode = response.getResponseCode(); + + if (response.getResponseCode() != 201 && response.getResponseCode() != 200) { + log.info("Cannot import API file: " + fileName + ", response code: " + response.getResponseCode()); + } + } catch (Exception e) { + log.error("IO error while importing API file: " + fileName + ", error: " + e.getMessage(), e); + } + return responseCode; + } +} diff --git a/src/main/java/cz/trask/migration/impl/ImportToApicurio.java b/src/main/java/cz/trask/migration/impl/ImportToApicurio.java new file mode 100644 index 0000000..ce4124b --- /dev/null +++ b/src/main/java/cz/trask/migration/impl/ImportToApicurio.java @@ -0,0 +1,357 @@ +package cz.trask.migration.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.migration.AbstractProcess; +import cz.trask.migration.model.APIInfo; +import cz.trask.migration.model.APIList; +import cz.trask.migration.model.FileType; +import cz.trask.migration.model.HttpResponse; +import cz.trask.migration.model.RegisterResponse; +import cz.trask.migration.model.TokenResponse; +import cz.trask.migration.model.ZipEntryData; +import cz.trask.migration.utils.ZipUtils; +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 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 = ZipUtils.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/migration/model/APIInfo.java b/src/main/java/cz/trask/migration/model/APIInfo.java new file mode 100644 index 0000000..026a92b --- /dev/null +++ b/src/main/java/cz/trask/migration/model/APIInfo.java @@ -0,0 +1,92 @@ +package cz.trask.migration.model; + +public class APIInfo { + + private String id; + private String name; + private String description; + private String context; + private String version; + private String provider; + private String lifeCycleStatus; + private String thumbnailUri; + private String type; + + 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 getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getContext() { + return context; + } + + public void setContext(String context) { + this.context = context; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public String getLifeCycleStatus() { + return lifeCycleStatus; + } + + public void setLifeCycleStatus(String lifeCycleStatus) { + this.lifeCycleStatus = lifeCycleStatus; + } + + public String getThumbnailUri() { + return thumbnailUri; + } + + public void setThumbnailUri(String thumbnailUri) { + this.thumbnailUri = thumbnailUri; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public String toString() { + return id + "\t" + name + "\t" + version + "\t" + provider + "\t" + lifeCycleStatus + "\t" + description; + } + +} diff --git a/src/main/java/cz/trask/migration/model/APIList.java b/src/main/java/cz/trask/migration/model/APIList.java new file mode 100644 index 0000000..a11fa66 --- /dev/null +++ b/src/main/java/cz/trask/migration/model/APIList.java @@ -0,0 +1,31 @@ +package cz.trask.migration.model; + +public class APIList { + + // "count": 102, "next": "", "previous": "", "list": [ + + private int count; + private APIInfo[] list = null; + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + public APIInfo[] getList() { + return list; + } + + public void setList(APIInfo[] list) { + this.list = list; + } + + @Override + public String toString() { + return count + "\t" + list; + } + +} diff --git a/src/main/java/cz/trask/migration/model/ApiDefinition.java b/src/main/java/cz/trask/migration/model/ApiDefinition.java new file mode 100644 index 0000000..4710b48 --- /dev/null +++ b/src/main/java/cz/trask/migration/model/ApiDefinition.java @@ -0,0 +1,96 @@ +package cz.trask.migration.model; + +import java.util.List; +import java.util.Map; + +public class ApiDefinition { + private String id; + private String name; + private Object description; + private String context; + private String version; + private String provider; + private String apiDefinition; + private Object wsdlUri; + private String lifeCycleStatus; + private boolean isDefaultVersion; + private String type; + private List transport; + private List operations; + private String authorizationHeader; + private List securityScheme; + private List tags; + private List tiers; + private boolean hasThumbnail; + private Map additionalProperties; + private Monetization monetization; + private List ingressURLs; + private List endpointURLs; + private BusinessInformation businessInformation; + private List labels; + private List environmentList; + private List scopes; + private String avgRating; + private AdvertiseInfo advertiseInfo; + private boolean isSubscriptionAvailable; + private List categories; + private List keyManagers; + + // Getters and Setters + + public static class Tier { + private String tierName; + private String tierPlan; + private Object monetizationAttributes; + + // Getters and Setters + } + + public static class EndpointURLs { + private String environmentName; + private String environmentType; + private URLs URLs; + private DefaultVersionURLs defaultVersionURLs; + + public static class URLs { + private String http; + private String https; + private Object ws; + private Object wss; + + // Getters and Setters + } + + public static class DefaultVersionURLs { + private Object http; + private Object https; + private Object ws; + private Object wss; + + // Getters and Setters + } + } + + public static class Monetization { + private boolean enabled; + + // Getters and Setters + } + + public static class BusinessInformation { + private String businessOwner; + private String businessOwnerEmail; + private String technicalOwner; + private String technicalOwnerEmail; + + // Getters and Setters + } + + public static class AdvertiseInfo { + private boolean advertised; + private String originalStoreUrl; + private String apiOwner; + + // Getters and Setters + } +} diff --git a/src/main/java/cz/trask/migration/model/EventAPIInfo.java b/src/main/java/cz/trask/migration/model/EventAPIInfo.java new file mode 100644 index 0000000..23de51c --- /dev/null +++ b/src/main/java/cz/trask/migration/model/EventAPIInfo.java @@ -0,0 +1,122 @@ +package cz.trask.migration.model; + +public class EventAPIInfo { + + String apiName; + String apiId; + String uuid; + String apiVersion; + String apiContext; + String apiProvider; + String apiType; + String apiStatus; + String eventId; + String timeStamp; + String type; + String tenantId; + String tenantDomain; + + public String getApiName() { + return apiName; + } + + public void setApiName(String apiName) { + this.apiName = apiName; + } + + public String getApiId() { + return apiId; + } + + public void setApiId(String apiId) { + this.apiId = apiId; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getApiVersion() { + return apiVersion; + } + + public void setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + } + + public String getApiContext() { + return apiContext; + } + + public void setApiContext(String apiContext) { + this.apiContext = apiContext; + } + + public String getApiProvider() { + return apiProvider; + } + + public void setApiProvider(String apiProvider) { + this.apiProvider = apiProvider; + } + + public String getApiType() { + return apiType; + } + + public void setApiType(String apiType) { + this.apiType = apiType; + } + + public String getApiStatus() { + return apiStatus; + } + + public void setApiStatus(String apiStatus) { + this.apiStatus = apiStatus; + } + + public String getEventId() { + return eventId; + } + + public void setEventId(String eventId) { + this.eventId = eventId; + } + + public String getTimeStamp() { + return timeStamp; + } + + public void setTimeStamp(String timeStamp) { + this.timeStamp = timeStamp; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getTenantDomain() { + return tenantDomain; + } + + public void setTenantDomain(String tenantDomain) { + this.tenantDomain = tenantDomain; + } +} diff --git a/src/main/java/cz/trask/migration/model/FileType.java b/src/main/java/cz/trask/migration/model/FileType.java new file mode 100644 index 0000000..92e2822 --- /dev/null +++ b/src/main/java/cz/trask/migration/model/FileType.java @@ -0,0 +1,5 @@ +package cz.trask.migration.model; + +public enum FileType { + APIDEF, OPENAPI, WSDL, POLICY, UNKNOWN +} diff --git a/src/main/java/cz/trask/migration/model/HttpResponse.java b/src/main/java/cz/trask/migration/model/HttpResponse.java new file mode 100644 index 0000000..a8a64bc --- /dev/null +++ b/src/main/java/cz/trask/migration/model/HttpResponse.java @@ -0,0 +1,49 @@ +package cz.trask.migration.model; + +import java.util.List; +import java.util.Map; + +public class HttpResponse { + + String response; + Map> headers; + int responseCode; + byte[] responseBytes; + + public String getResponse() { + return response; + } + + public void setResponse(String response) { + this.response = response; + } + + public Map> getHeaders() { + return headers; + } + + public void setHeaders(Map> headers) { + this.headers = headers; + } + + public int getResponseCode() { + return responseCode; + } + + public void setResponseCode(int responseCode) { + this.responseCode = responseCode; + } + + public List getResponseHeader(String key) { + return headers.get(key); + } + + public void setResponseBytes(byte[] byteArray) { + responseBytes = byteArray; + } + + public byte[] getResponseBytes() { + return responseBytes; + } + +} diff --git a/src/main/java/cz/trask/migration/model/RegisterResponse.java b/src/main/java/cz/trask/migration/model/RegisterResponse.java new file mode 100644 index 0000000..dadd293 --- /dev/null +++ b/src/main/java/cz/trask/migration/model/RegisterResponse.java @@ -0,0 +1,62 @@ +package cz.trask.migration.model; + +public class RegisterResponse { + + // {"clientId":"QRGgaoQpiRfd6K8KZfU2hZsXoqAa","clientName":"admin_rest_api_publisher","callBackURL":"www.google.lk","clientSecret":"5o_ncm_br0wr8yOXc_ouXOWfOWsa","isSaasApplication":true,"appOwner":"admin","jsonString":"{\"grant_types\":\"password + // refresh_token\",\"redirect_uris\":\"www.google.lk\",\"client_name\":\"admin_rest_api_publisher\"}","jsonAppAttribute":"{}","tokenType":null} + + private String clientId; + private String clientName; + private String callBackURL; + private String clientSecret; + private boolean isSaasApplication; + private String appOwner; + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientName() { + return clientName; + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } + + public String getCallBackURL() { + return callBackURL; + } + + public void setCallBackURL(String callBackURL) { + this.callBackURL = callBackURL; + } + + public String getClientSecret() { + return clientSecret; + } + + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + + public boolean isSaasApplication() { + return isSaasApplication; + } + + public void setSaasApplication(boolean isSaasApplication) { + this.isSaasApplication = isSaasApplication; + } + + public String getAppOwner() { + return appOwner; + } + + public void setAppOwner(String appOwner) { + this.appOwner = appOwner; + } +} diff --git a/src/main/java/cz/trask/migration/model/StartParameters.java b/src/main/java/cz/trask/migration/model/StartParameters.java new file mode 100644 index 0000000..2e9cda7 --- /dev/null +++ b/src/main/java/cz/trask/migration/model/StartParameters.java @@ -0,0 +1,35 @@ +package cz.trask.migration.model; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class StartParameters { + + private static Logger log = LogManager.getLogger(StartParameters.class); + + String command; + + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command.toLowerCase(); + } + + public static StartParameters parse(String commandLine) throws Exception { + StartParameters res = new StartParameters(); + + try { + String[] rawParams = commandLine.split("-"); + res.setCommand(rawParams[0]); + + } catch (Exception e) { + log.error("Error while parsing startup parameters.", e); + throw e; + } + + return res; + } + +} diff --git a/src/main/java/cz/trask/migration/model/TokenResponse.java b/src/main/java/cz/trask/migration/model/TokenResponse.java new file mode 100644 index 0000000..0f61c42 --- /dev/null +++ b/src/main/java/cz/trask/migration/model/TokenResponse.java @@ -0,0 +1,53 @@ +package cz.trask.migration.model; + +public class TokenResponse { + + // {"access_token":"6be1aa81-d0ae-321e-9e3e-2ea89dd9f847","refresh_token":"5cff194d-cb16-3dc2-ab60-3c44ba25ae4a","scope":"apim:api_view","token_type":"Bearer","expires_in":3600} + + private String access_token; + private String refresh_token; + private String scope; + private String token_type; + private long expires_in; + + public String getAccess_token() { + return access_token; + } + + public void setAccess_token(String access_token) { + this.access_token = access_token; + } + + public String getRefresh_token() { + return refresh_token; + } + + public void setRefresh_token(String refresh_token) { + this.refresh_token = refresh_token; + } + + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public String getToken_type() { + return token_type; + } + + public void setToken_type(String token_type) { + this.token_type = token_type; + } + + public long getExpires_in() { + return expires_in; + } + + public void setExpires_in(long expires_in) { + this.expires_in = expires_in; + } + +} diff --git a/src/main/java/cz/trask/migration/model/ZipEntryData.java b/src/main/java/cz/trask/migration/model/ZipEntryData.java new file mode 100644 index 0000000..6a4e8f3 --- /dev/null +++ b/src/main/java/cz/trask/migration/model/ZipEntryData.java @@ -0,0 +1,43 @@ +package cz.trask.migration.model; + +public class ZipEntryData { + private String name; + private FileType type; + private byte[] content; + + public ZipEntryData(String name, FileType type, byte[] content) { + this.name = name; + this.type = type; + this.content = content; + } + + // Gettery a settery + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public FileType getType() { + return type; + } + + public void setType(FileType type) { + this.type = type; + } + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } + + @Override + public String toString() { + return "ZipEntryData{name='" + name + "', type=" + type + "}"; + } +} diff --git a/src/main/java/cz/trask/migration/utils/ZipUtils.java b/src/main/java/cz/trask/migration/utils/ZipUtils.java new file mode 100644 index 0000000..bc874cf --- /dev/null +++ b/src/main/java/cz/trask/migration/utils/ZipUtils.java @@ -0,0 +1,110 @@ +package cz.trask.migration.utils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import cz.trask.migration.model.FileType; +import cz.trask.migration.model.ZipEntryData; +import io.apicurio.registry.rest.client.RegistryClient; +import io.apicurio.registry.rest.v2.beans.ArtifactMetaData; +import io.apicurio.registry.rest.v2.beans.ArtifactReference; +import io.apicurio.registry.rest.v2.beans.SearchedVersion; + +public class ZipUtils { + + private static final Logger log = LogManager.getLogger(ZipUtils.class); + + public static List extractFilesFromZip(byte[] data) throws IOException { + List fileList = new ArrayList<>(); + + try (ByteArrayInputStream fis = new ByteArrayInputStream(data); ZipInputStream zis = new ZipInputStream(fis)) { + + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + // Ignoruj adresáře + if (!entry.isDirectory()) { + String name = entry.getName().substring(entry.getName().lastIndexOf('/') + 1); + FileType type = determineFileType(entry.getName()); + byte[] content = readAllBytes(zis); + + fileList.add(new ZipEntryData(name, type, content)); + } + zis.closeEntry(); + } + } + + return fileList; + } + + private static FileType determineFileType(String fileName) { + String lowerFileName = fileName.toLowerCase(); + if (lowerFileName.endsWith("api.yaml")) { + return FileType.APIDEF; + } else if (lowerFileName.contains("/definitions/swagger.yaml")) { + return FileType.OPENAPI; + } else if (lowerFileName.endsWith(".wsdl")) { + return FileType.WSDL; + } else if (lowerFileName.contains("/policies/")) { + return FileType.POLICY; + } + return FileType.UNKNOWN; + } + + private static byte[] readAllBytes(ZipInputStream zis) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + byte[] data = new byte[1024]; + while ((nRead = zis.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + buffer.flush(); + return buffer.toByteArray(); + } + + public static byte[] prepareApiZipFile(RegistryClient client, SearchedVersion ver, List ref) + throws IOException { + + String baseDir = ver.getName() + "-" + ver.getVersion() + "/"; + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ZipOutputStream zos = new ZipOutputStream(baos)) { + + for (ArtifactReference r : ref) { + log.info(" - Reference: {} {} {}", r.getGroupId(), r.getArtifactId(), r.getVersion()); + + ArtifactMetaData amd = client.getArtifactMetaData(r.getGroupId(), r.getArtifactId()); + + String subDir = ""; + + if (FileType.OPENAPI.toString().equals(amd.getGroupId())) { + subDir = "Definitions/"; + } else if (FileType.POLICY.toString().equals(amd.getGroupId())) { + subDir = "Policies/"; + } + + String fileName = baseDir + subDir + r.getName(); + log.info(" - Adding file: {}", fileName); + + zos.putNextEntry(new ZipEntry(fileName)); + + try (InputStream is = client.getContentByGlobalId(amd.getGlobalId())) { + zos.write(is.readAllBytes()); + } + + zos.closeEntry(); + } + zos.finish(); + return baos.toByteArray(); + } + } +} diff --git a/src/main/resources/api-operator.properties b/src/main/resources/api-operator.properties new file mode 100644 index 0000000..52333ff --- /dev/null +++ b/src/main/resources/api-operator.properties @@ -0,0 +1,23 @@ +SOURCE_REGISTRATION_API_URL = https://localhost:9443/client-registration/v0.17/register +SOURCE_PUBLISHER_API_URL = https://localhost:9443/api/am/publisher +SOURCE_DEVPORTAL_API_URL = https://localhost:9443/api/am/devportal +SOURCE_PUBLISHER_TOKEN_URL = https://localhost:9443/oauth2/token +SOURCE_WSO2_USER = YWRtaW46YWRtaW4= + +TARGET_REGISTRATION_API_URL = https://localhost:9443/client-registration/v0.17/register +TARGET_PUBLISHER_API_URL = https://localhost:9443/api/am/publisher/v3/apis/import +TARGET_DEVPORTAL_API_URL = https://localhost:9443/api/am/devportal +TARGET_PUBLISHER_TOKEN_URL = https://localhost:9443/oauth2/token +TARGET_WSO2_USER = YWRtaW46YWRtaW4= + +TRUSTSTORE_PATH = client-truststore.jks +TRUSTSTORE_PASSWORD = wso2carbon + +PUBLISHER_URL_PATTERN = https://api-developers.dev.koop.appl.services/publisher/apis/{API_ID}/overview +DEVPORTAL_URL_PATTERN = https://api-developers.dev.koop.appl.services/devportal/apis/{API_ID}/overview + +#APICURIO_API_URL = http://10.0.0.190:8080/apis/registry/v2 +APICURIO_API_URL = http://apicurio:8095/apis/registry/v2 +DEFAULT_API_GROUP = api + +MAX_THREADS = 1 \ No newline at end of file diff --git a/src/main/resources/log4j2.properties b/src/main/resources/log4j2.properties new file mode 100644 index 0000000..e2c56a7 --- /dev/null +++ b/src/main/resources/log4j2.properties @@ -0,0 +1,28 @@ +rootLogger.level = info +rootLogger.appenderRefs = console, rolling +rootLogger.appenderRef.stdout.ref = STDOUT +rootLogger.appenderRef.file.ref = RollingFileAppender + +logger.cz-trask.name=cz.trask +logger.cz-trask.level = debug +logger.cz-trask.additivity=false +logger.cz-trask.appenderRefs = rolling, console +logger.cz-trask.appenderRef.file.ref = RollingFileAppender +logger.cz-trask.appenderRef.stdout.ref = STDOUT + +appender.console.type = Console +appender.console.name = STDOUT +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d [%t] %-5level %logger{36} - %msg%n + +appender.rolling.type = RollingFile +appender.rolling.name = RollingFileAppender +appender.rolling.fileName = api-operator.log +appender.rolling.filePattern = api-operator-%d{yyyy-MM-dd}.%i.log +appender.rolling.layout.type = PatternLayout +appender.rolling.layout.pattern = %d [%t] %-5level %logger{36} - %msg%n +appender.rolling.policies.type = Policies +appender.rolling.policies.size.type = SizeBasedTriggeringPolicy +appender.rolling.policies.size.size = 50MB +appender.rolling.policies.time.type = TimeBasedTriggeringPolicy +appender.rolling.policies.time.interval = 1