/*
 * 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.sling.scripting.jsp.jasper.compiler;

import java.io.File;
import java.io.FilePermission;
import java.net.URL;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Policy;
import java.security.cert.Certificate;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.jsp.JspApplicationContext;
import javax.servlet.jsp.JspEngineInfo;
import javax.servlet.jsp.JspFactory;
import javax.servlet.jsp.PageContext;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.path.Path;
import org.apache.sling.api.scripting.SlingBindings;
import org.apache.sling.scripting.jsp.SlingJspPageContext;
import org.apache.sling.scripting.jsp.jasper.Constants;
import org.apache.sling.scripting.jsp.jasper.IOProvider;
import org.apache.sling.scripting.jsp.jasper.Options;
import org.apache.sling.scripting.jsp.jasper.runtime.JspFactoryImpl;
import org.apache.sling.scripting.jsp.jasper.security.SecurityClassLoad;
import org.apache.sling.scripting.jsp.jasper.servlet.JspServletWrapper;

/**
 * Class for tracking JSP compile time file dependencies when the
 * &060;%@include file="..."%&062; directive is used.
 *
 * A background thread periodically checks the files a JSP page
 * is dependent upon.  If a dpendent file changes the JSP page
 * which included it is recompiled.
 *
 * Only used if a web application context is a directory.
 *
 * @author Glenn L. Nielsen
 * @version $Revision: 505593 $
 */
public final class JspRuntimeContext {

    // Logger
    private final Log log = LogFactory.getLog(JspRuntimeContext.class);

    /** The {@link IOProvider} used to get access to output */
    private final IOProvider ioProvider;

    /** This is a delegate forwarding either to our own factory
     * or the original one.
     * Depending on the USE_OWN_FACTORY flag.
     */
    public static final class JspFactoryHandler extends JspFactory {

        private final JspFactory original;

        private final JspFactory own;

        public JspFactoryHandler(final JspFactory orig, final JspFactory own) {
            this.original = (orig instanceof JspFactoryHandler ? ((JspFactoryHandler)orig).original : orig);
            this.own = own;
        }

        private JspFactory getFactory() {
            final Integer useOwnFactory = USE_OWN_FACTORY.get();
            if ( useOwnFactory == null || useOwnFactory.intValue() == 0 ) {
                return this.original;
            }
            return this.own;
        }

        @Override
        public PageContext getPageContext(Servlet paramServlet,
                ServletRequest paramServletRequest,
                ServletResponse paramServletResponse, String paramString,
                boolean paramBoolean1, int paramInt, boolean paramBoolean2) {
            PageContext context = this.getFactory().getPageContext(paramServlet, paramServletRequest,
                    paramServletResponse, paramString, paramBoolean1,
                    paramInt, paramBoolean2);
            if (paramServletRequest instanceof SlingHttpServletRequest) {
                SlingBindings slingBindings = (SlingBindings) paramServletRequest.getAttribute(SlingBindings.class.getName());
                if (slingBindings != null) {
                    context = new SlingJspPageContext(context, slingBindings);
                }
            }
            return context;
        }

        @Override
        public void releasePageContext(PageContext paramPageContext) {
            this.getFactory().releasePageContext(paramPageContext);
        }

        @Override
        public JspEngineInfo getEngineInfo() {
            return this.getFactory().getEngineInfo();
        }

        @Override
        public JspApplicationContext getJspApplicationContext(
                ServletContext paramServletContext) {
            return this.getFactory().getJspApplicationContext(paramServletContext);
        }

        /**
         * Reset the jsp factory.
         */
        public void destroy() {
            final JspFactory current = JspFactory.getDefaultFactory();
            if ( current == this ) {
                JspFactory.setDefaultFactory(this.original);
            }
        }

        public void incUsage() {
            final Integer count = JspRuntimeContext.USE_OWN_FACTORY.get();
            int newCount = 1;
            if ( count != null ) {
                newCount = count + 1;
            }
            JspRuntimeContext.USE_OWN_FACTORY.set(newCount);
        }

        public void decUsage() {
            final Integer count = JspRuntimeContext.USE_OWN_FACTORY.get();
            JspRuntimeContext.USE_OWN_FACTORY.set(count - 1);
        }

        public int resetUsage() {
            final Integer count = JspRuntimeContext.USE_OWN_FACTORY.get();
            JspRuntimeContext.USE_OWN_FACTORY.set(0);
            return count;
        }

        public void setUsage(int count) {
            JspRuntimeContext.USE_OWN_FACTORY.set(count);
        }
    }

