1  /*
     2   * Copyright the original author or authors.
     3   * 
     4   * Licensed under the MOZILLA PUBLIC LICENSE, Version 1.1 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   * 
     8   *      http://www.mozilla.org/MPL/MPL-1.1.html
     9   * 
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  import org.as2lib.core.BasicClass;
    18  import org.as2lib.env.except.IllegalArgumentException;
    19  import org.as2lib.env.log.level.AbstractLogLevel;
    20  import org.as2lib.env.log.LogConfigurationParser;
    21  import org.as2lib.env.log.LogManager;
    22  import org.as2lib.env.log.parser.LogConfigurationParseException;
    23  import org.as2lib.env.reflect.ClassNotFoundException;
    24  import org.as2lib.env.reflect.NoSuchMethodException;
    25  import org.as2lib.util.StringUtil;
    26  
    27  /**
    28   * {@code XmlLogConfigurationParser} parses log configuration files in XML format.
    29   * 
    30   * <p>The root node of the configuration file must be "&lt;logging&gt;". Its child
    31   * nodes must correspond to {@code set*} or {@code add*} methods on the given or
    32   * default log manager: {@link #XmlLogConfigurationParser}. There is one exception
    33   * to this rule.
    34   * 
    35   * <p>The register-node in the root-node is treated in a special way. It can be
    36   * used to register node names with specific classes. This way you do not have to
    37   * specify the class-attribute multiple times for nodes with the same name.
    38   * 
    39   * <p>Every node except the register- and root-nodes are beans. A bean is in this
    40   * case an instance defined in an XML format. To be able to instantiate a bean the
    41   * bean class must be set. It is thus necessary to either register node names with
    42   * bean classes:
    43   * <code>&lt;register name="logger" class="org.as2lib.env.log.logger.SimpleHierarchicalLogger"/&gt;</code>
    44   * 
    45   * <p>or to specify the class-attribute in beans:
    46   * <code>&lt;logger class="org.as2lib.env.log.logger.TraceLogger"/&gt;</code>
    47   * 
    48   * <p>It is also possible to set properties. Properties are basically methods that
    49   * follow a specific naming convention. If you specify an attribute named {@code "name"}
    50   * your bean class must provide a {@code setName} method. If you specify a child
    51   * node named {@code "handler"}, the bean class must provide a {@code addHandler}
    52   * or {@code setHandler} method. Child nodes can themselves be beans.
    53   * 
    54   * <p>The level-attribute is treated in a special way. The level corresponding to
    55   * the level name is resolved with the {@link AbstractLogLevel#forName} method.
    56   * 
    57   * <p>It is also possible to pass constructor arguments. You do this with the
    58   * constructor-arg-tag. Note that the order matters! As you can see in the
    59   * following example, the constructor-arg can itself be a bean.
    60   * 
    61   * <code>
    62   *   &lt;handler class="org.as2lib.env.log.handler.TraceHandler"&gt;
    63   *     &lt;constructor-arg class="org.as2lib.env.log.stringifier.SimpleLogMessageStringifier"/&gt;
    64   *   &lt;/handler&gt;
    65   * </code>
    66   * 
    67   * <p>If a node- or attribute-value is a primitive type it will automatically
    68   * be converted. This means the strings {@code "true"} and {@code "false"} are
    69   * converted to the booleans {@code true} and {@code false} respectively. The
    70   * strings {@code "1"}, {@code "2"}, ... are converted to numbers. Only if the
    71   * node- or attribute-value is non of the above 'special cases' it is used as
    72   * string.
    73   * 
    74   * <code>
    75   *   &lt;handler class="org.as2lib.env.log.handler.TraceHandler"&gt;
    76   *     &lt;constructor-arg class="org.as2lib.env.log.stringifier.PatternLogMessageStringifier"&gt;
    77   *       &lt;constructor-arg&gt;false&lt;/contructor-arg&gt;
    78   *       &lt;constructor-arg&gt;true&lt;/contructor-arg&gt;
    79   *       &lt;constructor-arg&gt;HH:nn:ss.S&lt;/contructor-arg&gt;
    80   *     &lt;/constructor-arg&gt;
    81   *   &lt;/handler&gt;
    82   * </code>
    83   * 
    84   * <p>Your complete log configuration may look something like this:
    85   * <code>
    86   *   &lt;logging&gt;
    87   *     &lt;register name="logger" class="org.as2lib.env.log.logger.SimpleHierarchicalLogger"/&gt;
    88   *     &lt;loggerRepository class="org.as2lib.env.log.repository.LoggerHierarchy"&gt;
    89   *       &lt;logger name="com.simonwacker" level="INFO"&gt;
    90   *         &lt;handler class="org.as2lib.env.log.handler.DebugItHandler"/&gt;
    91   *         &lt;handler class="org.as2lib.env.log.handler.TraceHandler"/&gt;
    92   *       &lt;/logger&gt;
    93   *       &lt;logger name="com.simonwacker.MyClass" level="ERROR"&gt;
    94   *         &lt;handler class="org.as2lib.env.log.handler.SosHandler"/&gt;
    95   *       &lt;/logger&gt;
    96   *     &lt;/repository&gt;
    97   *   &lt;/logging&gt;
    98   * </code>
    99   * 
   100   * <p>or this:
   101   * <code>
   102   *   &lt;logging&gt;
   103   *     &lt;logger level="INFO" class="org.as2lib.env.log.logger.TraceLogger"/&gt;
   104   *   &lt;/logging&gt;
   105   * </code>
   106   * 
   107   * @author Simon Wacker
   108   */
   109  class org.as2lib.env.log.parser.XmlLogConfigurationParser extends BasicClass implements LogConfigurationParser {
   110  	
   111  	/** Node name class registrations. */
   112  	private var nodes;
   113  	
   114  	/** The manager to configure. */
   115  	private var manager;
   116  	
   117  	/**
   118  	 * Constructs a new {@code XmlLogConfigurationParser} instance.
   119  	 * 
   120  	 * <p>If {@code logManager} is {@code null} or {@code undefined}, {@link LogManager}
   121  	 * will be used by default.
   122  	 * 
   123  	 * @param logManager (optional) the manager to configure with the beans specified
   124  	 * in the log configuration XML-file
   125  	 */
   126  	public function XmlLogConfigurationParser(logManager) {
   127  		if (logManager) {
   128  			this.manager = logManager;
   129  		} else {
   130  			this.manager = LogManager;
   131  		}
   132  	}
   133  	
   134  	/**
   135  	 * Parses the given {@code xmlLogConfiguration}.
   136  	 * 
   137  	 * @param xmlLogConfiguration the XML log configuration to parse
   138  	 * @throws IllegalArgumentException if argument {@code xmlLogConfiguration} is
   139  	 * {@code null} or {@code undefined}
   140  	 * @throws LogConfigurationParseException if the bean definition could not be parsed
   141  	 * because of a malformed xml
   142  	 * @throws ClassNotFoundException if a class corresponding to a given class name could
   143  	 * not be found
   144  	 * @throws NoSuchMethodException if a method with a given name does not exist on the
   145  	 * bean to create
   146  	 */
   147  	public function parse(xmlLogConfiguration:String):Void {
   148  		if (xmlLogConfiguration == null) {
   149  			throw new IllegalArgumentException("Argument 'xmlLogConfiguration' [" + xmlLogConfiguration + "] must not be 'null' nor 'undefined'", this, arguments);
   150  		}
   151  		var xml:XML = new XML();
   152  		xml.ignoreWhite = true;
   153  		xml.parseXML(xmlLogConfiguration);
   154  		if (xml.status != 0) {
   155  			throw new LogConfigurationParseException("XML log configuration [" + xmlLogConfiguration + "] is syntactically malformed.", this, arguments);
   156  		}
   157  		nodes = new Object();
   158  		if (xml.lastChild.nodeName != "logging") {
   159  			throw new LogConfigurationParseException("There must be a root node named 'logging'.", this, arguments);
   160  		}
   161  		var childNodes:Array = xml.firstChild.childNodes;
   162  		for (var i:Number = 0; i < childNodes.length; i++) {
   163  			var childNode:XMLNode = childNodes[i];
   164  			if (childNode.nodeName == "register") {
   165  				var name:String = childNode.attributes.name;
   166  				var clazz:String = childNode.attributes["class"];
   167  				if (name != null && clazz != null) {
   168  					nodes[name] = clazz;
   169  				}
   170  			} else {
   171  				var childName:String = childNode.nodeName;
   172  				var methodName:String = generateMethodName("set", childName);
   173  				if (!existsMethod(manager, methodName)) {
   174  					methodName = generateMethodName("add", childName);
   175  				}
   176  				if (!existsMethod(manager, methodName)) {
   177  					throw new NoSuchMethodException("Neither a method with name [" + generateMethodName("set", childName) + "] nor [" + methodName + "] does exist on log manager [" + manager + "].", this, arguments);
   178  				}
   179  				var childBean = parseBeanDefinition(childNode);
   180  				manager[methodName](childBean);
   181  			}
   182  		}
   183  	}
   184  	
   185  	/**
   186  	 * Parses the given {@code beanDefinition} and returns the resulting bean.
   187  	 * 
   188  	 * @param beanDefinition the definition to create a bean of
   189  	 * @return the bean resulting from the given {@code beanDefinition}
   190  	 * @throws LogConfigurationParseException if the bean definition could not be parsed
   191  	 * because of for example missing information
   192  	 * @throws ClassNotFoundException if a class corresponding to a given class name could
   193  	 * not be found
   194  	 * @throws NoSuchMethodException if a method with a given name does not exist on the
   195  	 * bean to create
   196  	 */
   197  	private function parseBeanDefinition(beanDefinition:XMLNode) {
   198  		if (!beanDefinition) {
   199  			throw new IllegalArgumentException("Argument 'beanDefinition' [" + beanDefinition + "] must not be 'null' nor 'undefined'", this, arguments);
   200  		}
   201  		var result = new Object();
   202  		var beanName:String = beanDefinition.attributes["class"];
   203  		if (beanName == null) {
   204  			beanName = nodes[beanDefinition.nodeName];
   205  		}
   206  		if (beanName == null) {
   207  			throw new LogConfigurationParseException("Node [" + beanDefinition.nodeName + "] has no class. You must either specify the 'class' attribute or register it to a class.", this, arguments);
   208  		}
   209  		var beanClass:Function = resolveClass(beanName);
   210  		if (!beanClass) {
   211  			throw new ClassNotFoundException("A class corresponding to the class name [" + beanClass + "] of node [" + beanDefinition.nodeName + "] could not be found. You either misspelled the class name or forgot to import the class in your swf.", this, arguments);
   212  		}
   213  		result.__proto__ = beanClass.prototype;
   214  		result.__constructor__ = beanClass;
   215  		var constructorArguments:Array = new Array();
   216  		var childNodes:Array = beanDefinition.childNodes;
   217  		for (var i:Number = 0; i < childNodes.length; i++) {
   218  			var childNode:XMLNode = childNodes[i];
   219  			if (childNode.nodeName == "constructor-arg") {
   220  				if (childNode.firstChild.nodeValue == null) {
   221  					constructorArguments.push(parseBeanDefinition(childNode));
   222  				} else {
   223  					constructorArguments.push(convertValue(childNode.firstChild.nodeValue));
   224  				}
   225  				childNodes.splice(i, 1);
   226  				i--;
   227  			}
   228  		}
   229  		beanClass.apply(result, constructorArguments);
   230  		for (var n:String in beanDefinition.attributes) {
   231  			if (n == "class") continue;
   232  			var methodName:String = generateMethodName("set", n);
   233  			if (!existsMethod(result, methodName)) {
   234  				throw new NoSuchMethodException("A method with name [" + methodName + "] does not exist on bean of class [" + beanName + "].", this, arguments);
   235  			}
   236  			var value:String = beanDefinition.attributes[n];
   237  			if (n == "level") {
   238  				result[methodName](AbstractLogLevel.forName(value));
   239  			} else {
   240  				result[methodName](convertValue(value));
   241  			}
   242  		}
   243  		for (var i:Number = 0; i < childNodes.length; i++) {
   244  			var childNode:XMLNode = childNodes[i];
   245  			var childName:String = childNode.nodeName;
   246  			var methodName:String = generateMethodName("add", childName);
   247  			if (!existsMethod(result, methodName)) {
   248  				methodName = generateMethodName("set", childName);
   249  			}
   250  			if (!existsMethod(result, methodName)) {
   251  				throw new NoSuchMethodException("Neither a method with name [" + generateMethodName("add", childName) + "] nor [" + methodName + "] exists on bean of class [" + beanName + "].", this, arguments);
   252  			}
   253  			if (childNode.firstChild.nodeValue == null) {
   254  				result[methodName](parseBeanDefinition(childNode));
   255  			} else {
   256  				result[methodName](convertValue(childNode.firstChild.nodeValue));
   257  			}
   258  		}
   259  		return result;
   260  	}
   261  	
   262  	/**
   263  	 * Finds the class for the given {@code className}.
   264  	 * 
   265  	 * @param className the name of the class to find
   266  	 * @return the concrete class corresponding to the given {@code className}
   267  	 */
   268  	private function resolveClass(className:String):Function {
   269  		return eval("_global." + className);
   270  	}
   271  	
   272  	/**
   273  	 * Generates a method name given a {@code prefix} and a {@code body}.
   274  	 * 
   275  	 * @param prefix the prefix of the method name
   276  	 * @param body the body of the method name
   277  	 * @return the generated method name
   278  	 */
   279  	private function generateMethodName(prefix:String, body:String):String {
   280  		return (prefix + StringUtil.ucFirst(body));
   281  	}
   282  	
   283  	/**
   284  	 * Checks whether a method with the given {@code methodName} exists on the given
   285  	 * {@code object}.
   286  	 * 
   287  	 * @param object the object that may have a method with the given name
   288  	 * @param methodName the name of the method
   289  	 * @return {@code true} if the method exists on the object else {@code false}
   290  	 */
   291  	private function existsMethod(object, methodName:String):Boolean {
   292  		try {
   293  			if (object[methodName]) {
   294  				return true;
   295  			}
   296  		} catch (e) {
   297  		}
   298  		return false;
   299  	}
   300  	
   301  	/**
   302  	 * Converts the given {@code value} into its actual type.
   303  	 * 
   304  	 * @param value the value to convert
   305  	 * @return the converted value
   306  	 */
   307  	private function convertValue(value:String) {
   308  		if (value == null) return value;
   309  		if (value == "true") return true;
   310  		if (value == "false") return false;
   311  		if (!isNaN(Number(value))) return Number(value);
   312  		return value;
   313  	}
   314  	
   315  }