/*
 * 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.
 */
package org.apache.felix.framework.cache;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import org.apache.felix.framework.Logger;
import org.apache.felix.framework.util.FelixConstants;
import org.apache.felix.framework.util.Util;
import org.apache.felix.framework.util.WeakZipFileFactory;
import org.osgi.framework.Constants;

public class DirectoryContent implements Content
{
    private static final int BUFSIZE = 4096;
    private static final transient String EMBEDDED_DIRECTORY = "-embedded";
    private static final transient String LIBRARY_DIRECTORY = "-lib";

    private final Logger m_logger;
    private final Map m_configMap;
    private final WeakZipFileFactory m_zipFactory;
    private final Object m_revisionLock;
    private final File m_rootDir;
    private final File m_dir;
    private Map m_nativeLibMap;

    public DirectoryContent(Logger logger, Map configMap,
        WeakZipFileFactory zipFactory, Object revisionLock, File rootDir, File dir)
    {
        m_logger = logger;
        m_configMap = configMap;
        m_zipFactory = zipFactory;
        m_revisionLock = revisionLock;
        m_rootDir = rootDir;
        m_dir = dir;
    }

    public void close()
    {
        // Nothing to clean up.
    }

    public boolean hasEntry(String name) throws IllegalStateException
    {
        if ((name.length() > 0) && (name.charAt(0) == '/'))
        {
            name = name.substring(1);
        }

        // Return true if the file associated with the entry exists,
        // unless the entry name ends with "/", in which case only
        // return true if the file is really a directory.
        File file = new File(m_dir, name);
        return BundleCache.getSecureAction().fileExists(file)
            && (name.endsWith("/")
                ? BundleCache.getSecureAction().isFileDirectory(file) : true);
    }

    public Enumeration<String> getEntries()
    {
        // Wrap entries enumeration to filter non-matching entries.
        Enumeration<String> e = new EntriesEnumeration(m_dir);

        // Spec says to return null if there are no entries.
        return (e.hasMoreElements()) ? e : null;
    }

    public byte[] getEntryAsBytes(String name) throws IllegalStateException
    {
        if ((name.length() > 0) && (name.charAt(0) == '/'))
        {
            name = name.substring(1);
        }

        // Get the embedded resource.
        InputStream is = null;
        ByteArrayOutputStream baos = null;

        try
        {
            is = new BufferedInputStream(
                BundleCache.getSecureAction().getFileInputStream(new File(m_dir, name)));
            baos = new ByteArrayOutputStream(BUFSIZE);
            byte[] buf = new byte[BUFSIZE];
            int n = 0;
            while ((n = is.read(buf, 0, buf.length)) >= 0)
            {
                baos.write(buf, 0, n);
            }
            return baos.toByteArray();

        }
        catch (Exception ex)
        {
            return null;
        }
        finally
        {
            try
            {
                if (baos != null) baos.close();
            }
            catch (Exception ex)
            {
            }
            try
            {
                if (is != null) is.close();
            }
            catch (Exception ex)
            {
            }
        }
    }

    public InputStream getEntryAsStream(String name)
        throws IllegalStateException, IOException
    {
        if ((name.length() > 0) && (name.charAt(0) == '/'))
        {
            name = name.substring(1);
        }

        return BundleCache.getSecureAction().getFileInputStream(new File(m_dir, name));
    }

    public URL getEntryAsURL(String name)
    {
        if ((name.length() > 0) && (name.charAt(0) == '/'))
        {
            name = name.substring(1);
        }

        try
        {
            return BundleCache.getSecureAction().toURI(new File(m_dir, name)).toURL();
        }
        catch (MalformedURLException e)
        {
            return null;
        }
    }

