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

import java.util.Properties;
import java.net.URL;
import org.apache.log4j.Level;
import org.apache.log4j.spi.Configurator;
import org.apache.log4j.spi.LoggerRepository;
import org.apache.log4j.PropertyConfigurator;

// Contributors:   Avy Sharell (sharell@online.fr)
//                 Matthieu Verbert (mve@zurich.ibm.com)
//                 Colin Sampaleanu

/**
   A convenience class to convert property values to specific types.

   @author Ceki G&uuml;lc&uuml;
   @author Simon Kitching;
   @author Anders Kristensen
*/
public class OptionConverter {

  static String DELIM_START = "${";
  static char   DELIM_STOP  = '}';
  static int DELIM_START_LEN = 2;
  static int DELIM_STOP_LEN  = 1;

  /** OptionConverter is a static class. */
  private OptionConverter() {}

  public
  static
  String[] concatanateArrays(String[] l, String[] r) {
    int len = l.length + r.length;
    String[] a = new String[len];

    System.arraycopy(l, 0, a, 0, l.length);
    System.arraycopy(r, 0, a, l.length, r.length);

    return a;
  }

  public
  static
  String convertSpecialChars(String s) {
    char c;
    int len = s.length();
    StringBuffer sbuf = new StringBuffer(len);

    int i = 0;
    while(i < len) {
      c = s.charAt(i++);
      if (c == '\\') {
	c =  s.charAt(i++);
	if(c == 'n')      c = '\n';
	else if(c == 'r') c = '\r';
	else if(c == 't') c = '\t';
	else if(c == 'f') c = '\f';
	else if(c == '\b') c = '\b';
	else if(c == '\"') c = '\"';
	else if(c == '\'') c = '\'';
	else if(c == '\\') c = '\\';
      }
      sbuf.append(c);
    }
    return sbuf.toString();
  }


  /**
     Very similar to <code>System.getProperty</code> except
     that the {@link SecurityException} is hidden.

     @param key The key to search for.
     @param def The default value to return.
     @return the string value of the system property, or the default
     value if there is no property with that key.

     @since 1.1 */
  public
  static
  String getSystemProperty(String key, String def) {
    try {
      return System.getProperty(key, def);
    } catch(Throwable e) { // MS-Java throws com.ms.security.SecurityExceptionEx
      LogLog.debug("Was not allowed to read system property \""+key+"\".");
      return def;
    }
  }


  public
  static
  Object instantiateByKey(Properties props, String key, Class superClass,
				Object defaultValue) {

    // Get the value of the property in string form
    String className = findAndSubst(key, props);
    if(className == null) {
      LogLog.error("Could not find value for key " + key);
      return defaultValue;
    }
    // Trim className to avoid trailing spaces that cause problems.
    return OptionConverter.instantiateByClassName(className.trim(), superClass,
						  defaultValue);
  }

  /**
     If <code>value</code> is "true", then <code>true</code> is
     returned. If <code>value</code> is "false", then
     <code>true</code> is returned. Otherwise, <code>default</code> is
     returned.

     <p>Case of value is unimportant.  */
  public
  static
  boolean toBoolean(String value, boolean dEfault) {
    if(value == null)
      return dEfault;
    String trimmedVal = value.trim();
    if("true".equalsIgnoreCase(trimmedVal))
      return true;
    if("false".equalsIgnoreCase(trimmedVal))
      return false;
    return dEfault;
  }

  public
  static
  int toInt(String value, int dEfault) {
    if(value != null) {
      String s = value.trim();
      try {
	return Integer.valueOf(s).intValue();
      }
      catch (NumberFormatException e) {
	 LogLog.error("[" + s + "] is not in proper int form.");
	e.printStackTrace();
      }
    }
    return dEfault;
  }

