/* $Id: CallMethodRuleTestCase.java 472840 2006-11-09 10:18:34Z skitching $
 *
 * 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.commons.digester;


import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

import org.xml.sax.SAXException;

//import org.apache.commons.logging.impl.SimpleLog;

/**
 * <p>Tests for the <code>CallMethodRule</code> and associated 
 * <code>CallParamRule</code>.
 *
 * @author Christopher Lenz 
 */
public class CallMethodRuleTestCase extends TestCase {


    // ----------------------------------------------------- Instance Variables


    /**
     * The digester instance we will be processing.
     */
    protected Digester digester = null;


    // ----------------------------------------------------------- Constructors


    /**
     * Construct a new instance of this test case.
     *
     * @param name Name of the test case
     */
    public CallMethodRuleTestCase(String name) {

        super(name);

    }


    // --------------------------------------------------- Overall Test Methods


    /**
     * Set up instance variables required by this test case.
     */
    public void setUp() {

        digester = new Digester();

    }


    /**
     * Return the tests included in this test suite.
     */
    public static Test suite() {

        return (new TestSuite(CallMethodRuleTestCase.class));

    }


    /**
     * Tear down instance variables required by this test case.
     */
    public void tearDown() {

        digester = null;

    }



    // ------------------------------------------------ Individual Test Methods


    /**
     * Test method calls with the CallMethodRule rule. It should be possible
     * to call a method with no arguments using several rule syntaxes.
     */
    public void testBasic() throws SAXException, IOException {
        
        // Configure the digester as required
        digester.addObjectCreate("employee", Employee.class);
        // try all syntax permutations
        digester.addCallMethod("employee", "toString", 0, (Class[])null);
        digester.addCallMethod("employee", "toString", 0, (String[])null);
        digester.addCallMethod("employee", "toString", 0, new Class[] {});
        digester.addCallMethod("employee", "toString", 0, new String[] {});
        digester.addCallMethod("employee", "toString");

        // Parse our test input.
        // An exception will be thrown if the method can't be found
        Object root1 = digester.parse(getInputStream("Test5.xml"));
        assertNotNull(root1);
    }


    /**
     * Test method calls with the CallMethodRule reading from the element
     * body, with no CallParamMethod rules added.
     */
    public void testCallMethodOnly() throws Exception {

        // Configure the digester as required
        digester.addObjectCreate("employee", Employee.class);
        digester.addCallMethod("employee/firstName", "setFirstName", 0);
        digester.addCallMethod("employee/lastName", "setLastName", 0);

        // Parse our test input
        Employee employee = (Employee)
            digester.parse(getInputStream("Test9.xml"));
        assertNotNull("parsed an employee", employee);

        // Validate that the property setters were called
        assertEquals("Set first name", "First Name", employee.getFirstName());
        assertEquals("Set last name", "Last Name", employee.getLastName());
    }