    /**
     * Preload classes required at runtime by a JSP servlet so that
     * we don't get a defineClassInPackage security exception.
     * And set jsp factory
     */
    public static JspFactoryHandler initFactoryHandler() {
        JspFactoryImpl factory = new JspFactoryImpl();
        SecurityClassLoad.securityClassLoad(factory.getClass().getClassLoader());
        if( System.getSecurityManager() != null ) {
            String basePackage = "org.apache.sling.scripting.jsp.jasper.";
            try {
                factory.getClass().getClassLoader().loadClass( basePackage +
                                                               "runtime.JspFactoryImpl$PrivilegedGetPageContext");
                factory.getClass().getClassLoader().loadClass( basePackage +
                                                               "runtime.JspFactoryImpl$PrivilegedReleasePageContext");
                factory.getClass().getClassLoader().loadClass( basePackage +
                                                               "runtime.JspRuntimeLibrary");
                factory.getClass().getClassLoader().loadClass( basePackage +
                                                               "runtime.ServletResponseWrapperInclude");
                factory.getClass().getClassLoader().loadClass( basePackage +
                                                               "servlet.JspServletWrapper");
            } catch (ClassNotFoundException ex) {
                throw new IllegalStateException(ex);
            }
        }

        final JspFactoryHandler key = new JspFactoryHandler(JspFactory.getDefaultFactory(), factory);
        JspFactory.setDefaultFactory(key);
        return key;
    }

    private static final ThreadLocal<Integer> USE_OWN_FACTORY = new ThreadLocal<Integer>();

    // ----------------------------------------------------------- Constructors

    /**
     * Create a JspRuntimeContext for a web application context.
     *
     * Loads in any previously generated dependencies from file.
     *
     * @param context ServletContext for web application
     */
    public JspRuntimeContext(ServletContext context, Options options, final IOProvider ioProvider) {

        this.context = context;
        this.options = options;
        this.ioProvider = ioProvider;

        if (Constants.IS_SECURITY_ENABLED) {
            initSecurity();
        }
    }

    // ----------------------------------------------------- Instance Variables

    /**
     * This web applications ServletContext
     */
    private ServletContext context;
    private Options options;
    private PermissionCollection permissionCollection;

    /**
     * Maps JSP pages to their JspServletWrapper's
     */
    private final ConcurrentHashMap<String, JspServletWrapper> jsps = new ConcurrentHashMap<String, JspServletWrapper>();

    /**
     * Maps dependencies to the using jsp.
     */
    private final Map<String, Set<String>> depToJsp = new HashMap<String, Set<String>>();

    /**
     * Locks for loading tag files.
     */
    private final ConcurrentHashMap<String, Lock> tagFileLoadingLocks = new ConcurrentHashMap<String, Lock>();

    // ------------------------------------------------------ Public Methods

    public void addJspDependencies(final JspServletWrapper jsw, final List<String> deps) {
        if ( deps != null ) {
            final String jspUri = jsw.getJspUri();
            synchronized ( depToJsp ) {
                for(final String dep : deps) {
                    Set<String> set = depToJsp.get(dep);
                    if ( set == null ) {
                        set = new HashSet<String>();
                        depToJsp.put(dep, set);
                    }
                    set.add(jspUri);
                }
            }
        }
    }

    /**
     * Handle jsp modifications
     */
    public boolean handleModification(final String scriptName, final boolean isRemove) {
        if ( log.isDebugEnabled() ) {
            log.debug("Handling modification " + scriptName);
        }

        final JspServletWrapper wrapper = jsps.remove(scriptName);
        if ( wrapper == null && isRemove ) {
            boolean removed = false;
            final Path path = new Path(scriptName);
            final Iterator<Map.Entry<String, JspServletWrapper>> iter = jsps.entrySet().iterator();
            while ( iter.hasNext() ) {
                final Map.Entry<String, JspServletWrapper> entry = iter.next();
                if ( path.matches(entry.getKey()) ) {
                    iter.remove();
                    removed |= handleModification(entry.getKey(), entry.getValue());
                }
            }
            return removed;
        } else {
            return handleModification(scriptName, wrapper);
        }
    }

    /**
     * Handle modification for a single script
     * @param scriptName The script name
     * @param wrapper The jsp wrapper
     * @return {@code true} if changed
     */
    private boolean handleModification(final String scriptName, final JspServletWrapper wrapper) {
        // first check if jsps contains this
        boolean removed = this.invalidate(wrapper);

        final Set<String> deps;
        synchronized ( depToJsp ) {
            deps = depToJsp.remove(scriptName);
        }
        if ( deps != null ) {
            for(final String dep : deps) {
                removed |= this.invalidate(jsps.remove(dep));
            }
        }
        return removed;
    }

