content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.camel.itest.springboot.arquillian.container;
  
  import java.io.File;
  import java.io.FilenameFilter;
  import java.io.IOException;
  import java.io.InputStream;
  import java.net.Socket;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Properties;
  import java.util.Set;
  import java.util.concurrent.Callable;
  import java.util.logging.ConsoleHandler;
  import java.util.logging.Level;
  import java.util.logging.Logger;
  import javax.management.MBeanServerConnection;
  import javax.management.ObjectName;
  import javax.management.remote.JMXConnector;
  import javax.management.remote.JMXConnectorFactory;
  import javax.management.remote.JMXServiceURL;
  
  import org.apache.camel.itest.springboot.arquillian.container.jmx.CustomJMXProtocol;
  import org.apache.camel.itest.springboot.arquillian.container.util.Await;
  import org.apache.camel.itest.springboot.arquillian.container.util.FileDeploymentUtils;
  import org.jboss.arquillian.container.se.api.ClassPath;
  import org.jboss.arquillian.container.se.api.ClassPathDirectory;
  import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
  import org.jboss.arquillian.container.spi.client.container.DeploymentException;
  import org.jboss.arquillian.container.spi.client.container.LifecycleException;
  import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription;
  import org.jboss.arquillian.container.spi.client.protocol.metadata.JMXContext;
  import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData;
  import org.jboss.arquillian.protocol.jmx.JMXTestRunnerMBean;
  import org.jboss.shrinkwrap.api.Archive;
  import org.jboss.shrinkwrap.api.Node;
  import org.jboss.shrinkwrap.api.asset.ArchiveAsset;
  import org.jboss.shrinkwrap.api.asset.Asset;
  import org.jboss.shrinkwrap.api.asset.ClassAsset;
  import org.jboss.shrinkwrap.api.exporter.ZipExporter;
  import org.jboss.shrinkwrap.descriptor.api.Descriptor;
  
  public class ManagedSEDeployableContainer implements DeployableContainer<ManagedSEContainerConfiguration> {
  
      private static final Logger LOGGER = Logger.getLogger(ManagedSEDeployableContainer.class.getName());
      private static final String SYSPROP_KEY_JAVA_HOME = "java.home";
      private static final String DEBUG_AGENT_STRING = "-agentlib:jdwp=transport=dt_socket,address=8787,server=y,suspend=y";
      private static final String TARGET = "target";
      private static final String SERVER_MAIN_CLASS_FQN = "org.jboss.arquillian.container.se.server.Main";
      private static final String SYSTEM_PROPERTY_SWITCH = "-D";
      private static final String EQUALS = "=";
  
      private boolean debugModeEnabled;
      private boolean keepDeploymentArchives;
      private Process process;
      private List<File> materializedFiles;
      private List<File> classpathDependencies;
      private String host;
      private int port;
      private String librariesPath;
      private List<String> additionalJavaOpts;
      private int waitTime;
  
      @Override
      public Class<ManagedSEContainerConfiguration> getConfigurationClass() {
          return ManagedSEContainerConfiguration.class;
      }
  
      public void setup(ManagedSEContainerConfiguration configuration) {
          debugModeEnabled = configuration.isDebug();
          host = configuration.getHost();
          port = configuration.getPort();
          materializedFiles = new ArrayList<>();
          classpathDependencies = new ArrayList<>();
          librariesPath = configuration.getLibrariesPath();
          keepDeploymentArchives = configuration.isKeepDeploymentArchives();
          additionalJavaOpts = initAdditionalJavaOpts(configuration.getAdditionalJavaOpts());
          configureLogging(configuration);
          waitTime = configuration.getWaitTime() > 0 ? configuration.getWaitTime() : 10;
      }
  
      private List<String> initAdditionalJavaOpts(String opts) {
          if (opts == null || opts.isEmpty()) {
              return Collections.emptyList();
          }
          List<String> additionalOpts = new ArrayList<>();
  
          for (String option : opts.split("\\s+")) {
              additionalOpts.add(option);
          }
          return additionalOpts;
      }
  
      private void configureLogging(ManagedSEContainerConfiguration configuration) {
          final String loggingConfigFile = System.getProperty("java.util.logging.config.file");
          if (loggingConfigFile == null) {
              ConsoleHandler consoleHandler = new ConsoleHandler();
              consoleHandler.setLevel(configuration.getLogLevel());
              LOGGER.setUseParentHandlers(false);
              LOGGER.addHandler(consoleHandler);
              LOGGER.info("No logging configured, using console");
          } else {
              // suppose that logging has been configured using the specified file
              LOGGER.info("Logging configured using file " + loggingConfigFile);
          }
          LOGGER.setLevel(configuration.getLogLevel());
      }
  
      @Override
      public void start() throws LifecycleException {
      }
  
      @Override
      public void stop() throws LifecycleException {
      }
  
      @Override
      public ProtocolDescription getDefaultProtocol() {
          return new ProtocolDescription(CustomJMXProtocol.NAME);
      }
  
      @Override
      public void deploy(Descriptor descriptor) throws DeploymentException {
          throw new UnsupportedOperationException();
      }
  
      @Override
      public void undeploy(Descriptor descriptor) throws DeploymentException {
          throw new UnsupportedOperationException();
      }
  
      @Override
      public void undeploy(Archive<?> archive) throws DeploymentException {
          LOGGER.info("Undeploying " + archive.getName());
          if (!keepDeploymentArchives) {
              for (File materializedFile : materializedFiles) {
                  if (materializedFile.isDirectory()) {
                      try {
                          FileDeploymentUtils.deleteRecursively(materializedFile.toPath());
                      } catch (IOException e) {
                          LOGGER.warning("Could not delete materialized directory: " + materializedFile);
                      }
                  } else {
                      materializedFile.delete();
                  }
              }
          }
          // Kill the subprocess (test JVM)
          if (process != null) {
              process.destroy();
              try {
                  process.waitFor();
              } catch (final InterruptedException e) {
                  Thread.interrupted();
                  throw new RuntimeException("Interrupted while awaiting server daemon process termination", e);
              }
          }
      }
  
      @Override
      public ProtocolMetaData deploy(final Archive<?> archive) throws DeploymentException {
          LOGGER.info("Deploying " + archive.getName());
  
          // First of all clear the list of previously materialized deployments - otherwise the class path would grow
          // indefinitely
          materializedFiles.clear();
  
          // Create a new classpath
          classpathDependencies.clear();
  
          if (ClassPath.isRepresentedBy(archive)) {
              for (Node child : archive.get(ClassPath.ROOT_ARCHIVE_PATH).getChildren()) {
                  Asset asset = child.getAsset();
                  if (asset instanceof ArchiveAsset) {
                      Archive<?> assetArchive = ((ArchiveAsset) asset).getArchive();
                      if (ClassPathDirectory.isRepresentedBy(assetArchive)) {
                          materializeDirectory(assetArchive);
                      } else {
                          materializeArchive(assetArchive);
                      }
                  }
              }
          } else {
              materializeArchive(archive);
          }
  
          Properties systemProperties = getSystemProperties(archive);
          readJarFilesFromDirectory();
          addTestResourcesDirectory(systemProperties);
  
          List<String> processCommand = buildProcessCommand(systemProperties);
          logExecutedCommand(processCommand);
          // Launch the process
          final ProcessBuilder processBuilder = new ProcessBuilder(processCommand);
  
          String path = systemProperties.getProperty("container.user.dir");
          if (path != null) {
              processBuilder.directory(new File(path));
          }
  
          processBuilder.environment().put("JAVA_HOME",
                  new File(System.getProperty(SYSPROP_KEY_JAVA_HOME)).getAbsolutePath());
  
          processBuilder.redirectErrorStream(true);
          processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
          processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
  
          try {
              process = processBuilder.start();
          } catch (final IOException e) {
              throw new DeploymentException("Could not start process", e);
          }
  
          int finalWaitTime = debugModeEnabled ? (3 * waitTime) : waitTime;
  
          // Wait for socket connection
          if (!isServerStarted(host, port, finalWaitTime)) {
              throw new DeploymentException("Child JVM process failed to start within " + finalWaitTime + " seconds.");
          }
          if (!isJMXTestRunnerMBeanRegistered(host, port, finalWaitTime)) {
              throw new DeploymentException("JMXTestRunnerMBean not registered within " + finalWaitTime + " seconds.");
          }
  
          ProtocolMetaData protocolMetaData = new ProtocolMetaData();
          protocolMetaData.addContext(new JMXContext(host, port));
          return protocolMetaData;
      }
  
      private Properties getSystemProperties(final Archive<?> archive) throws DeploymentException {
          Node systemPropertiesNode = archive.get(ClassPath.SYSTEM_PROPERTIES_ARCHIVE_PATH);
          if (systemPropertiesNode != null) {
              try (InputStream in = systemPropertiesNode.getAsset().openStream()) {
                  Properties systemProperties = new Properties();
                  systemProperties.load(in);
                  return systemProperties;
              } catch (IOException e) {
                  throw new DeploymentException("Could not load system properties", e);
              }
          }
          return null;
      }
  
      private boolean isServerStarted(final String host, final int port, int waitTime) {
          return new Await(waitTime, new Callable<Boolean>() {
              @Override
              public Boolean call() throws Exception {
                  try (Socket ignored = new Socket(host, port)) {
                      return true;
                  }
              }
          }).start();
      }
  
      private boolean isJMXTestRunnerMBeanRegistered(final String host, final int port, int waitTime)
              throws DeploymentException {
          // Taken from org.jboss.arquillian.container.spi.client.protocol.metadata.JMXContext
          final String jmxServiceUrl = "service:jmx:rmi:///jndi/rmi://" + host + ":" + port + "/jmxrmi";
          try (JMXConnector jmxc = JMXConnectorFactory.connect(new JMXServiceURL(jmxServiceUrl), null)) {
              final MBeanServerConnection mbsc = jmxc.getMBeanServerConnection();
              return new Await(waitTime, new Callable<Boolean>() {
                  @Override
                  public Boolean call() throws Exception {
                      mbsc.getObjectInstance(new ObjectName(JMXTestRunnerMBean.OBJECT_NAME));
                      LOGGER.fine("JMXTestRunnerMBean registered with the remote MBean server at: " + jmxServiceUrl);
                      return true;
                  }
              }).start();
          } catch (IOException e) {
              throw new DeploymentException("Could not verify JMXTestRunnerMBean registration", e);
          }
      }
  
      private void materializeArchive(Archive<?> archive) {
          File deploymentFile = new File(TARGET.concat(File.separator).concat(archive.getName()));
          archive.as(ZipExporter.class).exportTo(deploymentFile, true);
          materializedFiles.add(deploymentFile);
      }
  
      private void materializeDirectory(Archive<?> archive) throws DeploymentException {
          if (archive.getContent().isEmpty()) {
              // Do not materialize an empty directory
              return;
          }
          File entryDirectory = new File(TARGET.concat(File.separator).concat(archive.getName()));
          try {
              if (entryDirectory.exists()) {
                  // Always delete previous content
                  FileDeploymentUtils.deleteContent(entryDirectory.toPath());
              } else {
                  if (!entryDirectory.mkdirs()) {
                      throw new DeploymentException("Could not create class path directory: " + entryDirectory);
                  }
              }
              for (Node child : archive.get(ClassPath.ROOT_ARCHIVE_PATH).getChildren()) {
                  Asset asset = child.getAsset();
                  if (asset instanceof ClassAsset) {
                      FileDeploymentUtils.materializeClass(entryDirectory, (ClassAsset) asset);
                  } else if (asset == null) {
                      FileDeploymentUtils.materializeSubdirectories(entryDirectory, child);
                  }
              }
          } catch (IOException e) {
              throw new DeploymentException("Could not materialize class path directory: " + archive.getName(), e);
          }
          materializedFiles.add(entryDirectory);
      }
  
      private List<String> buildProcessCommand(Properties properties) {
          final List<String> command = new ArrayList<>();
          final File javaHome = new File(System.getProperty(SYSPROP_KEY_JAVA_HOME));
          command.add(javaHome.getAbsolutePath() + File.separator + "bin" + File.separator + "java");
          if (getJavaMajorVersion() >= 9) {
              command.add("--add-modules");
              command.add("java.sql");
          }
          command.add("-cp");
          StringBuilder builder = new StringBuilder();
          Set<File> classPathEntries = new HashSet<>(materializedFiles);
          classPathEntries.addAll(classpathDependencies);
          for (Iterator<File> iterator = classPathEntries.iterator(); iterator.hasNext();) {
              builder.append(iterator.next().getPath());
              if (iterator.hasNext()) {
                  builder.append(File.pathSeparator);
              }
          }
          command.add(builder.toString());
          command.add("-Dcom.sun.management.jmxremote");
          command.add("-Dcom.sun.management.jmxremote.port=" + port);
          command.add("-Dcom.sun.management.jmxremote.authenticate=false");
          command.add("-Dcom.sun.management.jmxremote.ssl=false");
  
          if (debugModeEnabled) {
              command.add(DEBUG_AGENT_STRING);
          }
          for (String option : additionalJavaOpts) {
              command.add(option);
          }
          if (properties != null) {
              for (Map.Entry<Object, Object> entry : properties.entrySet()) {
                  addSystemProperty(command, entry.getKey().toString(), entry.getValue().toString());
              }
          }
          command.add(SERVER_MAIN_CLASS_FQN);
          return command;
      }
  
      private void addSystemProperty(List<String> command, String key, String value) {
          command.add(SYSTEM_PROPERTY_SWITCH + key + EQUALS + value);
      }
  
      private void readJarFilesFromDirectory() throws DeploymentException {
          if (librariesPath == null) {
              return;
          }
          File lib = new File(librariesPath);
          if (!lib.exists() || lib.isFile()) {
              throw new DeploymentException("Cannot read files from " + librariesPath);
          }
  
          File[] dep = lib.listFiles(new FilenameFilter() {
              @Override
              public boolean accept(File dir, String name) {
                  return name.endsWith(".jar");
              }
          });
          classpathDependencies.addAll(Arrays.asList(dep));
  
      }
  
      private void addTestResourcesDirectory(Properties properties) throws DeploymentException {
          String testResources = properties.getProperty("container.test.resources.dir");
          if (testResources != null) {
              File testDir = new File(testResources);
              if (testDir.exists() && testDir.isDirectory()) {
                  classpathDependencies.add(testDir);
              }
          }
      }
  
      private void logExecutedCommand(List<String> processCommand) {
          if (LOGGER.isLoggable(Level.FINE)) {
              StringBuilder builder = new StringBuilder();
              for (String s : processCommand) {
                  builder.append(s);
                  builder.append(" ");
              }
              LOGGER.log(Level.FINE, "Executing command: " + builder);
          }
      }
  
      private static int getJavaMajorVersion() {
          String javaSpecVersion = System.getProperty("java.specification.version");
          if (javaSpecVersion.contains(".")) { // before jdk 9
              return Integer.parseInt(javaSpecVersion.split("\\.")[1]);
          } else {
              return Integer.parseInt(javaSpecVersion);
          }
      }
  }
  