    /**
     * Test CallMethodRule variants which specify the classes of the
     * parameters to target methods. String, int, boolean, float should all 
     * be acceptable as parameter types.
     */
    public void testSettingProperties() throws SAXException, IOException {
            
        // Configure the digester as required
        digester.addObjectCreate("employee", Employee.class);
        // try all syntax permutations
        digester.addCallMethod("employee", "setLastName", 1, 
                                new String[] {"java.lang.String"});
        digester.addCallParam("employee/lastName", 0);
                
        // Parse our test input
        Object root1 = null;
        
        // an exception will be thrown if the method can't be found
        root1 = digester.parse(getInputStream("Test5.xml"));
        Employee employee = (Employee) root1;
        assertEquals("Failed to call Employee.setLastName", 
                    "Last Name", employee.getLastName()); 
        

        digester = new Digester();
        // Configure the digester as required
        digester.addObjectCreate("employee", Employee.class);
        // try out primitive convertion
        digester.addCallMethod("employee", "setAge", 1, 
                                new Class[] {int.class});
        digester.addCallParam("employee/age", 0);         
                
        // Parse our test input
        root1 = null;
        
        // an exception will be thrown if the method can't be found
        root1 = digester.parse(getInputStream("Test5.xml"));
        employee = (Employee) root1;
        assertEquals("Failed to call Employee.setAge", 21, employee.getAge()); 
        
        digester = new Digester();
        // Configure the digester as required
        digester.addObjectCreate("employee", Employee.class);      
        digester.addCallMethod("employee", "setActive", 1, 
                                new Class[] {boolean.class});
        digester.addCallParam("employee/active", 0);    
                
        // Parse our test input
        root1 = null;

        // an exception will be thrown if the method can't be found
        root1 = digester.parse(getInputStream("Test5.xml"));
        employee = (Employee) root1;
        assertEquals("Failed to call Employee.setActive", 
                        true, employee.isActive()); 
        
        digester = new Digester();            
        // Configure the digester as required
        digester.addObjectCreate("employee", Employee.class); 
        digester.addCallMethod("employee", "setSalary", 1, 
                                new Class[] {float.class});
        digester.addCallParam("employee/salary", 0);    
                
        // Parse our test input
        root1 = null;
        // an exception will be thrown if the method can't be found
        root1 = digester.parse(getInputStream("Test5.xml"));
        employee = (Employee) root1;
        assertEquals("Failed to call Employee.setSalary", 
                        1000000.0f, employee.getSalary(), 0.1f); 
    }


    /**
     * This tests the call methods params enhancement that provides 
     * for more complex stack-based calls.
     */
    public void testParamsFromStack() throws SAXException, IOException {

        StringBuffer xml = new StringBuffer().
            append("<?xml version='1.0'?>").
            append("<map>").
            append("  <key name='The key'/>").
            append("  <value name='The value'/>").
            append("</map>");

        digester.addObjectCreate("map", HashMap.class);
        digester.addCallMethod("map", "put", 2);
        digester.addObjectCreate("map/key", AlphaBean.class);
        digester.addSetProperties("map/key");
        digester.addCallParam("map/key", 0, true);
        digester.addObjectCreate("map/value", BetaBean.class);
        digester.addSetProperties("map/value");
        digester.addCallParam("map/value", 1, true);

        Map map = (Map) digester.parse(new StringReader(xml.toString()));

        assertNotNull(map);
        assertEquals(1, map.size());
        assertEquals("The key",
                     ((AlphaBean)map.keySet().iterator().next()).getName());
        assertEquals("The value",
                     ((BetaBean)map.values().iterator().next()).getName());
    }


    /**
     * Test that the target object for a CallMethodRule is the object that was
     * on top of the object stack when the CallMethodRule fired, even when other
     * rules fire between the CallMethodRule and its associated CallParamRules.
     * <p>
     * The current implementation of CallMethodRule ensures this works by
     * firing only at the end of the tag that CallMethodRule triggered on.
     */
    public void testOrderNestedPartA() throws Exception {

        // Configure the digester as required

        // Here, we use the "grandchild element name" as a parameter to
        // the created element, to ensure that all the params aren't
        // avaiable to the CallMethodRule until some other rules have fired,
        // in particular an ObjectCreateRule. The CallMethodRule should still
        // function correctly in this scenario.
        digester.addObjectCreate("toplevel/element", NamedBean.class);
        digester.addCallMethod("toplevel/element", "setName", 1);
        digester.addCallParam("toplevel/element/element/element", 0, "name");
        
        digester.addObjectCreate("toplevel/element/element", NamedBean.class);

        // Parse our test input
        NamedBean root1 = null;
        try {
            // an exception will be thrown if the method can't be found
            root1 = (NamedBean) digester.parse(getInputStream("Test8.xml"));
            
        } catch (Throwable t) {
            // this means that the method can't be found and so the test fails
            fail("Digester threw Exception:  " + t);
        }
        
        // if the CallMethodRule were to incorrectly invoke the method call
        // on the second-created NamedBean instance, then the root one would
        // have a null name. If it works correctly, the target element will
        // be the first-created (root) one, despite the fact that a second
        // object instance was created between the firing of the 
        // CallMethodRule and its associated CallParamRule.
        assertEquals("Wrong method call order", "C", root1.getName());
    }

