//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.util.resource;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/* ------------------------------------------------------------ */
class JarFileResource extends JarResource
{
    private static final Logger LOG = Log.getLogger(JarFileResource.class);
    private JarFile _jarFile;
    private File _file;
    private String[] _list;
    private JarEntry _entry;
    private boolean _directory;
    private String _jarUrl;
    private String _path;
    private boolean _exists;
    
    /* -------------------------------------------------------- */
    protected JarFileResource(URL url)
    {
        super(url);
    }
    
    /* ------------------------------------------------------------ */
    protected JarFileResource(URL url, boolean useCaches)
    {
        super(url, useCaches);
    }
   

    /* ------------------------------------------------------------ */
    @Override
    public synchronized void close()
    {
        _list=null;
        _entry=null;
        _file=null;
        //if the jvm is not doing url caching, then the JarFiles will not be cached either,
        //and so they are safe to close
        if (!getUseCaches())
        {
            if ( _jarFile != null )
            {
                try
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("Closing JarFile "+_jarFile.getName());
                    _jarFile.close();
                }
                catch ( IOException ioe )
                {
                    LOG.ignore(ioe);
                }
            }
        }
        _jarFile=null;
        super.close();
    }
    
    /* ------------------------------------------------------------ */
    @Override
    protected synchronized boolean checkConnection()
    {
        try
        {
            super.checkConnection();
        }
        finally
        {
            if (_jarConnection==null)
            {
                _entry=null;
                _file=null;
                _jarFile=null;
                _list=null;
            }
        }
        return _jarFile!=null;
    }


    /* ------------------------------------------------------------ */
    @Override
    protected synchronized void newConnection()
        throws IOException
    {
        super.newConnection();
        
        _entry=null;
        _file=null;
        _jarFile=null;
        _list=null;
        
        int sep = _urlString.indexOf("!/");
        _jarUrl=_urlString.substring(0,sep+2);
        _path=_urlString.substring(sep+2);
        if (_path.length()==0)
            _path=null;   
        _jarFile=_jarConnection.getJarFile();
        _file=new File(_jarFile.getName());
    }
    
    
    /* ------------------------------------------------------------ */
    /**
     * Returns true if the represented resource exists.
     */
    @Override

    public boolean exists()
    {
        if (_exists)
            return true;

        if (_urlString.endsWith("!/"))
        {
            
            String file_url=_urlString.substring(4,_urlString.length()-2);
            try{return newResource(file_url).exists();}
            catch(Exception e) {LOG.ignore(e); return false;}
        }
        
        boolean check=checkConnection();
        
        // Is this a root URL?
        if (_jarUrl!=null && _path==null)
        {
            // Then if it exists it is a directory
            _directory=check;
            return true;
        }
        else 
        {
            // Can we find a file for it?
            boolean close_jar_file= false;
            JarFile jar_file=null;
            if (check)
                // Yes
                jar_file=_jarFile;
            else
            {
                // No - so lets look if the root entry exists.
                try
                {
                    JarURLConnection c=(JarURLConnection)((new URL(_jarUrl)).openConnection());
                    c.setUseCaches(getUseCaches());
                    jar_file=c.getJarFile();
                    close_jar_file = !getUseCaches();
                }
                catch(Exception e)
                {
                       LOG.ignore(e);
                }
            }

            // Do we need to look more closely?
            if (jar_file!=null && _entry==null && !_directory)
            {
                // OK - we have a JarFile, lets look at the entries for our path
                Enumeration<JarEntry> e=jar_file.entries();
                while(e.hasMoreElements())
                {
                    JarEntry entry = e.nextElement();
                    String name=entry.getName().replace('\\','/');
                    
                    // Do we have a match
                    if (name.equals(_path))
                    {
                        _entry=entry;
                        // Is the match a directory
                        _directory=_path.endsWith("/");
                        break;
                    }
                    else if (_path.endsWith("/"))
                    {
                        if (name.startsWith(_path))
                        {
                            _directory=true;
                            break;
                        }
                    }
                    else if (name.startsWith(_path) && name.length()>_path.length() && name.charAt(_path.length())=='/')
                    {
                        _directory=true;
                        break;
                    }
                }
            }

            if(close_jar_file && jar_file!=null) 
            {
                try 
                {
                    jar_file.close();
                } 
                catch (IOException ioe) 
                {
                    LOG.ignore(ioe);
                }
            }
        }    
        
        _exists= ( _directory || _entry!=null);
        return _exists;
    }

    
    /* ------------------------------------------------------------ */
    /**
     * Returns true if the represented resource is a container/directory.
     * If the resource is not a file, resources ending with "/" are
     * considered directories.
     */
    @Override
    public boolean isDirectory()
    {
        return _urlString.endsWith("/") || exists() && _directory;
    }
    
    /* ------------------------------------------------------------ */
    /**
     * Returns the last modified time
     */
    @Override
    public long lastModified()
    {
        if (checkConnection() && _file!=null)
        {
            if (exists() && _entry!=null)
                return _entry.getTime();
            return _file.lastModified();
        }
        return -1;
    }

    /* ------------------------------------------------------------ */
    @Override
    public synchronized String[] list()
    {
        if (isDirectory() && _list==null)
        {
            List<String> list = null;
            try
            {
                list = listEntries();
            }
            catch (Exception e)
            {
                //Sun's JarURLConnection impl for jar: protocol will close a JarFile in its connect() method if
                //useCaches == false (eg someone called URLConnection with defaultUseCaches==true).
                //As their sun.net.www.protocol.jar package caches JarFiles and/or connections, we can wind up in 
                //the situation where the JarFile we have remembered in our _jarFile member has actually been closed
                //by other code.
                //So, do one retry to drop a connection and get a fresh JarFile
                LOG.warn("Retrying list:"+e);
                LOG.debug(e);
                release();
                list = listEntries();
            }

            if (list != null)
            {
                _list=new String[list.size()];
                list.toArray(_list);
            }  
        }
        return _list;
    }
    
    
    /* ------------------------------------------------------------ */
    private List<String> listEntries ()
    {
        checkConnection();
        
        ArrayList<String> list = new ArrayList<String>(32);
        JarFile jarFile=_jarFile;
        if(jarFile==null)
        {
            try
            {
                JarURLConnection jc=(JarURLConnection)((new URL(_jarUrl)).openConnection());
                jc.setUseCaches(getUseCaches());
                jarFile=jc.getJarFile();
            }
            catch(Exception e)
            {

                e.printStackTrace();
                 LOG.ignore(e);
            }
                if(jarFile==null)
                    throw new IllegalStateException();
        }
        
        Enumeration<JarEntry> e=jarFile.entries();
        String dir=_urlString.substring(_urlString.indexOf("!/")+2);
        while(e.hasMoreElements())
        {
            JarEntry entry = e.nextElement();               
            String name=entry.getName().replace('\\','/');               
            if(!name.startsWith(dir) || name.length()==dir.length())
            {
                continue;
            }
            String listName=name.substring(dir.length());               
            int dash=listName.indexOf('/');
            if (dash>=0)
            {
                //when listing jar:file urls, you get back one
                //entry for the dir itself, which we ignore
                if (dash==0 && listName.length()==1)
                    continue;
                //when listing jar:file urls, all files and
                //subdirs have a leading /, which we remove
                if (dash==0)
                    listName=listName.substring(dash+1, listName.length());
                else
                    listName=listName.substring(0,dash+1);
                
                if (list.contains(listName))
                    continue;
            }
            
            list.add(listName);
        }
        
        return list;
    }
    
    
    
    
    
    /* ------------------------------------------------------------ */
    /**
     * Return the length of the resource
     */
    @Override
    public long length()
    {
        if (isDirectory())
            return -1;

        if (_entry!=null)
            return _entry.getSize();
        
        return -1;
    }

    
    /**
     * Take a Resource that possibly might use URLConnection caching
     * and turn it into one that doesn't.
     * @param resource
     * @return the non-caching resource
     */
    public static Resource getNonCachingResource (Resource resource)
    {
        if (!(resource instanceof JarFileResource))
            return resource;
        
        JarFileResource oldResource = (JarFileResource)resource;
        
        JarFileResource newResource = new JarFileResource(oldResource.getURL(), false);
        return newResource;
        
    }
    
    /**
     * Check if this jar:file: resource is contained in the
     * named resource. Eg <code>jar:file:///a/b/c/foo.jar!/x.html</code> isContainedIn <code>file:///a/b/c/foo.jar</code>
     * @param resource
     * @return true if resource is contained in the named resource
     * @throws MalformedURLException
     */
    @Override
    public boolean isContainedIn (Resource resource) 
    throws MalformedURLException
    {
        String string = _urlString;
        int index = string.indexOf("!/");
        if (index > 0)
            string = string.substring(0,index);
        if (string.startsWith("jar:"))
            string = string.substring(4);
        URL url = new URL(string);
        return url.sameFile(resource.getURL());     
    }
}








