All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.webforj.installer.WebforjInstaller Maven / Gradle / Ivy

There is a newer version: 24.12
Show newest version
package com.webforj.installer;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

import com.basis.api.admin.BBjAdminAppDeploymentApplication;
import com.basis.api.admin.BBjAdminAppDeploymentConfiguration;
import com.basis.api.admin.BBjAdminBase;
import com.basis.api.admin.BBjAdminFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.DefaultInvoker;
import org.apache.maven.shared.invoker.InvocationRequest;
import org.apache.maven.shared.invoker.Invoker;
import org.apache.maven.shared.invoker.MavenInvocationException;


/**
 * perform the installation of a Webforj app based on its JAR.
 */
public final class WebforjInstaller {

  Logger log = LogManager.getLogger(WebforjInstaller.class);
  // buffer size
  public static final String APP_HANDLE_PREFIX = "webforj_";

  public static final String POM_XML = "pom.xml";

  // a central StringBuilder instance to obtain the log
  private final StringBuilder out = new StringBuilder();

  /**
   * extract the POM file from the jar.
   *
   * @param zipFile the project's jar
   * @throws IOException when io fails.
   */
  private String unzipPom(String zipFile) throws IOException {
    log.info("extracting pom! zipFile = {}", zipFile);
    var checksum = "";
    out.append("webforj-installer: extracting pom.xml\n");

    File file = new File(zipFile);
    ZipFile zip = new ZipFile(file);
    Path path = Paths.get(zipFile);
    String directory = path.getParent().toString();
    Enumeration zipFileEntries = zip.entries();

    log.info("Searching zipfile for pom.xml");
    // Process each entry
    while (zipFileEntries.hasMoreElements()) {

      // grab a zip file entry
      ZipEntry entry = zipFileEntries.nextElement();

      String currentEntry = entry.getName();
      if (!currentEntry.endsWith(POM_XML)) {
        continue;
      }
      log.info("found pom.xml in entry {}", currentEntry);
      File destFile = new File(directory, POM_XML);

      // write the current file to disk
      log.info("Writing entry to disk...");
      copyZipEntryToFile(zip, entry, destFile);
    }
    zip.close();

    try {
      byte[] data = Files.readAllBytes(Paths.get(directory + "/" + POM_XML));
      byte[] hash = MessageDigest.getInstance("MD5").digest(data);
      checksum = new BigInteger(1, hash).toString(16);
    } catch (Exception e) {
      log.error("Exception handling unzip pom", e);
      throw new IOException(e);
    }
    return checksum;
  }

  /**
   * extract the BBj progs from the JAR (they would be in /bbj inside).
   *
   * @param zipFile the JAR file.
   * @param directory the directors where they should go.
   * @throws IOException an exception with IO.
   */
  private void unzipBbjProgs(String zipFile, String directory) throws IOException {
    log.info("unzipBbjProgs: zipFile = {}, directory = {}", zipFile, directory);
    File file = new File(zipFile);
    // Ensure valid path ends with system separator character for zip slip issues.
    // Normalize to deal with windows nonsense.
    String targetDirectory = FilenameUtils.normalize(directory + File.separator);
    log.info("targetDirectory = {}", targetDirectory);

    try (ZipFile zip = new ZipFile(file)) {
      Enumeration zipFileEntries = zip.entries();
      log.info("processing zip entries ...");
      // Process each entry
      while (zipFileEntries.hasMoreElements()) {
        // grab a zip file entry
        ZipEntry entry = zipFileEntries.nextElement();
        String currentEntry = FilenameUtils.normalize(entry.getName());
        if (!"bbj".equalsIgnoreCase(FilenameUtils.getExtension(currentEntry))) {
          continue;
        }
        log.info("processing a bbj file {}, proceeding", currentEntry);

        File destFile = new File(targetDirectory + currentEntry);
        String canonicalDestinationPath = FilenameUtils.normalize(destFile.getCanonicalPath());
        log.info("canonicalDestinationPath = {}, targetDirectory = {}", canonicalDestinationPath,
            targetDirectory);
        // Let FileameUtils deal with OS problems, case sensitivity issues.
        if (FilenameUtils.directoryContains(targetDirectory, canonicalDestinationPath)) {
          log.info("extracting {}", canonicalDestinationPath);
          out.append("webforj-installer: extracting %s%n".formatted(canonicalDestinationPath));
          Files.deleteIfExists(destFile.toPath());
          File parent = destFile.getParentFile();
          log.info("testing parent directory {}, attempting to create if not exists", parent);
          if (!parent.exists() && !parent.mkdirs()) {
            throw new IOException("Could not create parent directories " + parent);
          }

          log.info("writing current file {} to disk", destFile);
          copyZipEntryToFile(zip, entry, destFile);
        } else {
          log.info("failed test startsWithText: canonicalDestinationPath={}, targetDirectory={}",
              canonicalDestinationPath, targetDirectory);
        }

      }
    }
  }

