/*
 * 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.config;

import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.el.ELContext;
import javax.el.ELException;

import org.w3c.dom.Node;
import org.xml.sax.InputSource;

import com.caucho.config.attribute.Attribute;
import com.caucho.config.inject.InjectManager;
import com.caucho.config.type.ConfigType;
import com.caucho.config.type.TypeFactory;
import com.caucho.config.types.DirVar;
import com.caucho.config.types.FileVar;
import com.caucho.config.xml.XmlConfigContext;
import com.caucho.el.EL;
import com.caucho.el.EnvironmentContext;
import com.caucho.loader.*;
import com.caucho.relaxng.CompactVerifierFactoryImpl;
import com.caucho.relaxng.Schema;
import com.caucho.relaxng.Verifier;
import com.caucho.relaxng.VerifierFilter;
import com.caucho.util.DisplayableException;
import com.caucho.util.L10N;
import com.caucho.vfs.Path;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.Vfs;
import com.caucho.xml.DOMBuilder;
import com.caucho.xml.QAttr;
import com.caucho.xml.QDocument;
import com.caucho.xml.QName;
import com.caucho.xml.Xml;

/**
 * Facade for Resin's configuration builder.
 */
public class Config {
  private static final L10N L = new L10N(Config.class);
  private static final Logger log
    = Logger.getLogger(Config.class.getName());

  private static final EnvironmentLocal<ConfigProperties> _envProperties
    = new EnvironmentLocal<ConfigProperties>();
  
  // the context class loader of the config
  private ClassLoader _classLoader;

  private boolean _isEL = true;
  private boolean _isIgnoreEnvironment;
  private boolean _allowResinInclude;

  public Config()
  {
    this(Thread.currentThread().getContextClassLoader());
  }

  /**
   * @param loader the class loader environment to use.
   */
  public Config(ClassLoader loader)
  {
    _classLoader = loader;
 }

  /**
   * Set true if resin:include should be allowed.
   */
  public void setResinInclude(boolean useResinInclude)
  {
    _allowResinInclude = useResinInclude;
  }

  /**
   * True if EL expressions are allowed
   */
  public boolean isEL()
  {
    return _isEL;
  }

  /**
   * True if EL expressions are allowed
   */
  public void setEL(boolean isEL)
  {
    _isEL = isEL;
  }

  /**
   * True if environment tags are ignored
   */
  public boolean isIgnoreEnvironment()
  {
    return _isIgnoreEnvironment;
  }

  /**
   * True if environment tags are ignored
   */
  public void setIgnoreEnvironment(boolean isIgnore)
  {
    _isIgnoreEnvironment = isIgnore;
  }

  /**
   * Returns an environment property
   */
  public static Object getProperty(String key)
  {
    ConfigProperties props = _envProperties.get();

    if (props != null)
      return props.get(key);
    else
      return null;
  }
  
  public static ConfigProperties getConfigProperties()
  {
    return _envProperties.get();
  }

  /**
   * Sets a environment property
   */
  public static void setProperty(String key, Object value)
  {
    ClassLoader loader = Thread.currentThread().getContextClassLoader();

    setProperty(key, value, loader);
  }

  /**
   * Sets a environment property
   */
  public static void setProperty(String key, Object value, ClassLoader loader)
  {
    ConfigProperties props = _envProperties.getLevel(loader);

    if (props == null) {
      props = createConfigProperties(loader);
    }

    props.put(key, value);
  }

  private static ConfigProperties createConfigProperties(ClassLoader loader)
  {
    EnvironmentClassLoader envLoader
      = Environment.getEnvironmentClassLoader(loader);

    ConfigProperties props = _envProperties.getLevel(envLoader);

    if (props != null)
      return props;

    if (envLoader != null) {
      ConfigProperties parent = createConfigProperties(envLoader.getParent());

      props = new ConfigProperties(parent);
    }
    else
      props = new ConfigProperties(null);

    _envProperties.set(props, envLoader);

    return props;
  }

  /**
   * Configures a bean with a configuration file.
   */
  public Object configure(Object obj, Path path)
    throws ConfigException, IOException
  {
    try {
      QDocument doc = parseDocument(path, null);

      return configure(obj, doc.getDocumentElement());
    } catch (RuntimeException e) {
      throw e;
    } catch (IOException e) {
      throw e;
    } catch (Exception e) {
      throw ConfigException.create(e);
    }
  }