    public Content getEntryAsContent(String entryName)
    {
        // If the entry name refers to the content itself, then
        // just return it immediately.
        if (entryName.equals(FelixConstants.CLASS_PATH_DOT))
        {
            return new DirectoryContent(
                m_logger, m_configMap, m_zipFactory, m_revisionLock, m_rootDir, m_dir);
        }

        // Remove any leading slash, since all bundle class path
        // entries are relative to the root of the bundle.
        entryName = (entryName.startsWith("/")) ? entryName.substring(1) : entryName;

        // Any embedded JAR files will be extracted to the embedded directory.
        File embedDir = new File(m_rootDir, m_dir.getName() + EMBEDDED_DIRECTORY);

        // Determine if the entry is an emdedded JAR file or
        // directory in the bundle JAR file. Ignore any entries
        // that do not exist per the spec.
        File file = new File(m_dir, entryName);
        if (BundleCache.getSecureAction().isFileDirectory(file))
        {
            return new DirectoryContent(
                m_logger, m_configMap, m_zipFactory, m_revisionLock, m_rootDir, file);
        }
        else if (BundleCache.getSecureAction().fileExists(file)
            && entryName.endsWith(".jar"))
        {
            File extractDir = new File(embedDir,
                (entryName.lastIndexOf('/') >= 0)
                    ? entryName.substring(0, entryName.lastIndexOf('/'))
                    : entryName);
            synchronized (m_revisionLock)
            {
                if (!BundleCache.getSecureAction().fileExists(extractDir))
                {
                    if (!BundleCache.getSecureAction().mkdirs(extractDir))
                    {
                        m_logger.log(
                            Logger.LOG_ERROR,
                            "Unable to extract embedded directory.");
                    }
                }
            }
            return new JarContent(
                m_logger, m_configMap, m_zipFactory, m_revisionLock,
                extractDir, file, null);
        }

        // The entry could not be found, so return null.
        return null;
    }

// TODO: SECURITY - This will need to consider security.
    public String getEntryAsNativeLibrary(String entryName)
    {
        // Return result.
        String result = null;

        // Remove any leading slash, since all bundle class path
        // entries are relative to the root of the bundle.
        entryName = (entryName.startsWith("/")) ? entryName.substring(1) : entryName;

        // Any embedded native library files will be extracted to the lib directory.
        File libDir = new File(m_rootDir, m_dir.getName() + LIBRARY_DIRECTORY);

        // The entry must exist and refer to a file, not a directory,
        // since we are expecting it to be a native library.
        File entryFile = new File(m_dir, entryName);
        if (BundleCache.getSecureAction().fileExists(entryFile)
            && !BundleCache.getSecureAction().isFileDirectory(entryFile))
        {
            // Extracting the embedded native library file impacts all other
            // existing contents for this revision, so we have to grab the
            // revision lock first before trying to extract the embedded JAR
            // file to avoid a race condition.
            synchronized (m_revisionLock)
            {
                // Since native libraries cannot be shared, we must extract a
                // separate copy per request, so use the request library counter
                // as part of the extracted path.
                if (m_nativeLibMap == null)
                {
                    m_nativeLibMap = new HashMap();
                }
                Integer libCount = (Integer) m_nativeLibMap.get(entryName);
                // Either set or increment the library count.
                libCount = (libCount == null) ? new Integer(0) : new Integer(libCount.intValue() + 1);
                m_nativeLibMap.put(entryName, libCount);
                File libFile = new File(
                    libDir, libCount.toString() + File.separatorChar + entryName);

                if (!BundleCache.getSecureAction().fileExists(libFile))
                {
                    if (!BundleCache.getSecureAction().fileExists(libFile.getParentFile())
                        && !BundleCache.getSecureAction().mkdirs(libFile.getParentFile()))
                    {
                        m_logger.log(
                            Logger.LOG_ERROR,
                            "Unable to create library directory.");
                    }
                    else
                    {
                        InputStream is = null;

                        try
                        {
                            is = new BufferedInputStream(
                                BundleCache.getSecureAction().getFileInputStream(entryFile),
                                BundleCache.BUFSIZE);
                            if (is == null)
                            {
                                throw new IOException("No input stream: " + entryName);
                            }

                            // Create the file.
                            BundleCache.copyStreamToFile(is, libFile);

                            // Perform exec permission command on extracted library
                            // if one is configured.
                            String command = (String) m_configMap.get(
                                Constants.FRAMEWORK_EXECPERMISSION);
                            if (command != null)
                            {
                                Properties props = new Properties();
                                props.setProperty("abspath", libFile.toString());
                                command = Util.substVars(command, "command", null, props);
                                Process p = BundleCache.getSecureAction().exec(command);
                                p.waitFor();
                            }

                            // Return the path to the extracted native library.
                            result = BundleCache.getSecureAction().getAbsolutePath(libFile);
                        }
                        catch (Exception ex)
                        {
                            m_logger.log(
                                Logger.LOG_ERROR,
                                "Extracting native library.", ex);
                        }
                        finally
                        {
                            try
                            {
                                if (is != null) is.close();
                            }
                            catch (IOException ex)
                            {
                                // Not much we can do.
                            }
                        }
                    }
                }
                else
                {
                    // Return the path to the extracted native library.
                    result = BundleCache.getSecureAction().getAbsolutePath(libFile);
                }
            }
        }

        return result;
    }

    public String toString()
    {
        return "DIRECTORY " + m_dir;
    }

    private static class EntriesEnumeration implements Enumeration
    {
        private final File m_dir;
        private final File[] m_children;
        private int m_counter = 0;

        public EntriesEnumeration(File dir)
        {
            m_dir = dir;
            m_children = listFilesRecursive(m_dir);
        }

        public synchronized boolean hasMoreElements()
        {
            return (m_children != null) && (m_counter < m_children.length);
        }

        public synchronized Object nextElement()
        {
            if ((m_children == null) || (m_counter >= m_children.length))
            {
                throw new NoSuchElementException("No more entry paths.");
            }

            // Convert the file separator character to slashes.
            String abs = BundleCache.getSecureAction()
                .getAbsolutePath(m_children[m_counter]).replace(File.separatorChar, '/');

            // Remove the leading path of the reference directory, since the
            // entry paths are supposed to be relative to the root.
            StringBuffer sb = new StringBuffer(abs);
            sb.delete(0, BundleCache.getSecureAction().getAbsolutePath(m_dir).length() + 1);
            // Add a '/' to the end of directory entries.
            if (BundleCache.getSecureAction().isFileDirectory(m_children[m_counter]))
            {
                sb.append('/');
            }
            m_counter++;
            return sb.toString();
        }

        private File[] listFilesRecursive(File dir)
        {
            File[] children = BundleCache.getSecureAction().listDirectory(dir);
            File[] combined = children;
            for (int i = 0; i < children.length; i++)
            {
                if (BundleCache.getSecureAction().isFileDirectory(children[i]))
                {
                    File[] grandchildren = listFilesRecursive(children[i]);
                    if (grandchildren.length > 0)
                    {
                        File[] tmp = new File[combined.length + grandchildren.length];
                        System.arraycopy(combined, 0, tmp, 0, combined.length);
                        System.arraycopy(
                            grandchildren, 0, tmp, combined.length, grandchildren.length);
                        combined = tmp;
                    }
                }
            }
            return combined;
        }
    }
}