/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.jsp;

import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import com.caucho.config.Config;
import com.caucho.config.ConfigException;
import com.caucho.config.types.FileSetType;
import com.caucho.config.types.PathPatternType;
import com.caucho.jsp.cfg.JsfTldPreload;
import com.caucho.jsp.cfg.JspPropertyGroup;
import com.caucho.jsp.cfg.TldPreload;
import com.caucho.jsp.cfg.TldTaglib;
import com.caucho.loader.DynamicClassLoader;
import com.caucho.loader.Environment;
import com.caucho.loader.EnvironmentLocal;
import com.caucho.server.util.CauchoSystem;
import com.caucho.server.webapp.WebApp;
import com.caucho.util.Alarm;
import com.caucho.util.CurrentTime;
import com.caucho.util.L10N;
import com.caucho.vfs.JarPath;
import com.caucho.vfs.Path;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.Vfs;
import com.caucho.vfs.ZipScanner;

/**
 * Stores the parsed tlds.
 */
public class TldManager {
  static final L10N L = new L10N(TldManager.class);
  private static final Logger log
    = Logger.getLogger(TldManager.class.getName());

  private static ArrayList<TldPreload> _cauchoTaglibs;
  private static ArrayList<TldPreload> _globalTaglibs;
  private static ArrayList<Path> _globalPaths;

  private static EnvironmentLocal<TldManager> _localManager
    = new EnvironmentLocal<TldManager>();

  private JspResourceManager _resourceManager;
  private WebApp _webApp;

  private HashMap<Path,SoftReference<TldTaglib>> _tldMap
    = new HashMap<Path,SoftReference<TldTaglib>>();

  private JspParseException _loadAllTldException;
  private String _tldDir;
  private FileSetType _tldFileSet;

  private boolean _isFastJsf = false; // ioc/0560

  private volatile boolean _isInit;

  private Config _config = new Config();
  private ArrayList<TldPreload> _preloadTaglibs;

  private TldManager(JspResourceManager resourceManager,
                     WebApp app)
    throws JspParseException, IOException
  {
    _resourceManager = resourceManager;
    _webApp = app;

    if (app != null) {
      JspPropertyGroup jsp = app.getJsp();
      if (jsp != null)
        _tldFileSet = jsp.getTldFileSet();
    }

    // JSF has a global listener hidden in one of the *.tld which
    // requires Resin to search all the JSPs.
    initGlobal();
  }

  static TldManager create(JspResourceManager resourceManager,
                           WebApp webApp)
    throws JspParseException, IOException
  {

    TldManager manager = null;
    
    ClassLoader loader;
    
    if (webApp != null)
      loader = webApp.getClassLoader();
    else
      loader = Thread.currentThread().getContextClassLoader();

    synchronized (_localManager) {
      manager = _localManager.getLevel(loader);

      if (manager != null)
        return manager;

      manager = new TldManager(resourceManager, webApp);
      _localManager.set(manager, loader);
    }

    return manager;
  }

  /**
   * Sets the webApp.
   */
  void setWebApp(WebApp webApp)
  {
    _webApp = webApp;
  }

  public String getSchema()
  {
    return "com/caucho/jsp/cfg/jsp-tld.rnc";
  }

  public void setTldDir(String tldDir)
  {
    _tldDir = tldDir;
  }

  public void setTldFileSet(FileSetType tldFileSet)
  {
    _tldFileSet = tldFileSet;
  }

