/*******************************************************************************
 * Copyright (c) 2016, 2018 IBH SYSTEMS GmbH and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *     Red Hat Inc
 *     Yariv Amar - customize RPM final file name
 *******************************************************************************/
package org.eclipse.packagedrone.utils.rpm.build;

import static java.util.Comparator.comparing;
import static java.util.Optional.of;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.ToLongFunction;

import org.apache.commons.compress.archivers.cpio.CpioArchiveEntry;
import org.apache.commons.compress.archivers.cpio.CpioConstants;
import org.apache.commons.compress.compressors.zstandard.ZstdUtils;
import org.eclipse.packagedrone.utils.rpm.Architecture;
import org.eclipse.packagedrone.utils.rpm.FileFlags;
import org.eclipse.packagedrone.utils.rpm.OperatingSystem;
import org.eclipse.packagedrone.utils.rpm.PathName;
import org.eclipse.packagedrone.utils.rpm.RpmTag;
import org.eclipse.packagedrone.utils.rpm.RpmVersion;
import org.eclipse.packagedrone.utils.rpm.Rpms;
import org.eclipse.packagedrone.utils.rpm.build.PayloadRecorder.Result;
import org.eclipse.packagedrone.utils.rpm.deps.Dependencies;
import org.eclipse.packagedrone.utils.rpm.deps.Dependency;
import org.eclipse.packagedrone.utils.rpm.deps.RpmDependencyFlags;
import org.eclipse.packagedrone.utils.rpm.header.Header;
import org.eclipse.packagedrone.utils.rpm.signature.SignatureProcessor;
import org.eclipse.packagedrone.utils.rpm.signature.SignatureProcessors;

/**
 * Build RPM files
 *

 * This class takes care of most tasks building RPM files. The constructor only
 * requests the require attributes. There are a few more meta information
 * entries which can be set using the {@link PackageInformation} class and the
 * methods {@link #setInformation(PackageInformation)} and
 * {@link #getInformation()}.
 *

 *

 * In order to build an RPM file, create a new instance of the
 * {@link RpmBuilder} class, set package information, add files by using a
 * context created by {@link #newContext()} and finally call {@link #build()}.
 * The RPM file will only be built once the {@link #build()} method is called.
 * Closing the instance of {@link RpmBuilder} will not write the RPM
 * file, but simply clean up temporary files. Closing this instance will also
 * not delete target RPM file.
 *

 *

 * The implementation of this class uses the {@link PayloadRecorder} to create
 * the payload archive, {@link Header} class for the signature and package
 * header and the {@link RpmWriter} to finally write the RPM file.
 *

 *
Signature processors

 *

 * The RPM builder uses a default set of {@link SignatureProcessor}s. In order
 * to add additional ones use the {@link #addDefaultSignatureProcessors()}. It
 * is possible to remove all already registered processors (including the
 * default ones) using {@link #removeAllSignatureProcessors()}.
 *

 *
 * @author Jens Reimann
 */
public class RpmBuilder implements AutoCloseable
{
    @FunctionalInterface
    private interface RecorderFunction<T>
    {
        public Result record ( PayloadRecorder recorder, String targetName, T data, Consumer<CpioArchiveEntry> customizer ) throws IOException;
    }

    /**
     * Known versions of RPM.
     *

     * This is a enum of known versions of the RPM tool itself. It is incomplete
     * and should be used to ensure compatibility of created RPM file with
     * certain versions of RPM. The first known version we track is "4.11",
     * which may not be correct either.
     */
    public static enum Version
    {
        V4_11 ( "4.11" ),
        V4_12 ( "4.12" ),
        V4_14 ( "4.14" );

        private final String versionString;

        private Version ( final String versionString )
        {
            this.versionString = versionString;
        }

        @Override
        public String toString ()
        {
            return this.versionString;
        }

        public static Optional<Version> fromVersionString ( final String versionString )
        {
            if ( versionString == null )
            {
                return Optional.empty ();
            }

            for ( final Version version : Version.values () )
            {
                if ( version.versionString.equals ( versionString ) )
                {
                    return Optional.of ( version );
                }
            }

            return Optional.empty ();
        }
    }

    public static class Feature extends Dependency
    {
        private String description;

        public Feature ( final String name, final String version, final String description )
        {
            super ( "rpmlib(" + name + ")", version, RpmDependencyFlags.RPMLIB, RpmDependencyFlags.EQUAL );
            this.description = description;
        }

        public String getDescription ()
        {
            return this.description;
        }

        public void setDescription ( final String description )
        {
            this.description = description;
        }

        @Override
        public int hashCode ()
        {
            final int prime = 31;
            int result = 1;
            result = prime * result + ( getFlags () == null ? 0 : getFlags ().hashCode () );
            result = prime * result + ( getName () == null ? 0 : getName ().hashCode () );
            result = prime * result + ( getVersion () == null ? 0 : getVersion ().hashCode () );
            result = prime * result + ( getDescription () == null ? 0 : getDescription ().hashCode () );
            return result;
        }

        @Override
        public boolean equals ( final Object obj )
        {
            if ( this == obj )
            {
                return true;
            }
            if ( obj == null )
            {
                return false;
            }
            if ( getClass () != obj.getClass () )
            {
                return false;
            }
            final Feature other = (Feature)obj;
            if ( getFlags () == null )
            {
                if ( other.getFlags () != null )
                {
                    return false;
                }
            }
            else if ( !getFlags ().equals ( other.getFlags () ) )
            {
                return false;
            }
            if ( getName () == null )
            {
                if ( other.getName () != null )
                {
                    return false;
                }
            }
            else if ( !getName ().equals ( other.getName () ) )
            {
                return false;
            }
            if ( getVersion () == null )
            {
                if ( other.getVersion () != null )
                {
                    return false;
                }
            }
            else if ( !getVersion ().equals ( other.getVersion () ) )
            {
                return false;
            }
            if ( getDescription () == null )
            {
                if ( other.getDescription () != null )
                {
                    return false;
                }
            }
            else if ( !getDescription ().equals ( other.getDescription () ) )
            {
                return false;
            }
            return true;
        }

        @Override
        public String toString ()
        {
            return String.format ( "[%s, %s, %s, %s]", getName (), getVersion (), getFlags (), getDescription () );
        }

    }

    private static List<Feature> features = new ArrayList<> ();

    static
    {

        features.add ( new Feature ( "VersionedDependencies", "3.0.3-1", "PreReq:, Provides:, and Obsoletes: dependencies support versions." ) );
        features.add ( new Feature ( "CompressedFileNames", "3.0.4-1", "file name(s) stored as (dirName,baseName,dirIndex) tuple, not as path." ) );
        features.add ( new Feature ( "PayloadIsBzip2", "3.0.5-1", "package payload can be compressed using bzip2." ) );
        features.add ( new Feature ( "ExplicitPackageProvide", "4.0-1", "package name-version-release is not implicitly provided." ) );
        features.add ( new Feature ( "HeaderLoadSortsTags", "4.0.1-1", "header tags are always sorted after being loaded." ) );
        features.add ( new Feature ( "PayloadFilesHavePrefix", "4.0-1", "package payload file(s) have \"./\" prefix." ) );
        features.add ( new Feature ( "PayloadIsLzma", "4.4.2-1", "package payload can be compressed using lzma." ) );
        features.add ( new Feature ( "PayloadIsXz", "5.2-1", "package payload can be compressed using xz." ) );

        if ( ZstdUtils.isZstdCompressionAvailable () )
        {
            features.add ( new Feature ( "PayloadIsZstd", "5.4.18-1", "package payload can be compressed using zstd." ) );
        }

        features = Collections.unmodifiableList ( features );
    }

