JakartaTransformer.java

package io.swagger.v3.plugin.maven.jakarta;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URI;
import java.nio.file.CopyOption;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * Transforms swagger-maven-plugin jar file to be used with Jakarta namespace using artifactId swagger-maven-plugin-jakarta
 *
 */
public class JakartaTransformer {

    private static final Logger LOGGER = LoggerFactory.getLogger(JakartaTransformer.class);


    /*
     *  Substitution tokens + regex
     */
    private static Set<String> files =
            Stream.of(
                    // "META-INF/MANIFEST.MF",
                    "META-INF/maven/plugin.xml",
                    "META-INF/maven/io.swagger.core.v3/swagger-maven-plugin/plugin-help.xml",
                    // "META-INF/plexus/components.xml",
                    "META-INF/maven/io.swagger.core.v3/swagger-maven-plugin/pom.xml",
                    "META-INF/maven/io.swagger.core.v3/swagger-maven-plugin/pom.properties")
                    .collect(Collectors.toCollection(HashSet::new));

    private static final String jakartaXmlBindDep = "<dependency>\n" +
            "                <groupId>jakarta.xml.bind</groupId>\n" +
            "                <artifactId>jakarta.xml.bind-api</artifactId>\n" +
            "                <version>VERSION</version>\n" +
            "            </dependency>\n";

    private static final String jakartaXmlBindRegex = "^.*(<dependency>.*jakarta\\.xml\\.bind((?!</dependency>).)*</dependency>).*$";

    private static final String jakartaValidationDep = "<dependency>\n" +
            "                <groupId>jakarta.validation</groupId>\n" +
            "                <artifactId>jakarta.validation-api</artifactId>\n" +
            "                <version>VERSION</version>\n" +
            "            </dependency>\n";

    private static final String jakartaValidationRegex = "^.*(<dependency>.*jakarta\\.validation((?!</dependency>).)*</dependency>).*$";

    private static final String jacksonJsonDep = "<dependency>\n" +
            "                <groupId>com.fasterxml.jackson.jakarta.rs</groupId>\n" +
            "                <artifactId>jackson-jakarta-rs-json-provider</artifactId>\n" +
            "                <version>VERSION</version>\n" +
            "                <exclusions>\n" +
            "                    <exclusion>\n" +
            "                        <groupId>jakarta.activation</groupId>\n" +
            "                        <artifactId>jakarta.activation-api</artifactId>\n" +
            "                    </exclusion>\n" +
            "                    <exclusion>\n" +
            "                        <groupId>jakarta.xml.bind</groupId>\n" +
            "                        <artifactId>jakarta.xml.bind-api</artifactId>\n" +
            "                    </exclusion>\n" +
            "                </exclusions>\n" +
            "            </dependency>";

    private static final String jacksonJsonRegex = "^.*(<dependency>.*jackson\\-jaxrs\\-json((?!</dependency>).)*</dependency>).*$";

    private static final String jacksonBaseDep = "<dependency>\n" +
            "                <groupId>com.fasterxml.jackson.jakarta.rs</groupId>\n" +
            "                <artifactId>jackson-jakarta-rs-base</artifactId>\n" +
            "                <version>VERSION</version>\n" +
            "            </dependency>";

    private static final String jacksonBaseRegex = "^.*(<dependency>.*jackson\\-jaxrs\\-base((?!</dependency>).)*</dependency>).*$";

    private static final String jacksonJaxbDep = "<dependency>\n" +
            "                <groupId>com.fasterxml.jackson.module</groupId>\n" +
            "                <artifactId>jackson-module-jakarta-xmlbind-annotations</artifactId>\n" +
            "                <version>VERSION</version>\n" +
            "                <exclusions>\n" +
            "                    <exclusion>\n" +
            "                        <groupId>jakarta.activation</groupId>\n" +
            "                        <artifactId>jakarta.activation-api</artifactId>\n" +
            "                    </exclusion>\n" +
            "                    <exclusion>\n" +
            "                        <groupId>jakarta.xml.bind</groupId>\n" +
            "                        <artifactId>jakarta.xml.bind-api</artifactId>\n" +
            "                    </exclusion>\n" +
            "                    <exclusion>\n" +
            "                        <groupId>com.sun.activation</groupId>\n" +
            "                        <artifactId>jakarta.activation</artifactId>\n" +
            "                    </exclusion>" +
            "                </exclusions>\n" +
            "            </dependency>";

