package com.threatconnect.plugin.pkg.mojo;
import com.threatconnect.app.addons.util.config.InvalidCsvFileException;
import com.threatconnect.app.addons.util.config.InvalidJsonFileException;
import com.threatconnect.app.addons.util.config.attribute.AttributeReaderUtil;
import com.threatconnect.app.addons.util.config.install.Feed;
import com.threatconnect.app.addons.util.config.install.Install;
import com.threatconnect.app.addons.util.config.install.InstallUtil;
import com.threatconnect.app.addons.util.config.validation.ValidationException;
import com.threatconnect.plugin.pkg.PackageFileFilter;
import com.threatconnect.plugin.pkg.Profile;
import com.threatconnect.plugin.pkg.ZipUtil;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.util.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class AbstractPackageMojo extends AbstractMojo
{
private static final Logger logger = LoggerFactory.getLogger(AbstractPackageMojo.class);
public static final Pattern PATTERN_INSTALL_JSON = Pattern.compile("^(?:(.*)\\.)?install\\.json$");
public static final String TC_APP_FILE_EXTENSION = "zip";
public static final String TC_BUNDLED_FILE_EXTENSION = "bundle.zip";
/**
* The base directory for the application
*/
@Parameter(defaultValue = "${basedir}", required = true)
private String baseDirectory;
/**
* The directory for the generated app.
*/
@Parameter(defaultValue = "${project.build.directory}", required = true)
private String outputDirectory;
/**
* The name of the generated app name.
*/
@Parameter(defaultValue = "${project.build.finalName}", required = true)
private String appName;
/**
* The version of the app
*/
@Parameter(defaultValue = "${project.version}", required = true)
private String version;
@Parameter
private String[] exclude;
@Override
public void execute() throws MojoExecutionException, MojoFailureException
{
getLog().info("Building ThreatConnect App file");
try
{
// retrieve the list of profiles
List<Profile> profiles = getProfiles();
List<File> packagedApps = new ArrayList<File>();
// check to see if there are any profiles
if (!profiles.isEmpty())
{
// for each of the profiles
for (Profile profile : profiles)
{
// package an app profile
packagedApps.add(packageProfile(profile));
}
//check to see if there are multiple profiles
if (profiles.size() > 1)
{
//build a bundled archive of all the apps
buildBundledArchive(packagedApps);
}
}
else
{
// package a legacy app (no install.json file)
packageLegacy();
}
}
catch (InvalidJsonFileException | ValidationException | IOException | InvalidCsvFileException e)
{
throw new MojoFailureException(e.getMessage(), e);
}
}
/**
* Given a list of packed app files, this builds a bundled archive containing all of them
*
* @param packagedApps
*/
protected void buildBundledArchive(final List<File> packagedApps) throws IOException
{
File bundledFile =
new File(getOutputDirectory() + File.separator + getAppName() + "." + TC_BUNDLED_FILE_EXTENSION);
getLog().info("Packaging Bundle " + bundledFile.getName());
ZipUtil.zipFiles(packagedApps, bundledFile.getAbsolutePath());
}
/**
* Packages a profile. A profile is determined by an install.json file. If multiple install.json
* files are found, they are each built using their profile name. Otherwise, if only an
* install.json file is found, the default settings are used.
*
* @param profile
* @throws IOException
*/
protected File packageProfile(final Profile profile)
throws IOException, ValidationException, InvalidCsvFileException
{
// determine what this app name will be
final String appName = determineAppName(profile);
getLog().info("Packaging Profile " + appName);
File explodedDir = getExplodedDir(appName);
explodedDir.mkdirs();
// copy the install.json file
File installJsonDestination = new File(explodedDir.getAbsolutePath() + File.separator + "install.json");
FileUtils.copyFile(profile.getInstallFile(), installJsonDestination);
// write the rest of the app contents out to the target folder
writeAppContentsToDirectory(explodedDir);
//validate that all of the files referenced in the install.json file exist
validateReferencedFiles(explodedDir, profile);
// zip up the app and return the file of the packaged app
return ZipUtil.zipFolder(explodedDir, TC_APP_FILE_EXTENSION);
}
/**
* Packages a legacy app which consists of an install.conf file.
*
* @throws IOException
*/
protected void packageLegacy() throws IOException
{
File explodedDir = getExplodedDir(determineAppName(null));
explodedDir.mkdirs();
// copy the install conf file if it exists
copyFileToDirectoryIfExists(getInstallConfFile(), explodedDir);
// write the rest of the app contents out to the target folder
writeAppContentsToDirectory(explodedDir);
// zip up the app
ZipUtil.zipFolder(explodedDir, TC_APP_FILE_EXTENSION);
}
/**
* Given a profile, the name of the app is determined here. First, if the install.json file has
* an applicationName and programVersion attribute set, those are used. If not, the prefix of
* the install.json file is used if it exists, otherwise the default app name is used.
*
* @param profile
* @return
*/
protected String determineAppName(final Profile profile)
{
String appName;
// first check to see if the profile is not null
if (null != profile)
{
// retrieve the application name and program version from the install.json file
final String applicationName = profile.getInstall().getApplicationName();
final String programVersion = profile.getInstall().getProgramVersion();
// make sure that both the application name and program version are valid
if (null != applicationName && !applicationName.trim().isEmpty() && null != programVersion
&& !programVersion.trim().isEmpty())
{
appName = applicationName + "_v" + programVersion;
}
else
{
// check to see if a valid profile name exists
if (null != profile.getProfileName() && !profile.getProfileName().trim().isEmpty())
{
appName = profile.getProfileName();
// check to see if the app name needs the version
if (!appName.endsWith(getVersion()))
{
appName = appName + "_v" + getVersion();
}
}
// check to see if the app name does not already contain the version at the end
else if (!getAppName().endsWith(getVersion()))
{
appName = getAppName() + "_v" + getVersion();
}
else
{
appName = getAppName();
}
}
}
// there is not profile object
else
{
appName = getAppName();
}
return appName;
}
/**
* Returns the list of profiles for this app packager
*
* @return
*/
protected List<Profile> getProfiles() throws InvalidJsonFileException, IOException, ValidationException
{
// holds the list of install
List<Profile> profiles = new ArrayList<Profile>();
// retrieve the base directory file
File root = new File(baseDirectory);
// for each file in the root
for (File file : root.listFiles())
{
// make sure this is a file (not a directory)
if (file.isFile())
{
// retrieve the matcher for this filename
Matcher matcher = PATTERN_INSTALL_JSON.matcher(file.getName());
// make sure this is an install file
if (matcher.matches())
{
// retrieve the profile name for this install file
final String profileName = matcher.group(1);
// create the install json object
Install install = InstallUtil.load(file);
// create a new profile
Profile profile = new Profile(profileName, file, install);
profiles.add(profile);
}
}
}
return profiles;
}
protected void validateReferencedFiles(File explodedDir, final Profile profile)
throws ValidationException, InvalidCsvFileException
{
//for each of the feeds in the install object
for (Feed feed : profile.getInstall().getFeeds())
{
//check to see if there is an attribute file
if (null != feed.getAttributesFile())
{
//make sure this file exists or throw an exception
File file = new File(explodedDir.getAbsolutePath() + File.separator + feed.getAttributesFile());
if (!file.exists())
{
throw new ValidationException(generateReferencedFileMissingMessage(feed.getAttributesFile()));
}
else
{
//load the attributes file and validate it
AttributeReaderUtil.read(file);
}
}
//check to see if there is a job file
if (null != feed.getJobFile())
{
//make sure this file exists or throw an exception
File file = new File(explodedDir.getAbsolutePath() + File.separator + feed.getJobFile());
if (!file.exists())
{
throw new ValidationException(generateReferencedFileMissingMessage(feed.getJobFile()));
}
}
}
}
protected String generateReferencedFileMissingMessage(final String fileName)
{
return "Referenced file \"" + fileName + " does not exist. Make sure it is in the correct directory.";
}
protected File getInstallConfFile()
{
return new File(baseDirectory + File.separator + "install.conf");
}
protected File getExplodedDir(final String appName)
{
return new File(getOutputDirectory() + File.separator + appName);
}
/**
* Copies a file to a destination directory if the source file exists
*
* @param source
* @param destinationDirectory
* @throws IOException
*/
protected void copyFileToDirectoryIfExists(final File source, final File destinationDirectory)
throws IOException
{
// check to see if the source file exists
if (source.exists())
{
// check to see if this is a directory
if (source.isDirectory())
{
// for each of the files
for (File file : source.listFiles(createPackageFileFilter().createFilenameFilter()))
{
// check to see if this file is a directory
if (file.isDirectory())
{
// create the new destination folder
final File destination =
new File(destinationDirectory.getAbsoluteFile() + File.separator + file.getName());
// recursively copy this file
copyFileToDirectoryIfExists(file, destination);
}
else
{
// recursively copy this file
copyFileToDirectoryIfExists(file, destinationDirectory);
}
}
}
else
{
// check to see if the destination directory does not exist
if (!destinationDirectory.exists())
{
// make all the directories as needed
destinationDirectory.mkdirs();
}
// copy this file to the destination directory
copyFileToDirectory(source, destinationDirectory);
}
}
}
protected PackageFileFilter createPackageFileFilter()
{
return new PackageFileFilter(getExclude());
}
public String getOutputDirectory()
{
return outputDirectory;
}
public String getAppName()
{
return appName;
}
public String getBaseDirectory()
{
return baseDirectory;
}
public String getVersion()
{
return version;
}
public List<String> getExclude()
{
List<String> excludeList = new ArrayList<String>();
if (null != exclude)
{
//add all of the custom excludes to the list
excludeList.addAll(Arrays.asList(exclude));
}
return excludeList;
}
protected final void copyFileToDirectory(final File source, final File destinationDirectory)
throws IOException
{
// copy this file to the destination directory
logger.info("\tAdding file \"{}\" to package.", source.getName());
FileUtils.copyFileToDirectory(source, destinationDirectory);
}
/**
* Called when the app packager is ready to begin writing the files needed for building an app
*
* @param targetDirectory
* @throws IOException
*/
protected void writeAppContentsToDirectory(File targetDirectory) throws IOException
{
// retrieve the base directory folder
File baseDirectory = new File(getBaseDirectory());
File outputDirectory = new File(getOutputDirectory());
// loop through each of the files
for (File child : baseDirectory.listFiles(createPackageFileFilter().createFilenameFilter()))
{
// make sure that this file is not hidden and that it is not the output folder
if (!child.isHidden() && !child.equals(outputDirectory))
{
// copy this directory to the target folder
final File target;
//check to see if this child is a directory
if (child.isDirectory())
{
target = new File(targetDirectory.getAbsolutePath() + File.separator + child.getName());
}
else
{
target = targetDirectory;
}
copyFileToDirectoryIfExists(child, target);
}
}
}
}