  /**
     Converts a standard or custom priority level to a Level
     object.  <p> If <code>value</code> is of form
     "level#classname", then the specified class' toLevel method
     is called to process the specified level string; if no '#'
     character is present, then the default {@link org.apache.log4j.Level}
     class is used to process the level value.

     <p>As a special case, if the <code>value</code> parameter is
     equal to the string "NULL", then the value <code>null</code> will
     be returned.

     <p> If any error occurs while converting the value to a level,
     the <code>defaultValue</code> parameter, which may be
     <code>null</code>, is returned.

     <p> Case of <code>value</code> is insignificant for the level level, but is
     significant for the class name part, if present.

     @since 1.1 */
  public
  static
  Level toLevel(String value, Level defaultValue) {
    if(value == null)
      return defaultValue;
      
    value = value.trim();

    int hashIndex = value.indexOf('#');
    if (hashIndex == -1) {
      if("NULL".equalsIgnoreCase(value)) {
	return null;
      } else {
	// no class name specified : use standard Level class
	return(Level) Level.toLevel(value, defaultValue);
      }
    }

    Level result = defaultValue;

    String clazz = value.substring(hashIndex+1);
    String levelName = value.substring(0, hashIndex);

    // This is degenerate case but you never know.
    if("NULL".equalsIgnoreCase(levelName)) {
	return null;
    }

    LogLog.debug("toLevel" + ":class=[" + clazz + "]"
		 + ":pri=[" + levelName + "]");

    try {
      Class customLevel = Loader.loadClass(clazz);

      // get a ref to the specified class' static method
      // toLevel(String, org.apache.log4j.Level)
      Class[] paramTypes = new Class[] { String.class,
					 org.apache.log4j.Level.class
                                       };
      java.lang.reflect.Method toLevelMethod =
                      customLevel.getMethod("toLevel", paramTypes);

      // now call the toLevel method, passing level string + default
      Object[] params = new Object[] {levelName, defaultValue};
      Object o = toLevelMethod.invoke(null, params);

      result = (Level) o;
    } catch(ClassNotFoundException e) {
      LogLog.warn("custom level class [" + clazz + "] not found.");
    } catch(NoSuchMethodException e) {
      LogLog.warn("custom level class [" + clazz + "]"
        + " does not have a class function toLevel(String, Level)", e);
    } catch(java.lang.reflect.InvocationTargetException e) {
      LogLog.warn("custom level class [" + clazz + "]"
		   + " could not be instantiated", e);
    } catch(ClassCastException e) {
      LogLog.warn("class [" + clazz
        + "] is not a subclass of org.apache.log4j.Level", e);
    } catch(IllegalAccessException e) {
      LogLog.warn("class ["+clazz+
		   "] cannot be instantiated due to access restrictions", e);
    } catch(Exception e) {
      LogLog.warn("class ["+clazz+"], level ["+levelName+
		   "] conversion failed.", e);
    }
    return result;
   }

  public
  static
  long toFileSize(String value, long dEfault) {
    if(value == null)
      return dEfault;

    String s = value.trim().toUpperCase();
    long multiplier = 1;
    int index;

    if((index = s.indexOf("KB")) != -1) {
      multiplier = 1024;
      s = s.substring(0, index);
    }
    else if((index = s.indexOf("MB")) != -1) {
      multiplier = 1024*1024;
      s = s.substring(0, index);
    }
    else if((index = s.indexOf("GB")) != -1) {
      multiplier = 1024*1024*1024;
      s = s.substring(0, index);
    }
    if(s != null) {
      try {
	return Long.valueOf(s).longValue() * multiplier;
      }
      catch (NumberFormatException e) {
	LogLog.error("[" + s + "] is not in proper int form.");
	LogLog.error("[" + value + "] not in expected format.", e);
      }
    }
    return dEfault;
  }

  /**
     Find the value corresponding to <code>key</code> in
     <code>props</code>. Then perform variable substitution on the
     found value.

 */
  public
  static
  String findAndSubst(String key, Properties props) {
    String value = props.getProperty(key);
    if(value == null)
      return null;

    try {
      return substVars(value, props);
    } catch(IllegalArgumentException e) {
      LogLog.error("Bad option value ["+value+"].", e);
      return value;
    }
  }

  /**
     Instantiate an object given a class name. Check that the
     <code>className</code> is a subclass of
     <code>superClass</code>. If that test fails or the object could
     not be instantiated, then <code>defaultValue</code> is returned.

     @param className The fully qualified class name of the object to instantiate.
     @param superClass The class to which the new object should belong.
     @param defaultValue The object to return in case of non-fulfillment
   */
  public
  static
  Object instantiateByClassName(String className, Class superClass,
				Object defaultValue) {
    if(className != null) {
      try {
	Class classObj = Loader.loadClass(className);
	if(!superClass.isAssignableFrom(classObj)) {
	  LogLog.error("A \""+className+"\" object is not assignable to a \""+
		       superClass.getName() + "\" variable.");
	  LogLog.error("The class \""+ superClass.getName()+"\" was loaded by ");
	  LogLog.error("["+superClass.getClassLoader()+"] whereas object of type ");
	  LogLog.error("\"" +classObj.getName()+"\" was loaded by ["
		       +classObj.getClassLoader()+"].");
	  return defaultValue;
	}
	return classObj.newInstance();
      } catch (Exception e) {
	LogLog.error("Could not instantiate class [" + className + "].", e);
      }
    }
    return defaultValue;
  }


