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