    public static class FileEntry
    {
        private long size;

        private String user;

        private String group;

        private String linkTo;

        private short mode;

        private short rdevs;

        private int flags;

        private int modificationTime;

        private String digest;

        private int verifyFlags = -1;

        private String lang;

        private int device;

        private int inode;

        private PathName targetName;

        private long targetSize;

        public void setSize ( final long size )
        {
            this.size = size;
        }

        public long getSize ()
        {
            return this.size;
        }

        public void setUser ( final String user )
        {
            this.user = user;
        }

        public String getUser ()
        {
            return this.user;
        }

        public void setGroup ( final String group )
        {
            this.group = group;
        }

        public String getGroup ()
        {
            return this.group;
        }

        public void setLinkTo ( final String linkTo )
        {
            this.linkTo = linkTo;
        }

        public String getLinkTo ()
        {
            return this.linkTo;
        }

        public short getMode ()
        {
            return this.mode;
        }

        public void setMode ( final short mode )
        {
            this.mode = mode;
        }

        public short getRdevs ()
        {
            return this.rdevs;
        }

        public void setRdevs ( final short rdevs )
        {
            this.rdevs = rdevs;
        }

        public int getFlags ()
        {
            return this.flags;
        }

        public void setFlags ( final int flags )
        {
            this.flags = flags;
        }

        public void setModificationTime ( final int modificationTime )
        {
            this.modificationTime = modificationTime;
        }

        public int getModificationTime ()
        {
            return this.modificationTime;
        }

        public void setDigest ( final String digest )
        {
            this.digest = digest;
        }

        public String getDigest ()
        {
            return this.digest;
        }

        public void setVerifyFlags ( final int verifyFlags )
        {
            this.verifyFlags = verifyFlags;
        }

        public int getVerifyFlags ()
        {
            return this.verifyFlags;
        }

        public void setLang ( final String lang )
        {
            this.lang = lang;
        }

        public String getLang ()
        {
            return this.lang;
        }

        public void setDevice ( final int device )
        {
            this.device = device;
        }

        public int getDevice ()
        {
            return this.device;
        }

        public void setInode ( final int inode )
        {
            this.inode = inode;
        }

        public int getInode ()
        {
            return this.inode;
        }

        public void setTargetName ( final PathName targetName )
        {
            this.targetName = targetName;
        }

        public PathName getTargetName ()
        {
            return this.targetName;
        }

        public void setTargetSize ( final long targetSize )
        {
            this.targetSize = targetSize;
        }

        public long getTargetSize ()
        {
            return this.targetSize;
        }
    }

    public static class PackageInformation
    {
        private String distribution;

        private String packager;

        private String vendor;

        private String license = "unspecified";

        private String buildHost = "localhost";

        private String summary = "Unspecified";

        private String description = "Unspecified";

        private String group = "Unspecified";

        private String operatingSystem = "linux";

        private String url;

        private String sourcePackage;

        public void setDistribution ( final String distribution )
        {
            this.distribution = distribution;
        }

        public String getDistribution ()
        {
            return this.distribution;
        }

        public String getPackager ()
        {
            return this.packager;
        }

        public void setPackager ( final String packager )
        {
            this.packager = packager;
        }

        public String getVendor ()
        {
            return this.vendor;
        }

        public void setVendor ( final String vendor )
        {
            this.vendor = vendor;
        }

        public void setLicense ( final String license )
        {
            this.license = license;
        }

        public String getLicense ()
        {
            return this.license;
        }

        public void setBuildHost ( final String buildHost )
        {
            this.buildHost = buildHost;
        }

        public String getBuildHost ()
        {
            return this.buildHost;
        }

        public void setDescription ( final String description )
        {
            this.description = description;
        }

        public String getDescription ()
        {
            return this.description;
        }

        public void setSummary ( final String summary )
        {
            this.summary = summary;
        }

        public String getSummary ()
        {
            return this.summary;
        }

        public void setGroup ( final String group )
        {
            this.group = group;
        }

        public String getGroup ()
        {
            return this.group;
        }

        public void setOperatingSystem ( final String operatingSystem )
        {
            this.operatingSystem = operatingSystem;
        }

        public String getOperatingSystem ()
        {
            return this.operatingSystem;
        }

        public void setUrl ( final String url )
        {
            this.url = url;
        }

        public String getUrl ()
        {
            return this.url;
        }

        public void setSourcePackage ( final String sourcePackage )
        {
            this.sourcePackage = sourcePackage;
        }

        public String getSourcePackage ()
        {
            return this.sourcePackage;
        }
    }

    private static abstract class BuilderContextImpl implements BuilderContext
    {
        private FileInformationProvider<Object> defaultProvider = BuilderContext.defaultProvider ();

        @Override
        public void setDefaultInformationProvider ( final FileInformationProvider<Object> provider )
        {
            this.defaultProvider = provider;
        }

        @Override
        public FileInformationProvider<Object> getDefaultInformationProvider ()
        {
            return this.defaultProvider;
        }

        protected <T> FileInformation makeInformation ( final String targetName, final T source, final PayloadEntryType type, final FileInformationProvider<T> provider ) throws IOException
        {
            if ( provider != null )
            {
                return provider.provide ( targetName, source, type );
            }
            if ( this.defaultProvider != null )
            {
                return this.defaultProvider.provide ( targetName, source, type );
            }

            throw new IllegalStateException ( "There was neither a default provider nor a specfic provider set" );
        }

        private static String getNotEmptyOrDefault ( final String value, final String defaultValue )
        {
            if ( value == null || value.isEmpty () )
            {
                return defaultValue;
            }
            return value;
        }

        protected void customizeCommon ( final FileEntry entry, final FileInformation information )
        {
            entry.setUser ( getNotEmptyOrDefault ( information.getUser (), BuilderContext.DEFAULT_USER ) );
            entry.setGroup ( getNotEmptyOrDefault ( information.getGroup (), BuilderContext.DEFAULT_GROUP ) );
            // modes are set in specific add methods
        }

        protected void customizeDirectory ( final FileEntry entry, final FileInformation information )
        {
            customizeCommon ( entry, information );
        }

        protected void customizeFile ( final FileEntry entry, final FileInformation information )
        {
            customizeCommon ( entry, information );
            if ( !information.getFileFlags ().isEmpty () )
            {
                for ( final FileFlags fileFlag : information.getFileFlags () )
                {
                    entry.setFlags ( entry.getFlags () | fileFlag.getValue () );
                }
            }
        }

        protected void customizeSymbolicLink ( final FileEntry entry, final FileInformation information )
        {
            customizeCommon ( entry, information );
        }
    }

    private static final String DEFAULT_INTERPRETER = "/bin/sh";

    protected final Header<RpmTag> header = new Header<> ();

    private final String name;

    private final RpmVersion version;

    private final String architecture;

    protected final PayloadRecorder recorder;

