001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     * 
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     * 
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */ 
017    package org.apache.commons.betwixt.expression;
018    
019    import java.lang.reflect.Array;
020    import java.lang.reflect.Method;
021    import java.util.Collection;
022    
023    import org.apache.commons.logging.Log;
024    import org.apache.commons.logging.LogFactory;
025    
026    /** <p><code>MapEntryAdder</code> is used to add entries to a map.</p>
027      *
028      * <p>
029      * <code>MapEntryAdder</code> supplies two updaters:
030      * <ul>
031      *   <li>{@link #getKeyUpdater()} which allows the entry key to be updated</li>
032      *   <li>{@link #getValueUpdater()} which allows the entry value to be updated</li>
033      * </ul>
034      * When both of these updaters have been called, the entry adder method is called.
035      * Once this has happened then the values can be updated again.
036      * Note that only the <code>Context</code> passed by the last update will be used.
037      * </p>
038      *
039      * @author <a href="mailto:rdonkin@apache.org">Robert Burrell Donkin</a>
040      * @since 0.5
041      */
042    public class MapEntryAdder {
043    
044        
045        // Class Attributes
046        //-------------------------------------------------------------------------   
047        
048        /** Log used by this class */
049        private static Log log = LogFactory.getLog( MapEntryAdder.class );
050        
051        
052        // Class Methods
053        //-------------------------------------------------------------------------      
054        
055        /** 
056         * Sets the logger used by this class.
057         *
058         * @param newLog log to this
059         */
060        public static void setLog(Log newLog) {
061            log = newLog;
062        }
063        
064        // Attributes
065        //-------------------------------------------------------------------------    
066    
067        /** The method to be called to add a new map entry */
068        private Method adderMethod;
069        
070        /** Has the entry key been updated? */
071        private boolean keyUpdated = false;
072        /** The entry key */
073        private Object key;
074        
075        /** Has the entry value been updated? */
076        private boolean valueUpdated = false;
077        /** The entry value */
078        private Object value;
079        
080            
081        // Constructors
082        //-------------------------------------------------------------------------    
083        
084        /**
085         * Construct a <code>MapEntryAdder</code> which adds entries to given method.
086         *
087         * @param method the <code>Method</code> called to add a key-value entry
088         * @throws IllegalArgumentException if the given method does not take two parameters 
089         */
090        public MapEntryAdder(Method method) {
091            
092            Class[] types = method.getParameterTypes();
093            if ( types == null || types.length != 2) {
094                throw new IllegalArgumentException(
095                    "Method used to add entries to maps must have two parameter.");
096            }
097            this.adderMethod = method;
098        }
099        
100        // Properties
101        //-------------------------------------------------------------------------    
102        
103        /**
104         * Gets the entry key <code>Updater</code>.
105         * This is used to update the entry key value to the read value.
106         * If {@link #getValueUpdater} has been called previously, 
107         * then this trigger the updating of the adder method.
108         *
109         * @return the <code>Updater</code> which should be used to populate the entry key
110         */
111        public Updater getKeyUpdater() {
112            
113            return new Updater() {
114                public void update( Context context, Object keyValue ) {
115                    // might as well make sure that his can only be set once
116                    if ( !keyUpdated ) {
117                        keyUpdated = true;
118                        key = keyValue;
119                        if ( log.isTraceEnabled() ) {
120                            log.trace( "Setting entry key to " + key );
121                            log.trace( "Current entry value is " + value );
122                        }
123                        if ( valueUpdated ) {
124                            callAdderMethod( context );
125                        }
126                    }
127                }
128            };
129        }
130        
131        /**
132         * Gets the entry value <code>Updater</code>.
133         * This is used to update the entry key value to the read value.
134         * If {@link #getKeyUpdater} has been called previously, 
135         * then this trigger the updating of the adder method.
136         *
137         * @return the <code>Updater</code> which should be used to populate the entry value
138         */
139        public Updater getValueUpdater() {
140            
141            return new Updater() {
142                public void update( Context context, Object valueValue ) {
143                    // might as well make sure that his can only be set once
144                    if ( !valueUpdated ) {
145                        valueUpdated = true;
146                        value = valueValue;
147                        if ( log.isTraceEnabled() ) {
148                            log.trace( "Setting entry value to " + value);
149                            log.trace( "Current entry key is " + key );
150                        }
151                        if ( keyUpdated ) {
152                            callAdderMethod( context );
153                        }
154                    }
155                }
156            };
157        }
158        
159        
160        
161        // Implementation methods
162        //-------------------------------------------------------------------------    
163    
164        /**
165         * Call the adder method on the bean associated with the <code>Context</code>
166         * with the key, value entry values stored previously.
167         *
168         * @param context the Context against whose bean the adder method will be invoked
169         */
170        private void callAdderMethod(Context context) {
171            log.trace("Calling adder method");
172            
173            // this allows the same instance to be used multiple times.
174            keyUpdated = false;
175            valueUpdated = false;
176            
177            //
178            // XXX This is (basically) cut and pasted from the MethodUpdater code
179            // I haven't abstracted this code just yet since I think that adding
180            // handling for non-beans will mean adding quite a lot more structure
181            // and only once this is added will the proper position for this method 
182            // become clear.
183            //
184            
185            Class[] types = adderMethod.getParameterTypes();
186            // key is first parameter
187            Class keyType = types[0];
188            // value is the second
189            Class valueType = types[1];
190            
191            Object bean = context.getBean();
192            if ( bean != null ) {
193                if ( key instanceof String ) {
194                    // try to convert into primitive types
195                    key = context.getObjectStringConverter()
196                            .stringToObject( (String) key, keyType, context );
197                }
198                
199                if ( value instanceof String ) {
200                    // try to convert into primitive types
201                    value = context.getObjectStringConverter()
202                            .stringToObject( (String) value, valueType, context );
203                }
204                
205                // special case for collection objects into arrays                    
206                if (value instanceof Collection && valueType.isArray()) {
207                    Collection valuesAsCollection = (Collection) value;
208                    Class componentType = valueType.getComponentType();
209                    if (componentType != null) {
210                        Object[] valuesAsArray = 
211                            (Object[]) Array.newInstance(componentType, valuesAsCollection.size());
212                        value = valuesAsCollection.toArray(valuesAsArray);
213                    }
214                }
215                
216                     
217                Object[] arguments = { key, value };
218                try {
219                    if ( log.isTraceEnabled() ) {
220                        log.trace( 
221                            "Calling adder method: " + adderMethod.getName() + " on bean: " + bean 
222                            + " with key: " + key + " and value: " + value
223                        );
224                    }
225                    adderMethod.invoke( bean, arguments );
226                    
227                } catch (Exception e) {
228                    log.warn( 
229                        "Cannot evaluate adder method: " + adderMethod.getName() + " on bean: " + bean 
230                        + " of type: " + bean.getClass().getName() + " with value: " + value 
231                        + " of type: " + valueType + " and key: " + key
232                        + " of type: " + keyType 
233                    );
234                    log.debug(e);
235                }
236            }
237        }
238    }