001    /*
002     * Copyright 2006 Stephen J. McConnell.
003     *
004     * Licensed  under the  Apache License,  Version 2.0  (the "License");
005     * you may not use  this file  except in  compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *   http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed  under the  License is distributed on an "AS IS" BASIS,
012     * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
013     * implied.
014     *
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    
019    package net.dpml.lang;
020    
021    import java.io.Writer;
022    import java.io.IOException;
023    import java.lang.reflect.Constructor;
024    import java.lang.reflect.InvocationTargetException;
025    import java.lang.reflect.Array;
026    import java.beans.Expression;
027    import java.util.Arrays;
028    import java.util.ArrayList;
029    import java.util.Iterator;
030    
031    import net.dpml.util.Logger;
032    
033    /**
034     * Plugin part strategy implementation datatype.
035     * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
036     * @version 1.1.0
037     */
038    public class Plugin extends Part
039    {
040        private final String m_classname;
041        private final Value[] m_params;
042        
043       /**
044        * Creation of an new plugin datatype.
045        * @param logger the assigned logging channel
046        * @param info the part info descriptor
047        * @param classpath the classpath descriptor
048        * @param classname the target class
049        * @exception IOException if an I/O error occurs
050        */ 
051        public Plugin( 
052          Logger logger, Info info, Classpath classpath, String classname )
053          throws IOException
054        {
055            this( logger, info, classpath, classname, new Value[0] );
056        }
057        
058       /**
059        * Creation of an new plugin datatype.
060        * @param logger the assigned logging channel
061        * @param info the part info descriptor
062        * @param classpath the classpath descriptor
063        * @param classname the target class
064        * @param params an array of default value arguments
065        * @exception IOException if an I/O error occurs
066        */ 
067        public Plugin( 
068          Logger logger, Info info, Classpath classpath, String classname, Value[] params )
069          throws IOException
070        {
071            super( logger, info, classpath );
072            if( null == classname )
073            {
074                throw new NullPointerException( "classname" );
075            }
076            if( null == params )
077            {
078                throw new NullPointerException( "params" );
079            }
080            m_classname = classname;
081            m_params = params;
082        }
083        
084       /**
085        * Return the part content or null if the result type is unresolvable 
086        * relative to the supplied class argument. 
087        * @param c the content class
088        * @return the content
089        * @exception IOException if an IO error occurs
090        */
091        protected Object getContent( Class c ) throws IOException
092        {
093            if( Class.class.equals( c ) )
094            {
095                return getPluginClass();
096            }
097            else
098            {
099                return super.getContent( c );
100            }
101        }
102        
103       /**
104        * Get the target classname.
105        * @return the classname
106        */ 
107        public String getClassname()
108        {
109            return m_classname;
110        }
111        
112       /**
113        * Get the array of default constructor values.
114        * @return the value array
115        */ 
116        public Value[] getValues()
117        {
118            return m_params;
119        }
120        
121       /**
122        * Get the default plugin class.
123        * @return the plugin class
124        */
125        public Class getPluginClass()
126        {
127            ClassLoader classloader = getClassLoader();
128            String classname = getClassname();
129            try
130            {
131                return classloader.loadClass( classname );
132            }
133            catch( ClassNotFoundException e )
134            {
135                final String error = 
136                  "Plugin class [" + classname + "] not found.";
137                throw new IllegalStateException( error );
138            }
139        }
140        
141       /**
142        * Instantiate a value.
143        * @param args supplimentary arguments
144        * @return the resolved instance
145        * @exception Exception if a deployment error occurs
146        */
147        public Object instantiate( Object[] args ) throws Exception
148        {
149            ClassLoader classloader = getClassLoader();
150            ClassLoader context = Thread.currentThread().getContextClassLoader();
151            Thread.currentThread().setContextClassLoader( classloader );
152            try
153            {
154                Value[] values = getValues();
155                Class c = getPluginClass();
156                Object[] params = Construct.getArgs( null, values, args );
157                return instantiate( c, params );
158            }
159            finally
160            {
161                Thread.currentThread().setContextClassLoader( context );
162            }
163        }
164        
165       /**
166        * Test if this instance is equal to the supllied object.
167        * @param other the other object
168        * @return true if the supplied object is equal to this instance
169        */
170        public boolean equals( Object other )
171        {
172            if( super.equals( other ) )
173            {
174                if( other instanceof Plugin )
175                {
176                    Plugin plugin = (Plugin) other;
177                    if( !m_classname.equals( plugin.m_classname ) )
178                    {
179                        return false;
180                    }
181                    else
182                    {
183                        return Arrays.equals( m_params, plugin.m_params );
184                    }
185                }
186                else
187                {
188                    return false;
189                }
190            }
191            else
192            {
193                return false;
194            }
195        }
196        
197       /**
198        * Get the has code for this instance.
199        * @return the hash value
200        */
201        public int hashCode()
202        {
203            int hash = m_classname.hashCode();
204            for( int i=0; i<m_params.length; i++ )
205            {
206                hash ^= m_params[i].hashCode();
207            }
208            return hash;
209        }
210        
211        
212       /**
213        * Encode the pluginstrategy to XML.
214        * @param writer the output stream writer
215        * @param pad the character offset
216        * @exception IOException if an I/O error occurs
217        */
218        protected void encodeStrategy( Writer writer, String pad ) throws IOException
219        {
220            String classname = getClassname();
221            writer.write( "\n" + pad + "<strategy xsi:type=\"plugin\" class=\"" );
222            writer.write( classname );
223            writer.write( "\"" );
224            if( getValues().length > 0 )
225            {
226                writer.write( ">" );
227                Value[] values = getValues();
228                VALUE_ENCODER.encodeValues( writer, values, pad + "  " );
229                writer.write( "\n" + pad + "</strategy>" );
230            }
231            else
232            {
233                writer.write( "/>" );
234            }
235        }
236    
237       /**
238        * Create a factory using a supplied class and command line arguments.
239        *
240        * @param clazz the the factory class
241        * @param args the command line args
242        * @return the plugin instance
243        * @exception IOException if a plugin creation error occurs
244        * @exception InvocationTargetException if a plugin constructor invocation error occurs
245        */
246        public static Object instantiate( Class clazz, Object[] args ) throws IOException, InvocationTargetException
247        {
248            if( null == clazz )
249            {
250                throw new NullPointerException( "clazz" );
251            }
252            if( null == args )
253            {
254                throw new NullPointerException( "args" );
255            }
256            for( int i=0; i < args.length; i++ )
257            {
258                Object p = args[i];
259                if( null == p )
260                {
261                    final String error = 
262                      "User supplied instantiation argument at position [" 
263                      + i 
264                      + "] for the class ["
265                      + clazz.getName()
266                      + "] is a null value.";
267                    throw new NullPointerException( error );
268                }
269            }
270            
271            if( clazz.getConstructors().length == 1 )
272            {
273                Constructor constructor = getSingleConstructor( clazz );
274                return instantiate( constructor, args );
275            }
276            else
277            {
278                try
279                {
280                    Expression expression = new Expression( clazz, "new", args );
281                    return expression.getValue();
282                }
283                catch( InvocationTargetException e )
284                {
285                    throw e;
286                }
287                catch( PartException e )
288                {
289                    throw e;
290                }
291                catch( Throwable e )
292                {
293                    final String error = 
294                    "Class instantiation error [" + clazz.getName() + "]";
295                    throw new PartException( error, e );
296                }
297            }
298        }
299        
300        private static Object instantiate( Constructor constructor, Object[] args ) 
301          throws PartException, InvocationTargetException
302        {
303            Object[] arguments = populate( constructor, args );
304            return newInstance( constructor, arguments );
305        }
306        
307        private static Object[] populate( Constructor constructor, Object[] args ) throws PartException
308        {
309            if( null == constructor )
310            {
311                throw new NullPointerException( "constructor" );
312            }
313            if( null == args )
314            {
315                throw new NullPointerException( "args" );
316            }
317            
318            Class[] classes = constructor.getParameterTypes();
319            Object[] arguments = new Object[ classes.length ];
320            ArrayList list = new ArrayList();
321            for( int i=0; i < args.length; i++ )
322            {
323                list.add( args[i] );
324            }
325    
326            //
327            // sweep though the construct arguments one by one and
328            // see if we can assign a value based on the supplied args
329            //
330    
331            for( int i=0; i < classes.length; i++ )
332            {
333                Class clazz = classes[i];
334                Iterator iterator = list.iterator();
335                while( iterator.hasNext() )
336                {
337                    Object object = iterator.next();
338                    Class c = object.getClass();
339                    if( isAssignableFrom( clazz, c ) )
340                    {
341                        arguments[i] = object;
342                        list.remove( object );
343                        break;
344                    }
345                }
346            }
347    
348            //
349            // if any arguments are unresolved then check if the argument type
350            // is something we can implicity establish
351            //
352    
353            for( int i=0; i < arguments.length; i++ )
354            {
355                if( null == arguments[i] )
356                {
357                    Class c = classes[i];
358                    if( c.isArray() )
359                    {
360                        arguments[i] = getEmptyArrayInstance( c );
361                    }
362                    else
363                    {
364                        final String error =
365                          "Unable to resolve a value for a constructor parameter."
366                          + "\nConstructor class: " + constructor.getDeclaringClass().getName()
367                          + "\nParameter class: " + c.getName()
368                          + "\nParameter position: " + ( i + 1 );
369                        throw new PartException( error );
370                    }
371                }
372            }
373            return arguments;
374        }
375    
376        private static boolean isAssignableFrom( Class clazz, Class c )
377        {
378            if( clazz.isPrimitive() )
379            {
380                if( Integer.TYPE == clazz )
381                {
382                    return Integer.class.isAssignableFrom( c );
383                }
384                else if( Boolean.TYPE == clazz )
385                {
386                    return Boolean.class.isAssignableFrom( c );
387                }
388                else if( Byte.TYPE == clazz )
389                {
390                    return Byte.class.isAssignableFrom( c );
391                }
392                else if( Short.TYPE == clazz )
393                {
394                    return Short.class.isAssignableFrom( c );
395                }
396                else if( Long.TYPE == clazz )
397                {
398                    return Long.class.isAssignableFrom( c );
399                }
400                else if( Float.TYPE == clazz )
401                {
402                    return Float.class.isAssignableFrom( c );
403                }
404                else if( Double.TYPE == clazz )
405                {
406                    return Double.class.isAssignableFrom( c );
407                }
408                else
409                {
410                    final String error =
411                      "Primitive type ["
412                      + c.getName()
413                      + "] not supported.";
414                    throw new RuntimeException( error );
415                }
416            }
417            else
418            {
419                return clazz.isAssignableFrom( c );
420            }
421        }
422    
423        private static Object newInstance( Constructor constructor, Object[] arguments )
424          throws PartException, InvocationTargetException
425        {
426            try
427            {
428                Object instance = constructor.newInstance( arguments );
429                //getMonitor().pluginInstantiated( instance );
430                return instance;
431            }
432            catch( InvocationTargetException e )
433            {
434                throw e;
435            }
436            catch( Throwable e )
437            {
438                final String error =
439                  "Cannot create an instance of ["
440                  + constructor.getDeclaringClass().getName()
441                  + "] due to an instantiation failure.";
442                throw new PartException( error, e );
443            }
444        }
445        
446        private static Constructor getSingleConstructor( Class clazz ) throws PartException
447        {
448            if( null == clazz )
449            {
450                throw new NullPointerException( "clazz" );
451            }
452            Constructor[] constructors = clazz.getConstructors();
453            if( constructors.length < 1 )
454            {
455                final String error =
456                  "Target class ["
457                  + clazz.getName()
458                  + "] does not declare a public constructor.";
459                throw new PartException( error );
460            }
461            else if( constructors.length > 1 )
462            {
463                final String error =
464                  "Target class ["
465                  + clazz.getName()
466                  + "] declares multiple public constructors.";
467                throw new PartException( error );
468            }
469            else
470            {
471                return constructors[0];
472            }
473        }
474    
475       /**
476        * Constructs an empty array instance.
477        * @param clazz the array class
478        * @return the empty array instance
479        * @exception RepositoryException if an error occurs
480        */
481        private static Object[] getEmptyArrayInstance( Class clazz ) throws PartException
482        {
483            try
484            {
485                return (Object[]) Array.newInstance( clazz.getComponentType(), 0 );
486            }
487            catch( Throwable e )
488            {
489                final String error =
490                  "Internal error while attempting to construct an empty array for the class: "
491                  + clazz.getName();
492                throw new PartException( error, e );
493            }
494        }
495    }