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 }