  /**
     Perform variable substitution in string <code>val</code> from the
     values of keys found in the system propeties.

     <p>The variable substitution delimeters are <b>${</b> and <b>}</b>.

     <p>For example, if the System properties contains "key=value", then
     the call
     <pre>
     String s = OptionConverter.substituteVars("Value of key is ${key}.");
     </pre>

     will set the variable <code>s</code> to "Value of key is value.".

     <p>If no value could be found for the specified key, then the
     <code>props</code> parameter is searched, if the value could not
     be found there, then substitution defaults to the empty string.

     <p>For example, if system propeties contains no value for the key
     "inexistentKey", then the call

     <pre>
     String s = OptionConverter.subsVars("Value of inexistentKey is [${inexistentKey}]");
     </pre>
     will set <code>s</code> to "Value of inexistentKey is []"

     <p>An {@link java.lang.IllegalArgumentException} is thrown if
     <code>val</code> contains a start delimeter "${" which is not
     balanced by a stop delimeter "}". </p>

     <p><b>Author</b> Avy Sharell</a></p>

     @param val The string on which variable substitution is performed.
     @throws IllegalArgumentException if <code>val</code> is malformed.

  */
  public static
  String substVars(String val, Properties props) throws
                        IllegalArgumentException {

    StringBuffer sbuf = new StringBuffer();

    int i = 0;
    int j, k;

    while(true) {
      j=val.indexOf(DELIM_START, i);
      if(j == -1) {
	// no more variables
	if(i==0) { // this is a simple string
	  return val;
	} else { // add the tail string which contails no variables and return the result.
	  sbuf.append(val.substring(i, val.length()));
	  return sbuf.toString();
	}
      } else {
	sbuf.append(val.substring(i, j));
	k = val.indexOf(DELIM_STOP, j);
	if(k == -1) {
	  throw new IllegalArgumentException('"'+val+
		      "\" has no closing brace. Opening brace at position " + j
					     + '.');
	} else {
	  j += DELIM_START_LEN;
	  String key = val.substring(j, k);
	  // first try in System properties
	  String replacement = getSystemProperty(key, null);
	  // then try props parameter
	  if(replacement == null && props != null) {
	    replacement =  props.getProperty(key);
	  }

	  if(replacement != null) {
	    // Do variable substitution on the replacement string
	    // such that we can solve "Hello ${x2}" as "Hello p1" 
            // the where the properties are
	    // x1=p1
            // x2=${x1}
	    String recursiveReplacement = substVars(replacement, props);
	    sbuf.append(recursiveReplacement);
	  }
	  i = k + DELIM_STOP_LEN;
	}
      }
    }
  }


  /**
     Configure log4j given a URL.

     <p>The url must point to a file or resource which will be interpreted by
     a new instance of a log4j configurator.

     <p>All configurations steps are taken on the
     <code>hierarchy</code> passed as a parameter.

     <p>
     @param url The location of the configuration file or resource.
     @param clazz The classname, of the log4j configurator which will parse
     the file or resource at <code>url</code>. This must be a subclass of
     {@link Configurator}, or null. If this value is null then a default
     configurator of {@link PropertyConfigurator} is used, unless the
     filename pointed to by <code>url</code> ends in '.xml', in which case
     {@link org.apache.log4j.xml.DOMConfigurator} is used.
     @param hierarchy The {@link org.apache.log4j.Hierarchy} to act on.

     @since 1.1.4 */

  static
  public
  void selectAndConfigure(URL url, String clazz, LoggerRepository hierarchy) {
   Configurator configurator = null;
   String filename = url.getFile();

   if(clazz == null && filename != null && filename.endsWith(".xml")) {
     clazz = "org.apache.log4j.xml.DOMConfigurator";
   }

   if(clazz != null) {
     LogLog.debug("Preferred configurator class: " + clazz);
     configurator = (Configurator) instantiateByClassName(clazz,
							  Configurator.class,
							  null);
     if(configurator == null) {
   	  LogLog.error("Could not instantiate configurator ["+clazz+"].");
   	  return;
     }
   } else {
     configurator = new PropertyConfigurator();
   }

   configurator.doConfigure(url, hierarchy);
  }
}
