001package org.apache.commons.digester3;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import static java.lang.System.arraycopy;
023import static java.lang.String.format;
024import static org.apache.commons.beanutils.ConstructorUtils.getAccessibleConstructor;
025import static org.apache.commons.beanutils.ConvertUtils.convert;
026
027import java.lang.reflect.Constructor;
028import java.lang.reflect.Method;
029import java.util.ArrayList;
030import java.util.Arrays;
031
032import net.sf.cglib.proxy.Callback;
033import net.sf.cglib.proxy.Enhancer;
034import net.sf.cglib.proxy.Factory;
035import net.sf.cglib.proxy.MethodInterceptor;
036import net.sf.cglib.proxy.MethodProxy;
037
038import org.xml.sax.Attributes;
039import org.xml.sax.SAXException;
040
041/**
042 * Rule implementation that creates a new object and pushes it onto the object stack. When the element is complete, the
043 * object will be popped
044 */
045public class ObjectCreateRule
046    extends Rule
047{
048    private static class DeferredConstructionCallback implements MethodInterceptor
049    {
050        Constructor<?> constructor;
051        Object[] constructorArgs;
052        ArrayList<RecordedInvocation> invocations = new ArrayList<RecordedInvocation>();
053        Object delegate;
054
055        DeferredConstructionCallback( Constructor<?> constructor, Object[] constructorArgs )
056        {
057            this.constructor = constructor;
058            this.constructorArgs = constructorArgs;
059        }
060
061        public Object intercept( Object obj, Method method, Object[] args, MethodProxy proxy )
062            throws Throwable
063        {
064            boolean hasDelegate = delegate != null;
065            if ( !hasDelegate )
066            {
067                invocations.add( new RecordedInvocation( method, args ) );
068            }
069            if ( hasDelegate )
070            {
071                return proxy.invoke( delegate, args );
072            }
073            return proxy.invokeSuper( obj, args );
074        }
075
076        void establishDelegate()
077            throws Exception
078        {
079            convertTo( constructor.getParameterTypes(), constructorArgs );
080            delegate = constructor.newInstance( constructorArgs );
081            for ( RecordedInvocation invocation : invocations )
082            {
083                invocation.getInvokedMethod().invoke( delegate, invocation.getArguments() );
084            }
085            constructor = null;
086            constructorArgs = null;
087            invocations = null;
088        }
089    }
090
091    private static class ProxyManager
092    {
093        private final Class<?> clazz;
094        private final Constructor<?> constructor;
095        private final Object[] templateConstructorArguments;
096        private final Digester digester;
097        private final boolean hasDefaultConstructor;
098        private Factory factory;
099
100        ProxyManager( Class<?> clazz, Constructor<?> constructor, Object[] constructorArguments, Digester digester )
101        {
102            this.clazz = clazz;
103            hasDefaultConstructor = getAccessibleConstructor( clazz, new Class[0] ) != null;
104            this.constructor = constructor;
105            Class<?>[] argTypes = constructor.getParameterTypes();
106            templateConstructorArguments = new Object[argTypes.length];
107            if ( constructorArguments == null )
108            {
109                for ( int i = 0; i < templateConstructorArguments.length; i++ )
110                {
111                    if ( argTypes[i].equals( boolean.class ) )
112                    {
113                        templateConstructorArguments[i] = Boolean.FALSE;
114                        continue;
115                    }
116                    if ( argTypes[i].isPrimitive() )
117                    {
118                        templateConstructorArguments[i] = convert( "0", argTypes[i] );
119                        continue;
120                    }
121                    templateConstructorArguments[i] = null;
122                }
123            }
124            else
125            {
126                if ( constructorArguments.length != argTypes.length )
127                {
128                    throw new IllegalArgumentException(
129                        format( "wrong number of constructor arguments specified: %s instead of %s",
130                        constructorArguments.length, argTypes.length ) );
131                }
132                arraycopy( constructorArguments, 0, templateConstructorArguments, 0, constructorArguments.length );
133            }
134            convertTo( argTypes, templateConstructorArguments );
135            this.digester = digester;
136        }
137
138        Object createProxy()
139        {
140            Object[] constructorArguments = new Object[templateConstructorArguments.length];
141            arraycopy( templateConstructorArguments, 0, constructorArguments, 0, constructorArguments.length );
142            digester.pushParams( constructorArguments );
143
144            DeferredConstructionCallback callback =
145                new DeferredConstructionCallback( constructor, constructorArguments );
146
147            Object result;
148
149            if ( factory == null )
150            {
151                Enhancer enhancer = new Enhancer();
152                enhancer.setSuperclass( clazz );
153                enhancer.setCallback( callback );
154                enhancer.setClassLoader( digester.getClassLoader() );
155                enhancer.setInterceptDuringConstruction( false );
156                if ( hasDefaultConstructor )
157                {
158                    result = enhancer.create();
159                }
160                else
161                {
162                    result = enhancer.create( constructor.getParameterTypes(), constructorArguments );
163                }
164                factory = (Factory) result;
165                return result;
166            }
167
168            if ( hasDefaultConstructor )
169            {
170                result = factory.newInstance( callback );
171            }
172            else
173            {
174                result = factory.newInstance( constructor.getParameterTypes(),
175                    constructorArguments, new Callback[] { callback } );
176            }
177            return result;
178        }
179
180        void finalize( Object proxy )
181            throws Exception
182        {
183            digester.popParams();
184            ( (DeferredConstructionCallback) ( (Factory) proxy ).getCallback( 0 ) ).establishDelegate();
185        }
186    }
187
188    // ----------------------------------------------------------- Constructors
189
190    /**
191     * Construct an object create rule with the specified class name.
192     *
193     * @param className Java class name of the object to be created
194     */
195    public ObjectCreateRule( String className )
196    {
197        this( className, (String) null );
198    }
199
200    /**
201     * Construct an object create rule with the specified class.
202     *
203     * @param clazz Java class name of the object to be created
204     */
205    public ObjectCreateRule( Class<?> clazz )
206    {
207        this( clazz.getName(), (String) null );
208        this.clazz = clazz;
209    }
210
211    /**
212     * Construct an object create rule with the specified class name and an optional attribute name containing an
213     * override.
214     *
215     * @param className Java class name of the object to be created
216     * @param attributeName Attribute name which, if present, contains an override of the class name to create
217     */
218    public ObjectCreateRule( String className, String attributeName )
219    {
220        this.className = className;
221        this.attributeName = attributeName;
222    }
223
224    /**
225     * Construct an object create rule with the specified class and an optional attribute name containing an override.
226     *
227     * @param attributeName Attribute name which, if present, contains an
228     * @param clazz Java class name of the object to be created override of the class name to create
229     */
230    public ObjectCreateRule( String attributeName, Class<?> clazz )
231    {
232        this( clazz != null ? clazz.getName() : null, attributeName );
233        this.clazz = clazz;
234    }
235
236    // ----------------------------------------------------- Instance Variables
237
238    /**
239     * The attribute containing an override class name if it is present.
240     */
241    protected String attributeName = null;
242
243    /**
244     * The Java class of the object to be created.
245     */
246    protected Class<?> clazz = null;
247
248    /**
249     * The Java class name of the object to be created.
250     */
251    protected String className = null;
252
253    /**
254     * The constructor argument types.
255     *
256     * @since 3.2
257     */
258    private Class<?>[] constructorArgumentTypes;
259
260    /**
261     * The explictly specified default constructor arguments which may be overridden by CallParamRules.
262     *
263     * @since 3.2
264     */
265    private Object[] defaultConstructorArguments;
266
267    /**
268     * Helper object for managing proxies.
269     *
270     * @since 3.2
271     */
272    private ProxyManager proxyManager;
273
274    // --------------------------------------------------------- Public Methods
275
276    /**
277     * Allows users to specify constructor argument types.
278     *
279     * @param constructorArgumentTypes the constructor argument types
280     * @since 3.2
281     */
282    public void setConstructorArgumentTypes( Class<?>... constructorArgumentTypes )
283    {
284        if ( constructorArgumentTypes == null )
285        {
286            throw new IllegalArgumentException( "Parameter 'constructorArgumentTypes' must not be null" );
287        }
288
289        this.constructorArgumentTypes = constructorArgumentTypes;
290    }
291
292    /**
293     * Allows users to specify default constructor arguments.  If a default/no-arg constructor is not available
294     * for the target class, these arguments will be used to create the proxy object.  For any argument
295     * not supplied by a {@link CallParamRule}, the corresponding item from this array will be used
296     * to construct the final object as well.
297     *
298     * @param constructorArguments the default constructor arguments.
299     * @since 3.2
300     */
301    public void setDefaultConstructorArguments( Object... constructorArguments )
302    {
303        if ( constructorArguments == null )
304        {
305            throw new IllegalArgumentException( "Parameter 'constructorArguments' must not be null" );
306        }
307
308        this.defaultConstructorArguments = constructorArguments;
309    }
310
311    /**
312     * {@inheritDoc}
313     */
314    @Override
315    public void begin( String namespace, String name, Attributes attributes )
316        throws Exception
317    {
318        Class<?> clazz = this.clazz;
319
320        if ( clazz == null )
321        {
322            // Identify the name of the class to instantiate
323            String realClassName = className;
324            if ( attributeName != null )
325            {
326                String value = attributes.getValue( attributeName );
327                if ( value != null )
328                {
329                    realClassName = value;
330                }
331            }
332            if ( getDigester().getLogger().isDebugEnabled() )
333            {
334                getDigester().getLogger().debug( format( "[ObjectCreateRule]{%s} New '%s'",
335                                                         getDigester().getMatch(),
336                                                         realClassName ) );
337            }
338
339            // Instantiate the new object and push it on the context stack
340            clazz = getDigester().getClassLoader().loadClass( realClassName );
341        }
342        Object instance;
343        if ( constructorArgumentTypes == null || constructorArgumentTypes.length == 0 )
344        {
345            if ( getDigester().getLogger().isDebugEnabled() )
346            {
347                getDigester()
348                    .getLogger()
349                    .debug( format( "[ObjectCreateRule]{%s} New '%s' using default empty constructor",
350                                    getDigester().getMatch(),
351                                    clazz.getName() ) );
352            }
353
354            instance = clazz.newInstance();
355        }
356        else
357        {
358            if ( proxyManager == null )
359            {
360                Constructor<?> constructor = getAccessibleConstructor( clazz, constructorArgumentTypes );
361
362                if ( constructor == null )
363                {
364                    throw new SAXException(
365                                   format( "[ObjectCreateRule]{%s} Class '%s' does not have a construcor with types %s",
366                                           getDigester().getMatch(),
367                                           clazz.getName(),
368                                           Arrays.toString( constructorArgumentTypes ) ) );
369                }
370                proxyManager = new ProxyManager( clazz, constructor, defaultConstructorArguments, getDigester() );
371            }
372            instance = proxyManager.createProxy();
373        }
374        getDigester().push( instance );
375    }
376
377    /**
378     * {@inheritDoc}
379     */
380    @Override
381    public void end( String namespace, String name )
382        throws Exception
383    {
384        Object top = getDigester().pop();
385
386        if ( proxyManager != null )
387        {
388            proxyManager.finalize( top );
389        }
390
391        if ( getDigester().getLogger().isDebugEnabled() )
392        {
393            getDigester().getLogger().debug( format( "[ObjectCreateRule]{%s} Pop '%s'",
394                                                     getDigester().getMatch(),
395                                                     top.getClass().getName() ) );
396        }
397    }
398
399    /**
400     * {@inheritDoc}
401     */
402    @Override
403    public String toString()
404    {
405        return format( "ObjectCreateRule[className=%s, attributeName=%s]", className, attributeName );
406    }
407
408    private static void convertTo( Class<?>[] types, Object[] array )
409    {
410        if ( array.length != types.length )
411        {
412            throw new IllegalArgumentException();
413        }
414        // this piece of code is adapted from CallMethodRule
415        for ( int i = 0; i < array.length; i++ )
416        {
417            // convert nulls and convert stringy parameters for non-stringy param types
418            if ( array[i] == null
419                    || ( array[i] instanceof String && !String.class.isAssignableFrom( types[i] ) ) )
420            {
421                array[i] = convert( (String) array[i], types[i] );
422            }
423        }
424    }
425
426}