    private static final String jacksonJaxbRegex = "^.*(<dependency>.*jackson\\-module\\-jaxb((?!</dependency>).)*</dependency>).*$";

    private static final String jakartaActivationDep = "<dependency>\n" +
            "                <groupId>jakarta.activation</groupId>\n" +
            "                <artifactId>jakarta.activation-api</artifactId>\n" +
            "                <version>VERSION</version>\n" +
            "            </dependency>";

    private static final String jakartaActivationRegex = "^.*(<dependency>.*jakarta\\.activation((?!</dependency>).)*</dependency>).*$";

    /**
     *
     * Extracts Jar into temp directory
     *
     */
    private static void extract(String inPath, String outPath) throws Exception {

        Path target = Paths.get(outPath);
        Path source = Paths.get(inPath);

        try (ZipInputStream zis = new ZipInputStream(new FileInputStream(source.toFile()))) {
            ZipEntry zipEntry = zis.getNextEntry();

            while (zipEntry != null) {
                Path path = target.resolve(zipEntry.getName()).normalize();

                if (!path.startsWith(target + File.separator)) {
                    throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
                }

                if (zipEntry.isDirectory()) {
                    if (!Files.isDirectory(path)) {
                        Files.createDirectories(path);
                    }
                } else {
                    // fix for Windows-created archives
                    Path parent = path.getParent();
                    if (!Files.isDirectory(parent)) {
                        Files.createDirectories(parent);
                    }

                    // write file content
                    try (OutputStream os = Files.newOutputStream(path)) {
                        byte[] buffer = new byte[1024];
                        int len;
                        while ((len = zis.read(buffer)) > 0) {
                            os.write(buffer, 0, len);
                        }
                    }
                }
                zipEntry = zis.getNextEntry();
            }
            zis.closeEntry();
        }
    }

