package org.apache.maven.shared.jar;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipException;

/**
 * Open a JAR file to be analyzed. Note that once created, the {@link #closeQuietly()} method should be called to
 * release the associated file handle.
 * <p/>
 * Typical usage:
 * <pre>
 *  JarAnalyzer jar = new JarAnalyzer( jarFile );
 * <p/>
 *  try
 *  {
 *      // do some analysis, such as:
 *      jarClasses = jarClassAnalyzer.analyze( jar );
 *  }
 *  finally
 *  {
 *      jar.closeQuietly();
 *  }
 * <p/>
 *  // use jar.getJarData() in some way, or the data returned by the JAR analyzer. jar itself can no longer be used.
 * </pre>
 * <p/>
 * Note: that the actual data is separated from this class by design to minimise the chance of forgetting to close the
 * JAR file. The {@link org.apache.maven.shared.jar.JarData} class exposed, as well as any data returned by actual
 * analyzers that use this class, can be used safely once this class is out of scope.
 *
 * @see org.apache.maven.shared.jar.identification.JarIdentificationAnalysis#analyze(JarAnalyzer)
 * @see org.apache.maven.shared.jar.classes.JarClassesAnalysis#analyze(JarAnalyzer)
 */
public class JarAnalyzer
{
    /**
     * Pattern to filter JAR entries for class files.
     *
     * @todo why are inner classes and other potentially valid classes omitted? (It flukes it by finding everything after $)
     */
    private static final Pattern CLASS_FILTER = Pattern.compile( "[A-Za-z0-9]*\\.class$" );

    /**
     * Pattern to filter JAR entries for Maven POM files.
     */
    private static final Pattern MAVEN_POM_FILTER = Pattern.compile( "META-INF/maven/.*/pom\\.xml$" );

    /**
     * Pattern to filter JAR entries for text files that may contain a version.
     */
    private static final Pattern VERSION_FILTER = Pattern.compile( "[Vv][Ee][Rr][Ss][Ii][Oo][Nn]" );

    /**
     * The associated JAR file.
     */
    private final JarFile jarFile;

    /**
     * Contains information about the data collected so far.
     */
    private final JarData jarData;

    /**
     * Constructor. Opens the JAR file, so should be matched by a call to {@link #closeQuietly()}.
     *
     * @param file the JAR file to open
     * @throws java.io.IOException if there is a problem opening the JAR file, or reading the manifest. The JAR file will be closed if this occurs.
     */
    public JarAnalyzer( File file )
        throws IOException
    {
        try
        {
            this.jarFile = new JarFile( file );
        }
        catch ( ZipException e )
        {
            ZipException ioe = new ZipException( "Failed to open file " + file + " : " + e.getMessage() );
            ioe.initCause( e );
            throw ioe;
        }

        // Obtain entries list.
        List entries = Collections.list( jarFile.entries() );

        // Sorting of list is done by name to ensure a bytecode hash is always consistent.
        Collections.sort( entries, new Comparator()
        {
            public int compare( Object o1, Object o2 )
            {
                JarEntry entry1 = (JarEntry) o1;
                JarEntry entry2 = (JarEntry) o2;

                return entry1.getName().compareTo( entry2.getName() );
            }
        } );

        Manifest manifest;
        try
        {
            manifest = jarFile.getManifest();
        }
        catch ( IOException e )
        {
            closeQuietly();
            throw e;
        }
        this.jarData = new JarData( file, manifest, entries );
    }

    /**
     * Get the data for an individual entry in the JAR. The caller should closeQuietly the input stream, and should not retain
     * the stream as the JAR file may be closed elsewhere.
     *
     * @param entry the JAR entry to read from
     * @return the input stream of the individual JAR entry.
     * @throws java.io.IOException if there is a problem opening the individual entry
     */
    public InputStream getEntryInputStream( JarEntry entry )
        throws IOException
    {
        return jarFile.getInputStream( entry );
    }

    /**
     * Close the associated JAR file, ignoring any errors that may occur.
     */
    public void closeQuietly()
    {
        try
        {
            jarFile.close();
        }
        catch ( IOException e )
        {
            // not much we can do about it but ignore it
        }
    }

    /**
     * Filter a list of JAR entries against the pattern.
     *
     * @param pattern the pattern to filter against
     * @return the list of files found, in {@link java.util.jar.JarEntry} elements
     */
    public List filterEntries( Pattern pattern )
    {
        List ret = new ArrayList();

        Iterator it = getEntries().iterator();
        while ( it.hasNext() )
        {
            JarEntry entry = (JarEntry) it.next();

            Matcher mat = pattern.matcher( entry.getName() );
            if ( mat.find() )
            {
                ret.add( entry );
            }
        }
        return ret;
    }

    /**
     * Get all the classes in the JAR.
     *
     * @return the list of files found, in {@link java.util.jar.JarEntry} elements
     */
    public List getClassEntries()
    {
        return filterEntries( CLASS_FILTER );
    }

    /**
     * Get all the Maven POM entries in the JAR.
     *
     * @return the list of files found, in {@link java.util.jar.JarEntry} elements
     */
    public List getMavenPomEntries()
    {
        return filterEntries( MAVEN_POM_FILTER );
    }

    /**
     * Get all the version text files in the JAR.
     *
     * @return the list of files found, in {@link java.util.jar.JarEntry} elements
     */
    public List getVersionEntries()
    {
        return filterEntries( VERSION_FILTER );
    }

    /**
     * Get all the contained files in the JAR.
     *
     * @return the list of files found, in {@link java.util.jar.JarEntry} elements
     */
    public List getEntries()
    {
        return jarData.getEntries();
    }

    /**
     * Get the file that was opened by this analyzer.
     *
     * @return the JAR file reference
     */
    public File getFile()
    {
        return jarData.getFile();
    }

    public JarData getJarData()
    {
        return jarData;
    }
}
