AipBuilderImpl.java

package esa.bscs.pds4.packager.aip;

import static esa.bscs.pds4.reader.model.Dictionary.*;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import java.util.function.ToIntFunction;
import java.util.stream.Collectors;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;

import esa.bscs.pds4.packager.exception.ClassificationException;
import esa.bscs.pds4.packager.exception.PackagerException;
import esa.bscs.pds4.packager.exception.PackagerMessage;
import esa.bscs.pds4.packager.groovy.ProductClassifier;
import esa.bscs.pds4.packager.resolver.DeliveryResolver;
import esa.bscs.pds4.packager.resolver.ProductResolver;
import esa.bscs.pds4.packager.resolver.TimeResolver;
import esa.bscs.pds4.reader.model.Pds4Label;
import esa.bscs.pds4.reader.model.extension.Pds4File;

public class AipBuilderImpl implements AipBuilder {
    
    @Autowired(required=true)
    private BeanFactory factory;

    @Autowired(required=true)
    private DeliveryResolver deliveryResolver;
    
    @Autowired(required=true)
    private MessageSource messageSource;
    
    @Autowired(required=true)
    private TimeResolver timeResolver;
    
    @Autowired(required=true)
    private VelocityEngine velocityEngine;
    
    public PackageInfo buildArchiveInformationProduct(List<Pds4Label> pds4Labels) {
        
        if(pds4Labels == null || pds4Labels.isEmpty()) {
            throw new IllegalArgumentException();
        }
                
        // Get bundle id
        
        final Function<Pds4Label, String> takeBundleId = value -> value.getRootClass().searchValue(PDS.uri(), "logical_identifier", String.class).split(":")[3];
        final List<String> bundleIds = pds4Labels.stream().map(takeBundleId).distinct().collect(Collectors.toList());
        
        if(bundleIds.size() != 1) {
            final PackagerMessage message = new PackagerMessage("error.packager.multibundle" , messageSource.getMessage("error.packager.multibundle", new String[] {bundleIds.toString()}, Locale.getDefault()));
            throw new PackagerException(Arrays.asList(message));
        }

        final String bundleId = bundleIds.get(0);        
        
        // Resolve product paths
        
        final Map<Pds4Label, String> productPaths = resolveProductPaths(bundleId, pds4Labels);
        final Map<File, String> packageEntries = resolveEntries(productPaths);
                
        // Generate checksum and manifest info 
        
        final ManifestInfo checksumInfo = buildChecksumManifest(packageEntries);
        final ManifestInfo transferInfo = buildTransferManifest(productPaths);
        
        // Get base name to fill the manifest filenames 
        
        final String basename = deliveryResolver.getBaseName(bundleId);
        
        checksumInfo.setFilename(basename + "-checksum_manifest.tab");
        transferInfo.setFilename(basename + "-transfer_manifest.tab");
        
        // Write AIP label file
        
        try (StringWriter writer = new StringWriter()) {
                        
            // Fill velocity context
            
            final VelocityContext context = new VelocityContext();            
            
            context.put("bundleId", pds4Labels.get(0).getRootClass().searchValue(PDS.uri(), "logical_identifier", String.class).split(":")[3]);
            context.put("productId", basename.toLowerCase());
            context.put("checksumInfo", checksumInfo);
            context.put("transferInfo", transferInfo);            
            
            // Merge data with velocity template
            
            velocityEngine.getTemplate("velocity/deliveryLabel.vm").merge(context, writer);
            
            writer.flush();
                        
            // Fill package info 
            
            final PackageInfo packageInfo = new PackageInfo();
            
            packageInfo.setBasename(basename);
            packageInfo.setFilename(basename + ".xml");
            packageInfo.setBundleId(bundleId);
            packageInfo.setProductId(basename.toLowerCase());
            packageInfo.setContent(writer.toString());
            packageInfo.setChecksumInfo(checksumInfo);
            packageInfo.setTransferInfo(transferInfo);
            packageInfo.getPackageEntries().putAll(packageEntries);
            
            // Return
            
            return packageInfo;
        
        } catch(IOException e) {
            throw new UncheckedIOException(e);
        }
    }
    
    @SuppressWarnings("java:S1166")
    private Map<Pds4Label, String> resolveProductPaths(String bundleId, List<Pds4Label> pds4Labels) {
        
        final Map<Pds4Label, String> result = new LinkedHashMap<>();
        final List<String> classificationErrors = new ArrayList<>();
        
        // Get product resolver and classifier
        
        final ProductResolver resolver = factory.getBean(ProductResolver.class, pds4Labels);
        final ProductClassifier classifier = factory.getBean(ProductClassifier.class, bundleId, resolver);
                
        // Resolve path for each product
        
        for(Pds4Label label : pds4Labels) {
            
            try {
                result.put(label, classifier.resolvePath(label));
            
            } catch(Exception e) {
                final String name = label.getLabelLocation().getFile().contains("/") ? label.getLabelLocation().getFile().substring(label.getLabelLocation().getFile().lastIndexOf('/') + 1) : label.getLabelLocation().getFile();
                classificationErrors.add(messageSource.getMessage("error.packager.classification", new String[] {name, e.getMessage()}, Locale.getDefault()));
            }
        }
        
        // Check if there has been errors while classifying
        
        if(!classificationErrors.isEmpty()) {
            throw new ClassificationException(classificationErrors);
        }
        
        // Return
        
        return result;
    }
    
