001    /*
002     * Copyright 2004-2005 Stephen J. McConnell.
003     * Copyright 2004 Niclas Hedhman.
004     *
005     * Licensed  under the  Apache License,  Version 2.0  (the "License");
006     * you may not use  this file  except in  compliance with the License.
007     * You may obtain a copy of the License at
008     *
009     *   http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed  under the  License is distributed on an "AS IS" BASIS,
013     * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
014     * implied.
015     *
016     * See the License for the specific language governing permissions and
017     * limitations under the License.
018     */
019    
020    package net.dpml.transit;
021    
022    import java.io.Serializable;
023    import java.net.URLStreamHandler;
024    import java.net.MalformedURLException;
025    import java.net.URI;
026    import java.net.URISyntaxException;
027    import java.net.URL;
028    
029    /**
030     * A utility class the handles validation of <code>artifact</code> style uri
031     * strings.
032     *
033     * @author <a href="http://www.dpml.net">Digital Product Management Library</a>
034     * @version 2.1.1
035     */
036    public final class Artifact implements Serializable, Comparable
037    {
038       /**
039        * Constant scheme name for the artifact protocol.
040        */
041        public static final String ARTIFACT = "artifact";
042        
043       /**
044        * Constant scheme name for the link protocol.
045        */
046        public static final String LINK = "link";
047        
048       /**
049        * Constant scheme name for the local protocol.
050        */
051        public static final String LOCAL = "local";
052    
053       /**
054        * Constant scheme name for the configuration protocol.
055        */
056        public static final String CONFIGURATION = "configuration";
057        
058        static final long serialVersionUID = 2L;
059        
060        // ------------------------------------------------------------------------
061        // static
062        // ------------------------------------------------------------------------
063    
064        /**
065         * Creation of a new artifact instance using a supplied uri specification.
066         * An artifact uri contains the protocol identifier, a type, a group
067         * designator, a name, and an optional version identifier.
068         * <p>The following represent valid artifact uri examples:</p>
069         *
070         * <ul>
071         * <li>artifact:jar:dpml/transit/dpml-transit-main#1234</li>
072         * <li>artifact:jar:dpml/transit/dpml-transit-main</li>
073         * <li>link:jar:dpml/transit/dpml-transit-main#1.0</li>
074         * </ul>
075         *
076         * <p>
077         *  If there is a internal reference identifier which is marked by the
078         *  exclamation mark followed by slash (!/) it will be stripped. The
079         *  version part can be either before or after such identifier. Example;
080         *  <pre>
081         *   artifact:war:jmx-html/jmx-html#1.3!/images/abc.png
082         *   artifact:war:jmx-html/jmx-html!/images/abc.png#1.3
083         *  </pre>
084         *  The above uris will both be referencing
085         *  <code>artifact:war:jmx-html/jmx-html#1.3</code>
086         * </p>
087         * @param uri the artifact uri
088         * @return the new artifact
089         * @exception java.net.URISyntaxException if the supplied uri is not valid.
090         * @exception UnsupportedSchemeException if the URI does not have "artifact"
091         *         or "link" as its <strong>scheme</strong>.
092         * @exception NullPointerException if the supplied uri argument is null
093         */
094        public static final Artifact createArtifact( String uri )
095            throws URISyntaxException, UnsupportedSchemeException, NullPointerException
096        {
097            if( null == uri )
098            {
099                throw new NullPointerException( "uri" );
100            }
101            int asterix = uri.indexOf( "!" );
102            if( asterix == -1 )
103            {
104                return createArtifact( new URI( uri ) );
105            }
106            else
107            {
108                String path = uri.substring( 0, asterix );
109                int versionPos = uri.indexOf( "#" );
110                if( versionPos < asterix )
111                {
112                    return createArtifact( path );
113                }
114                else
115                {
116                    path = path + uri.substring( versionPos );
117                    return createArtifact( path );
118                }
119            }
120        }
121    
122        /**
123         * Creation of a new artifact instance using a supplied uri specification. An
124         * artifact uri contains the protocol identifier, an optional type, a group
125         * designator, a name, and an optional version identifier.
126         * <p>The following represent valid artifact uri examples:</p>
127         *
128         * <ul>
129         * <li>artifact:jar:metro/cache/dpml-cache-main#1.0.0</li>
130         * <li>artifact:metro/cache/dpml-cache-main#1.0.0</li>
131         * <li>artifact:metro/cache/dpml-cache-main</li>
132         * </ul>
133         *
134         * @param uri the artifact uri
135         * @return the new artifact
136         * @exception UnsupportedSchemeException if the URI does not have "artifact"
137         *     or "link" as its <strong>scheme</strong>.
138         * @exception NullPointerException if the supplied uri argument is null
139         */
140        public static final Artifact createArtifact( URI uri )
141            throws UnsupportedSchemeException, NullPointerException
142        {
143            if( null == uri )
144            {
145                throw new NullPointerException( "uri" );
146            }
147            String scheme = uri.getScheme();
148            if( null == scheme )
149            {
150                final String error = 
151                  "URI does not declare a scheme: " + uri;
152                throw new UnsupportedSchemeException( error );
153            }
154            else
155            {
156                return new Artifact( uri );
157            }
158        }
159    
160        /**
161         * Creation of a new artifact instance using a supplied group, name,
162         * version and type arguments.
163         *
164         * @param group the artifact group identifier
165         * @param name the artifact name
166         * @param version the version
167         * @param type the type
168         * @return the new artifact
169         * @exception NullPointerException if any of the <code>group</code>,
170         *            <code>name</code> or <code>type</code> arguments are
171         *            <code>null</code>.
172         */
173        public static Artifact createArtifact( 
174          String group, String name, String version, String type )
175            throws NullPointerException
176        {
177            return createArtifact( ARTIFACT, group, name, version, type );
178        }
179        
180        /**
181         * Creation of a new artifact instance using a supplied group, name,
182         * version and type arguments.
183         *
184         * @param scheme the artifact scheme
185         * @param group the artifact group identifier
186         * @param name the artifact name
187         * @param version the version
188         * @param type the type
189         * @return the new artifact
190         * @exception NullPointerException if any of the <code>group</code>,
191         *            <code>name</code> or <code>type</code> arguments are
192         *            <code>null</code>.
193         */
194        public static Artifact createArtifact( 
195          String scheme, String group, String name, String version, String type )
196            throws NullPointerException
197        {
198            if( name == null )
199            {
200                throw new NullPointerException( "name" );
201            }
202            if( type == null )
203            {
204                throw new NullPointerException( "type" );
205            }
206            if( scheme == null )
207            {
208                throw new NullPointerException( "scheme" );
209            }
210            String composite = buildComposite( scheme, group, name, version, type );
211            try
212            {
213                URI uri = new URI( composite );
214                return new Artifact( uri );
215            } 
216            catch( URISyntaxException e )
217            {
218                // Can not happen.
219                final String error =
220                  "An internal error has occurred. "
221                  + "The following URI could not be constructed: " + composite;
222                throw new TransitRuntimeException( error );
223            }
224        }
225        
226        private static String buildComposite( 
227          final String scheme, final String group, final String name, 
228          final String version, final String type )
229        {
230            StringBuffer buffer = new StringBuffer();
231            buffer.append( scheme );
232            buffer.append( ":" );
233            buffer.append( type );
234            buffer.append( ":" );
235            if( null != group )
236            {
237                buffer.append( group );
238                buffer.append( "/" );
239            }
240            buffer.append( name );
241            if( null != version )
242            {
243                buffer.append( "#" );
244                buffer.append( version );
245            }
246            String spec = buffer.toString();
247            return spec;
248        }
249        
250       /**
251        * Construct a new URL form a given URI.  If the URI is a Transit URI the 
252        * returned URL will be associated with the appropriate handler.
253        * @param uri the uri to convert
254        * @return the converted url
255        * @exception MalformedURLException if the url could not be created
256        */
257        public static URL toURL( URI uri ) throws MalformedURLException
258        {
259            try
260            {
261                Artifact artifact = Artifact.createArtifact( uri );
262                return artifact.toURL();
263            }
264            catch( UnsupportedSchemeException e )
265            {
266            }
267            catch( IllegalArgumentException e )
268            {
269            }
270            
271            try
272            {
273                return uri.toURL();
274            }
275            catch( IllegalArgumentException iae )
276            {
277                throw new InvalidArtifactException( iae.getMessage() );
278            }
279            
280            catch( MalformedURLException mue )
281            {
282                throw mue;
283            }
284            catch( Throwable t )
285            {
286                final String error = 
287                  "Unexpected error while attempting to convert a uri to a url."
288                  + "\n  URI: " 
289                  + uri;
290                throw new TransitRuntimeException( error, t );
291            }
292        }
293    
294       /**
295        * Test if the supplied uri is from the artifact family.  Specificially
296        * the test validates that the supplied uri has a scheme corresponding to 
297        * 'artifact', link', or 'local'.
298        * @param uri the uri to check
299        * @return true if thie uri is artifact based
300        */
301        public static boolean isRecognized( URI uri )
302        {
303            String scheme = uri.getScheme();
304            if( ARTIFACT.equals( scheme ) )
305            {
306                return true;
307            }
308            else if( LINK.equals( scheme ) )
309            {
310                return true;
311            }
312            else if( CONFIGURATION.equals( scheme ) )
313            {
314                return true;
315            }
316            else
317            {
318                return LOCAL.equals( scheme );
319            }
320        }
321    
322        // ------------------------------------------------------------------------
323        // state
324        // ------------------------------------------------------------------------
325    
326        /**
327         * The artifact uri.
328         */
329        private final URI m_uri;
330    
331        /**
332         * The artifact group.
333         */
334        private final String m_group;
335    
336        /**
337         * The artifact name.
338         */
339        private final String m_name;
340    
341        /**
342         * The artifact type.
343         */
344        private final String m_type;
345    
346        // ------------------------------------------------------------------------
347        // constructor
348        // ------------------------------------------------------------------------
349    
350        /**
351         * Creation of a new Artifact using a supplied uri.
352         * @param uri a uri of the form [scheme]:[type]:[group]/[name]#[version]
353         *   where [scheme] is one of 'link', 'artifact' or 'local'.
354         */
355        private Artifact( URI uri ) throws InvalidArtifactException
356        {
357            m_uri = reconstructURI( uri );
358            String ssp = m_uri.getSchemeSpecificPart();
359            
360            if( ssp.indexOf( '?' ) > -1 )
361            {
362                ssp = ssp.substring( 0, ssp.indexOf( '?' ) );
363            }
364    
365            if( ssp.indexOf( "//" ) > -1
366              || ssp.indexOf( ":/" ) > -1
367              || ssp.endsWith( "/" ) )
368            {
369                final String error =
370                  "Invalid character sequence in uri ["
371                  + uri + "].";
372                throw new InvalidArtifactException( error );
373            }
374            
375            int typeIndex = ssp.indexOf( ':' );
376            if( typeIndex > -1 )
377            {
378                String type = ssp.substring( 0, typeIndex );
379                m_type = type;
380                ssp = ssp.substring( typeIndex + 1 );
381            }
382            else
383            {
384                final String error = "Supplied artifact specification ["
385                  + uri + "] does not contain a type.";
386                throw new InvalidArtifactException( error );
387            }
388            
389            // ssp now contains group and name
390            
391            int groupIndex = ssp.lastIndexOf( '/' );
392            if( groupIndex > -1 )
393            {
394                String group = ssp.substring( 0, groupIndex );
395                m_group = group;
396                m_name = ssp.substring( groupIndex + 1 );
397            }
398            else
399            {
400                m_group = null;
401                m_name = ssp;
402            }
403            
404            String ver = m_uri.getFragment();
405            if( ver != null )
406            {
407                if( ver.indexOf( '/' ) >= 0
408                  || ver.indexOf( '%' ) >= 0
409                  || ver.indexOf( '\\' ) >= 0
410                  || ver.indexOf( '*' ) >= 0
411                  || ver.indexOf( '!' ) >= 0
412                  || ver.indexOf( '(' ) >= 0
413                  || ver.indexOf( '@' ) >= 0
414                  || ver.indexOf( ')' ) >= 0
415                  || ver.indexOf( '+' ) >= 0
416                  || ver.indexOf( '\'' ) >= 0
417                  || ver.indexOf( '{' ) >= 0
418                  || ver.indexOf( '}' ) >= 0
419                  || ver.indexOf( '[' ) >= 0
420                  || ver.indexOf( '}' ) >= 0
421                  || ver.indexOf( '?' ) >= 0
422                  || ver.indexOf( ',' ) >= 0
423                  || ver.indexOf( '#' ) >= 0
424                  || ver.indexOf( '=' ) >= 0
425                )
426                {
427                    final String error =
428                      "Supplied artifact specification ["
429                        + m_uri
430                        + "] contains illegal characters in the Version part.";
431                    throw new InvalidArtifactException( error );
432                }
433            }
434        }
435    
436        private URI reconstructURI( URI uri ) 
437        {
438            String fragment = uri.getFragment();
439            
440            // if the fragment cointains a '?' character then reconstruct the uri
441            // such that the query is in the ssp
442            
443            if( null != fragment )
444            {
445                int n = fragment.indexOf( '?' );
446                if( n > -1 )
447                {
448                    try
449                    {
450                        String version = fragment.substring( 0, n );
451                        String query = fragment.substring( n + 1 );
452                        String scheme = uri.getScheme();
453                        String ssp = uri.getSchemeSpecificPart();
454                        return new URI( scheme, ssp + "?" + query, version );
455                    }
456                    catch( Exception e )
457                    {
458                        throw new InvalidArtifactException( e.getMessage() );
459                    }
460                }
461            }
462            return uri;
463            
464            /*
465            String ssp = uri.getSchemeSpecificPart();
466            int n = ssp.indexOf( '?' );
467            if( n > -1 )
468            {
469                String body = ssp.substring( 0, n );
470                String query = ssp.substring( n+1 );
471                System.out.println( "  SSP: " + ssp );
472                System.out.println( " BODY: " + body );
473                System.out.println( "QUERY: " + query );
474            }
475            return uri;
476            */
477        }
478        
479    
480        // ------------------------------------------------------------------------
481        // public
482        // ------------------------------------------------------------------------
483    
484        /**
485         * Return the protocol for the artifact.
486         *
487         * @return the protocol scheme
488         */
489        public final String getScheme()
490        {
491            return m_uri.getScheme();
492        }
493    
494        /**
495         * Return the group identifier for the artifact.  The group identifier
496         * is composed of a sequence of named separated by the '/' character.
497         *
498         * @return the group identifier
499         */
500        public final String getGroup()
501        {
502            return m_group;
503        }
504    
505        /**
506         * Return the name of the artifact.
507         *
508         * @return the artifact name
509         */
510        public final String getName()
511        {
512            return m_name;
513        }
514    
515        /**
516         * Return the type of the artifact.
517         *
518         * @return the artifact type
519         */
520        public final String getType()
521        {
522            return m_type;
523        }
524    
525        /**
526         * Return the posssibly null version identifier.  The value of the version
527         * is an opaque string.
528         * @return the artifact version
529         */
530        public final String getVersion()
531        {
532            String ver = m_uri.getFragment();
533            if( ver == null )
534            {
535                return null;
536            }
537            else if( ver.length() == 0 )
538            {
539                return null;
540            }
541            else
542            {
543                return ver;
544            }
545        }
546    
547       /**
548        * Test if the artifact scheme is recognized.  Specificially
549        * the test validates that the artifact scheme corresponding to 
550        * 'artifact', link', or 'local'.
551        * @return true if the uri scheme is recognized
552        */
553        public boolean isRecognized()
554        {
555            return isRecognized( m_uri );
556        }
557    
558        /**
559         * Create an artifact url backed by the repository.
560         *
561         * @return the artifact url
562         */
563        public URL toURL()
564        {
565            String scheme = getScheme();
566            if( ARTIFACT.equals( scheme ) )
567            {
568                return toURL( new dpml.transit.artifact.Handler() );
569            }
570            else if( LINK.equals( scheme ) )
571            {
572                return toURL( new dpml.transit.link.Handler() );
573            }
574            else if( LOCAL.equals( scheme ) )
575            {
576                return toURL( new dpml.transit.local.Handler() );
577            }
578            else if( CONFIGURATION.equals( scheme ) )
579            {
580                return toURL( new dpml.transit.configuration.Handler() );
581            }
582            else
583            {
584                final String error = 
585                  "URI scheme not recognized: " + m_uri;
586                throw new UnsupportedSchemeException( error );
587            }
588        }
589    
590        /**
591         * Create an artifact url backed by the repository.
592         * @param handler the protocol handler
593         * @return the artifact url
594         */
595        public URL toURL( URLStreamHandler handler )
596        {
597            try
598            {
599                return new URL( null, m_uri.toASCIIString(), handler );
600            }
601            catch( MalformedURLException e )
602            {
603                // Can not happen!
604                final String error =
605                  "An artifact URI could not be converted to a URL [" 
606                  + m_uri 
607                  + "].";
608                throw new TransitRuntimeException( error );
609            }
610        }
611    
612        /**
613         * Create an artifact url backed by the repository.
614         *
615         * @return the artifact url
616         */
617        public URI toURI()
618        {
619            return m_uri;
620        }
621    
622        // ------------------------------------------------------------------------
623        // Comparable
624        // ------------------------------------------------------------------------
625    
626        /**
627         * Compare this artifact with another artifact.  Artifact comparisom is
628         * based on a comparison of the string representation of the artifact with
629         * the string representation of the supplied object.
630         *
631         * @param object the object to compare with this instance
632         * @return the comparative order of the supplied object relative to this
633         *   artifact
634         * @exception NullPointerException if the supplied object argument is null.
635         * @exception ClassCastException if the supplied object is not an Artifact.
636         */
637        public int compareTo( Object object )
638            throws NullPointerException, ClassCastException
639        {
640            if( object instanceof Artifact )
641            {
642                String name = this.toString();
643                return name.compareTo( object.toString() );
644            }
645            else if( null == object )
646            {
647                throw new NullPointerException( "object" );
648            }
649            else
650            {
651                final String error =
652                  "Object ["
653                  + object.getClass().getName()
654                  + "] does not implement ["
655                  + this.getClass().getName() + "].";
656                throw new ClassCastException( error );
657            }
658        }
659    
660        // ------------------------------------------------------------------------
661        // Object
662        // ------------------------------------------------------------------------
663    
664        /**
665         * Return a string representation of the artifact.
666         * @return the artifact as a uri
667         */
668        public String toString()
669        {
670             return m_uri.toString();
671        }
672    
673        /**
674         * Compare this artifact with the supplied object for equality.  This method
675         * will return true if the supplied object is an Artifact and has an equal
676         * uri.
677         *
678         * @param other the object to compare with this instance
679         * @return TRUE if this artifact is equal to the supplied object
680         */
681        public boolean equals( Object other )
682        {
683            if( null == other )
684            {
685                return false;
686            }
687            else if( this == other )
688            {
689                return true;
690            }
691            else if( other instanceof Artifact )
692            {
693                Artifact art = (Artifact) other;
694                return m_uri.equals( art.m_uri );
695            }
696            else
697            {
698                return false;
699            }
700        }
701    
702       /**
703        * Return the hashcode for the artifact.
704        * @return the hashcode value
705        */
706        public int hashCode()
707        {
708            return m_uri.hashCode();
709        }
710    }
711