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}