  /**
   * Loads all the .tld files in the WEB-INF and the META-INF for
   * the entire classpath.
   */
  public synchronized void init()
    throws JspParseException, IOException
  {
    if (_isInit)
      return;
    _isInit = true;

    log.fine("Loading .tld files");

    String dir;

    if (_tldDir == null)
      dir = "WEB-INF";
    else if (_tldDir.startsWith("/"))
      dir = _tldDir.substring(1);
    else if (_tldDir.startsWith("WEB-INF"))
      dir = _tldDir;
    else
      dir = "WEB-INF/" + _tldDir;

    FileSetType fileSet = _tldFileSet;
    if (fileSet == null) {
      fileSet = new FileSetType();
      fileSet.setDir(_resourceManager.resolvePath(dir));
      fileSet.addInclude(new PathPatternType("**/*.tld"));
      fileSet.addInclude(new PathPatternType("**/*.ftld"));
      try {
        fileSet.init();
      } catch (Exception e) {
        log.config(e.toString());
      }
    }

    ArrayList<TldPreload> taglibs = new ArrayList<TldPreload>();

    taglibs.addAll(_globalTaglibs);

    ArrayList<Path> paths = getClassPath();

    for (int i = 0; i < paths.size(); i++) {
      Path subPath = paths.get(i);

      if (_globalPaths.contains(subPath)) {
        continue;
      }

      if (subPath instanceof JarPath) {
        loadJarTlds(taglibs, ((JarPath) subPath).getContainer(), "META-INF");
      }
      else if (subPath.getPath().endsWith(".jar")) {
        loadJarTlds(taglibs, subPath, "META-INF");
      }
      else
        loadAllTlds(taglibs, subPath.lookup("META-INF"), 64, "META-INF");
    }

    if (fileSet != null)
      loadAllTlds(taglibs, fileSet);

    /*
    for (int i = 0; i < taglibs.size(); i++) {
      TldTaglib taglib = taglibs.get(i);

      if (taglib.getConfigException() != null &&
          taglib.getURI() == null) {
        _loadAllTldException = JspParseException.create(taglib.getConfigException());
      }
    }
    */
    taglibs.addAll(_cauchoTaglibs);

    _preloadTaglibs = taglibs;

    for (int i = 0; i < taglibs.size(); i++) {
      try {
        taglibs.get(i).initListeners(_webApp);
      } catch (Exception e) {
        throw new JspParseException(e);
      }
    }
  }

  public synchronized void initGlobal()
  {
    // loads tag libraries from the global context (so there's no
    // need to reparse the jars for each web-app
    if (_globalTaglibs == null) {
      if (! CurrentTime.isTest()) {
        log.info("Loading .tld files from global classpath");
      }

      ArrayList<TldPreload> globalTaglibs = new ArrayList<TldPreload>();
      ArrayList<TldPreload> cauchoTaglibs = new ArrayList<TldPreload>();

      Thread thread = Thread.currentThread();
      ClassLoader oldLoader = thread.getContextClassLoader();
      ClassLoader globalLoader = TldManager.class.getClassLoader();
      thread.setContextClassLoader(globalLoader);
      try {
        ArrayList<Path> paths = getClassPath(globalLoader);
        _globalPaths = paths;

        loadClassPathTlds(globalTaglibs, paths, "");

        for (int i = globalTaglibs.size() - 1; i >= 0; i--) {
          TldPreload tld = globalTaglibs.get(i);

          if (tld.getPath() == null || tld.getPath().getPath() == null)
            continue;

          String tldPathName = tld.getPath().getPath();

          if (tldPathName.startsWith("/com/caucho")) {
            cauchoTaglibs.add(globalTaglibs.remove(i));
          }
        }
      } catch (Exception e) {
        log.log(Level.WARNING, e.toString(), e);
      } finally {
        thread.setContextClassLoader(oldLoader);
      }

      _globalTaglibs = globalTaglibs;
      _cauchoTaglibs = cauchoTaglibs;
    }
  }

  private void loadClassPathTlds(ArrayList<TldPreload> taglibs,
                                 ArrayList<Path> paths,
                                 String prefix)
    throws JspParseException, IOException
  {
    for (int i = 0; i < paths.size(); i++) {
      Path subPath = paths.get(i);

      // skip jre libraries
      String pathName = subPath.getFullPath();
      if (pathName.endsWith("/jre/lib/rt.jar")
          || pathName.endsWith("/jre/lib/charsets.jar")
          || pathName.endsWith("/jre/lib/deploy.jar")) {
        continue;
      }

      if (subPath.getPath().endsWith(".jar")) {
        loadJarTlds(taglibs, subPath, prefix);
      }
      else if (prefix != null && ! prefix.equals(""))
        loadAllTlds(taglibs, subPath.lookup(prefix), 64, prefix);
      else
        loadAllTlds(taglibs, subPath.lookup("META-INF"), 64, "META-INF");
    }
  }

  /*
  ArrayList<TldTaglib> getTaglibs()
  {
    return new ArrayList<TldTaglib>(_preloadTaglibs);
  }
  */

  private void loadAllTlds(ArrayList<TldPreload> taglibs,
                           FileSetType fileSet)
    throws JspParseException, IOException
  {
    for (Path path : fileSet.getPaths()) {
      if (path.getPath().startsWith(".")) {
      }
      else if ((path.getPath().endsWith(".tld")
                || path.getPath().endsWith(".ftld"))
               && path.isFile() && path.canRead()) {
        try {
          TldPreload taglib = parseTldPreload(path);

          taglibs.add(taglib);

          if (taglib.getURI() == null &&
              taglib.getConfigException() != null &&
              _loadAllTldException == null)
            _loadAllTldException = new JspLineParseException(taglib.getConfigException());
        } catch (Exception e) {
          log.warning(e.getMessage());
        }
      }
    }
  }