    /**
     * Test nested CallMethod rules.
     * <p>
     * The current implementation of CallMethodRule, in which the method is
     * invoked in its end() method, causes behaviour which some users find
     * non-intuitive. In this test it can be seen to "reverse" the order of
     * data processed. However this is the way CallMethodRule has always 
     * behaved, and it is expected that apps out there rely on this call order 
     * so this test is present to ensure that no-one changes this behaviour.
     */
    public void testOrderNestedPartB() throws Exception {
        
        // Configure the digester as required
        StringBuffer word = new StringBuffer();
        digester.push(word);
        digester.addCallMethod("*/element", "append", 1);
        digester.addCallParam("*/element", 0, "name");
        
        // Parse our test input
        Object root1 = null;
        try {
            // an exception will be thrown if the method can't be found
            root1 = digester.parse(getInputStream("Test8.xml"));
            assertNotNull(root1);
        } catch (Throwable t) {
            // this means that the method can't be found and so the test fails
            fail("Digester threw Exception:  " + t);
        }
        
        assertEquals("Wrong method call order", "CBA", word.toString());
    }

    public void testPrimitiveReading() throws Exception {
        StringReader reader = new StringReader(
            "<?xml version='1.0' ?><root><bean good='true'/><bean good='false'/><bean/>"
            + "<beanie bad='Fee Fie Foe Fum' good='true'/><beanie bad='Fee Fie Foe Fum' good='false'/>"
            + "<beanie bad='Fee Fie Foe Fum'/></root>");
            
        Digester digester = new Digester();
        
        //SimpleLog log = new SimpleLog("[testPrimitiveReading:Digester]");
        //log.setLevel(SimpleLog.LOG_LEVEL_TRACE);
        //digester.setLogger(log);
        
        digester.addObjectCreate("root/bean", PrimitiveBean.class);
        digester.addSetNext("root/bean", "add");
        Class [] params = { Boolean.TYPE };
        digester.addCallMethod("root/bean", "setBoolean", 1, params);
        digester.addCallParam("root/bean", 0, "good");
        
        digester.addObjectCreate("root/beanie", PrimitiveBean.class);
        digester.addSetNext("root/beanie", "add");
        Class [] beanieParams = { String.class, Boolean.TYPE };
        digester.addCallMethod("root/beanie", "testSetBoolean", 2, beanieParams);
        digester.addCallParam("root/beanie", 0, "bad");
        digester.addCallParam("root/beanie", 1, "good");
        
        ArrayList list = new ArrayList();
        digester.push(list);
        digester.parse(reader);
        
        assertEquals("Wrong number of beans in list", 6, list.size());
        PrimitiveBean bean = (PrimitiveBean) list.get(0);
        assertTrue("Bean 0 property not called", bean.getSetBooleanCalled());
        assertEquals("Bean 0 property incorrect", true, bean.getBoolean());
        bean = (PrimitiveBean) list.get(1);
        assertTrue("Bean 1 property not called", bean.getSetBooleanCalled());
        assertEquals("Bean 1 property incorrect", false, bean.getBoolean());
        bean = (PrimitiveBean) list.get(2);
        // no attibute, no call is what's expected
        assertTrue("Bean 2 property called", !bean.getSetBooleanCalled());
        bean = (PrimitiveBean) list.get(3);
        assertTrue("Bean 3 property not called", bean.getSetBooleanCalled());
        assertEquals("Bean 3 property incorrect", true, bean.getBoolean());
        bean = (PrimitiveBean) list.get(4);
        assertTrue("Bean 4 property not called", bean.getSetBooleanCalled());
        assertEquals("Bean 4 property incorrect", false, bean.getBoolean());
        bean = (PrimitiveBean) list.get(5);
        assertTrue("Bean 5 property not called", bean.getSetBooleanCalled());
        assertEquals("Bean 5 property incorrect", false, bean.getBoolean());       
    }
    
