001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 * 027 * 028 * This file is based upon: 029 * org.apache.commons.digester3.CallMethodRule. 030 * 031 * Copyright 2001-2004 The Apache Software Foundation. 032 * 033 * Licensed under the Apache License, Version 2.0 (the "License"); 034 * you may not use this file except in compliance with the License. 035 * You may obtain a copy of the License at 036 * 037 * http://www.apache.org/licenses/LICENSE-2.0 038 * 039 * Unless required by applicable law or agreed to in writing, software 040 * distributed under the License is distributed on an "AS IS" BASIS, 041 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 042 * See the License for the specific language governing permissions and 043 * limitations under the License. 044 */ 045 046package org.opencms.configuration; 047 048import org.opencms.file.CmsObject; 049import org.opencms.main.CmsLog; 050 051import org.apache.commons.beanutils.ConvertUtils; 052import org.apache.commons.beanutils.MethodUtils; 053import org.apache.commons.digester3.Digester; 054import org.apache.commons.digester3.Rule; 055import org.apache.commons.logging.Log; 056 057import org.xml.sax.Attributes; 058 059/** 060 * Rule implementation that invokes a method on the (top-1) (parent) object, 061 * passing as implicit first argument of type <code>{@link org.opencms.file.CmsObject}</code> 062 * and as a further argument the top stack instance. <p> 063 * 064 * If no subsequent <code>CallParamRule</code> are matched for <code>CmsObject</code> 065 * which is the case in the OpenCms usage the first argument <code>CmsObject</code> 066 * will be null at method invocation time. <p> 067 068 * This is an alternative for <code>{@link org.apache.commons.digester3.SetNextRule}</code> 069 * if a parent to child-property configuration has been done but the setter for that 070 * property requires additional arguments that are only available at real runtime 071 * of the application.<p> 072 * 073 * The top stack element (child) that has to be set is matched against the constructor 074 * given <code>{@link java.lang.Class}[]</code>: It is used as argument on the position 075 * where the <code>Class[]</code> has an instance of the same type as it's own <code>Class</code>.<p> 076 * 077 * @see org.apache.commons.digester3.CallMethodRule 078 * @see org.apache.commons.digester3.SetNextRule 079 * 080 * @since 6.0.0 081 */ 082 083public class CmsSetNextRule extends Rule { 084 085 /** The log object of this class. */ 086 private static final Log LOG = CmsLog.getLog(CmsSetNextRule.class); 087 /** 088 * The body text collected from this element. 089 */ 090 protected String m_bodyText; 091 092 /** 093 * The method name to call on the parent object. 094 */ 095 protected String m_methodName; 096 097 /** 098 * The number of parameters to collect from <code>MethodParam</code> rules. 099 * If this value is zero, a single parameter will be collected from the 100 * body of this element. 101 */ 102 protected int m_paramCount; 103 104 /** 105 * The parameter types of the parameters to be collected. 106 */ 107 protected Class<?>[] m_paramTypes; 108 109 /** 110 * Should <code>MethodUtils.invokeExactMethod</code> be used for reflection. 111 */ 112 protected boolean m_useExactMatch; 113 114 /** 115 * location of the target object for the call, relative to the 116 * top of the digester object stack. The default value of zero 117 * means the target object is the one on top of the stack. 118 */ 119 private int m_targetOffset; 120 121 /** 122 * Construct a "call method" rule with the specified method name.<p> 123 * 124 * 125 * The 1<sup>st</sup> argument of the method will be of type <code>{@link CmsObject}</code>. 126 * It's value will remain null (except subsequent 127 * <code>{@link org.apache.commons.digester3.CallParamRule}</code> would put a value 128 * which currently is impossible at initialization time within OpenCms).<p> 129 * 130 * The 2<sup>nd</sup> argument will be the top-stack element at digestion time. 131 * That instance has to be of the same type as the <code>clazz</code> argument to succeed.<p> 132 * 133 * 134 * @param methodName Method name of the parent method to call 135 * @param clazz The class of the top-stack element (child) that will be present at digestion-time 136 */ 137 public CmsSetNextRule(String methodName, Class<?> clazz) { 138 139 this(methodName, new Class[] {clazz}); 140 } 141 142 /** 143 * Construct a "call method" rule with the specified method name 144 * and additional parameters.<p> 145 * 146 * 147 * The 1<sup>st</sup> argument of the method will be of type <code>{@link CmsObject}</code>. 148 * It's value will remain null (except subsequent 149 * <code>{@link org.apache.commons.digester3.CallParamRule}</code> would put a value 150 * which currently is impossible at initialization time within OpenCms).<p> 151 * 152 * The further arguments will be filled by the subsequent <code>{@link org.apache.commons.digester3.CallParamRule}</code> 153 * matches. If the first <code>Class</code> in the given array matches the top stack element 154 * (child) that value will be used. If at digestion time no parameters are found for the given 155 * types their values for invocation of the method remain null.<p> 156 * 157 * 158 * @param methodName Method name of the parent method to call 159 * @param clazzes an array with all parameter types for the method to invoke at digestion time 160 */ 161 public CmsSetNextRule(String methodName, Class<?>[] clazzes) { 162 163 m_targetOffset = 0; 164 m_methodName = methodName; 165 m_paramCount = clazzes.length + 1; 166 m_paramTypes = new Class[m_paramCount]; 167 m_paramTypes[0] = CmsObject.class; 168 System.arraycopy(clazzes, 0, m_paramTypes, 1, clazzes.length); 169 } 170 171 /** 172 * Process the start of this element. 173 * 174 * @param attributes The attribute list for this element 175 * @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace 176 * aware or the element has no namespace 177 * @param name the local name if the parser is namespace aware, or just the element name otherwise 178 * @throws Exception if something goes wrong 179 */ 180 @Override 181 public void begin(String namespace, String name, Attributes attributes) throws Exception { 182 183 // not now: 6.0.0 184 // digester.setLogger(CmsLog.getLog(digester.getClass())); 185 186 // Push an array to capture the parameter values if necessary 187 if (m_paramCount > 0) { 188 Object[] parameters = new Object[m_paramCount]; 189 for (int i = 0; i < parameters.length; i++) { 190 parameters[i] = null; 191 } 192 getDigester().pushParams(parameters); 193 } 194 } 195 196 /** 197 * Process the body text of this element.<p> 198 * 199 * @param bodyText The body text of this element 200 * @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace 201 * aware or the element has no namespace 202 * @param name the local name if the parser is namespace aware, or just the element name otherwise 203 * @throws Exception if something goes wrong 204 */ 205 @Override 206 public void body(String namespace, String name, String bodyText) throws Exception { 207 208 if (m_paramCount == 0) { 209 m_bodyText = bodyText.trim(); 210 } 211 } 212 213 /** 214 * Process the end of this element.<p> 215 * 216 * @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace 217 * aware or the element has no namespace 218 * @param name the local name if the parser is namespace aware, or just the element name otherwise 219 * @throws Exception if something goes wrong 220 */ 221 @Override 222 public void end(String namespace, String name) throws Exception { 223 224 // Determine the target object for the method call: the parent object 225 Object parent = getDigester().peek(1); 226 Object child = getDigester().peek(0); 227 228 // Retrieve or construct the parameter values array 229 Object[] parameters = null; 230 if (m_paramCount > 0) { 231 parameters = getDigester().popParams(); 232 if (LOG.isTraceEnabled()) { 233 for (int i = 0, size = parameters.length; i < size; i++) { 234 LOG.trace("[SetNextRuleWithParams](" + i + ")" + parameters[i]); 235 } 236 } 237 238 // In the case where the target method takes a single parameter 239 // and that parameter does not exist (the CallParamRule never 240 // executed or the CallParamRule was intended to set the parameter 241 // from an attribute but the attribute wasn't present etc) then 242 // skip the method call. 243 // 244 // This is useful when a class has a "default" value that should 245 // only be overridden if data is present in the XML. I don't 246 // know why this should only apply to methods taking *one* 247 // parameter, but it always has been so we can't change it now. 248 if ((m_paramCount == 1) && (parameters[0] == null)) { 249 return; 250 } 251 252 } else if (m_paramTypes.length != 0) { 253 // Having paramCount == 0 and paramTypes.length == 1 indicates 254 // that we have the special case where the target method has one 255 // parameter being the body text of the current element. 256 257 // There is no body text included in the source XML file, 258 // so skip the method call 259 if (m_bodyText == null) { 260 return; 261 } 262 263 parameters = new Object[1]; 264 parameters[0] = m_bodyText; 265 if (m_paramTypes.length == 0) { 266 m_paramTypes = new Class[1]; 267 m_paramTypes[0] = String.class; 268 } 269 270 } else { 271 // When paramCount is zero and paramTypes.length is zero it 272 // means that we truly are calling a method with no parameters. 273 // Nothing special needs to be done here. 274 parameters = new Object[0]; 275 } 276 277 // Construct the parameter values array we will need 278 // We only do the conversion if the param value is a String and 279 // the specified paramType is not String. 280 Object[] paramValues = new Object[m_paramTypes.length]; 281 282 Class<?> propertyClass = child.getClass(); 283 for (int i = 0; i < m_paramTypes.length; i++) { 284 if (m_paramTypes[i] == propertyClass) { 285 // implant the original child to set if Class matches: 286 paramValues[i] = child; 287 } else if ((parameters[i] == null) 288 || ((parameters[i] instanceof String) && !String.class.isAssignableFrom(m_paramTypes[i]))) { 289 // convert nulls and convert stringy parameters 290 // for non-stringy param types 291 if (parameters[i] == null) { 292 paramValues[i] = null; 293 } else { 294 paramValues[i] = ConvertUtils.convert((String)parameters[i], m_paramTypes[i]); 295 } 296 297 } else { 298 paramValues[i] = parameters[i]; 299 } 300 } 301 302 if (parent == null) { 303 StringBuffer sb = new StringBuffer(); 304 sb.append("[SetNextRuleWithParams]{"); 305 sb.append(getDigester().getMatch()); 306 sb.append("} Call target is null ("); 307 sb.append("targetOffset="); 308 sb.append(m_targetOffset); 309 sb.append(",stackdepth="); 310 sb.append(getDigester().getCount()); 311 sb.append(")"); 312 throw new org.xml.sax.SAXException(sb.toString()); 313 } 314 315 // Invoke the required method on the top object 316 if (LOG.isDebugEnabled()) { 317 StringBuffer sb = new StringBuffer("[SetNextRuleWithParams]{"); 318 sb.append(getDigester().getMatch()); 319 sb.append("} Call "); 320 sb.append(parent.getClass().getName()); 321 sb.append("."); 322 sb.append(m_methodName); 323 sb.append("("); 324 for (int i = 0; i < paramValues.length; i++) { 325 if (i > 0) { 326 sb.append(","); 327 } 328 if (paramValues[i] == null) { 329 sb.append("null"); 330 } else { 331 sb.append(paramValues[i].toString()); 332 } 333 sb.append("/"); 334 if (m_paramTypes[i] == null) { 335 sb.append("null"); 336 } else { 337 sb.append(m_paramTypes[i].getName()); 338 } 339 } 340 sb.append(")"); 341 LOG.debug(sb.toString()); 342 } 343 344 Object result = null; 345 if (m_useExactMatch) { 346 // invoke using exact match 347 result = MethodUtils.invokeExactMethod(parent, m_methodName, paramValues, m_paramTypes); 348 349 } else { 350 // invoke using fuzzier match 351 result = MethodUtils.invokeMethod(parent, m_methodName, paramValues, m_paramTypes); 352 } 353 354 processMethodCallResult(result); 355 } 356 357 /** 358 * Clean up after parsing is complete.<p> 359 * 360 * @param namespace the namespace URI of the matching element, or an empty string if the parser is not namespace 361 * aware or the element has no namespace 362 * @param name the local name if the parser is namespace aware, or just the element name otherwise 363 * @throws Exception if something goes wrong 364 */ 365 public void finish(String namespace, String name) throws Exception { 366 367 String dummy = name; 368 dummy = namespace; 369 dummy = null; 370 m_bodyText = dummy; 371 } 372 373 /** 374 * Returns true if <code>MethodUtils.invokeExactMethod</code> 375 * shall be used for the reflection.<p> 376 * 377 * @return true if <code>MethodUtils.invokeExactMethod</code> 378 * shall be used for the reflection. 379 */ 380 public boolean getUseExactMatch() { 381 382 return m_useExactMatch; 383 } 384 385 /** 386 * Set the associated digester.<p> 387 * 388 * The digester gets assigned to use the OpenCms conform logging 389 * 390 * If needed, this class loads the parameter classes from their names.<p> 391 * 392 * @param aDigester the associated digester to set 393 */ 394 @Override 395 public void setDigester(Digester aDigester) { 396 397 aDigester.setLogger(CmsLog.getLog(aDigester.getClass())); 398 // call superclass 399 super.setDigester(aDigester); 400 } 401 402 /** 403 * Set the value to use for <code>MethodUtils.invokeExactMethod</code> 404 * to use.<p> 405 * 406 * @param useExactMatch the value to use for <code>MethodUtils.invokeExactMethod</code> 407 * to use 408 */ 409 public void setUseExactMatch(boolean useExactMatch) { 410 411 m_useExactMatch = useExactMatch; 412 } 413 414 /** 415 * Returns a printable version of this Rule.<p> 416 * 417 * @return a printable version of this Rule 418 */ 419 @Override 420 public String toString() { 421 422 StringBuffer sb = new StringBuffer("CallMethodRule["); 423 sb.append("methodName="); 424 sb.append(m_methodName); 425 sb.append(", paramCount="); 426 sb.append(m_paramCount); 427 sb.append(", paramTypes={"); 428 if (m_paramTypes != null) { 429 for (int i = 0; i < m_paramTypes.length; i++) { 430 if (i > 0) { 431 sb.append(", "); 432 } 433 sb.append(m_paramTypes[i].getName()); 434 } 435 } 436 sb.append("}"); 437 sb.append("]"); 438 return (sb.toString()); 439 440 } 441 442 /** 443 * Subclasses may override this method to perform additional processing of the 444 * invoked method's result.<p> 445 * 446 * @param result the Object returned by the method invoked, possibly null 447 */ 448 protected void processMethodCallResult(Object result) { 449 450 // do nothing but to fool checkstyle 451 if (result != null) { 452 // nop 453 } 454 } 455}