    private final Path targetFile;

    private final Set<Dependency> provides = new HashSet<> ();

    private final Set<Dependency> requirements = new HashSet<> ();

    private final Set<Dependency> conflicts = new HashSet<> ();

    private final Set<Dependency> obsoletes = new HashSet<> ();

    private final Set<Dependency> suggests = new HashSet<> ();

    private final Set<Dependency> recommends = new HashSet<> ();

    private final Set<Dependency> supplements = new HashSet<> ();

    private final Set<Dependency> enhances = new HashSet<> ();

    private final Map<String, FileEntry> files = new HashMap<> ();

    private PackageInformation information = new PackageInformation ();

    private boolean hasBuilt;

    private int currentInode = 1;

    private final List<SignatureProcessor> signatureProcessors = new LinkedList<> ();

    private final BuilderOptions options;

    private Architecture leadOverrideArchitecture;

    private OperatingSystem leadOverrideOperatingSystem;

    private Version requiredRpmVersion = Version.V4_11;

    private Consumer<Header<RpmTag>> headerCustomizer;

    public RpmBuilder ( final String name, final String version, final String release, final Path target ) throws IOException
    {
        this ( name, version, release, "noarch", target );
    }

    public RpmBuilder ( final String name, final RpmVersion version, final String architecture, final Path targetFile, final OpenOption... openOptions ) throws IOException
    {
        this ( name, version, architecture, targetFile, makeBuilderOptions ( openOptions ) );
    }

    private static BuilderOptions makeBuilderOptions ( final OpenOption[] openOptions )
    {
        final BuilderOptions options = new BuilderOptions ();
        options.setOpenOptions ( openOptions );
        return options;
    }

    public RpmBuilder ( final String name, final RpmVersion version, final String architecture, final Path targetFile, final BuilderOptions options ) throws IOException
    {
        this.name = name;
        this.version = version;
        this.architecture = architecture;

        this.options = options == null ? new BuilderOptions () : new BuilderOptions ( options );

        this.targetFile = makeTargetFile ( targetFile );

        this.recorder = new PayloadRecorder ( true, this.options.getPayloadCoding (), this.options.getPayloadFlags (), this.options.getFileDigestAlgorithm () );

        addDefaultSignatureProcessors ();
    }

    public RpmBuilder ( final String name, final String version, final String release, final String architecture, final Path target, final OpenOption... openOptions ) throws IOException
    {
        this ( name, new RpmVersion ( null, version, release ), architecture, target, openOptions );
    }

    public void addSignatureProcessor ( final SignatureProcessor processor )
    {
        this.signatureProcessors.add ( processor );
    }

    public void removeAllSignatureProcessors ()
    {
        this.signatureProcessors.clear ();
    }

    public void addDefaultSignatureProcessors ()
    {
        addSignatureProcessor ( SignatureProcessors.size () );
        addSignatureProcessor ( SignatureProcessors.sha256Header () );
        addSignatureProcessor ( SignatureProcessors.sha1Header () );
        addSignatureProcessor ( SignatureProcessors.md5 () );
        addSignatureProcessor ( SignatureProcessors.payloadSize () );
    }

    public void setLeadOverrideArchitecture ( final Architecture leadOverrideArchitecture )
    {
        this.leadOverrideArchitecture = leadOverrideArchitecture;
    }

    public void setLeadOverrideOperatingSystem ( final OperatingSystem leadOverrideOperatingSystem )
    {
        this.leadOverrideOperatingSystem = leadOverrideOperatingSystem;
    }

    public void setHeaderCustomizer ( final Consumer<Header<RpmTag>> headerCustomizer )
    {
        this.headerCustomizer = headerCustomizer;
    }

    /**
     * Fill extra requirements the RPM file itself may have
     *
     * @throws IOException
     */
    private void fillRequirements () throws IOException
    {
        this.requirements.add ( new Dependency ( "rpmlib(CompressedFileNames)", "3.0.4-1", RpmDependencyFlags.LESS, RpmDependencyFlags.EQUAL, RpmDependencyFlags.RPMLIB ) );

        if ( !this.options.getFileDigestAlgorithm ().equals ( DigestAlgorithm.MD5 ) )
        {
            this.requirements.add ( new Dependency ( "rpmlib(FileDigests)", "4.6.0-1", RpmDependencyFlags.LESS, RpmDependencyFlags.EQUAL, RpmDependencyFlags.RPMLIB ) );
        }

        this.requirements.add ( new Dependency ( "rpmlib(PayloadFilesHavePrefix)", "4.0-1", RpmDependencyFlags.LESS, RpmDependencyFlags.EQUAL, RpmDependencyFlags.RPMLIB ) );
        this.options.getPayloadCoding ().createProvider ().fillRequirements ( this.requirements::add );
    }

    private void fillProvides ()
    {
        this.provides.add ( new Dependency ( this.name, this.version.toString (), RpmDependencyFlags.EQUAL ) );
    }