  /**
   * Configures a bean with a configuration file.
   */
  public Object configure(Object obj, InputStream is)
    throws Exception
  {
    QDocument doc = parseDocument(is, null);

    return configure(obj, doc.getDocumentElement());
  }

  /**
   * Configures a bean with a configuration file and schema.
   */
  public Object configure(Object obj, Path path, String schemaLocation)
    throws ConfigException
  {
    try {
      Schema schema = findCompactSchema(schemaLocation);

      QDocument doc = parseDocument(path, schema);

      return configure(obj, doc.getDocumentElement());
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw LineConfigException.create(e);
    }
  }

  /**
   * Configures a bean with a configuration file and schema.
   */
  public Object configure(Object obj, Path path, Schema schema)
    throws ConfigException
  {
    try {
      QDocument doc = parseDocument(path, schema);

      return configure(obj, doc.getDocumentElement());
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw ConfigException.create(e);
    }
  }

  /**
   * Configures a bean with a configuration file.
   */
  public Object configure(Object obj,
                          InputStream is,
                          String schemaLocation)
    throws Exception
  {
    Schema schema = findCompactSchema(schemaLocation);

    QDocument doc = parseDocument(is, schema);

    return configure(obj, doc.getDocumentElement());
  }

  /**
   * Configures a bean with a configuration file.
   */
  public Object configure(Object obj,
                          InputStream is,
                          Schema schema)
    throws Exception
  {
    QDocument doc = parseDocument(is, schema);

    return configure(obj, doc.getDocumentElement());
  }

  /**
   * Configures a bean with a DOM.
   */
  public Object configure(Object obj, Node topNode)
    throws Exception
  {
    Thread thread = Thread.currentThread();
    ClassLoader oldLoader = thread.getContextClassLoader();

    try {
      thread.setContextClassLoader(_classLoader);

      XmlConfigContext builder = createBuilder();

      setProperty("__FILE__", FileVar.__FILE__);
      setProperty("__DIR__", DirVar.__DIR__);

      return builder.configure(obj, topNode);
    } finally {
      thread.setContextClassLoader(oldLoader);
    }
  }

  /**
   * Configures a bean with a configuration file and schema.
   */
  public void configureBean(Object obj,
                            Path path,
                            String schemaLocation)
    throws Exception
  {
    Schema schema = findCompactSchema(schemaLocation);

    QDocument doc = parseDocument(path, schema);

    configureBean(obj, doc.getDocumentElement());
  }

  /**
   * Configures a bean with a configuration file and schema.
   */
  public void configureBean(Object obj, Path path)
    throws Exception
  {
    QDocument doc = parseDocument(path, null);

    configureBean(obj, doc.getDocumentElement());
  }

  /**
   * Configures a bean with a DOM.  configureBean does not
   * apply init() or replaceObject().
   */
  public void configureBean(Object obj, Node topNode)
    throws Exception
  {
    Thread thread = Thread.currentThread();
    ClassLoader oldLoader = thread.getContextClassLoader();

    try {
      thread.setContextClassLoader(_classLoader);

      XmlConfigContext builder = createBuilder();

      InjectManager webBeans = InjectManager.create();

      setProperty("__FILE__", FileVar.__FILE__);
      setProperty("__DIR__", DirVar.__DIR__);

      builder.configureBean(obj, topNode);
    } finally {
      thread.setContextClassLoader(oldLoader);
    }
  }

  private XmlConfigContext createBuilder()
  {
    return new XmlConfigContext(this);
  }

  /**
   * Configures a bean with a configuration file and schema.
   */
  public void configureBean(Object obj,
                            Path path,
                            Schema schema)
    throws Exception
  {
    QDocument doc = parseDocument(path, schema);

    configureBean(obj, doc.getDocumentElement());
  }

