use crate::files_proc::model::file_ident::BomFileIdentifier;
use crate::files_proc::model::files_pending_proc::FilesPendingProc;
use crate::files_proc::model::input_file_type::InputFileType;
use crate::files_proc::traits::{
FileSearchProvider, MultipleFilesProcProvider, SingleFileProcProvider,
};
use crate::lib_utils::config::Config;
use crate::lib_utils::errors::Vex2PdfError;
use crate::pdf::generator::PdfGenerator;
use crate::utils::{get_output_pdf_path, parse_vex_json, parse_vex_xml};
#[cfg(feature = "concurrency")]
use jlizard_simple_threadpool::threadpool::ThreadPool;
use log::{error, info, warn};
use std::collections::HashSet;
use std::fs;
use std::hash::Hash;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[derive(Default)]
pub(crate) struct DefaultFilesProcessor {
config: Config,
}
impl DefaultFilesProcessor {
pub(crate) fn new(config: Config) -> Self {
Self { config }
}
}
impl FileSearchProvider for DefaultFilesProcessor {
type OkType = ProcessorReady<PathBuf>;
type ErrType = Vex2PdfError;
fn find_files(self) -> Result<Self::OkType, Self::ErrType> {
let ignored_patterns_map = self.config.file_types_to_process.as_ref();
let working_path = &self.config.working_path;
let ignored_patterns: HashSet<&InputFileType> =
if let Some(ignore_map) = ignored_patterns_map {
ignore_map
.iter()
.filter_map(|(k, v)| {
if !(*v) {
info!(
"Skipping {} files : deactivated by user",
k.as_str_uppercase()
);
Some(k)
} else {
None
}
})
.collect()
} else {
HashSet::new()
};
let mut ret = FilesPendingProc::new();
if working_path.is_file() {
ret.add_sup_file_ignore(working_path.to_path_buf(), &ignored_patterns)?;
} else {
info!(
"Scanning for BoM/Vex Files in {}",
&self.config.working_path.display()
);
for entry in fs::read_dir(&self.config.working_path)? {
let path = entry?.path();
if path.is_file() {
if let Err(e) = ret.add_sup_file_ignore(path.to_path_buf(), &ignored_patterns) {
match e {
Vex2PdfError::IgnoredByUser => info!("{e}"),
_ => error!("{e}"),
};
}
}
}
}
if ret.get_files_ref().is_empty() {
info!("No parseable files in selected path");
} else {
info!(
"Found {} JSON files",
ret.get_file_count_by_type(InputFileType::JSON)
);
info!(
"Found {} XML files",
ret.get_file_count_by_type(InputFileType::XML)
);
}
Ok(ProcessorReady {
config: Arc::new(self.config),
files: ret,
})
}
}
#[derive(Default)]
pub(crate) struct DefaultSingleFileProcessor;
impl<P: AsRef<Path> + Eq + Hash + Send + 'static> SingleFileProcProvider<P>
for DefaultSingleFileProcessor
{
fn process_single_file(
&self,
file: BomFileIdentifier<P>,
config: Arc<Config>,
) -> Result<(), Vex2PdfError> {
info!("Processing {}", file.get_path().as_ref().display());
let bom =
match file.get_type() {
InputFileType::XML => parse_vex_xml(file.get_path())
.map_err(|e| Vex2PdfError::Parse(e.to_string()))?,
InputFileType::JSON => parse_vex_json(file.get_path())
.map_err(|e| Vex2PdfError::Parse(e.to_string()))?,
InputFileType::UNSUPPORTED => return Err(Vex2PdfError::UnsupportedFileType),
};
let generator = PdfGenerator::new(Arc::clone(&config));
info!("Generating PDF: {}", file.get_path().as_ref().display());
let output_path =
get_output_pdf_path(Some(config.output_dir.as_path()), file.get_path().as_ref())?;
match generator.generate_pdf(&bom, &output_path) {
Ok(_) => info!("Successfully generated PDF: {}", output_path.display()),
Err(e) => {
warn!(
"Failed to generate PDF for {}: {}",
output_path.display(),
e
)
}
}
Ok(())
}
}
pub(crate) struct ProcessorReady<P: AsRef<Path> + Eq + Hash> {
config: Arc<Config>,
pub(super) files: FilesPendingProc<P>,
}
impl<P: AsRef<Path> + Eq + Hash + Send + 'static> MultipleFilesProcProvider<P>
for ProcessorReady<P>
{
type OkType = ();
type ErrType = Vex2PdfError;
fn process(self) -> Result<Self::OkType, Self::ErrType> {
#[cfg(feature = "concurrency")]
let pool = if let Some(num_jobs) = self.config.max_jobs {
ThreadPool::new(num_jobs)
} else {
ThreadPool::default()
};
#[cfg(feature = "concurrency")]
info!("{pool}");
let config = self.config;
let file_count = self.files.get_file_count();
let failure_count = Arc::new(AtomicUsize::new(0));
for file in self.files {
let single_file_proc = DefaultSingleFileProcessor;
let config_clone = Arc::clone(&config);
let failure_count_clone = Arc::clone(&failure_count);
#[cfg(feature = "concurrency")]
{
pool.execute(move || {
if let Err(e) = single_file_proc.process_single_file(file, config_clone) {
error!("{e}");
failure_count_clone.fetch_add(1, Ordering::Relaxed);
}
})
.expect(
"Failed to send job to pool. Consider disabling Multithreading if issues persist",
); }
#[cfg(not(feature = "concurrency"))]
{
if let Err(e) = single_file_proc.process_single_file(file, config_clone) {
error!("{e}");
failure_count_clone.fetch_add(1, Ordering::Relaxed);
}
}
}
#[cfg(feature = "concurrency")]
drop(pool);
info!("Processed {file_count} files");
let failures = failure_count.load(Ordering::Relaxed);
if failures > 0 {
return Err(Vex2PdfError::ProcessingFailures(failures));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_default_files_processor_new() {
let config = Config::default();
let processor = DefaultFilesProcessor::new(config);
assert!(processor.config.working_path.exists());
}
#[test]
fn test_processor_ready_holds_state() {
let config = Arc::new(Config::default());
let files: FilesPendingProc<PathBuf> = FilesPendingProc::new();
let processor_ready = ProcessorReady {
config: Arc::clone(&config),
files,
};
assert_eq!(processor_ready.files.get_file_count(), 0);
assert!(processor_ready.config.working_path.exists());
}
#[test]
fn test_single_file_processor_creation() {
let _processor = DefaultSingleFileProcessor::default();
fn assert_send<T: Send>() {}
assert_send::<DefaultSingleFileProcessor>();
}
}