    private void fillHeader ()
    {
        this.header.putString ( RpmTag.PAYLOAD_FORMAT, "cpio" );

        if ( this.recorder.getPayloadCoding () != null )
        {
            this.header.putString ( RpmTag.PAYLOAD_CODING, this.recorder.getPayloadCoding ().getValue () );
        }

        if ( this.recorder.getPayloadFlags ().isPresent () )
        {
            this.header.putString ( RpmTag.PAYLOAD_FLAGS, this.recorder.getPayloadFlags ().get () );
        }

        this.header.putStringArray ( 100, "C" );

        this.header.putString ( RpmTag.NAME, this.name );
        this.header.putString ( RpmTag.VERSION, this.version.getVersion () );
        if ( this.version.getRelease ().isPresent () )
        {
            this.header.putString ( RpmTag.RELEASE, this.version.getRelease ().get () );
        }
        if ( this.version.getEpoch ().isPresent () )
        {
            this.header.putInt ( RpmTag.EPOCH, this.version.getEpoch ().get () );
        }

        this.header.putString ( RpmTag.LICENSE, this.information.getLicense () );
        this.header.putStringOptional ( RpmTag.DISTRIBUTION, this.information.getDistribution () );
        this.header.putStringOptional ( RpmTag.PACKAGER, this.information.getPackager () );
        this.header.putStringOptional ( RpmTag.VENDOR, this.information.getVendor () );
        this.header.putStringOptional ( RpmTag.URL, this.information.getUrl () );

        this.header.putInt ( RpmTag.BUILDTIME, (int) ( System.currentTimeMillis () / 1000 ) );
        this.header.putString ( RpmTag.BUILDHOST, this.information.getBuildHost () );

        this.header.putI18nString ( RpmTag.SUMMARY, this.information.getSummary () );
        this.header.putI18nString ( RpmTag.DESCRIPTION, this.information.getDescription () );

        this.header.putI18nString ( RpmTag.GROUP, this.information.getGroup () );
        this.header.putString ( RpmTag.ARCH, this.architecture );
        this.header.putString ( RpmTag.OS, this.information.getOperatingSystem () );
        this.header.putStringOptional ( RpmTag.SOURCE_PACKAGE, this.information.getSourcePackage () );

        Dependencies.putProvides ( this.header, this.provides );
        Dependencies.putRequirements ( this.header, this.requirements );
        Dependencies.putConflicts ( this.header, this.conflicts );
        Dependencies.putObsoletes ( this.header, this.obsoletes );
        Dependencies.putSuggests ( this.header, this.suggests );
        Dependencies.putRecommends ( this.header, this.recommends );
        Dependencies.putSupplements ( this.header, this.supplements );
        Dependencies.putEnhances ( this.header, this.enhances );

        if ( !this.files.isEmpty () )
        {
            if ( !this.options.getFileDigestAlgorithm ().equals ( DigestAlgorithm.MD5 ) )
            {
                this.header.putInt ( RpmTag.FILE_DIGESTALGO, this.options.getFileDigestAlgorithm ().getTag () );
            }

            final FileEntry[] files = this.files.values ().toArray ( new FileEntry[this.files.size ()] );
            Arrays.sort ( files, comparing ( FileEntry::getTargetName ) );

            final long installedSize = Arrays.stream ( files ).mapToLong ( FileEntry::getTargetSize ).sum ();
            this.header.putSize ( installedSize, RpmTag.SIZE, RpmTag.LONGSIZE );

            final Collection<FileEntry> filesList = Arrays.asList ( files );

            // TODO: implement LONG file sizes
            Header.putIntFields ( this.header, filesList, RpmTag.FILE_SIZES, entry -> (int)entry.getSize () );
            Header.putShortFields ( this.header, filesList, RpmTag.FILE_MODES, FileEntry::getMode );
            Header.putShortFields ( this.header, filesList, RpmTag.FILE_RDEVS, FileEntry::getRdevs );
            Header.putIntFields ( this.header, filesList, RpmTag.FILE_MTIMES, FileEntry::getModificationTime );
            Header.putFields ( this.header, filesList, RpmTag.FILE_DIGESTS, String[]::new, FileEntry::getDigest, Header::putStringArray );
            Header.putFields ( this.header, filesList, RpmTag.FILE_LINKTO, String[]::new, FileEntry::getLinkTo, Header::putStringArray );
            Header.putIntFields ( this.header, filesList, RpmTag.FILE_FLAGS, FileEntry::getFlags );
            Header.putFields ( this.header, filesList, RpmTag.FILE_USERNAME, String[]::new, FileEntry::getUser, Header::putStringArray );
            Header.putFields ( this.header, filesList, RpmTag.FILE_GROUPNAME, String[]::new, FileEntry::getGroup, Header::putStringArray );

            Header.putIntFields ( this.header, filesList, RpmTag.FILE_VERIFYFLAGS, FileEntry::getVerifyFlags );

            putNumber ( this.options.getLongMode (), this.header, filesList, RpmTag.FILE_DEVICES, FileEntry::getDevice );
            putNumber ( this.options.getLongMode (), this.header, filesList, RpmTag.FILE_INODES, FileEntry::getInode );

            Header.putFields ( this.header, filesList, RpmTag.FILE_LANGS, String[]::new, FileEntry::getLang, Header::putStringArray );

            Header.putFields ( this.header, filesList, RpmTag.BASENAMES, String[]::new, fe -> fe.getTargetName ().getBasename (), Header::putStringArray );

            {
                // compress file names

                String currentDirName = null;
                final List<String> dirnames = new ArrayList<> ();
                final int[] dirIndexes = new int[files.length];
                int pos = -1;
                int i = 0;
                for ( final FileEntry f : files )
                {
                    final String dirname = f.getTargetName ().getDirname ();
                    if ( currentDirName == null || !currentDirName.equals ( dirname ) )
                    {
                        currentDirName = dirname;
                        if ( !dirname.isEmpty () )
                        {
                            dirnames.add ( "/" + dirname + "/" );
                        }
                        else
                        {
                            dirnames.add ( "/" );
                        }
                        pos++;
                    }
                    dirIndexes[i] = pos;
                    i++;
                }
                this.header.putInt ( RpmTag.DIR_INDEXES, dirIndexes );
                this.header.putStringArray ( RpmTag.DIRNAMES, dirnames.toArray ( new String[dirnames.size ()] ) );
            }
        }
        else
        {
            this.header.putSize ( 0, RpmTag.SIZE, RpmTag.LONGSIZE );
        }
    }

    private static void putNumber ( final LongMode longMode, final Header<RpmTag> header, final Collection<FileEntry> files, final RpmTag tag, final ToLongFunction<FileEntry> func )
    {
        boolean useLong;
        if ( longMode == LongMode.FORCE_64BIT )
        {
            // no need to check, got with 64bit
            useLong = true;
        }
        else
        {
            // check if we need 64bit
            final boolean needLong = needLong ( files, func );
            if ( longMode == LongMode.FORCE_32BIT )
            {
                if ( needLong )
                {
                    // we need it but are forced to 32bit
                    throw new IllegalStateException ( "Requested 32bit mode, but 64bit is necessary" );
                }
                else
                {
                    // we don't need it, so we can accept 32bit
                    useLong = false;
                }
            }
            else // everything else is DEFAULT
            {
                // we can choose, so choose
                useLong = needLong;
            }
        }

        if ( useLong )
        {
            // write as 64bit
            Header.putLongFields ( header, files, tag, func );
        }
        else
        {
            // write as 32bit
            Header.putIntFields ( header, files, tag, file -> (int)func.applyAsLong ( file ) );
        }
    }

    private static boolean needLong ( final Collection<FileEntry> files, final ToLongFunction<FileEntry> func )
    {
        return files.stream ().anyMatch ( file -> func.applyAsLong ( file ) > Integer.MAX_VALUE );
    }

    private Path makeTargetFile ( final Path target )
    {
        if ( Files.isDirectory ( target ) )
        {
            return target.resolve ( makeDefaultFileName () );
        }
        else
        {
            return target;
        }
    }

    private String makeDefaultFileName ()
    {
        return this.options.getFileNameProvider ().getRpmFileName ( this.name, this.version, this.architecture );
    }

    /**
     * Return the list of features supported by this builder.
     *
     * @return the list of features supported by this builder
     */
    public static List<Feature> getFeatures ()
    {
        return features;
    }

    /**
     * Get the current package information
     *
     * @return the current package information. Never returns
     *         {@code null}.
     */
    public PackageInformation getInformation ()
    {
        return this.information;
    }

    /**
     * Completely set the current package information
     *
     * @param information
     *            the new package information, may be {@code null}, in which
     *            case the package information is reset to its defaults.
     */
    public void setInformation ( final PackageInformation information )
    {
        this.information = information != null ? information : new PackageInformation ();
    }

    public Path getTargetFile ()
    {
        return this.targetFile;
    }

    public String getArchitecture ()
    {
        return this.architecture;
    }

    public RpmVersion getVersion ()
    {
        return this.version;
    }

    public String getName ()
    {
        return this.name;
    }

    /**
     * Actually build the RPM file
     *

     * Note:  this method may only be called once per instance
     *

     *
     * @throws IOException
     *             in case of any IO error
     */
    public void build () throws IOException
    {
        if ( this.hasBuilt )
        {
            throw new IllegalStateException ( "RPM file has already been built. Can only be built once." );
        }

        this.hasBuilt = true;

        fillProvides ();
        fillRequirements ();

        fillHeader ();

        final LeadBuilder leadBuilder = new LeadBuilder ( this.name, this.version );

        leadBuilder.fillFlagsFromHeader ( this.header, createLeadArchitectureMapper (), createLeadOperatingSystemMapper () );

        if ( this.headerCustomizer != null )
        {
            this.headerCustomizer.accept ( this.header );
        }

        try ( final RpmWriter writer = new RpmWriter ( this.targetFile, leadBuilder, this.header, this.options.getHeaderCharset (), this.options.getOpenOptions () ) )
        {
            writer.addAllSignatureProcessors ( this.signatureProcessors );
            writer.setPayload ( this.recorder );
        }
    }

