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.ambari.server.view;
  
  import java.io.File;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.net.MalformedURLException;
  import java.net.URL;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.jar.JarEntry;
  import java.util.jar.JarInputStream;
  
  import jakarta.inject.Inject;
  
  import org.apache.ambari.server.orm.entities.ViewEntity;
  import org.apache.ambari.server.view.configuration.ViewConfig;
  import org.apache.commons.io.FileUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * Extractor for view archives.
   */
  public class ViewExtractor {
  
    /**
     * Constants
     */
    private static final String ARCHIVE_CLASSES_DIR = "WEB-INF/classes";
    private static final String ARCHIVE_LIB_DIR     = "WEB-INF/lib";
    private static final int    BUFFER_SIZE         = 1024;
  
    @Inject
    ViewArchiveUtility archiveUtility;
  
    /**
     * The logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(ViewExtractor.class);
  
  
    // ----- ViewExtractor -----------------------------------------------------
  
    /**
     * Extract the given view archive to the given archive directory.
     *
     * @param view         the view entity
     * @param viewArchive  the view archive file
     * @param archiveDir   the view archive directory
     *
     * @param viewsAdditionalClasspath: list of additional paths to be added to every view's classpath
     * @return the class loader for the archive classes
     *
     * @throws ExtractionException if the archive can not be extracted
     */
    public ClassLoader extractViewArchive(ViewEntity view, File viewArchive, File archiveDir, List<File> viewsAdditionalClasspath)
        throws ExtractionException {
  
      String archivePath = archiveDir.getAbsolutePath();
  
      try {
        // Remove directory if jar was updated since last extracting
        if (archiveDir.exists() && viewArchive != null && viewArchive.lastModified() > archiveDir.lastModified()) {
          FileUtils.deleteDirectory(archiveDir);
        }
  
        // Skip if the archive has already been extracted
        if (!archiveDir.exists()) {
          String msg = "Creating archive folder " + archivePath + ".";
  
          view.setStatusDetail(msg);
          LOG.info(msg);
  
          if (archiveDir.mkdir()) {
            JarInputStream jarInputStream = archiveUtility.getJarFileStream(viewArchive);
            try {
              msg = "Extracting files from " + viewArchive.getName() + ".";
              view.setStatusDetail(msg);
              LOG.info(msg);
  
              // pre-create the META-INF directory since JAR compression ordering
              // can sometimes cause problems with creation of the files inside of
              // META-INF if the directory appears after
              //
              // 473 12-07-2018 11:37   META-INF/MANIFEST.MF
              // 0   12-07-2018 11:37   META-INF/
              // 0   12-07-2018 11:37   META-INF/maven/
              //
              File metaInfDir = archiveUtility.getFile(archivePath + File.separator + "META-INF");
              if (!metaInfDir.mkdir()) {
                msg = "Could not create archive META-INF directory.";
  
                view.setStatusDetail(msg);
                LOG.error(msg);
                throw new ExtractionException(msg);
              }
  
              JarEntry jarEntry;
              while ((jarEntry = jarInputStream.getNextJarEntry())!= null){
                try {
                  String   entryPath = archivePath + File.separator + jarEntry.getName();
  
                  LOG.debug("Extracting {}", entryPath);
  
                  File entryFile = archiveUtility.getFile(entryPath);
  
                  if (jarEntry.isDirectory()) {
  
                    // only try to create the directory if it doesn't already
                    // exist (like META-INFO might)
                    if (!entryFile.exists()) {
                      LOG.debug("Making directory {}", entryPath);
  
                      if (!entryFile.mkdir()) {
                        msg = "Could not create archive entry directory " + entryPath + ".";
  
                        view.setStatusDetail(msg);
                        LOG.error(msg);
                        throw new ExtractionException(msg);
                      }
                    }
                  } else {
  
                    FileOutputStream fos = archiveUtility.getFileOutputStream(entryFile);
                    try {
                      LOG.debug("Begin copying from {} to {}", jarEntry.getName(), entryPath);
  
                      byte[] buffer = new byte[BUFFER_SIZE];
                      int n;
                      while((n = jarInputStream.read(buffer)) > -1) {
                        fos.write(buffer, 0, n);
                      }
                      LOG.debug("Finish copying from {} to {}", jarEntry.getName(), entryPath);
  
                    } finally {
                      fos.flush();
                      fos.close();
                    }
                  }
                } finally {
                  jarInputStream.closeEntry();
                }
              }
            } finally {
              jarInputStream.close();
            }
          } else {
            msg = "Could not create archive directory " + archivePath + ".";
  
            view.setStatusDetail(msg);
            LOG.error(msg);
            throw new ExtractionException(msg);
          }
        }
  
        ViewConfig viewConfig = archiveUtility.getViewConfigFromExtractedArchive(archivePath, false);
  
        return getArchiveClassLoader(viewConfig, archiveDir, viewsAdditionalClasspath);
  
      } catch (Exception e) {
        String msg = "Caught exception trying to extract the view archive " + archivePath + ".";
  
        view.setStatusDetail(msg);
        LOG.error(msg, e);
        throw new ExtractionException(msg, e);
      }
    }
  
    /**
     * Ensure that the extracted view archive directory exists.
     *
     * @param extractedArchivesPath  the path
     *
     * @return false if the directory does not exist and can not be created
     */
    public boolean ensureExtractedArchiveDirectory(String extractedArchivesPath) {
  
      File extractedArchiveDir = archiveUtility.getFile(extractedArchivesPath);
  
      return extractedArchiveDir.exists() || extractedArchiveDir.mkdir();
    }
  
  
    // ----- archiveUtility methods ----------------------------------------------------
  
    // get a class loader for the given archive directory
    private ClassLoader getArchiveClassLoader(ViewConfig viewConfig, File archiveDir, List<File> viewsAdditionalClasspath)
        throws IOException {
  
      String    archivePath = archiveDir.getAbsolutePath();
      List<URL> urlList     = new LinkedList<>();
  
      // include the classes directory
      String classesPath = archivePath + File.separator + ARCHIVE_CLASSES_DIR;
      File   classesDir  = archiveUtility.getFile(classesPath);
      if (classesDir.exists()) {
        urlList.add(classesDir.toURI().toURL());
      }
  
          // include libs in additional classpath
      for (File file : viewsAdditionalClasspath) {
        if (file.isDirectory()) {
          // add all files inside this dir.
          addDirToClasspath(urlList, file);
        } else if (file.isFile()) {
          urlList.add(file.toURI().toURL());
        }
      }
  
      // include any libraries in the lib directory
      String libPath = archivePath + File.separator + ARCHIVE_LIB_DIR;
      File libDir = archiveUtility.getFile(libPath);
      addDirToClasspath(urlList, libDir);
  
      // include the archive directory
      urlList.add(archiveDir.toURI().toURL());
  
      LOG.trace("classpath for view {} is : {}", viewConfig.getName(), urlList);
      return new ViewClassLoader(viewConfig, urlList.toArray(new URL[urlList.size()]));
    }
  
    /**
     * Add all the files in libDir to urlList ignoring directories.
     * @param urlList: the list to which all paths needs to be appended
     * @param libDir: the path of which all the files needs to be appended to urlList
     */
    private void addDirToClasspath(List<URL> urlList, File libDir) throws MalformedURLException {
      if (libDir.exists()) {
        File[] files = libDir.listFiles();
        if (files != null) {
          for (final File fileEntry : files) {
            if (!fileEntry.isDirectory()) {
              urlList.add(fileEntry.toURI().toURL());
            }
          }
        }
      }
    }
  
  
    // ----- inner class : ExtractionException ---------------------------------
  
    /**
     * General exception for view archive extraction.
     */
    public static class ExtractionException extends Exception {
  
      // ----- Constructors ----------------------------------------------------
  
      /**
       * Construct an extraction exception.
       *
       * @param msg  the exception message
       */
      public ExtractionException(String msg) {
        super(msg);
      }
  
      /**
       * Construct an extraction exception.
       *
       * @param msg        the exception message
       * @param throwable  the root cause
       */
      public ExtractionException(String msg, Throwable throwable) {
        super(msg, throwable);
      }
    }
  }
  