    public void testFromStack() throws Exception {
    
        StringReader reader = new StringReader(
            "<?xml version='1.0' ?><root><one/><two/><three/><four/><five/></root>");
            
        Digester digester = new Digester();
        
        Class [] params = { String.class };
        
        digester.addObjectCreate("root/one", NamedBean.class);
        digester.addSetNext("root/one", "add");
        digester.addCallMethod("root/one", "setName", 1, params);
        digester.addCallParam("root/one", 0, 2);
        
        digester.addObjectCreate("root/two", NamedBean.class);
        digester.addSetNext("root/two", "add");
        digester.addCallMethod("root/two", "setName", 1, params);
        digester.addCallParam("root/two", 0, 3);
        
        digester.addObjectCreate("root/three", NamedBean.class);
        digester.addSetNext("root/three", "add");
        digester.addCallMethod("root/three", "setName", 1, params);
        digester.addCallParam("root/three", 0, 4);
        
        digester.addObjectCreate("root/four", NamedBean.class);
        digester.addSetNext("root/four", "add");
        digester.addCallMethod("root/four", "setName", 1, params);
        digester.addCallParam("root/four", 0, 5);
        
        digester.addObjectCreate("root/five", NamedBean.class);
        digester.addSetNext("root/five", "add");
        Class [] newParams = { String.class, String.class };
        digester.addCallMethod("root/five", "test", 2, newParams);
        digester.addCallParam("root/five", 0, 10);
        digester.addCallParam("root/five", 1, 3);
        
        // prepare stack
        digester.push("That lamb was sure to go.");
        digester.push("And everywhere that Mary went,");
        digester.push("It's fleece was white as snow.");
        digester.push("Mary had a little lamb,");
        
        ArrayList list = new ArrayList();
        digester.push(list);
        digester.parse(reader);
        
        assertEquals("Wrong number of beans in list", 5, list.size());
        NamedBean bean = (NamedBean) list.get(0);
        assertEquals("Parameter not set from stack (1)", "Mary had a little lamb,", bean.getName());
        bean = (NamedBean) list.get(1);
        assertEquals("Parameter not set from stack (2)", "It's fleece was white as snow.", bean.getName());
        bean = (NamedBean) list.get(2);
        assertEquals("Parameter not set from stack (3)", "And everywhere that Mary went,", bean.getName());
        bean = (NamedBean) list.get(3);
        assertEquals("Parameter not set from stack (4)", "That lamb was sure to go.", bean.getName());
        bean = (NamedBean) list.get(4);
        assertEquals("Out of stack not set to null", null , bean.getName());
    }
    
    public void testTwoCalls() throws Exception {
        
    
        StringReader reader = new StringReader(
            "<?xml version='1.0' ?><root>"
            + "<param class='int' coolness='true'>25</param>"
            + "<param class='long'>50</param>"
            + "<param class='float' coolness='false'>90</param></root>");
            
        Digester digester = new Digester();
        //SimpleLog log = new SimpleLog("{testTwoCalls:Digester]");
        //log.setLevel(SimpleLog.LOG_LEVEL_TRACE);
        //digester.setLogger(log);
        
        digester.addObjectCreate( "root/param", ParamBean.class );
        digester.addSetNext( "root/param", "add" );
        digester.addCallMethod( "root/param", "setThisAndThat", 2 );
        digester.addCallParam( "root/param", 0, "class" );
        digester.addCallParam( "root/param", 1 );
        digester.addCallMethod( "root/param", "setCool", 1, new Class[] {boolean.class } );
        digester.addCallParam( "root/param", 0, "coolness" );
        
        ArrayList list = new ArrayList();
        digester.push(list);
        digester.parse(reader);
    
        assertEquals("Wrong number of objects created", 3, list.size());
        ParamBean bean = (ParamBean) list.get(0);
        assertEquals("Coolness wrong (1)", true, bean.isCool());
        assertEquals("This wrong (1)", "int", bean.getThis());
        assertEquals("That wrong (1)", "25", bean.getThat());
        bean = (ParamBean) list.get(1);
        assertEquals("Coolness wrong (2)", false, bean.isCool());
        assertEquals("This wrong (2)", "long", bean.getThis());
        assertEquals("That wrong (2)", "50", bean.getThat());
        bean = (ParamBean) list.get(2);
        assertEquals("Coolness wrong (3)", false, bean.isCool());
        assertEquals("This wrong (3)", "float", bean.getThis());
        assertEquals("That wrong (3)", "90", bean.getThat());
    }