    private Function<String, Optional<Architecture>> createLeadArchitectureMapper ()
    {
        if ( this.leadOverrideArchitecture != null )
        {
            return s -> of ( this.leadOverrideArchitecture );
        }
        else
        {
            return Architecture::fromAlias;
        }
    }

    private Function<String, Optional<OperatingSystem>> createLeadOperatingSystemMapper ()
    {
        if ( this.leadOverrideOperatingSystem != null )
        {
            return s -> of ( this.leadOverrideOperatingSystem );
        }
        else
        {
            return OperatingSystem::fromAlias;
        }
    }

    @Override
    public void close () throws IOException
    {
        this.recorder.close ();
    }

    private void triggerVersion ( final Version version )
    {
        if ( version.compareTo ( this.requiredRpmVersion ) <= 0 )
        {
            return;
        }

        this.requiredRpmVersion = version;
    }

    public void addRequirement ( final String name, final String version, final RpmDependencyFlags... flags )
    {
        this.requirements.add ( new Dependency ( name, version, flags ) );
    }

    public void addProvides ( final String name, final String version, final RpmDependencyFlags... flags )
    {
        this.provides.add ( new Dependency ( name, version, flags ) );
    }

    public void addConflicts ( final String name, final String version, final RpmDependencyFlags... flags )
    {
        this.conflicts.add ( new Dependency ( name, version, flags ) );
    }

    public void addObsoletes ( final String name, final String version, final RpmDependencyFlags... flags )
    {
        this.obsoletes.add ( new Dependency ( name, version, flags ) );
    }

    public void addSuggests ( final String name, final String version, final RpmDependencyFlags... flags )
    {
        triggerVersion ( Version.V4_12 );
        this.suggests.add ( new Dependency ( name, version, flags ) );
    }

    public void addRecommends ( final String name, final String version, final RpmDependencyFlags... flags )
    {
        triggerVersion ( Version.V4_12 );
        this.recommends.add ( new Dependency ( name, version, flags ) );
    }

    public void addSupplements ( final String name, final String version, final RpmDependencyFlags... flags )
    {
        triggerVersion ( Version.V4_12 );
        this.supplements.add ( new Dependency ( name, version, flags ) );
    }

    public void addEnhances ( final String name, final String version, final RpmDependencyFlags... flags )
    {
        triggerVersion ( Version.V4_12 );
        this.enhances.add ( new Dependency ( name, version, flags ) );
    }

    private void addFile ( final String targetName, final Path sourcePath, final int mode, final Instant mtime, final Consumer<FileEntry> customizer ) throws IOException
    {
        addFile ( targetName, sourcePath, customizer, mode, mtime, PayloadRecorder::addFile );
    }

    private <T> void addFile ( final String targetName, final T sourcePath, final Consumer<FileEntry> customizer, final int mode, final Instant fileModificationInstant, final RecorderFunction<T> func ) throws IOException
    {
        final PathName pathName = PathName.parse ( targetName );

        final long mtime = fileModificationInstant.getEpochSecond ();
        final int inode = this.currentInode++;

        final short smode = (short) ( mode | CpioConstants.C_ISREG );

        final Result result = func.record ( this.recorder, "./" + pathName.toString (), sourcePath, cpioCustomizer ( mtime, inode, smode ) );

        Consumer<FileEntry> c = this::initEntry;
        c = c.andThen ( entry -> {
            entry.setModificationTime ( (int)mtime );
            entry.setInode ( inode );
            entry.setMode ( smode );
        } );

        if ( customizer != null )
        {
            c = c.andThen ( customizer );
        }

        addResult ( pathName, result, c );
    }

    private void addDirectory ( final String targetName, final int mode, final Instant modInstant, final Consumer<FileEntry> customizer ) throws IOException
    {
        final PathName pathName = PathName.parse ( targetName );

        final long mtime = modInstant.getEpochSecond ();
        final int inode = this.currentInode++;

        final short smode = (short) ( mode | CpioConstants.C_ISDIR );

        final Result result = this.recorder.addDirectory ( "./" + pathName.toString (), cpioCustomizer ( mtime, inode, smode ) );

        Consumer<FileEntry> c = this::initEntry;
        c = c.andThen ( entry -> {
            entry.setModificationTime ( (int)mtime );
            entry.setInode ( inode );
            entry.setMode ( smode );
            entry.setSize ( 0 );
            entry.setTargetSize ( 4096 );
        } );

        if ( customizer != null )
        {
            c = c.andThen ( customizer );
        }

        addResult ( pathName, result, c );
    }

    private void addSymbolicLink ( final String targetName, final String linkTo, final int mode, final Instant modInstant, final Consumer<FileEntry> customizer ) throws IOException
    {
        final PathName pathName = PathName.parse ( targetName );

        final long mtime = modInstant.getEpochSecond ();
        final int inode = this.currentInode++;

        final short smode = (short) ( mode | CpioConstants.C_ISLNK );

        final Result result = this.recorder.addSymbolicLink ( "./" + pathName.toString (), linkTo, cpioCustomizer ( mtime, inode, smode ) );

        Consumer<FileEntry> c = this::initEntry;
        c = c.andThen ( entry -> {
            entry.setModificationTime ( (int)mtime );
            entry.setInode ( inode );
            entry.setMode ( smode );
            entry.setSize ( result.getSize () );
            entry.setTargetSize ( result.getSize () );
            entry.setLinkTo ( linkTo );
        } );

        if ( customizer != null )
        {
            c = c.andThen ( customizer );
        }

        addResult ( pathName, result, c );
    }

    private void addFile ( final String targetName, final InputStream stream, final int mode, final Instant modInstant, final Consumer<FileEntry> customizer ) throws IOException
    {
        addFile ( targetName, stream, customizer, mode, modInstant, PayloadRecorder::addFile );
    }

    private void addFile ( final String targetName, final ByteBuffer data, final int mode, final Instant modInstant, final Consumer<FileEntry> customizer ) throws IOException
    {
        addFile ( targetName, data, customizer, mode, modInstant, PayloadRecorder::addFile );
    }

    private Consumer<CpioArchiveEntry> cpioCustomizer ( final long mtime, final int inode, final short mode )
    {
        return entry -> {
            entry.setTime ( mtime );
            entry.setInode ( inode );
            entry.setMode ( mode & 0xFFFF );
            entry.setDeviceMaj ( 8 );
            entry.setDeviceMin ( 17 );
        };
    }

    private void initEntry ( final FileEntry entry )
    {
        entry.setDevice ( 1 );
        entry.setModificationTime ( (int) ( System.currentTimeMillis () / 1000 ) );
    }