    private Map<File, String> resolveEntries(Map<Pds4Label, String> productPaths) {
        
        try {
            final Map<File, String> result = new LinkedHashMap<>();
            
            for(Map.Entry<Pds4Label, String> entry : productPaths.entrySet()) {
                
                final Pds4Label label = entry.getKey();
                final String path = entry.getValue();
                final File labelFile = new File(label.getLabelLocation().toURI());
    
                // Add label file
                
                result.put(labelFile, path + "/" + labelFile.getName());
                
                // Add pointed files
                
                for(Pds4File pds4File : label.getRootClass().searchEntries(Pds4File.class)) {
                    final File pointedFile = new File(pds4File.resolveLocation().toURI());
                    
                    result.put(pointedFile, path + "/" + pointedFile.getName());
                }
            }
            
            // Return entry map
            
            return result;
        
        } catch(URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
    }
    
    private ManifestInfo buildChecksumManifest(Map<File, String> packageEntries) {
        
        try (StringWriter writer = new StringWriter()) {
            
            // Initialize MD5 checksum generator
            
            final DigestUtils utils = new DigestUtils(DigestUtils.getMd5Digest());
            
            // Generate checksum manifest content
            
            for(Map.Entry<File, String> entry : packageEntries.entrySet()) {
                
                writer.write(utils.digestAsHex(entry.getKey()));
                 writer.write("\t");
                writer.write(entry.getValue());
                writer.write("\r\n");
                writer.flush();
            }
                        
            // Fill manifest info
            
            final Instant now = timeResolver.currentInstant();
            final ManifestInfo manifestInfo = new ManifestInfo();
            
            manifestInfo.setChecksum(utils.digestAsHex(writer.toString()));
            manifestInfo.setContent(writer.getBuffer().toString());
            manifestInfo.setCreationTime(DateTimeFormatter.ISO_INSTANT.format(now));
            manifestInfo.setRecords(packageEntries.size());
            
            // Return result
            
            return manifestInfo;
        
        } catch(IOException e) {
            throw new UncheckedIOException(e);
        }
    }
    
    private ManifestInfo buildTransferManifest(Map<Pds4Label, String> productPaths) {
        
        final ToIntFunction<Pds4Label> logicalIdentifierLength = label -> label.getRootClass().searchEntry(PDS.uri(), "Identification_Area").searchValue(PDS.uri(), "logical_identifier", String.class).length();
        final ToIntFunction<Map.Entry<Pds4Label, String>> fullPathLength = entry -> entry.getValue().length() + 1 + filenameFromUrl(entry.getKey().getLabelLocation()).length();
        
        final int lidMaxLength = productPaths.keySet().stream().mapToInt(logicalIdentifierLength).max().getAsInt();
        final int pathMaxLength = productPaths.entrySet().stream().mapToInt(fullPathLength).max().getAsInt();        
        
        try (StringWriter writer = new StringWriter()) {
            
            // Generate transfer manifest content
            
            for(Map.Entry<Pds4Label, String> entry : productPaths.entrySet()) {
                
                final Pds4Label label = entry.getKey();
                final File labelFile = new File(label.getLabelLocation().toURI());
                final String logicalId = StringUtils.rightPad(label.getRootClass().searchEntry(PDS.uri(), "Identification_Area").searchValue(PDS.uri(), "logical_identifier", String.class), lidMaxLength);
                final String path = StringUtils.rightPad(entry.getValue() + "/" + labelFile.getName(), pathMaxLength);
                
                writer.write(logicalId);
                writer.write("\t");
                writer.write(path);
                writer.write("\r\n");
                writer.flush();
            }
            
            // Fill manifest info
            
            final Instant now = timeResolver.currentInstant();
            final DigestUtils utils = new DigestUtils(DigestUtils.getMd5Digest());
            final ManifestInfo manifestInfo = new ManifestInfo();
            
            manifestInfo.setChecksum(utils.digestAsHex(writer.toString()));
            manifestInfo.setContent(writer.getBuffer().toString());
            manifestInfo.setCreationTime(DateTimeFormatter.ISO_INSTANT.format(now));
            manifestInfo.setRecords(productPaths.size());
                        
            // Return
            
            return manifestInfo;
        
        } catch(IOException e) {
            throw new UncheckedIOException(e);
        
        } catch(URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
    }
    
    private String filenameFromUrl(URL url) {
        
        try {
            return new File(url.toURI()).getName();
        
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
    }
}