    public void testNestedBody() throws Exception {
        
        StringReader reader = new StringReader(
            "<?xml version='1.0' ?><root>"
            + "<spam>Simple</spam>"
            + "<spam>Complex<spam>Deep<spam>Deeper<spam>Deepest</spam></spam></spam></spam>"
            + "</root>");
            
        Digester digester = new Digester();        

        //SimpleLog log = new SimpleLog("[testPrimitiveReading:Digester]");
        //log.setLevel(SimpleLog.LOG_LEVEL_TRACE);
        //digester.setLogger(log);
        
        
        digester.addObjectCreate("root/spam", NamedBean.class);
        digester.addSetRoot("root/spam", "add");
        digester.addCallMethod( "root/spam", "setName", 1 );
        digester.addCallParam( "root/spam", 0);
        
        digester.addObjectCreate("root/spam/spam", NamedBean.class);
        digester.addSetRoot("root/spam/spam", "add");
        digester.addCallMethod( "root/spam/spam", "setName", 1 );
        digester.addCallParam( "root/spam/spam", 0);        
        
        digester.addObjectCreate("root/spam/spam/spam", NamedBean.class);
        digester.addSetRoot("root/spam/spam/spam", "add");
        digester.addCallMethod( "root/spam/spam/spam", "setName", 1 );
        digester.addCallParam( "root/spam/spam/spam", 0);      

        
        digester.addObjectCreate("root/spam/spam/spam/spam", NamedBean.class);
        digester.addSetRoot("root/spam/spam/spam/spam", "add");
        digester.addCallMethod( "root/spam/spam/spam/spam", "setName", 1 );
        digester.addCallParam( "root/spam/spam/spam/spam", 0);   
        
        ArrayList list = new ArrayList();
        digester.push(list);
        digester.parse(reader);
        
        NamedBean bean = (NamedBean) list.get(0);
        assertEquals("Wrong name (1)", "Simple", bean.getName());
        // these are added in deepest first order by the addRootRule
        bean = (NamedBean) list.get(4);
        assertEquals("Wrong name (2)", "Complex", bean.getName());
        bean = (NamedBean) list.get(3);
        assertEquals("Wrong name (3)", "Deep", bean.getName());
        bean = (NamedBean) list.get(2);
        assertEquals("Wrong name (4)", "Deeper", bean.getName());
        bean = (NamedBean) list.get(1);
        assertEquals("Wrong name (5)", "Deepest", bean.getName());
    }
    
    public void testProcessingHook() throws Exception {
        
        class TestCallMethodRule extends CallMethodRule {
            Object result;
            TestCallMethodRule(String methodName, int paramCount)
            {
                super(methodName, paramCount);
            }
            protected void processMethodCallResult(Object result) {
                this.result = result;
            }
        }
    
        StringReader reader = new StringReader(
            "<?xml version='1.0' ?><root>"
            + "<param class='float' coolness='false'>90</param></root>");
        
            
        Digester digester = new Digester();
        //SimpleLog log = new SimpleLog("{testTwoCalls:Digester]");
        //log.setLevel(SimpleLog.LOG_LEVEL_TRACE);
        //digester.setLogger(log);
        
        digester.addObjectCreate( "root/param", ParamBean.class );
        digester.addSetNext( "root/param", "add" );
        TestCallMethodRule rule = new TestCallMethodRule( "setThisAndThat" , 2 );
        digester.addRule( "root/param", rule );
        digester.addCallParam( "root/param", 0, "class" );
        digester.addCallParam( "root/param", 1, "coolness" );
        
        ArrayList list = new ArrayList();
        digester.push(list);
        digester.parse(reader);
    
        assertEquals("Wrong number of objects created", 1, list.size());
        assertEquals("Result not passed into hook", "The Other", rule.result);
    }