    private void addResult ( final PathName targetName, final Result result, final Consumer<FileEntry> customizer )
    {
        final FileEntry entry = new FileEntry ();

        // set basic file attributes

        entry.setSize ( result.getSize () );
        entry.setTargetSize ( result.getSize () );
        entry.setDigest ( result.getDigest () != null ? Rpms.toHex ( result.getDigest () ).toLowerCase () : "" );
        entry.setTargetName ( targetName );

        // run customizer

        if ( customizer != null )
        {
            customizer.accept ( entry );
        }

        // record file entry

        this.files.put ( targetName.toString (), entry );
    }

    public BuilderContext newContext ()
    {
        return new BuilderContextImpl () {

            @Override
            public void addFile ( final String targetName, final Path source, final FileInformationProvider<? super Path> provider ) throws IOException
            {
                if ( !Files.isRegularFile ( source ) )
                {
                    throw new IllegalArgumentException ( String.format ( "'%s' is not a regular file", source ) );
                }
                final FileInformation info = makeInformation ( targetName, source, PayloadEntryType.FILE, provider );
                RpmBuilder.this.addFile ( targetName, source, info.getMode (), info.getTimestamp (), entry -> customizeFile ( entry, info ) );
            }

            @Override
            public void addFile ( final String targetName, final InputStream source, final FileInformationProvider<Object> provider ) throws IOException
            {
                final FileInformation info = makeInformation ( targetName, source, PayloadEntryType.FILE, provider );
                RpmBuilder.this.addFile ( targetName, source, info.getMode (), info.getTimestamp (), entry -> customizeFile ( entry, info ) );
            }

            @Override
            public void addFile ( final String targetName, final ByteBuffer source, final FileInformationProvider<Object> provider ) throws IOException
            {
                final FileInformation info = makeInformation ( targetName, source, PayloadEntryType.FILE, provider );
                RpmBuilder.this.addFile ( targetName, source, info.getMode (), info.getTimestamp (), entry -> customizeFile ( entry, info ) );
            }

            @Override
            public void addDirectory ( final String targetName, final FileInformationProvider<? super Directory> provider ) throws IOException
            {
                final FileInformation info = makeInformation ( targetName, BuilderContext.DIRECTORY, PayloadEntryType.DIRECTORY, provider );
                RpmBuilder.this.addDirectory ( targetName, info.getMode (), info.getTimestamp (), entry -> customizeDirectory ( entry, info ) );
            }

            @Override
            public void addSymbolicLink ( final String targetName, final String linkTo, final FileInformationProvider<? super SymbolicLink> provider ) throws IOException
            {
                final FileInformation info = makeInformation ( targetName, BuilderContext.SYMBOLIC_LINK, PayloadEntryType.SYMBOLIC_LINK, provider );
                RpmBuilder.this.addSymbolicLink ( targetName, linkTo, info.getMode (), info.getTimestamp (), entry -> customizeSymbolicLink ( entry, info ) );
            }
        };
    }

    public void setPreInstallationScript ( final String interpreter, final String script )
    {
        addRequirement ( interpreter, null, RpmDependencyFlags.INTERPRETER );
        addRequirement ( interpreter, null, RpmDependencyFlags.SCRIPT_PRE, RpmDependencyFlags.INTERPRETER );
        setScript ( RpmTag.PREINSTALL_SCRIPT_PROG, RpmTag.PREINSTALL_SCRIPT, interpreter, script );
    }

    public void setPreInstallationScript ( final String script )
    {
        setPreInstallationScript ( DEFAULT_INTERPRETER, script );
    }

    public void setPostInstallationScript ( final String interpreter, final String script )
    {
        addRequirement ( interpreter, null, RpmDependencyFlags.INTERPRETER );
        addRequirement ( interpreter, null, RpmDependencyFlags.SCRIPT_POST, RpmDependencyFlags.INTERPRETER );
        setScript ( RpmTag.POSTINSTALL_SCRIPT_PROG, RpmTag.POSTINSTALL_SCRIPT, interpreter, script );
    }

    public void setPostInstallationScript ( final String script )
    {
        setPostInstallationScript ( DEFAULT_INTERPRETER, script );
    }

    public void setPreRemoveScript ( final String interpreter, final String script )
    {
        addRequirement ( interpreter, null, RpmDependencyFlags.INTERPRETER );
        addRequirement ( interpreter, null, RpmDependencyFlags.SCRIPT_PREUN, RpmDependencyFlags.INTERPRETER );
        setScript ( RpmTag.PREREMOVE_SCRIPT_PROG, RpmTag.PREREMOVE_SCRIPT, interpreter, script );
    }

    public void setPreRemoveScript ( final String script )
    {
        setPreRemoveScript ( DEFAULT_INTERPRETER, script );
    }

    public void setPostRemoveScript ( final String interpreter, final String script )
    {
        addRequirement ( interpreter, null, RpmDependencyFlags.INTERPRETER );
        addRequirement ( interpreter, null, RpmDependencyFlags.SCRIPT_POSTUN, RpmDependencyFlags.INTERPRETER );
        setScript ( RpmTag.POSTREMOVE_SCRIPT_PROG, RpmTag.POSTREMOVE_SCRIPT, interpreter, script );
    }

    public void setPostRemoveScript ( final String script )
    {
        setPostRemoveScript ( DEFAULT_INTERPRETER, script );
    }

    public void setVerifyScript ( final String interpreter, final String script )
    {
        addRequirement ( interpreter, null, RpmDependencyFlags.INTERPRETER );
        addRequirement ( interpreter, null, RpmDependencyFlags.SCRIPT_VERIFY, RpmDependencyFlags.INTERPRETER );
        setScript ( RpmTag.VERIFY_SCRIPT_PROG, RpmTag.VERIFY_SCRIPT, interpreter, script );
    }

    public void setVerifyScript ( final String script )
    {
        setVerifyScript ( DEFAULT_INTERPRETER, script );
    }

    private void setScript ( final RpmTag interpreterTag, final RpmTag scriptTag, final String interpreter, final String script )
    {
        if ( interpreter == null || script == null )
        {
            this.header.remove ( interpreterTag );
            this.header.remove ( scriptTag );
        }
        else
        {
            this.header.putString ( interpreterTag, interpreter );
            this.header.putString ( scriptTag, script );
        }
    }

    /**
     * Return the minimum required version of RPM for supporting all features of
     * this generated RPM.
     *
     * @return After the method {@link #build()} was called it returns the
     *         minimum required version of RPM. Before it will return the
     *         default version {@link Version#V4_11}.
     */
    public Version getRequiredRpmVersion ()
    {
        return this.requiredRpmVersion;
    }
}


/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 *
 *
 * This file incorporates work covered by the following copyright and
 * permission notice:
 *
 * Copyright 2004 The Apache Software Foundation
 *
 * Licensed 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.jasper.compiler;

import java.io.InputStream;
import java.net.URL;
import java.util.*;
import javax.servlet.ServletContext;
import javax.servlet.jsp.tagext.FunctionInfo;
import javax.servlet.jsp.tagext.TagLibraryInfo;
import javax.servlet.jsp.tagext.TagInfo;
import javax.servlet.jsp.tagext.TagFileInfo;
import org.apache.jasper.Constants;
import org.apache.jasper.JspCompilationContext;
import org.apache.jasper.JasperException;
import org.apache.jasper.xmlparser.ParserUtils;
import org.apache.jasper.xmlparser.TreeNode;

