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 "<logging>". 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><register name="logger" class="org.as2lib.env.log.logger.SimpleHierarchicalLogger"/></code> 44 * 45 * <p>or to specify the class-attribute in beans: 46 * <code><logger class="org.as2lib.env.log.logger.TraceLogger"/></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 * <handler class="org.as2lib.env.log.handler.TraceHandler"> 63 * <constructor-arg class="org.as2lib.env.log.stringifier.SimpleLogMessageStringifier"/> 64 * </handler> 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 * <handler class="org.as2lib.env.log.handler.TraceHandler"> 76 * <constructor-arg class="org.as2lib.env.log.stringifier.PatternLogMessageStringifier"> 77 * <constructor-arg>false</contructor-arg> 78 * <constructor-arg>true</contructor-arg> 79 * <constructor-arg>HH:nn:ss.S</contructor-arg> 80 * </constructor-arg> 81 * </handler> 82 * </code> 83 * 84 * <p>Your complete log configuration may look something like this: 85 * <code> 86 * <logging> 87 * <register name="logger" class="org.as2lib.env.log.logger.SimpleHierarchicalLogger"/> 88 * <loggerRepository class="org.as2lib.env.log.repository.LoggerHierarchy"> 89 * <logger name="com.simonwacker" level="INFO"> 90 * <handler class="org.as2lib.env.log.handler.DebugItHandler"/> 91 * <handler class="org.as2lib.env.log.handler.TraceHandler"/> 92 * </logger> 93 * <logger name="com.simonwacker.MyClass" level="ERROR"> 94 * <handler class="org.as2lib.env.log.handler.SosHandler"/> 95 * </logger> 96 * </repository> 97 * </logging> 98 * </code> 99 * 100 * <p>or this: 101 * <code> 102 * <logging> 103 * <logger level="INFO" class="org.as2lib.env.log.logger.TraceLogger"/> 104 * </logging> 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 }