  /**
   * Configures the bean from a path
   */
  private QDocument parseDocument(Path path, Schema schema)
    throws LineConfigException, IOException, org.xml.sax.SAXException
  {
    // server/2d33
    SoftReference<QDocument> docRef = null;//_parseCache.get(path);
    QDocument doc;

    if (docRef != null) {
      doc = docRef.get();

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

    ReadStream is = path.openRead();

    try {
      doc = parseDocument(is, schema);

      // _parseCache.put(path, new SoftReference<QDocument>(doc));

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

  /**
   * Configures the bean from an input stream.
   */
  private QDocument parseDocument(InputStream is, Schema schema)
    throws LineConfigException,
           IOException,
           org.xml.sax.SAXException
  {
    QDocument doc = new QDocument();
    DOMBuilder builder = new DOMBuilder();

    builder.init(doc);
    String systemId = null;
    String filename = null;
    Path path = null;
    if (is instanceof ReadStream) {
      path = ((ReadStream) is).getPath();
      systemId = path.getURL();
      filename = path.getUserPath();
    }

    doc.setSystemId(systemId);
    builder.setSystemId(systemId);
    doc.setRootFilename(filename);
    builder.setFilename(filename);
    builder.setSkipWhitespace(true);

    InputSource in = new InputSource();
    in.setByteStream(is);
    in.setSystemId(systemId);

    Xml xml = new Xml();
    xml.setOwner(doc);
    xml.setResinInclude(_allowResinInclude);
    xml.setFilename(filename);

    if (schema != null) {
      Verifier verifier = schema.newVerifier();
      VerifierFilter filter = verifier.getVerifierFilter();

      filter.setParent(xml);
      filter.setContentHandler(builder);
      filter.setErrorHandler(builder);

      filter.parse(in);
    }
    else {
      xml.setContentHandler(builder);
      xml.parse(in);
    }
    
    if (path != null) {
      ConfigAdmin.registerPath(path);
    }

    return doc;
  }

  private Schema findCompactSchema(String location)
    throws IOException, ConfigException
  {
    try {
      if (location == null)
        return null;

      Thread thread = Thread.currentThread();
      ClassLoader loader = thread.getContextClassLoader();

      if (loader == null)
        loader = ClassLoader.getSystemClassLoader();

      URL url = loader.getResource(location);

      if (url == null)
        return null;

      Path path = Vfs.lookup(URLDecoder.decode(url.toString()));

      // VerifierFactory factory = VerifierFactory.newInstance("http://caucho.com/ns/compact-relax-ng/1.0");

      CompactVerifierFactoryImpl factory;
      factory = new CompactVerifierFactoryImpl();

      return factory.compileSchema(path);
    } catch (IOException e) {
      throw e;
    } catch (Exception e) {
      throw ConfigException.create(e);
    }
  }

  /**
   * Returns true if the class can be instantiated.
   */
  public static void checkCanInstantiate(Class beanClass)
    throws ConfigException
  {
    if (beanClass == null)
      throw new ConfigException(L.l("null classes can't be instantiated."));
    else if (beanClass.isInterface())
      throw new ConfigException(L.l("'{0}' must be a concrete class.  Interfaces cannot be instantiated.", beanClass.getName()));
    else if (! Modifier.isPublic(beanClass.getModifiers()))
      throw new ConfigException(L.l("Custom bean class '{0}' is not public.  Bean classes must be public, concrete, and have a zero-argument constructor.", beanClass.getName()));
    else if (Modifier.isAbstract(beanClass.getModifiers()))
      throw new ConfigException(L.l("Custom bean class '{0}' is abstract.  Bean classes must be public, concrete, and have a zero-argument constructor.", beanClass.getName()));

    Constructor []constructors = beanClass.getDeclaredConstructors();

    Constructor constructor = null;

    for (int i = 0; i < constructors.length; i++) {
      if (constructors[i].getParameterTypes().length == 0) {
        constructor = constructors[i];
        break;
      }
    }

    if (constructor == null)
      throw new ConfigException(L.l("Custom bean class '{0}' doesn't have a zero-arg constructor.  Bean classes must be have a zero-argument constructor.", beanClass.getName()));

    // #5701, server/1768
    /*
    if (! Modifier.isPublic(constructor.getModifiers())) {
      throw new ConfigException(L.l("The zero-argument constructor for '{0}' isn't public.  Bean classes must have a public zero-argument constructor.", beanClass.getName()));
    }
    */
  }

  /**
   * Returns true if the class can be instantiated.
   */
  public static void validate(Class cl, Class api)
    throws ConfigException
  {
    checkCanInstantiate(cl);

    if (! api.isAssignableFrom(cl)) {
      throw new ConfigException(L.l("{0} must implement {1}.",
                                    cl.getName(), api.getName()));
    }
  }

  /**
   * Returns true if the class can be instantiated using zero args constructor
   * or constructor that accepts an instance of class passed in type argument
   */
  public static void checkCanInstantiate(Class beanClass,
                                         Class type)
    throws ConfigException
  {
    if (beanClass == null)
      throw new ConfigException(L.l("null classes can't be instantiated."));
    else if (beanClass.isInterface())
      throw new ConfigException(L.l(
        "'{0}' must be a concrete class.  Interfaces cannot be instantiated.",
        beanClass.getName()));
    else if (! Modifier.isPublic(beanClass.getModifiers()))
      throw new ConfigException(L.l(
        "Custom bean class '{0}' is not public.  Bean classes must be public, concrete, and have a zero-argument constructor.",
        beanClass.getName()));
    else if (Modifier.isAbstract(beanClass.getModifiers()))
      throw new ConfigException(L.l(
        "Custom bean class '{0}' is abstract.  Bean classes must be public, concrete, and have a zero-argument constructor.",
        beanClass.getName()));

    Constructor [] constructors = beanClass.getDeclaredConstructors();

    Constructor zeroArgsConstructor = null;

    Constructor singleArgConstructor = null;

    for (int i = 0; i < constructors.length; i++) {
           if (constructors [i].getParameterTypes().length == 0) {
             zeroArgsConstructor = constructors [i];

             if (singleArgConstructor != null)
               break;
           }
           else if (type != null
                    && constructors [i].getParameterTypes().length == 1 &&
                    type.isAssignableFrom(constructors[i].getParameterTypes()[0])) {
             singleArgConstructor = constructors [i];

             if (zeroArgsConstructor != null)
               break;
           }
         }

    if (zeroArgsConstructor == null
        && singleArgConstructor == null)
      if (type != null)
        throw new ConfigException(L.l(
                                      "Custom bean class '{0}' doesn't have a zero-arg constructor, or a constructor accepting parameter of type '{1}'.  Bean class '{0}' must have a zero-argument constructor, or a constructor accepting parameter of type '{1}'",
                                      beanClass.getName(),
                                      type.getName()));
      else
        throw new ConfigException(L.l(
                                      "Custom bean class '{0}' doesn't have a zero-arg constructor.  Bean classes must have a zero-argument constructor.",
                                      beanClass.getName()));


    if (singleArgConstructor != null) {
      if (! Modifier.isPublic(singleArgConstructor.getModifiers()) &&
          (zeroArgsConstructor == null ||
           ! Modifier.isPublic(zeroArgsConstructor.getModifiers()))) {
        throw new ConfigException(L.l(
          "The constructor for bean '{0}' accepting parameter of type '{1}' is not public.  Constructor accepting parameter of type '{1}' must be public.",
          beanClass.getName(),
          type.getName()));
      }
    }
    else if (zeroArgsConstructor != null) {
      if (! Modifier.isPublic(zeroArgsConstructor.getModifiers()))
        throw new ConfigException(L.l(
          "The zero-argument constructor for '{0}' isn't public.  Bean classes must have a public zero-argument constructor.",
          beanClass.getName()));
    }
  }

  public static void validate(Class cl, Class api, Class type)
    throws ConfigException
  {
    checkCanInstantiate(cl, type);

    if (! api.isAssignableFrom(cl)) {
      throw new ConfigException(L.l("{0} must implement {1}.",
                                    cl.getName(), api.getName()));
    }
  }

  /**
   * Sets an attribute with a value.
   *
   * @param obj the bean to be set
   * @param attr the attribute name
   * @param value the attribute value
   */
  public static void setAttribute(Object obj, String attr, Object value)
  {
    ConfigType<?> type = TypeFactory.getType(obj.getClass());

    QName attrName = new QName(attr);
    Attribute attrStrategy = type.getAttribute(attrName);
    if (attrStrategy == null)
      throw new ConfigException(L.l("{0}: '{1}' is an unknown attribute.",
                                    obj.getClass().getName(),
                                    attrName.getName()));

    value = attrStrategy.getConfigType().valueOf(value);

    attrStrategy.setValue(obj, attrName, value);
  }

  /**
   * Sets an attribute with a value.
   *
   * @param obj the bean to be set
   * @param attr the attribute name
   * @param value the attribute value
   */
  public static void setStringAttribute(Object obj, String attr, String value)
    throws Exception
  {
    XmlConfigContext builder = new XmlConfigContext();
    QAttr qAttr = new QAttr(attr);
    qAttr.setValue(value);

    builder.configureAttribute(obj, qAttr);
  }

  public static void init(Object bean)
    throws ConfigException
  {
    try {
      ConfigType type = TypeFactory.getType(bean.getClass());

      type.init(bean);
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw ConfigException.create(e);
    }
  }

  public static void inject(Object bean)
    throws ConfigException
  {
    try {
      ConfigType type = TypeFactory.getType(bean.getClass());

      type.inject(bean);
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw ConfigException.create(e);
    }
  }

  public static Object replaceObject(Object bean) throws Exception
  {
    ConfigType type = TypeFactory.getType(bean.getClass());

    return type.replaceObject(bean);
  }

  /**
   * Returns the variable resolver.
   */
  public static ELContext getEnvironment()
  {
    XmlConfigContext builder = XmlConfigContext.getCurrentBuilder();

    if (builder != null) {
      return builder.getELContext();
    }
    else
      return EL.getEnvironment();
  }

  /**
   * Returns the variable resolver.
   */
  public static ConfigELContext getELContext()
  {
    XmlConfigContext builder = XmlConfigContext.getCurrentBuilder();

    if (builder != null) {
      return builder.getELContext();
    }
    else
      return null;
  }
  
  public static Object getElVar(String var)
  {
    ELContext context = getEnvironment();
    
    if (context != null)
      return context.getELResolver().getValue(context, null, var);
    
    return getProperty(var);
  }

  /**
   * Sets an EL configuration variable.
   */
  public static Object getCurrentVar(String var)
  {
    // return InjectManager.create().findByName(var);
    return getProperty(var);
  }

  /**
   * Evaluates an EL string in the context.
   */
  public static String evalString(String str)
    throws ELException
  {
    ELContext elContext = ConfigELContext.EL_CONTEXT;
    
    if (elContext == null) {
      elContext = getEnvironment();
    }

    return EL.evalString(str, elContext);
  }

  /**
   * Evaluates an EL string in the context.
   */
  public static String evalString(String str, HashMap<String,Object> varMap)
         throws ELException
  {
    return EL.evalString(str, getEnvironment(varMap));
  }

  /**
   * Evaluates an EL boolean in the context.
   */
  public static boolean evalBoolean(String str)
    throws ELException
  {
    return EL.evalBoolean(str, getEnvironment());
  }

  public static ELContext getEnvironment(HashMap<String,Object> varMap)
  {
    if (varMap != null)
      return new EnvironmentContext(varMap);
    else
      return new EnvironmentContext();
  }

  public static ConfigException error(Field field, String msg)
  {
    return new ConfigException(location(field) + msg);
  }

  public static ConfigException error(Method method, String msg)
  {
    return new ConfigException(location(method) + msg);
  }

  public static RuntimeException createLine(String systemId, int line,
                                            Throwable e)
  {
    while (e.getCause() != null
           && (e instanceof InstantiationException
               || e instanceof InvocationTargetException
               || e.getClass().equals(ConfigRuntimeException.class))) {
      e = e.getCause();
    }

    if (e instanceof LineConfigException)
      throw (LineConfigException) e;

    String lines = getSourceLines(systemId, line);
    String loc = systemId + ":" + line + ": ";

    if (e instanceof DisplayableException) {
      return new LineConfigException(loc + e.getMessage() + "\n" + lines, e);
    }
    else
      return new LineConfigException(loc + e + "\n" + lines, e);
  }

  public static String location(Field field)
  {
    String className = field.getDeclaringClass().getName();

    return className + "." + field.getName() + ": ";
  }

  public static String location(Method method)
  {
    String className = method.getDeclaringClass().getName();

    return className + "." + method.getName() + ": ";
  }

  private static String getSourceLines(String systemId, int errorLine)
  {
    if (systemId == null)
      return "";

    ReadStream is = null;
    try {
      is = Vfs.lookup().lookup(systemId).openRead();
      int line = 0;
      StringBuilder sb = new StringBuilder("\n\n");
      String text;
      while ((text = is.readLine()) != null) {
        line++;

        if (errorLine - 2 <= line && line <= errorLine + 2) {
          sb.append(line);
          sb.append(": ");
          sb.append(text);
          sb.append("\n");
        }
      }

      return sb.toString();
    } catch (IOException e) {
      log.log(Level.FINEST, e.toString(), e);

      return "";
    } finally {
      if (is != null)
        is.close();
    }
  }
  
  static class ConfigProperties {
    private ConfigProperties _parent;
    private HashMap<String,Object> _properties = new HashMap<String,Object>(8);

    ConfigProperties(ConfigProperties parent)
    {
      _parent = parent;
    }

    public Object get(String key)
    {
      Object value = _properties.get(key);

      if (value != null)
        return value;
      else if (_parent != null)
        return _parent.get(key);
      else
        return null;
    }

    public void put(String key, Object value)
    {
      _properties.put(key, value);
    }
    
    public String toString()
    {
      return getClass().getSimpleName() + "[]";
    }
  }
}