/**
 * Class responsible for generating an implicit tag library containing tag
 * handlers corresponding to the tag files in "/WEB-INF/tags/" or a
 * subdirectory of it.
 *
 * @author Jan Luehe
 */
class ImplicitTagLibraryInfo extends TagLibraryInfo {

    private static final String WEB_INF_TAGS = "/WEB-INF/tags";
    private static final String TAG_FILE_SUFFIX = ".tag";
    private static final String TAGX_FILE_SUFFIX = ".tagx";
    private static final String TAGS_SHORTNAME = "tags";
    private static final String TLIB_VERSION = "1.0";
    private static final String JSP_VERSION = "2.0";
    private static final String IMPLICIT_TLD = "implicit.tld";

    // Maps tag names to tag file paths
    private HashMap<String, String> tagFileMap;

    private ParserController pc;
    private PageInfo pageInfo;
    private ArrayList<TagFileInfo> vec;
    private ErrorDispatcher err;

    /**
     * Constructor.
     */
    public ImplicitTagLibraryInfo(JspCompilationContext ctxt,
				  ParserController pc,
				  String prefix,
				  String tagdir,
				  ErrorDispatcher err) throws JasperException {
        super(prefix, null);
	this.pc = pc;
        this.err = err;
        this.pageInfo = pc.getCompiler().getPageInfo();
	this.tagFileMap = new HashMap<String, String>();
	this.vec = new ArrayList<TagFileInfo>();

        // Implicit tag libraries have no functions:
        this.functions = new FunctionInfo[0];

	tlibversion = TLIB_VERSION;
	jspversion = JSP_VERSION;

	if (!tagdir.startsWith(WEB_INF_TAGS)) {
	    err.jspError("jsp.error.invalid.tagdir", tagdir);
	}

	// Determine the value of the <short-name> subelement of the
	// "imaginary" <taglib> element
	if (tagdir.equals(WEB_INF_TAGS)
	        || tagdir.equals( WEB_INF_TAGS + "/")) {
	    shortname = TAGS_SHORTNAME;
	} else {
	    shortname = tagdir.substring(WEB_INF_TAGS.length());
	    shortname = shortname.replace('/', '-');
	}

	// Populate mapping of tag names to tag file paths
	Set<String> dirList = ctxt.getResourcePaths(tagdir);
	if (dirList != null) {
	    Iterator it = dirList.iterator();
	    while (it.hasNext()) {
		String path = (String) it.next();
		if (path.endsWith(TAG_FILE_SUFFIX)
		        || path.endsWith(TAGX_FILE_SUFFIX)) {
		    /*
		     * Use the filename of the tag file, without the .tag or
		     * .tagx extension, respectively, as the <name> subelement
		     * of the "imaginary" <tag-file> element
		     */
		    String suffix = path.endsWith(TAG_FILE_SUFFIX) ?
			TAG_FILE_SUFFIX : TAGX_FILE_SUFFIX;
		    String tagName = path.substring(path.lastIndexOf("/") + 1);
		    tagName = tagName.substring(0,
						tagName.lastIndexOf(suffix));
		    tagFileMap.put(tagName, path);
                } else if (path.endsWith(IMPLICIT_TLD)) {
                    String tldName = path.substring(path.lastIndexOf("/") + 1);
                    if (IMPLICIT_TLD.equals(tldName)) {
                        parseImplicitTld(ctxt, path);
                    }
                }
	    }
	}
    }

    /**
     * Returns an array of TagLibraryInfo objects representing the entire set
     * of tag libraries (including this TagLibraryInfo) imported by taglib
     * directives in the translation unit that references this
     * TagLibraryInfo.
     *
     * If a tag library is imported more than once and bound to different
     * prefices, only the TagLibraryInfo bound to the first prefix must be
     * included in the returned array.
     *
     * @return Array of TagLibraryInfo objects representing the entire set
     * of tag libraries (including this TagLibraryInfo) imported by taglib
     * directives in the translation unit that references this TagLibraryInfo.
     *
     * @since 2.1
     */
    public TagLibraryInfo[] getTagLibraryInfos() {

        TagLibraryInfo[] taglibs = null;

        Collection c = pageInfo.getTaglibs();
        if (c != null) {
            Object[] objs = c.toArray();
            if (objs != null && objs.length > 0) {
                taglibs = new TagLibraryInfo[objs.length];
                for (int i=0; i<objs.length; i++) {
                    taglibs[i] = (TagLibraryInfo) objs[i];
                }
            }
        }

        return taglibs;
    }

    /**
     * Checks to see if the given tag name maps to a tag file path,
     * and if so, parses the corresponding tag file.
     *
     * @return The TagFileInfo corresponding to the given tag name, or null if
     * the given tag name is not implemented as a tag file
     */
    public TagFileInfo getTagFile(String shortName) {

	TagFileInfo tagFile = super.getTagFile(shortName);
	if (tagFile == null) {
	    String path = tagFileMap.get(shortName);
	    if (path == null) {
		return null;
	    }

	    TagInfo tagInfo = null;
	    try {
		tagInfo = TagFileProcessor.parseTagFileDirectives(pc,
								  shortName,
								  path,
								  this);
	    } catch (JasperException je) {
		throw new RuntimeException(je.toString());
	    }

	    tagFile = new TagFileInfo(shortName, path, tagInfo);
	    vec.add(tagFile);

            this.tagFiles = vec.toArray(new TagFileInfo[vec.size()]);
	}

	return tagFile;
    }

    /**
     * Parses the JSP version and tlib-version from the implicit.tld at the
     * given path.
     */
    private void parseImplicitTld(JspCompilationContext ctxt, String path)
            throws JasperException {

        InputStream is = null;
        TreeNode tld = null;

        try {
            URL uri = ctxt.getResource(path);
            if (uri == null) {
                // no implicit.tld
                return;
            }

            is = uri.openStream();
            /* SJSAS 6384538
            tld = new ParserUtils().parseXMLDocument(IMPLICIT_TLD, is);
            */
            // START SJSAS 6384538
            tld = new ParserUtils().parseXMLDocument(
                IMPLICIT_TLD, is, ctxt.getOptions().isValidationEnabled());
            // END SJSAS 6384538
        } catch (Exception ex) {
            throw new JasperException(ex);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (Throwable t) {}
            }
        }

        this.jspversion = tld.findAttribute("version");

        Iterator list = tld.findChildren();
        while (list.hasNext()) {
            TreeNode element = (TreeNode) list.next();
            String tname = element.getName();
            if ("tlibversion".equals(tname)
                    || "tlib-version".equals(tname)) {
                this.tlibversion = element.getBody();
            } else if ("jspversion".equals(tname)
                    || "jsp-version".equals(tname)) {
                this.jspversion = element.getBody();
            } else if (!"shortname".equals(tname)
                    && !"short-name".equals(tname)) {
                err.jspError("jsp.error.implicitTld.additionalElements",
                             path, tname);
            }
        }

        // JSP version in implicit.tld must be 2.0 or greater
        Double jspVersionDouble = Double.valueOf(this.jspversion);
        if (Double.compare(jspVersionDouble, Constants.JSP_VERSION_2_0) < 0) {
            err.jspError("jsp.error.implicitTld.jspVersion", path,
                         this.jspversion);
        }
    }

}