  /**
   * Copy the zip entry from the zip file to the destFile location.
   *
   * @param zip the zip file.
   * @param entry the zip entry.
   * @param destFile the destination of the zip entry.
   * @throws IOException input stream or zip errors.
   */
  private void copyZipEntryToFile(ZipFile zip, ZipEntry entry, File destFile) throws IOException {
    log.info("copying entry = {} destFile = {}}", entry, destFile);
    try (InputStream is = zip.getInputStream(entry)) {
      Files.copy(is, destFile.toPath(), REPLACE_EXISTING);
    }

  }

  private Set getWebforjDeps(String dir) {
    log.info("getWebforjDeps: dir = {}", dir);
    Pattern pattern = Pattern.compile("webforj-*");
    out.append("webforj-installer: Scanning Dependencies in %s!%n".formatted(dir));
    final File[] filesList = new File(dir).listFiles();
    if (filesList == null || filesList.length == 0) {
      log.warn("No dependencies found in dir {}!  Check write permissions", dir);
      out.append("ERROR: No Dependencies found in dir %s!%n".formatted(dir));
      out.append("ERROR: Check write permissions? \n");
      return new HashSet<>();
    }
    return Stream.of(filesList).filter(file -> !file.isDirectory()).map(File::getName)
        .filter(pattern.asPredicate()).collect(Collectors.toSet());
  }


  void deleteDirectory(File directoryToBeDeleted) throws IOException {
    File[] allContents = directoryToBeDeleted.listFiles();
    if (allContents != null) {
      for (File file : allContents) {
        deleteDirectory(file);
      }
    }
    Files.delete(directoryToBeDeleted.toPath());
  }

  /**
   * installs the DWC project based on its JAR file.
   *
   * @param sourceFilePath the path of the JAR file - it's considered a temporary location. The file
   *        will be removed from here.
   * @param jarFileName the name of the file.
   * @param bbxdir the home directory of BBj.
   * @param deployroot The directory to which the deployment should be unpacked and installed.
   * @return The log of the activities.
   * @throws MavenInvocationException if there is a problem.
   * @throws IOException if there is a problem.
   */
  public String install(String sourceFilePath, String jarFileName, String bbxdir, String deployroot)
      throws MavenInvocationException, IOException {
    return install(sourceFilePath, jarFileName, bbxdir, deployroot, "");
  }