    /**
     *
     * Compress temp directory back into Jar after transformed
     *
     */
    public static void jar(Path sourcePath, String outJarPath) throws IOException {

        URI uri = URI.create("jar:" + Paths.get(outJarPath).normalize().toUri());

        Files.walkFileTree(sourcePath, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file,
                                             BasicFileAttributes attributes) {

                if (attributes.isSymbolicLink()) {
                    return FileVisitResult.CONTINUE;
                }

                Map<String, String> env = new HashMap<>();
                env.put("create", "true");

                try (FileSystem zipfs = FileSystems.newFileSystem(uri, env)) {

                    Path targetFile = sourcePath.relativize(file);
                    Path pathInZipfile = zipfs.getPath(targetFile.toString());

                    if (pathInZipfile.getParent() != null) {
                        Files.createDirectories(pathInZipfile.getParent());
                    }

                    CopyOption[] options = {
                            StandardCopyOption.REPLACE_EXISTING,
                            StandardCopyOption.COPY_ATTRIBUTES,
                            LinkOption.NOFOLLOW_LINKS
                    };
                    Files.copy(file, pathInZipfile, options);

                } catch (IOException e) {
                    LOGGER.error("Error copying file '{}' into ZIP filesystem", file, e);
                }

                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) {
                //
                return FileVisitResult.CONTINUE;
            }

        });

    }

    /**
     *
     * Replaces swagger artifact adding `-jakarta` suffix
     *
     */
    private static void replaceInJar(String filePath) throws Exception {
        try (FileInputStream fis = new FileInputStream(filePath);
             BufferedInputStream bis = new BufferedInputStream(fis);
             ZipInputStream zis = new ZipInputStream(bis);
             ZipOutputStream out = new ZipOutputStream(new FileOutputStream(filePath + ".jar"))) {

            for (ZipEntry zipEntry; (zipEntry = zis.getNextEntry()) != null; ) {
                out.putNextEntry(new ZipEntry(zipEntry.getName()));

                if (files.contains(zipEntry.getName())) {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(zis));
                    PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(out)));
                    for (String line; (line = reader.readLine()) != null; ) {
                        line = line.replace("swagger-maven-plugin", "swagger-maven-plugin-jakarta");
                        line = line.replace("swagger-project", "swagger-project-jakarta");
                        line = line.replace("swagger-jaxrs2", "swagger-jaxrs2-jakarta");
                        line = line.replace("swagger-models", "swagger-models-jakarta");
                        line = line.replace("swagger-annotations", "swagger-annotations-jakarta");
                        line = line.replace("swagger-core", "swagger-core-jakarta");
                        line = line.replace("swagger-integration", "swagger-integration-jakarta");
                        writer.println(line);
                    }
                    writer.flush();
                } else {
                    byte[] buf = new byte[8192];
                    int length;
                    while ((length = zis.read(buf)) > 0) {
                        out.write(buf, 0, length);
                    }
                }

            }
        }
        Files.delete(Paths.get(filePath));
        Files.move(Paths.get(filePath + ".jar"), Paths.get(filePath));

    }

    public static void main(String[] args) throws Exception {
        String filePath = args[0];
        String outPath = args[1];
        String jakartaVersion = args[2];
        String jakartaActivationVersion = args[3];
        String jacksonVersion = args[4];

        replaceInJar(filePath);
        extract(filePath, outPath);

        // rename dir
        Files.move(Paths.get(outPath + File.separator +
                        "META-INF" + File.separator +
                        "maven" + File.separator +
                        "io.swagger.core.v3" + File.separator +
                        "swagger-maven-plugin"),
                Paths.get(outPath + File.separator +
                        "META-INF" + File.separator +
                        "maven" + File.separator +
                        "io.swagger.core.v3" + File.separator +
                        "swagger-maven-plugin-jakarta")
        );

        Path pluginXmlPath = Paths.get(
                outPath +
                        File.separator + "META-INF" +
                        File.separator + "maven" +
                        File.separator + "plugin.xml");
        String pluginXml = new String(Files.readAllBytes(pluginXmlPath));
        Matcher m = Pattern.compile(jakartaXmlBindRegex, Pattern.DOTALL).matcher(pluginXml);
        while (m.find()) {
            pluginXml = pluginXml.substring(m.start(), m.start(1)) +
                    jakartaXmlBindDep.replace("VERSION", jakartaVersion) +
                    pluginXml.substring(m.end(1), m.end());
        }

        m = Pattern.compile(jakartaValidationRegex, Pattern.DOTALL).matcher(pluginXml);
        while (m.find()) {
            pluginXml = pluginXml.substring(m.start(), m.start(1)) +
                    jakartaValidationDep.replace("VERSION", jakartaVersion) +
                    pluginXml.substring(m.end(1), m.end());
        }

        m = Pattern.compile(jacksonJsonRegex, Pattern.DOTALL).matcher(pluginXml);
        while (m.find()) {
            pluginXml = pluginXml.substring(m.start(), m.start(1)) +
                    jacksonJsonDep.replace("VERSION", jacksonVersion) +
                    pluginXml.substring(m.end(1), m.end());
        }

        m = Pattern.compile(jacksonBaseRegex, Pattern.DOTALL).matcher(pluginXml);
        while (m.find()) {
            pluginXml = pluginXml.substring(m.start(), m.start(1)) +
                    jacksonBaseDep.replace("VERSION", jacksonVersion) +
                    pluginXml.substring(m.end(1), m.end());
        }

        m = Pattern.compile(jacksonJaxbRegex, Pattern.DOTALL).matcher(pluginXml);
        while (m.find()) {
            pluginXml = pluginXml.substring(m.start(), m.start(1)) +
                    jacksonJaxbDep.replace("VERSION", jacksonVersion) +
                    pluginXml.substring(m.end(1), m.end());
        }

        m = Pattern.compile(jakartaActivationRegex, Pattern.DOTALL).matcher(pluginXml);
        while (m.find()) {
            pluginXml = pluginXml.substring(m.start(), m.start(1)) +
                    jakartaActivationDep.replace("VERSION", jakartaActivationVersion) +
                    pluginXml.substring(m.end(1), m.end());
        }

        // write back to plugin.xml file
        Files.write(pluginXmlPath, pluginXml.getBytes());

        // delete the original jar
        Files.delete(Paths.get(filePath));
        // compress the new jar
        jar(Paths.get(outPath), filePath);
        // delete the temp directory
        Files.walk(Paths.get(outPath))
                .sorted(Comparator.reverseOrder())
                .map(Path::toFile)
                .forEach(File::delete);

    }

}