  private void loadAllTlds(ArrayList<TldPreload> taglibs,
                           Path path, int depth, String userPath)
    throws JspParseException, IOException
  {
    if (depth < 0)
      throw new JspParseException(L.l("max depth exceeded while reading .tld files.  Probable loop in filesystem detected at `{0}'.", path));

    path.setUserPath(userPath);

    if (path.getPath().startsWith(".")) {
    }
    else if ((path.getPath().endsWith(".tld")
              || path.getPath().endsWith(".ftld"))
             && path.isFile() && path.canRead()) {
      try {
        TldPreload taglib = parseTldPreload(path);

        taglibs.add(taglib);

        if (taglib.getURI() == null &&
            taglib.getConfigException() != null &&
            _loadAllTldException == null)
          _loadAllTldException = new JspLineParseException(taglib.getConfigException());
      } catch (Exception e) {
        /*
        if (_loadAllTldException == null) {
        }
        else if (e instanceof JspParseException)
          _loadAllTldException = (JspParseException) e;
        else
          _loadAllTldException = new JspParseException(e);
        */

        log.warning(e.getMessage());
      }
    }
    else if (path.isDirectory()) {
      String []fileNames = path.list();

      for (int i = 0; fileNames != null && i < fileNames.length; i++) {
        String name = fileNames[i];

        ArrayList<Path> resources = path.getResources("./" + name);

        for (int j = 0; resources != null && j < resources.size(); j++) {
          Path subpath = resources.get(j);

          loadAllTlds(taglibs, subpath, depth - 1, userPath + "/" + name);
        }
      }
    }
  }

  private void loadJarTlds(ArrayList<TldPreload> taglibs,
                           Path jarBacking,
                           String prefix)
    throws JspParseException, IOException
  {
    if (! jarBacking.canRead())
      return;

    JarPath jar = JarPath.create(jarBacking);

    ArrayList<Path> tldPaths = new ArrayList<Path>();

    boolean isValidScan = false;

    ZipScanner scan = null;
    try {
      if (true)
        scan = new ZipScanner(jarBacking);

      if (scan != null && scan.open()) {
        while (scan.next()) {
          String name = scan.getName();

          if (name.startsWith(prefix)
              && name.endsWith(".tld") || name.endsWith(".ftld")) {
            tldPaths.add(jar.lookup(name));
          }
        }

        isValidScan = true;
      }
    } catch (Exception e) {
      log.log(Level.INFO, e.toString(), e);
    }

    if (! isValidScan) {
      ZipFile zipFile = jar.getJar().getZipFile();
      
      try {
        Enumeration<? extends ZipEntry> en = zipFile.entries();
        while (en.hasMoreElements()) {
          ZipEntry entry = en.nextElement();
          String name = entry.getName();

          if (name.startsWith(prefix)
              && (name.endsWith(".tld") || name.endsWith(".ftld"))) {
            tldPaths.add(jar.lookup(name));
          }
        }
      } finally {
        jar.getJar().closeZipFile(zipFile);
      }
    }

    for (Path path : tldPaths) {
      try {
        TldPreload taglib = parseTldPreload(path);
        taglibs.add(taglib);

        if (taglib.getURI() == null
            && taglib.getConfigException() != null
            && _loadAllTldException == null)
          _loadAllTldException = new JspLineParseException(taglib.getConfigException());
      } catch (Exception e) {
        /*
          if (_loadAllTldException == null) {
          }
          else if (e instanceof JspParseException)
          _loadAllTldException = (JspParseException) e;
          else
          _loadAllTldException = new JspParseException(e);
        */

        log.warning(e.getMessage());
      }
    }
  }

  /**
   * Returns the tld parsed at the given location.
   */
  TldTaglib parseTld(String uri, String mapLocation, String location)
    throws JspParseException, IOException
  {
    init();

    TldTaglib taglib = null;
    TldTaglib jsfTaglib = null;

    for (int i = 0; i < _preloadTaglibs.size(); i++) {
      TldPreload preload = _preloadTaglibs.get(i);

      if (uri.equals(preload.getURI())
          && (mapLocation == null
              || mapLocation.equals(preload.getLocation())
              || mapLocation.equals(uri))) {
        if (preload.isJsf()) {
          if (_isFastJsf)
            jsfTaglib = parseTld(preload.getPath());
        }
        else if (taglib == null)
          taglib = parseTld(preload.getPath());
      }
    }

    if (jsfTaglib != null && taglib != null) {
      taglib.mergeJsf(jsfTaglib);

      return taglib;
    }
    else if (taglib != null)
      return taglib;

    return parseTld(location);
  }

  /**
   * Returns the tld parsed at the given location.
   */
  TldTaglib parseTld(String location)
    throws JspParseException, IOException
  {
    init();

    TldTaglib tld = findTld(location);

    /* XXX: jsp/18n0 handled on init
    if (tld != null) {
      try {
        tld.init(_webApp);
      } catch (Exception e) {
        throw new JspParseException(e);
      }
    }
    */

    return tld;
  }

  /**
   * Returns the tld parsed at the given location.
   */
  private TldTaglib findTld(String location)
    throws JspParseException, IOException
  {
    InputStream is = null;

    Path path;

    if (location.startsWith("file:")) {
      path = _resourceManager.resolvePath(location);
    }
    else if (location.indexOf(':') >= 0 && ! location.startsWith("file:")
             && location.indexOf(':') < location.indexOf('/')) {
      if (_loadAllTldException != null)
        throw _loadAllTldException;

      return null;
      /* XXX: jsp/0316
      throw new JspParseException(L.l("Unknown taglib `{0}'.  Taglibs specified with an absolute URI must either be:\n1) specified in the web.xml\n2) defined in a jar's .tld in META-INF\n3) defined in a .tld in WEB-INF\n4) predefined by Resin",
                                      location));
      */
    }
    else if (location.startsWith("WEB-INF/")) // jsp/189a
      path = _resourceManager.resolvePath(location);
    else if (! location.startsWith("/"))
      path = _resourceManager.resolvePath("WEB-INF/" + location);
    else
      path = _resourceManager.resolvePath("." + location);

    path.setUserPath(location);

    Path jar = null;

    if (location.endsWith(".jar")) {
      path = findJar(location);

      if (path != null && path.exists()) {
        jar = JarPath.create(path);
        if (jar.lookup("META-INF/taglib.tld").exists())
          return parseTld(jar.lookup("META-INF/taglib.tld"));
        else if (jar.lookup("meta-inf/taglib.tld").exists())
          return parseTld(jar.lookup("meta-inf/taglib.tld"));
        else
          throw new JspParseException(L.l("can't find META-INF/taglib.tld in `{0}'",
                                          location));
      }
      else {
        throw new JspParseException(L.l("Can't find taglib `{0}'.  A taglib uri ending in *.jar must point to an actual jar or match a URI in a .tld file.", location));
      }
    }
    else if (path.exists() && path.canRead() && path.isFile())
      return parseTld(path);

    if (_loadAllTldException != null)
      throw _loadAllTldException;
    else
      throw new JspParseException(L.l("Can't find taglib-location `{0}'.  The taglib-location must match a tag library either:\n1) by pointing to a .tld directly, relative to the application's root directory\n2) specified in the web.xml\n3) defined in a jar's .tld in META-INF\n4) defined in a .tld in WEB-INF\n5) predefined by Resin",
                                      location));
  }

  /**
   * Parses the .tld
   *
   * @param is the input stream to the taglib
   */
  private TldTaglib parseTld(Path path)
    throws JspParseException, IOException
  {
    SoftReference<TldTaglib> taglibRef = _tldMap.get(path);
    TldTaglib taglib;

    if (taglibRef != null) {
      taglib = taglibRef.get();

      if (taglib != null && ! taglib.isModified())
        return taglib;
    }

    ReadStream is = path.openRead();

    try {
      taglib = parseTld(is);

      if (path instanceof JarPath)
        taglib.setJarPath(path.lookup("/"));

      _tldMap.put(path, new SoftReference<TldTaglib>(taglib));

      return taglib;
    } finally {
      is.close();
    }
  }

  /**
   * Parses the .tld
   *
   * @param is the input stream to the taglib
   */
  private TldTaglib parseTld(InputStream is)
    throws JspParseException, IOException
  {
    TldTaglib taglib = new TldTaglib();

    if (is instanceof ReadStream) {
      Path path = ((ReadStream) is).getPath();

      path.setUserPath(path.getURL());
    }

    String schema = null;

    if (_webApp.getJsp() == null
        || _webApp.getJsp().isValidateTaglibSchema()) {
      schema = getSchema();
    }

    try {
      Config config = new Config();
      config.setEL(false);
      config.configure(taglib, is, schema);
    } catch (ConfigException e) {
      log.warning(e.toString());
      log.log(Level.FINER, e.toString(), e);

      taglib.setConfigException(e);
    } catch (Exception e) {
      log.warning(e.toString());
      log.log(Level.FINER, e.toString(), e);

      taglib.setConfigException(e);
    } finally {
      is.close();
    }

    /* XXX: jsp/18n0 handled on init
    try {
      taglib.init(_webApp);
    } catch (Exception e) {
      throw new JspParseException(e);
    }
    */

    return taglib;
  }

  /**
   * Parses the .tld
   *
   * @param path location of the taglib
   */
  private TldPreload parseTldPreload(Path path)
    throws JspParseException, IOException
  {
    ReadStream is = path.openRead();

    try {
      TldPreload taglib = parseTldPreload(is);

      taglib.setPath(path);
      String appDir = _webApp.getRootDirectory().getPath();
      String tagPath = path.getPath();

      if (tagPath.startsWith(appDir))
        taglib.setLocation(tagPath.substring(appDir.length()));

      return taglib;
    } finally {
      is.close();
    }
  }

  /**
   * Parses the .tld
   *
   * @param is the input stream to the taglib
   */
  private TldPreload parseTldPreload(InputStream is)
    throws JspParseException, IOException
  {
    boolean isJsfTld = false;

    if (is instanceof ReadStream) {
      Path path = ((ReadStream) is).getPath();

      isJsfTld = path.getPath().endsWith(".ftld");

      path.setUserPath(path.getURL());
    }

    String schema = null;

    if (_webApp.getJsp() == null
        || _webApp.getJsp().isValidateTaglibSchema()) {
      schema = getSchema();
    }

    TldPreload taglib;

    if (isJsfTld)
      taglib = new JsfTldPreload();
    else
      taglib = new TldPreload();

    try {
      _config.configure(taglib, is, schema);
    } catch (ConfigException e) {
      log.warning(e.toString());
      log.log(Level.FINER, e.toString(), e);

      taglib.setConfigException(e);
    } catch (Exception e) {
      log.warning(e.toString());
      log.log(Level.FINER, e.toString(), e);

      taglib.setConfigException(e);
    } finally {
      is.close();
    }

    return taglib;
  }

  /**
   * Finds the path to the jar specified by the location.
   *
   * @param location the tag-location specified in the web.xml
   *
   * @return the found jar or null
   */
  private Path findJar(String location)
  {
    Path path;

    if (location.startsWith("file:"))
      path = Vfs.lookup(location);
    else if (location.startsWith("/"))
      path = _resourceManager.resolvePath("." + location);
    else
      path = _resourceManager.resolvePath(location);

    if (path.exists())
      return path;

    DynamicClassLoader loader;
    loader = (DynamicClassLoader) Thread.currentThread().getContextClassLoader();
    String classPath = loader.getClassPath();
    char sep = CauchoSystem.getPathSeparatorChar();

    int head = 0;
    int tail = 0;

    while ((tail = classPath.indexOf(sep, head)) >= 0) {
      String sub = classPath.substring(head, tail);

      path = Vfs.lookup(sub);

      if (sub.endsWith(location) && path.exists())
        return path;

      head = tail + 1;
    }

    if (classPath.length() <= head)
      return null;

    String sub = classPath.substring(head);

    path = Vfs.lookup(sub);

    if (sub.endsWith(location) && path.exists())
      return path;
    else
      return null;
  }

  /**
   * Adds the classpath as paths in the MergePath.
   */
  private ArrayList<Path> getClassPath()
  {
    return getClassPath(Thread.currentThread().getContextClassLoader());
  }

  /**
   * Adds the classpath for the loader as paths in the MergePath.
   *
   * @param loader class loader whose classpath should be used to search.
   */
  private ArrayList<Path> getClassPath(ClassLoader loader)
  {
    String classpath = null;
    
    loader = Environment.getDynamicClassLoader(loader);

    if (loader instanceof DynamicClassLoader)
      classpath = ((DynamicClassLoader) loader).getClassPath();
    else
      classpath = CauchoSystem.getClassPath();

    return getClassPath(classpath);
  }

  /**
   * Adds the classpath for the loader as paths in the MergePath.
   *
   * @param classpath class loader whose classpath should be used to search.
   */
  private ArrayList<Path> getClassPath(String classpath)
  {
    ArrayList<Path> list = new ArrayList<Path>();

    char sep = CauchoSystem.getPathSeparatorChar();
    int head = 0;
    int tail = 0;
    while (head < classpath.length()) {
      tail = classpath.indexOf(sep, head);

      String segment = null;
      if (tail < 0) {
        segment = classpath.substring(head);
        head = classpath.length();
      }
      else {
        segment = classpath.substring(head, tail);
        head = tail + 1;
      }

      if (! segment.equals("")) {
        Path path = Vfs.lookup(segment);

        if (! list.contains(path))
          list.add(path);
      }
    }

    return list;
  }
}