  /**
   * installs the DWC project based on its JAR file.
   *
   * @param sourceFilePath the path of the JAR file - it's considered a temporary location. The file
   *        will be removed from here.
   * @param jarFileName the name of the file.
   * @param bbxdir the home directory of BBj.
   * @param deployroot The directory to which the deployment should be unpacked and installed.
   * @param requestUrl The URL from which this was invoked. Used to display the resulting link
   * @return The log of the activities.
   * @throws MavenInvocationException if there is a problem.
   * @throws IOException if there is a problem.
   */
  public String install(String sourceFilePath, String jarFileName, String bbxdir, String deployroot,
      String requestUrl) throws MavenInvocationException, IOException {
    log.info("""
        install arguments:
          sourceFilePath = {}
          jarFileName = {}
          bbxdir = {}
          deployroot = {}
          requestUrl = {}
        """, sourceFilePath, jarFileName, bbxdir, deployroot, requestUrl);


    String normalizedJarFileName = FilenameUtils.getBaseName(FilenameUtils.normalize(jarFileName));

    String basedir = FilenameUtils.normalize(deployroot + normalizedJarFileName + File.separator);

    log.info("attempting to create directories for basedir {}", basedir);

    File basdirFile = new File(basedir);
    if (!basdirFile.exists() && (!basdirFile.mkdirs())) {
      throw new IOException("Unable to create basedir " + basdirFile);
    }

    String zipFilePathName = basedir + jarFileName;
    Path p0 = Path.of(sourceFilePath);
    Path zipFilePath = Path.of(zipFilePathName);
    log.info("attempting to copy {} to {} with REPLACE_EXISTING", p0, zipFilePath);

    try {
      Files.copy(p0, zipFilePath, REPLACE_EXISTING);
    } catch (IOException e) {
      log.warn("Failed to copy source file to zip file, reason {}", e.getMessage());
      String timestampedZipFilePathName = basedir + System.currentTimeMillis() + "_" + jarFileName;
      Path timestampedZipFilePath = Path.of(timestampedZipFilePathName);
      out.append("WARNING: %s was locked!%n".formatted(zipFilePath));
      log.info("Trying Again! attempting to copy {} to {} with REPLACE_EXISTING", p0,
          timestampedZipFilePath);
      Files.copy(p0, timestampedZipFilePath, REPLACE_EXISTING);
      out.append("WARNING: Using %s instead %n".formatted(timestampedZipFilePath));
      zipFilePath.toFile().deleteOnExit();
      zipFilePathName = timestampedZipFilePathName;
    }


    String pomFile = basedir + POM_XML;
    String checksum = unzipPom(zipFilePathName);

    String targetDependency = "target/dependency/";
    File depdir = new File("%s%s/%s".formatted(basedir, checksum, targetDependency));
    InvocationRequest request = new DefaultInvocationRequest();
    request.setPomFile(new File(pomFile));
    request.setOutputHandler(new MavenOutputHandler(out))
        .setErrorHandler(new MavenOutputHandler(out))
        .setGoals(List.of("dependency:copy-dependencies")).addArg("-U")
        .addArg("-DincludeScope=compile").addArg("-DoutputDirectory=" + depdir);
    log.info("Created invocation request: args = {}", request.getArgs());
    out.append("request ").append(request.getArgs()).append("\n");
    String mvn = MavenBinaryInstaller.getMavenBinary(deployroot);
    out.append("INFO: Using Maven %s in location %s%n".formatted(mvn, deployroot));
    Invoker invoker = new DefaultInvoker();
    invoker.setMavenHome(new File(mvn));

    out.append("INFO: %s%n".formatted(request));
    log.info("invoking request {}", request);
    invoker.execute(request);

    try {
      Set deps = getWebforjDeps(depdir.getAbsolutePath());
      for (String nextFile : deps) {
        if (nextFile.toLowerCase().endsWith(".jar")) {
          log.info("processing dependency {}", nextFile);
          log.info("calling unzipBBjProgs ...");
          unzipBbjProgs(depdir.getAbsolutePath() + File.separator + nextFile, basedir);
        }
      }

      PomParser pomParser = new PomParser(pomFile);
      Map configuration = pomParser.parse();
      String appname = configuration.get("publishname");

      if (appname == null) {
        log.info("appname is null, setting appname to normalizedJarFileName {}",
            normalizedJarFileName);
        appname = normalizedJarFileName;
      }
      String user = Optional.ofNullable(configuration.get("username")).orElse("admin");
      String password = Optional.ofNullable(configuration.get("password")).orElse("admin123");
      String token = configuration.get("token");

      BBjAdminBase api;
      if (token != null) {
        api = BBjAdminFactory.getBBjAdmin(token);
      } else {
        api = BBjAdminFactory.getBBjAdmin(user, password);
      }


      // create BBj classpath
      log.info("Creating BBj classpath");
      ArrayList cpEntries = new ArrayList<>();
      cpEntries.add("(_webforj_default)");
      cpEntries.add(FilenameUtils.normalize(depdir.getAbsolutePath() + File.separator, true) + "*");

      Boolean useclassfiles = configuration.get("useclassfiles") != null
          && configuration.get("useclassfiles").equals("true");
      String classfolder = Optional.ofNullable(configuration.get("classfolder")).orElse("");

      if (Boolean.TRUE.equals(useclassfiles) && !classfolder.isEmpty()) {
        cpEntries.add(FilenameUtils.normalize(classfolder, true));
      } else {
        cpEntries.add(FilenameUtils.normalize(zipFilePathName, true));
      }

      String apphandle = APP_HANDLE_PREFIX + appname.toLowerCase();
      log.info("set entries {}", cpEntries);
      api.setClasspath(apphandle, cpEntries);
      api.commit();
      out.append(
          "INFO: Written Classpath %s with %d entries".formatted(apphandle, cpEntries.size()));

      // create DWC app entry
      BBjAdminAppDeploymentConfiguration config = api.getRemoteConfiguration();
      // see
      // https://documentation.basis.com/BASISHelp/WebHelp/javadocs/com/basis/api/admin/BBjAdminAppDeploymentApplication.html

      BBjAdminAppDeploymentApplication newApp = config.createApplication();
      newApp.setString(BBjAdminAppDeploymentApplication.NAME, appname);
      newApp.setString(BBjAdminAppDeploymentApplication.PROGRAM, basedir + "bbj/webforj.bbj");
      newApp.setString(BBjAdminAppDeploymentApplication.CLASSPATH, apphandle);
      // newApp.setString(BBjAdminAppDeploymentApplication.CONFIG_FILE, "/path/to/config.bbx")
      newApp.setString(BBjAdminAppDeploymentApplication.WORKING_DIRECTORY, basedir + "bbj/");
      newApp.setBoolean(BBjAdminAppDeploymentApplication.EXE_ENABLED, false);
      newApp.setBoolean(BBjAdminAppDeploymentApplication.BUI_ENABLED, false);
      newApp.setBoolean(BBjAdminAppDeploymentApplication.DWC_ENABLED, true);


      Boolean debug =
          configuration.get("debug") != null && configuration.get("debug").equals("true");
      if (Boolean.TRUE.equals(debug)) {
        newApp.getArguments().add("DEBUG");
      }

      Boolean watch =
          configuration.get("watch") != null && configuration.get("watch").equals("true");
      if (Boolean.TRUE.equals(watch)) {
        newApp.getArguments().add("WATCH");
      }

      String classname = configuration.get("classname");
      if (classname != null && !classname.isEmpty()) {
        newApp.getArguments().add("class=" + classname);
      }

      log.info("committing newApp {}", newApp);
      newApp.commit();

      String deployUrl = "http://localhost:8888/webapp/" + appname;
      if (!requestUrl.isEmpty()) {
        deployUrl = requestUrl + "/webapp/" + appname;
      }
      out.append("INFO: Created App Deployment for %s%n ".formatted(deployUrl));
    } catch (Exception e) {
      log.error("Error during processing!", e);
      out.append("ERROR : %s%n".formatted(e.toString()));
      StringWriter sw = new StringWriter();
      e.printStackTrace(new PrintWriter(sw));
      out.append(sw);
      return out.toString();
    }

    return out.toString();
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy