001    /*
002     * Copyright (c) 2005 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.metro.tools;
020    
021    import java.beans.IntrospectionException;
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.io.File;
025    import java.io.FileOutputStream;
026    import java.io.OutputStream;
027    import java.lang.reflect.Method;
028    import java.net.URI;
029    import java.net.URL;
030    import java.net.URISyntaxException;
031    import java.util.ArrayList;
032    import java.util.List;
033    import java.util.Map;
034    import java.util.Properties;
035    
036    import net.dpml.metro.builder.ComponentTypeEncoder;
037    
038    import net.dpml.metro.info.CategoryDescriptor;
039    import net.dpml.metro.info.ContextDescriptor;
040    import net.dpml.metro.info.InfoDescriptor;
041    import net.dpml.metro.info.LifestylePolicy;
042    import net.dpml.metro.info.CollectionPolicy;
043    import net.dpml.metro.info.Type;
044    import net.dpml.metro.info.EntryDescriptor;
045    import net.dpml.metro.info.ServiceDescriptor;
046    import net.dpml.metro.info.ThreadSafePolicy;
047    
048    import net.dpml.state.State;
049    import net.dpml.state.DefaultState;
050    import net.dpml.state.StateDecoder;
051    
052    import net.dpml.library.info.Scope;
053    
054    import net.dpml.tools.tasks.GenericTask;
055    
056    import org.apache.tools.ant.BuildException;
057    import org.apache.tools.ant.Project;
058    import org.apache.tools.ant.AntClassLoader;
059    import org.apache.tools.ant.types.Path;
060    
061    
062    /**
063     * The TypeTask creates a serialized descriptor of a component type.
064     *
065     * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
066     * @version 1.1.3
067     */
068    public class TypeBuilderTask extends GenericTask implements TypeBuilder
069    {
070        private static final StateDecoder STATE_DECODER = new StateDecoder();
071        private static final ComponentTypeEncoder COMPONENT_TYPE_ENCODER = 
072          new ComponentTypeEncoder();
073          
074        //---------------------------------------------------------------
075        // state
076        //---------------------------------------------------------------
077    
078        private String m_name;
079        private String m_classname;
080        private Class m_class;
081        private LifestylePolicy m_lifestyle;
082        private CollectionPolicy m_collection;
083        private ThreadSafePolicy m_threadsafe = ThreadSafePolicy.UNKNOWN;
084        private StateDataType m_state;
085        private ServicesDataType m_services;
086        private CategoriesDescriptorDataType m_categories;
087    
088        //---------------------------------------------------------------
089        // setters
090        //---------------------------------------------------------------
091    
092       /**
093        * Set the name of the type.
094        * @param name the component name
095        */
096        public void setName( String name )
097        {
098            m_name = name;
099        }
100    
101       /**
102        * Set the classname of the type.
103        * @param classname the component type classname
104        */
105        public void setClass( String classname )
106        {
107            m_classname = classname;
108        }
109    
110       /**
111        * Set the threadsafe flag.
112        * @param flag true if the component type is threadsafe
113        */
114        public void setThreadsafe( boolean flag )
115        {
116            if( flag )
117            {
118                m_threadsafe = ThreadSafePolicy.TRUE;
119            }
120            else
121            {
122                m_threadsafe = ThreadSafePolicy.FALSE;
123            }
124        }
125    
126       /**
127        * Set the type collection policy.
128        * @param value the collection policy value
129        */
130        public void setCollection( String value )
131        {
132            m_collection = CollectionPolicy.parse( value );
133        }
134    
135       /**
136        * Set the type lifestyle policy.
137        * @param value the lifestyle policy value
138        */
139        public void setLifestyle( String value )
140        {
141            m_lifestyle = LifestylePolicy.parse( value );
142        }
143    
144       /**
145        * Create a new services datatype.
146        * @return a new services datatype
147        */
148        public ServicesDataType createServices()
149        {
150            if( m_services == null )
151            {
152                m_services = new ServicesDataType();
153                return m_services;
154            }
155            else
156            {
157                 final String error =
158                  "Illegal attempt to create a duplicate services element.";
159                 throw new BuildException( error, getLocation() );
160            }
161        }
162        
163       /**
164        * Create a new services datatype.
165        * @return a new services datatype
166        */
167        public CategoriesDescriptorDataType createCategories()
168        {
169            if( m_categories == null )
170            {
171                m_categories = new CategoriesDescriptorDataType();
172                return m_categories;
173            }
174            else
175            {
176                 final String error =
177                  "Illegal attempt to create a duplicate categories element.";
178                 throw new BuildException( error, getLocation() );
179            }
180        }
181        
182       /**
183        * Create a state descriptor for the component.
184        * @return a state graph descriptor
185        */
186        public StateDataType createState()
187        {
188            if( m_state == null )
189            {
190                m_state = new StateDataType( this, true );
191                return m_state;
192            }
193            else
194            {
195                 final String error =
196                  "Illegal attempt to create a duplicate state element.";
197                 throw new BuildException( error, getLocation() );
198            }
199        }
200        
201        //---------------------------------------------------------------
202        // Builder
203        //---------------------------------------------------------------
204    
205       /**
206        * Return a urn identitifying the component type strategy that this 
207        * builder is supporting.
208        *
209        * @return a uri identifiying the type strategy
210        */
211        public URI getTypeHandlerURI()
212        {
213            return TYPE_HANDLER_URI;
214        }
215    
216       /**
217        * Return a uri identitifying the builder.
218        *
219        * @return a uri identifiying the type builder
220        */
221        public URI getBuilderURI()
222        {
223            return COMPONENT_TYPE_DECODER_URI;
224        }
225        
226        //---------------------------------------------------------------
227        // TypeBuilder
228        //---------------------------------------------------------------
229    
230       /**
231        * Build the type.
232        * @param classloader the classloader to use for type creation
233        * @return the component type
234        * @exception IntrospectionException if a class introspection error occurs
235        * @exception IOException if an I/O error occurs
236        */
237        public Type buildType( ClassLoader classloader ) 
238           throws IntrospectionException, IOException
239        {
240            Class subject = loadSubjectClass( classloader );
241            return buildType( subject );
242        }
243    
244       /**
245        * Build the type.
246        * @param subject the implementation class
247        * @return the component type
248        * @exception IntrospectionException if a class introspection error occurs
249        * @exception IOException if an I/O error occurs
250        */
251        public Type buildType( Class subject )
252           throws IntrospectionException, IOException
253        {
254            log( "creating [" + subject.getName() + "]" );
255    
256            InfoDescriptor info = createInfoDescriptor( subject );
257            ServiceDescriptor[] services = createServiceDescriptors( subject );
258            CategoryDescriptor[] categories = createCategoryDescriptors();
259            ContextDescriptor context = createContextDescriptor( subject );
260            State graph = getStateGraph( subject );
261            return new Type( info, categories, context, services, graph );
262        }
263    
264        private File getReportDestination( File dir, Type type )
265        {
266            final String classname = type.getInfo().getClassname();
267            String path = classname.replace( '.', '/' );
268            String filename = path + ".xml";
269            return new File( dir, filename );
270        }
271    
272        //---------------------------------------------------------------
273        // Task
274        //---------------------------------------------------------------
275    
276       /**
277        * Execute the task.
278        */
279        public void execute()
280        {
281            Project proj = getProject();
282            
283            Path path = getContext().getPath( Scope.RUNTIME );
284            File classes = getContext().getTargetClassesMainDirectory();
285            path.createPathElement().setLocation( classes );
286            ClassLoader classloader = new AntClassLoader( proj, path );
287            ClassLoader current = Thread.currentThread().getContextClassLoader();
288            try
289            {
290                final Type type = buildType( classloader );
291                OutputStream output = getOutputStream( type );
292                try
293                {
294                    COMPONENT_TYPE_ENCODER.export( type, output );
295                }
296                finally
297                {
298                    try
299                    {
300                        output.close();
301                    }
302                    catch( IOException ioe )
303                    {
304                        ioe.printStackTrace();
305                    }
306                }
307            }
308            catch( IntrospectionException e )
309            {
310                final String error = e.getMessage();
311                throw new BuildException( error, e, getLocation() );
312            }
313            catch( BuildException e )
314            {
315                throw e;
316            }
317            catch( Throwable e )
318            {
319                final String error = 
320                  "Internal error while attempting to build types."
321                  + "\nCause: " + e.getClass().getName()
322                  + "\nMessage: " + e.getMessage();
323                throw new BuildException( error, e, getLocation() );
324            }
325        }
326    
327        private OutputStream getOutputStream( Type type ) throws IOException
328        {
329            final String classname = type.getInfo().getClassname();
330            final String resource = getEmbeddedResourcePath( classname );
331            final File file = getEmbeddedOutputFile( resource );
332            file.getParentFile().mkdirs();
333            return new FileOutputStream( file );
334        }
335    
336        private String getEmbeddedResourcePath( String classname )
337        {
338            String path = classname.replace( '.', '/' );
339            String filename = path + ".type";
340            return filename;
341        }
342    
343        private File getEmbeddedOutputFile( String filename )
344        {
345            File classes = getContext().getTargetClassesMainDirectory();
346            File destination = new File( classes, filename );
347            return destination;
348        }
349    
350        //---------------------------------------------------------------
351        // internals
352        //---------------------------------------------------------------
353    
354       /**
355        * Return the type name.
356        * @return the name
357        */
358        protected String getName()
359        {
360            if( null == m_name )
361            {
362                return "untitled";
363            }
364            return m_name;
365        }
366    
367       /**
368        * Return the type classname.
369        * @return the classname
370        */
371        protected String getClassname()
372        {
373            if( null == m_classname )
374            {
375                final String error = 
376                  "Component type does not declare a classname.";
377                throw new BuildException( error, getLocation() );
378            }
379            return m_classname;
380        }
381        
382        private InfoDescriptor createInfoDescriptor( Class subject ) 
383          throws IntrospectionException
384        {
385            String name = getName();
386            String classname = subject.getName();
387            ThreadSafePolicy threadsafe = getThreadSafeCapability( subject );
388            Properties properties = getTypeProperties( subject );
389            return new InfoDescriptor( 
390              name, classname, null, m_lifestyle, m_collection, threadsafe, properties );
391        }
392    
393        private ThreadSafePolicy getThreadSafeCapability( Class subject ) 
394          throws IntrospectionException
395        {
396            return m_threadsafe;
397        }
398    
399        private Properties getTypeProperties( Class subject ) 
400          throws IntrospectionException
401        {
402            String path = subject.getClass().getName().replace( '.', '/' ) + ".properties";
403            URL url = subject.getResource( path );
404            if( null == url )
405            {
406                return null;
407            }
408            else
409            {
410                try
411                {
412                    Properties properties = new Properties();
413                    InputStream input = url.openStream();
414                    try
415                    {
416                        properties.load( input );
417                        return properties;
418                    }
419                    finally
420                    {
421                        input.close();
422                    }
423                }
424                catch( IOException e )
425                {
426                    final String error = 
427                      "Unable to load the property file for the path: "
428                      + path;
429                    throw new BuildException( error, e );
430                }
431            }
432        }
433        
434        private ServiceDescriptor[] createServiceDescriptors( Class subject )
435        {
436            if( null == m_services )
437            {
438                ArrayList list = new ArrayList();
439                return createServiceDescriptors( subject, list );
440            }
441            else
442            {
443                return m_services.getServiceDescriptors();
444            }
445        }
446        
447        private CategoryDescriptor[] createCategoryDescriptors()
448        {
449            if( null == m_categories )
450            {
451               return new CategoryDescriptor[0];
452            }
453            else
454            {
455                return m_categories.getCategoryDescriptors();
456            }
457        }
458        
459        private ServiceDescriptor[] createServiceDescriptors( Class subject, List list )
460        {
461            Class[] interfaces = subject.getInterfaces();
462            for( int i=0; i<interfaces.length; i++ )
463            {
464                Class service = interfaces[i];
465                ServiceDescriptor descriptor = createServiceDescriptor( subject, service );
466                if( null != descriptor )
467                {
468                    if( !list.contains( descriptor ) )
469                    {
470                        list.add( descriptor );
471                    }
472                }
473            }
474            Class superclass = subject.getSuperclass();
475            if( null != superclass )
476            {
477                return createServiceDescriptors( superclass, list );
478            }
479            else
480            {
481                return (ServiceDescriptor[]) list.toArray( new ServiceDescriptor[0] );
482            }
483        }
484    
485        private ServiceDescriptor createServiceDescriptor( Class type, Class subject )
486        {
487            String classname = subject.getName();
488            Class parent = subject.getDeclaringClass();
489            if( classname.startsWith( "java." ) )
490            {
491                return null; // ignore java.* interfaces 
492            }
493            else if( classname.startsWith( "net.dpml.activity." ) )
494            {
495                return null;
496            }
497            else if( type == parent )
498            {
499                return null; // ignore immediate inner interfaces
500            }
501            else
502            {
503                return new ServiceDescriptor( classname );
504            }
505        }
506    
507        private ContextDescriptor createContextDescriptor( Class subject ) 
508          throws IntrospectionException
509        {
510            EntryDescriptor[] entries = createEntryDescriptors( subject );
511            return new ContextDescriptor( entries );
512        }
513    
514        private EntryDescriptor[] createEntryDescriptors( Class subject ) 
515           throws IntrospectionException
516        {
517            String classname = subject.getName();
518            Class[] classes = subject.getClasses();
519            Class param = locateClass( "$Context", classes );
520            if( null == param )
521            {
522                return new EntryDescriptor[0];
523            }
524            else
525            {
526                //
527                // For each method in the Context inner-interface we construct a 
528                // descriptor that establishes the part key, type, and required status.
529                //
530    
531                Method[] methods = param.getMethods();
532                ArrayList list = new ArrayList();
533                for( int i=0; i<methods.length; i++ )
534                {
535                    Method method = methods[i];
536                    String name = method.getName();
537                    if( name.startsWith( "get" ) )
538                    {
539                        EntryDescriptor descriptor = createEntryDescriptor( method );
540                        list.add( descriptor );
541                    }
542                }
543                return (EntryDescriptor[]) list.toArray( new EntryDescriptor[0] );
544            }
545        }
546    
547       /**
548        * Creation of a new parameter descriptor using a supplied method.
549        * The method is the method used by the component implementation to get the parameter 
550        * instance. 
551        */
552        private EntryDescriptor createEntryDescriptor( Method method ) 
553          throws IntrospectionException
554        {
555            validateMethodName( method );
556            validateNoExceptions( method );
557    
558            String key = EntryDescriptor.getEntryKey( method );
559    
560            Class returnType = method.getReturnType();
561            if( method.getParameterTypes().length == 0 )
562            {
563                //
564                // required context entry
565                //
566    
567                validateNonNullReturnType( method );
568                //validateNonArrayReturnType( method, returnType );
569                String type = returnType.getName();
570                return new EntryDescriptor( key, type, EntryDescriptor.REQUIRED );
571            }
572            else if( method.getParameterTypes().length == 1 )
573            {
574                Class[] params = method.getParameterTypes();
575                Class param = params[0];
576                if( returnType.isAssignableFrom( param ) )
577                {
578                    String type = param.getName();
579                    return new EntryDescriptor( key, type, EntryDescriptor.OPTIONAL );
580                }
581                else
582                {
583                    final String error = 
584                      "Context entry assessor declares an optional default parameter class ["
585                      + param.getName()
586                      + "] which is not assignable to the return type ["
587                      + returnType.getName()
588                      + "]";
589                    throw new IntrospectionException( error );
590                }
591            }
592            else
593            {
594                final String error =
595                  "Unable to establish a required or optional context entry method pattern on ["
596                  + method.getName()
597                  + "]";
598                throw new IntrospectionException( error );
599            }
600        }
601        
602        private State getStateGraph( Class subject )
603        {
604            if( null == m_state )
605            {
606                return new DefaultState( "" );
607            }
608            else
609            {
610                return m_state.getState();
611            }
612        }
613        
614        private State loadStateFromResource( Class subject )
615        {
616            String resource = subject.getName().replace( '.', '/' ) + ".xgraph";
617            try
618            {
619                URL url = subject.getClassLoader().getResource( resource );
620                if( null == url )
621                {
622                    return null;
623                }
624                else
625                {
626                    URI uri = new URI( url.toString() );
627                    return STATE_DECODER.loadState( uri );
628                }
629            }
630            catch( Throwable e )
631            {
632                final String error = 
633                  "Internal error while attempting to load component state graph resource [" 
634                  + resource 
635                  + "].";
636                throw new BuildException( error, e );
637            }
638        }
639    
640        private String formatKey( String key, int offset )
641        {
642            String k = key.substring( offset );
643            return formatKey( k );
644        }
645    
646        private String formatKey( String key )
647        {
648            if( key.length() < 1 ) 
649            {
650                throw new IllegalArgumentException( "key" );
651            }
652            String first = key.substring( 0, 1 ).toLowerCase();
653            String remainder = key.substring( 1 );
654            return first + remainder;
655        }
656    
657        private Class locateClass( String postfix, Class[] classes )
658        {
659            for( int i=0; i<classes.length; i++ )
660            {
661                Class inner = classes[i];
662                String name = inner.getName();
663                if( name.endsWith( postfix ) )
664                {
665                    return inner;
666                }
667            }
668            return null;
669        }
670    
671        private void validateMapReturnType( Method method )
672           throws IntrospectionException
673        {
674            Class returnType = method.getReturnType();
675            if( Map.class != returnType )
676            {
677                String name = method.getName();
678                final String error = 
679                  "The method ["
680                  + name
681                  + "] does not declare java.util.Map as a return type.";
682                throw new IntrospectionException( error );
683            }
684        }
685        
686        private void validateEntryReturnType( Method method )
687           throws IntrospectionException
688        {
689            Class returnType = method.getReturnType();
690            if( Map.Entry.class != returnType )
691            {
692                String name = method.getName();
693                final String error = 
694                  "The method ["
695                  + name
696                  + "] does not declare java.util.Map.Entry as a return type.";
697                throw new IntrospectionException( error );
698            }
699        }
700        
701        private void validateReturnTypeIsAssignable( Method method, Class type ) 
702          throws IntrospectionException
703        {
704            Class c = method.getReturnType();
705            if( !type.isAssignableFrom( c ) )
706            {
707                final String error = 
708                  "Method ["
709                  + method.getName()
710                  + "] declares a return type ["
711                  + c.getName()
712                  + "] that is not assignable from the class ["
713                  + type.getName()
714                  + "].";
715                throw new IntrospectionException( error );
716            }
717        }
718    
719        private void validateNonNullParameter( Method method, Class type ) 
720           throws IntrospectionException
721        {
722            if( Void.TYPE.equals( type ) ) 
723            {
724                final String error = 
725                  "Method ["
726                  + method.getName()
727                  + "] declares a null parameter.";
728                throw new IntrospectionException( error );
729            }
730        }
731    
732    
733        private void validateNonNullReturnType( Method method ) 
734           throws IntrospectionException
735        {
736            Class returnType = method.getReturnType();
737            if( Void.TYPE.equals( returnType ) ) 
738            {
739                final String error = 
740                  "Method ["
741                  + method.getName()
742                  + "] does not declare a return type.";
743                throw new IntrospectionException( error );
744            }
745        }
746    
747        private void validateNullReturnType( Method method, Class returnType ) 
748           throws IntrospectionException
749        {
750            if( !Void.TYPE.equals( returnType ) ) 
751            {
752                final String error = 
753                  "Method ["
754                  + method.getName()
755                  + "] does not declare a null return type.";
756                throw new IntrospectionException( error );
757            }
758        }
759    
760        private void validateNonArrayReturnType( Method method, Class returnType )
761            throws IntrospectionException
762        {
763            if( null != returnType.getComponentType() )
764            {
765                final String error = 
766                  "Method ["
767                  + method.getName()
768                  + "] declares an array return type.";
769                throw new IntrospectionException( error );
770            }
771        }
772    
773        private void validateNonArrayType( Method method, Class type )
774           throws IntrospectionException
775        {
776            if( null != type.getComponentType() )
777            {
778                final String error = 
779                  "Method ["
780                  + method.getName()
781                  + "] declares an array type.";
782                throw new IntrospectionException( error );
783            }
784        }
785    
786        private void validateInterfaceReturnType( Method method, Class returnType )
787          throws IntrospectionException
788        {
789            if( !returnType.isInterface() )
790            {
791                final String error = 
792                  "Method ["
793                  + method.getName()
794                  + "] declares a return type ["
795                  + returnType.getName()
796                  + "] that is not an interface.";
797                throw new IntrospectionException( error );
798            }
799        }
800    
801        private void validateMethodName( Method method ) 
802          throws IntrospectionException
803        {
804            if( !method.getName().startsWith( "get" ) )
805            {
806                final String error = 
807                  "Method ["
808                  + method.getName()
809                  + "] does not start with 'get'.";
810                throw new IntrospectionException( error );
811            }
812        }
813    
814        private void validateNoExceptions( Method method ) 
815          throws IntrospectionException
816        {
817            Class[] exceptionTypes = method.getExceptionTypes();
818            if( exceptionTypes.length > 0 )
819            {
820                final String error = 
821                  "Method ["
822                  + method.getName()
823                  + "] declares one or more exceptions.";
824                throw new IntrospectionException( error );
825            }
826        }
827    
828        private void validateNoParameters( Method method ) 
829          throws IntrospectionException
830        {
831            Class[] parameterTypes = method.getParameterTypes();
832            if( parameterTypes.length > 0 )
833            {
834                final String error = 
835                  "Method ["
836                  + method.getName()
837                  + "] declares one or more parameters.";
838                throw new IntrospectionException( error );
839            }
840        }
841    
842        private void validateAtMostOneParameter( Method method ) 
843          throws IntrospectionException
844        {
845            Class[] parameterTypes = method.getParameterTypes();
846            if( parameterTypes.length > 1 )
847            {
848                final String error = 
849                  "Method ["
850                  + method.getName()
851                  + "] declares more than one parameters.";
852                throw new IntrospectionException( error );
853            }
854        }
855    
856        private Class validateSingleParameter( Method method ) 
857          throws IntrospectionException
858        {
859            Class[] parameterTypes = method.getParameterTypes();
860            if( parameterTypes.length != 1 )
861            {
862                final String error = 
863                  "Method ["
864                  + method.getName()
865                  + "] does not declare a single parameter argument type.";
866                throw new IntrospectionException( error );
867            }
868            return parameterTypes[0];
869        }
870    
871        private void validateNonArrayParameter( Method method, Class type ) 
872          throws IntrospectionException
873        {
874            if( null != type.getComponentType() )
875            {
876                final String error = 
877                  "Method ["
878                  + method.getName()
879                  + "] declares an array parameter type.";
880                throw new IntrospectionException( error );
881            }
882        }
883    
884        private void validateSelectPattern( Class subject, Method method ) 
885          throws IntrospectionException
886        {
887            Class[] parameterTypes = method.getParameterTypes();
888            int n = parameterTypes.length;
889            if( n == 1 )
890            {
891                Class b = parameterTypes[0];
892                if( !Boolean.TYPE.isAssignableFrom( b ) )
893                {
894                    String name = method.getName();
895                    final String error = 
896                      "Part accessor ["
897                      + subject.getName() + "#" + name
898                      + "] is declaring an illegal non-boolean parameter.";
899                    throw new IntrospectionException( error );
900                }
901            }
902        }
903    
904        private Class loadSubjectClass( ClassLoader classloader )
905        {
906            if( null == m_classname )
907            {
908                final String error = 
909                  "Missing component descriptor class attribute.";
910                throw new IllegalStateException( error );
911            }
912            try
913            {
914                return classloader.loadClass( m_classname );
915            }
916            catch( ClassNotFoundException e )
917            {
918                final String error = 
919                  "Cannot build a component type because the class ["
920                  + m_classname 
921                  + "] is not present in the project path.";
922                throw new BuildException( error );
923            }
924        }
925    
926        private static final URI TYPE_HANDLER_URI = setupURI( "artifact:part:dpml/metro/dpml-metro-runtime#1.0.4" );
927        private static final URI COMPONENT_TYPE_DECODER_URI = setupURI( "artifact:part:dpml/metro/dpml-metro-tools#1.1.3" );
928    
929       /**
930        * Internal utility to create a static uri.
931        * @param spec the uri spec
932        * @return the uri
933        */
934        protected static URI setupURI( String spec )
935        {
936            try
937            {
938                return new URI( spec );
939            }
940            catch( URISyntaxException ioe )
941            {
942                return null;
943            }
944        }
945    }