    /**
     * Invalidate a wrapper and destroy it.
     */
    private boolean invalidate(final JspServletWrapper wrapper) {
        if ( wrapper != null ) {
            if ( log.isDebugEnabled() ) {
                log.debug("Invalidating jsp " + wrapper.getJspUri());
            }
            wrapper.destroy(true);
            return true;
        }
        return false;
    }

    /**
     * Add a new wrapper
     *
     * @param jspUri JSP URI
     * @param jsw Servlet wrapper for JSP
     */
    public JspServletWrapper addWrapper(final String jspUri, final JspServletWrapper jsw) {
        final JspServletWrapper previous = jsps.putIfAbsent(jspUri, jsw);
        if ( previous == null ) {
            addJspDependencies(jsw, jsw.getDependants());
            return jsw;
        }
        return previous;
    }

    /**
     * Get an already existing JspServletWrapper.
     *
     * @param jspUri JSP URI
     * @return JspServletWrapper for JSP
     */
    public JspServletWrapper getWrapper(final String jspUri) {
        return jsps.get(jspUri);
    }

    /**
     * Locks a tag file path. Use this before loading it.
     * @param tagFilePath Tag file path
     */
    public void lockTagFileLoading(final String tagFilePath) {
        final Lock lock = getTagFileLoadingLock(tagFilePath);
        lock.lock();
    }

    /**
     * Unlocks a tag file path. Use this after loading it.
     * @param tagFilePath Tag file path
     */
    public void unlockTagFileLoading(final String tagFilePath) {
        final Lock lock = getTagFileLoadingLock(tagFilePath);
        lock.unlock();
    }

    /**
     * Process a "destroy" event for this web application context.
     */
    public void destroy() {
        Iterator<JspServletWrapper> servlets = jsps.values().iterator();
        while (servlets.hasNext()) {
            servlets.next().destroy(false);
        }
        jsps.clear();
        synchronized ( depToJsp ) {
            depToJsp.clear();
        }
    }

    /**
     * Returns the current {@link IOProvider} of this context.
     */
    public IOProvider getIOProvider() {
        return ioProvider;
    }

    // -------------------------------------------------------- Private Methods

    /**
     * Method used to initialize SecurityManager data.
     */
    private void initSecurity() {

        // Setup the PermissionCollection for this web app context
        // based on the permissions configured for the root of the
        // web app context directory, then add a file read permission
        // for that directory.
        Policy policy = Policy.getPolicy();
        if( policy != null ) {
            try {
                // Get the permissions for the web app context
                String docBase = context.getRealPath("/");
                if( docBase == null ) {
                    docBase = options.getScratchDir().toString();
                }
                String codeBase = docBase;
                if (!codeBase.endsWith(File.separator)){
                    codeBase = codeBase + File.separator;
                }
                File contextDir = new File(codeBase);
                URL url = contextDir.getCanonicalFile().toURL();
                final CodeSource codeSource = new CodeSource(url,(Certificate[])null);
                permissionCollection = policy.getPermissions(codeSource);

                // Create a file read permission for web app context directory
                if (!docBase.endsWith(File.separator)){
                    permissionCollection.add
                        (new FilePermission(docBase,"read"));
                    docBase = docBase + File.separator;
                } else {
                    permissionCollection.add
                        (new FilePermission
                            (docBase.substring(0,docBase.length() - 1),"read"));
                }
                docBase = docBase + "-";
                permissionCollection.add(new FilePermission(docBase,"read"));

                // Create a file read permission for web app tempdir (work)
                // directory
                String workDir = options.getScratchDir().toString();
                if (!workDir.endsWith(File.separator)){
                    permissionCollection.add
                        (new FilePermission(workDir,"read"));
                    workDir = workDir + File.separator;
                }
                workDir = workDir + "-";
                permissionCollection.add(new FilePermission(workDir,"read"));

                // Allow the JSP to access org.apache.sling.scripting.jsp.jasper.runtime.HttpJspBase
                permissionCollection.add( new RuntimePermission(
                    "accessClassInPackage.org.apache.jasper.runtime") );
            } catch (final Exception e) {
                context.log("Security Init for context failed",e);
            }
        }
    }

    /**
     * Returns and optionally creates a lock to load a tag file.
     */
    private Lock getTagFileLoadingLock(final String tagFilePath) {
        Lock lock = tagFileLoadingLocks.get(tagFilePath);
        if (lock == null) {
            lock = new ReentrantLock();
            final Lock existingLock = tagFileLoadingLocks.putIfAbsent(tagFilePath, lock);
            if (existingLock != null) {
                lock = existingLock;
            }
        }

        return lock;
    }

}