    /** Test for the PathCallParamRule */
    public void testPathCallParam() throws Exception {
        String xml = "<?xml version='1.0'?><main>"
            + "<alpha><beta>Ignore this</beta></alpha>"
            + "<beta><epsilon><gamma>Ignore that</gamma></epsilon></beta>"
            + "</main>";
    
        SimpleTestBean bean = new SimpleTestBean();
        bean.setAlphaBeta("[UNSET]", "[UNSET]");
        
        StringReader in = new StringReader(xml);
        Digester digester = new Digester();
        digester.setRules(new ExtendedBaseRules());
        digester.addCallParamPath("*/alpha/?", 0);
        digester.addCallParamPath("*/epsilon/?", 1);
        digester.addCallMethod("main", "setAlphaBeta", 2);
        
        digester.push(bean);
        
        digester.parse(in);
        
        assertEquals("Test alpha property setting", "main/alpha/beta" , bean.getAlpha());
        assertEquals("Test beta property setting", "main/beta/epsilon/gamma" , bean.getBeta());
    }


    /** 
     * Test invoking an object which does not exist on the stack.
     */
    public void testCallInvalidTarget() throws Exception {
    
        Digester digester = new Digester();
        digester.addObjectCreate("employee", HashMap.class);

        // there should be only one object on the stack (index zero),
        // so selecting a target object with index 1 on the object stack
        // should result in an exception.
        CallMethodRule r = new CallMethodRule(1, "put", 0);
        digester.addRule("employee", r);
        
        try {
            digester.parse(getInputStream("Test5.xml"));
            fail("Exception should be thrown for invalid target offset");
        }
        catch(SAXException e) {
            // ok, exception expected
        }
    }

    /** 
     * Test invoking an object which is at top-1 on the stack, like
     * SetNextRule does...
     */
    public void testCallNext() throws Exception {
    
        Digester digester = new Digester();
        digester.addObjectCreate("employee", HashMap.class);

        digester.addObjectCreate("employee/address", Address.class);
        digester.addSetNestedProperties("employee/address");
        CallMethodRule r = new CallMethodRule(1, "put", 2);
        digester.addRule("employee/address", r);
        digester.addCallParam("employee/address/type", 0);
        digester.addCallParam("employee/address", 1, 0);
        
        HashMap map = (HashMap) digester.parse(getInputStream("Test5.xml"));
        
        assertNotNull(map);
        java.util.Set keys = map.keySet();
        assertEquals(2, keys.size());
        Address home = (Address) map.get("home");
        assertNotNull(home);
        assertEquals("HmZip", home.getZipCode());
        Address office = (Address) map.get("office");
        assertNotNull(office);
        assertEquals("OfZip", office.getZipCode());
    }

    /** 
     * Test invoking an object which is at the root of the stack, like
     * SetRoot does...
     */
    public void testCallRoot() throws Exception {
    
        Digester digester = new Digester();
        digester.addObjectCreate("employee", HashMap.class);

        digester.addObjectCreate("employee/address", Address.class);
        digester.addSetNestedProperties("employee/address");
        CallMethodRule r = new CallMethodRule(-1, "put", 2);
        digester.addRule("employee/address", r);
        digester.addCallParam("employee/address/type", 0);
        digester.addCallParam("employee/address", 1, 0);
        
        HashMap map = (HashMap) digester.parse(getInputStream("Test5.xml"));
        
        assertNotNull(map);
        java.util.Set keys = map.keySet();
        assertEquals(2, keys.size());
        Address home = (Address) map.get("home");
        assertNotNull(home);
        assertEquals("HmZip", home.getZipCode());
        Address office = (Address) map.get("office");
        assertNotNull(office);
        assertEquals("OfZip", office.getZipCode());
    }

    // ------------------------------------------------ Utility Support Methods


    /**
     * Return an appropriate InputStream for the specified test file (which
     * must be inside our current package.
     *
     * @param name Name of the test file we want
     *
     * @exception IOException if an input/output error occurs
     */
    protected InputStream getInputStream(String name) throws IOException {

        return (this.getClass().getResourceAsStream
                ("/org/apache/commons/digester/" + name));

    }


}