/*******************************************************************************
 * Copyright (c) 2015 IBH SYSTEMS GmbH.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.packagedrone.utils.io;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Stream;

import org.eclipse.packagedrone.utils.Strings;

public class OutputSpooler
{

    public class RecordingDigestOutputStream extends DigestOutputStream
    {
        private final String key;

        public RecordingDigestOutputStream ( final OutputStream stream, final MessageDigest digest, final String key )
        {
            super ( stream, digest );
            this.key = key;
        }

        @Override
        public void close () throws IOException
        {
            super.close ();

            final MessageDigest digest = getMessageDigest ();
            final byte[] result = digest.digest ();

            setResult ( this.key, result );
        }
    }

    public class CountingOutputStream extends FilterOutputStream
    {
        private final String key;

        private long count;

        public CountingOutputStream ( final String key, final OutputStream out )
        {
            super ( out );
            this.key = key;
        }

        @Override
        public void write ( final byte[] b, final int off, final int len ) throws IOException
        {
            this.out.write ( b, off, len );
            this.count += len;
        }

        @Override
        public void write ( final int b ) throws IOException
        {
            this.out.write ( b );
            this.count++;
        }

        @Override
        public void close () throws IOException
        {
            super.close ();
            setResultSize ( this.key, this.count );
        }

    }

    private class MultiplexStream extends OutputStream
    {
        private final OutputStream[] streams;

        public MultiplexStream ( final List<OutputStream> streams )
        {
            this.streams = streams.toArray ( new OutputStream[streams.size ()] );
        }

        @Override
        public void write ( final int b ) throws IOException
        {
            write ( new byte[] { (byte) ( b & 0xFF ) } );
        }

        @Override
        public void write ( final byte[] b, final int off, final int len ) throws IOException
        {
            forEach ( stream -> stream.write ( b, off, len ) );
        }

        @Override
        public void flush () throws IOException
        {
            forEach ( OutputStream::flush );
        }

        @Override
        public void close () throws IOException
        {
            final java.util.stream.Stream<OutputStream> s = Arrays.stream ( this.streams );
            OutputSpooler.closeAll ( s );
        }

        protected void forEach ( final IOConsumer<OutputStream> consumer ) throws IOException
        {
            for ( final OutputStream stream : this.streams )
            {
                consumer.accept ( stream );
            }
        }
    }

    private static class OutputEntry
    {
        private final String mimeType;

        private final IOFunction<OutputStream, OutputStream> transformer;

        public OutputEntry ( final String mimeType, final IOFunction<OutputStream, OutputStream> transformer )
        {
            this.mimeType = mimeType;
            this.transformer = transformer;
        }

        public String getMimeType ()
        {
            return this.mimeType;
        }

        public IOFunction<OutputStream, OutputStream> getTransformer ()
        {
            return this.transformer;
        }
    }

    private final Set<String> digests = new HashSet<> ();

    private final SpoolOutTarget target;

    private final Map<String, OutputEntry> outputs = new HashMap<> ();

    private final Map<String, String> checksums = new HashMap<> ();

    private final Map<String, Long> sizes = new HashMap<> ();

    public OutputSpooler ( final SpoolOutTarget target )
    {
        this.target = target;
    }

    public void addDigest ( final String algorithm )
    {
        this.digests.add ( algorithm );
    }

    public void addOutput ( final String fileName, final String mimeType )
    {
        addOutput ( fileName, mimeType, null );
    }

    public void addOutput ( final String fileName, final String mimeType, final IOFunction<OutputStream, OutputStream> transformer )
    {
        if ( transformer == null )
        {
            this.outputs.put ( fileName, new OutputEntry ( mimeType, output -> output ) );
        }
        else
        {
            this.outputs.put ( fileName, new OutputEntry ( mimeType, transformer ) );
        }
    }

    public void open ( final IOConsumer<OutputStream> consumer ) throws IOException
    {
        final List<OutputStream> streams = new LinkedList<> ();

        final Iterator<Entry<String, OutputEntry>> entries = this.outputs.entrySet ().iterator ();

        openNext ( streams, entries, stream -> {
            try ( final MultiplexStream multiplexStream = new MultiplexStream ( streams ) )
            {
                consumer.accept ( multiplexStream );
            }
        } );
    }

    protected void openNext ( final List<OutputStream> streams, final Iterator<Entry<String, OutputEntry>> entries, final IOConsumer<List<OutputStream>> streamsConsumer ) throws IOException
    {
        if ( !entries.hasNext () )
        {
            streamsConsumer.accept ( streams );
        }
        else
        {
            final Entry<String, OutputEntry> entry = entries.next ();
            this.target.spoolOut ( entry.getKey (), entry.getValue ().getMimeType (), stream -> {

                // add digesters

                for ( final String algo : this.digests )
                {
                    final String key = entry.getKey () + ":" + algo;
                    try
                    {
                        stream = new RecordingDigestOutputStream ( stream, MessageDigest.getInstance ( algo ), key );
                    }
                    catch ( final NoSuchAlgorithmException e )
                    {
                        throw new IOException ( e );
                    }
                }

                // add counter

                stream = new CountingOutputStream ( entry.getKey (), stream );

                // apply transformer

                stream = entry.getValue ().getTransformer ().apply ( stream );

                // add stream

                streams.add ( stream );

                // next

                openNext ( streams, entries, streamsConsumer );
            } );

        }
    }

    private void setResult ( final String key, final byte[] result )
    {
        this.checksums.put ( key, Strings.hex ( result ).toLowerCase () );
    }

    private void setResultSize ( final String key, final long length )
    {
        this.sizes.put ( key, length );
    }

    static void closeAll ( final Stream<OutputStream> stream ) throws IOException
    {
        final List<Exception> ex = new LinkedList<> ();

        stream.forEach ( s -> {
            try
            {
                s.close ();
            }
            catch ( final IOException e )
            {
                ex.add ( e );
            }
        } );

        if ( !ex.isEmpty () )
        {
            final IOException base = new IOException ();
            for ( final Exception e : ex )
            {
                base.addSuppressed ( e );
            }
            throw base;
        }
    }

    /**
     * Get the digest of a closed file
     *
     * @param fileName
     *            the file name to get the digest for
     * @param algorithm
     *            the digest algorithm
     * @return the digest or null if the digest was not requested.
     *         The digest will be lower case hex encoded.
     * @throws IllegalStateException
     *             If the file is still open or was never opened
     */
    public String getChecksum ( final String fileName, final String algorithm )
    {
        if ( !this.digests.contains ( algorithm ) )
        {
            return null;
        }

        final String result = this.checksums.get ( fileName + ":" + algorithm );

        if ( result == null )
        {
            throw new IllegalStateException ( String.format ( "Stream '%s' not closed.", fileName ) );
        }

        return result;
    }

    /**
     * Get the size of a closed file
     *
     * @param fileName
     *            the file name to get the size for
     * @return the size
     * @throws IllegalStateException
     *             If the file is still open or was never opened
     */
    public long getSize ( final String fileName )
    {
        final Long result = this.sizes.get ( fileName );

        if ( result == null )
        {
            throw new IllegalStateException ( String.format ( "Stream '%s' not closed or was not added", fileName ) );
        }

        return result;
    }
}