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    
025    import java.net.MalformedURLException;
026    import java.net.URI;
027    import java.net.URISyntaxException;
028    import java.net.URL;
029    
030    /**
031     * A utility class the handles validation of <code>artifact</code> style uri
032     * strings.
033     *
034     * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
035     * @version 1.0.3
036     */
037    public final class Artifact implements Serializable, Comparable
038    {
039       /**
040        * Constant scheme name for the artifact protocol.
041        */
042        public static final String ARTIFACT = "artifact";
043        
044       /**
045        * Constant scheme name for the link protocol.
046        */
047        public static final String LINK = "link";
048        
049       /**
050        * Constant scheme name for the local protocol.
051        */
052        public static final String LOCAL = "local";
053        
054        static final long serialVersionUID = 1L;
055        
056        // ------------------------------------------------------------------------
057        // static
058        // ------------------------------------------------------------------------
059    
060        /**
061         * Creation of a new artifact instance using a supplied uri specification.
062         * An artifact uri contains the protocol identifier, a type, a group
063         * designator, a name, and an optional version identifier.
064         * <p>The following represent valid artifact uri examples:</p>
065         *
066         * <ul>
067         * <li>artifact:jar:dpml/transit/dpml-transit-main#1234</li>
068         * <li>artifact:jar:dpml/transit/dpml-transit-main</li>
069         * <li>link:jar:dpml/transit/dpml-transit-main#1.0</li>
070         * </ul>
071         *
072         * <p>
073         *  If there is a internal reference identifier which is marked by the
074         *  exclamation mark followed by slash (!/) it will be stripped. The
075         *  version part can be either before or after such identifier. Example;
076         *  <pre>
077         *   artifact:war:jmx-html/jmx-html#1.3!/images/abc.png
078         *   artifact:war:jmx-html/jmx-html!/images/abc.png#1.3
079         *  </pre>
080         *  The above uris will both be referencing
081         *  <code>artifact:war:jmx-html/jmx-html#1.3</code>
082         * </p>
083         * @param uri the artifact uri
084         * @return the new artifact
085         * @exception java.net.URISyntaxException if the supplied uri is not valid.
086         * @exception UnsupportedSchemeException if the URI does not have "artifact"
087         *         or "link" as its <strong>scheme</strong>.
088         */
089        public static final Artifact createArtifact( String uri )
090            throws URISyntaxException, UnsupportedSchemeException
091        {
092            if( null == uri )
093            {
094                throw new NullArgumentException( "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         */
134        public static final Artifact createArtifact( URI uri )
135            throws UnsupportedSchemeException
136        {
137            if( null == uri )
138            {
139                throw new NullArgumentException( "uri" );
140            }
141            String scheme = uri.getScheme();
142            if( null == scheme )
143            {
144                final String error = 
145                  "URI does not declare a scheme: " + uri;
146                throw new UnsupportedSchemeException( error );
147            }
148            //if( !scheme.equals( ARTIFACT ) && !scheme.equals( LINK ) && !scheme.equals( LOCAL ) )
149            //{
150            //    final String error = 
151            //      "URI contains a scheme that is not recognized: " + uri;
152            //    throw new UnsupportedSchemeException( error );
153            //}
154            return new Artifact( uri );
155        }
156    
157        /**
158         * Creation of a new artifact instance using a supplied group, name,
159         * version and type arguments.
160         *
161         * @param group the artifact group identifier
162         * @param name the artifact name
163         * @param version the version
164         * @param type the type
165         * @return the new artifact
166         * @exception NullArgumentException if any of the <code>group</code>,
167         *            <code>name</code> or <code>type</code> arguments are
168         *            <code>null</code>.
169         */
170        public static Artifact createArtifact( String group, String name, String version, String type )
171            throws NullArgumentException
172        {
173            return createArtifact( ARTIFACT, group, name, version, type );
174        }
175        
176        /**
177         * Creation of a new artifact instance using a supplied group, name,
178         * version and type arguments.
179         *
180         * @param scheme the artifact scheme
181         * @param group the artifact group identifier
182         * @param name the artifact name
183         * @param version the version
184         * @param type the type
185         * @return the new artifact
186         * @exception NullArgumentException if any of the <code>group</code>,
187         *            <code>name</code> or <code>type</code> arguments are
188         *            <code>null</code>.
189         */
190        public static Artifact createArtifact( String scheme, String group, String name, String version, String type )
191            throws NullArgumentException
192        {
193            if( name == null )
194            {
195                throw new NullArgumentException( "name" );
196            }
197            if( type == null )
198            {
199                throw new NullArgumentException( "type" );
200            }
201            if( scheme == null )
202            {
203                throw new NullArgumentException( "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 != query )
237            //{
238            //    buffer.append( "?" );
239            //    buffer.append( query );
240            //}
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( 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 )
347            throws IllegalArgumentException
348        {
349            m_uri = uri;
350            String ssp = uri.getSchemeSpecificPart();
351    
352            if( ssp.indexOf( "//" ) > -1
353              || ssp.indexOf( ":/" ) > -1
354              || ssp.endsWith( "/" ) )
355            {
356                final String error =
357                  "Invalid character sequence in uri ["
358                  + uri + "].";
359                throw new IllegalArgumentException( error );
360            }
361            
362            int typeIndex = ssp.indexOf( ':' );
363            if( typeIndex > -1 )
364            {
365                String type = ssp.substring( 0, typeIndex );
366                m_type = type;
367                ssp = ssp.substring( typeIndex + 1 );
368            }
369            else
370            {
371                final String error = "Supplied artifact specification ["
372                  + uri + "] does not contain a type.";
373                throw new IllegalArgumentException( error );
374            }
375            
376            // ssp now contains group, name and version
377            
378            int groupIndex = ssp.lastIndexOf( '/' );
379            if( groupIndex > -1 )
380            {
381                String group = ssp.substring( 0, groupIndex );
382                m_group = group;
383                m_name = ssp.substring( groupIndex + 1 );
384            }
385            else
386            {
387                m_group = null;
388                m_name = ssp;
389            }
390            
391            String ver = uri.getFragment();
392            if( ver != null )
393            {
394                if( ver.indexOf( '/' ) >= 0
395                  || ver.indexOf( '%' ) >= 0
396                  || ver.indexOf( '\\' ) >= 0
397                  || ver.indexOf( '*' ) >= 0
398                  || 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                )
413                {
414                    final String error =
415                      "Supplied artifact specification ["
416                        + uri
417                        + "] contains illegal characters in the Version part.";
418                    throw new IllegalArgumentException( error );
419                }
420            }
421        }
422    
423        // ------------------------------------------------------------------------
424        // public
425        // ------------------------------------------------------------------------
426    
427        /**
428         * Return the protocol for the artifact.
429         *
430         * @return the protocol scheme
431         */
432        public final String getScheme()
433        {
434            return m_uri.getScheme();
435        }
436    
437        /**
438         * Return the group identifier for the artifact.  The group identifier
439         * is composed of a sequence of named separated by the '/' character.
440         *
441         * @return the group identifier
442         */
443        public final String getGroup()
444        {
445            return m_group;
446        }
447    
448        /**
449         * Return the name of the artifact.
450         *
451         * @return the artifact name
452         */
453        public final String getName()
454        {
455            return m_name;
456        }
457    
458        /**
459         * Return the type of the artifact.
460         *
461         * @return the artifact type
462         */
463        public final String getType()
464        {
465            return m_type;
466        }
467    
468        /**
469         * Return the posssibly null version identifier.  The value of the version
470         * is an opaque string.
471         * @return the artifact version
472         */
473        public final String getVersion()
474        {
475            String ver = m_uri.getFragment();
476            if( ver == null )
477            {
478                return null;
479            }
480            else if( ver.length() == 0 )
481            {
482                return null;
483            }
484            else
485            {
486                return ver;
487            }
488        }
489    
490       /**
491        * Test if the artifact scheme is recognized.  Specificially
492        * the test validates that the artifact scheme corresponding to 
493        * 'artifact', link', or 'local'.
494        * @return true if the uri scheme is recognized
495        */
496        public boolean isRecognized()
497        {
498            return isRecognized( m_uri );
499        }
500    
501        /**
502         * Create an artifact url backed by the repository.
503         *
504         * @return the artifact url
505         */
506        public URL toURL()
507        {
508            String scheme = getScheme();
509            if( ARTIFACT.equals( scheme ) )
510            {
511                return toURL( new net.dpml.transit.artifact.Handler() );
512            }
513            else if( LINK.equals( scheme ) )
514            {
515                return toURL( new net.dpml.transit.link.Handler() );
516            }
517            else if( LOCAL.equals( scheme ) )
518            {
519                return toURL( new net.dpml.transit.local.Handler() );
520            }
521            else
522            {
523                final String error = 
524                  "URI scheme not recognized: " + m_uri;
525                throw new UnsupportedSchemeException( error );
526            }
527        }
528    
529        /**
530         * Create an artifact url backed by the repository.
531         * @param handler the protocol handler
532         * @return the artifact url
533         */
534        public URL toURL( URLStreamHandler handler )
535        {
536            try
537            {
538                return new URL( null, m_uri.toASCIIString(), handler );
539            }
540            catch( MalformedURLException e )
541            {
542                // Can not happen!
543                final String error =
544                  "An artifact URI could not be converted to a URL [" 
545                  + m_uri 
546                  + "].";
547                throw new TransitRuntimeException( error );
548            }
549        }
550    
551        /**
552         * Create an artifact url backed by the repository.
553         *
554         * @return the artifact url
555         */
556        public URI toURI()
557        {
558            return m_uri;
559        }
560    
561        // ------------------------------------------------------------------------
562        // Comparable
563        // ------------------------------------------------------------------------
564    
565        /**
566         * Compare this artifact with another artifact.  Artifact comparisom is
567         * based on a comparison of the string representation of the artifact with
568         * the string representation of the supplied object.
569         *
570         * @param object the object to compare with this instance
571         * @return the comparative order of the supplied object relative to this
572         *   artifact
573         * @exception NullArgumentException if the supplied object argument is null.
574         * @exception ClassCastException if the supplied object is not an Artifact.
575         */
576        public int compareTo( Object object )
577            throws NullArgumentException, ClassCastException
578        {
579            if( object instanceof Artifact )
580            {
581                String name = this.toString();
582                return name.compareTo( object.toString() );
583            }
584            else if( null == object )
585            {
586                throw new NullArgumentException( "object" );
587            }
588            else
589            {
590                final String error =
591                  "Object ["
592                  + object.getClass().getName()
593                  + "] does not implement ["
594                  + this.getClass().getName() + "].";
595                throw new ClassCastException( error );
596            }
597        }
598    
599        // ------------------------------------------------------------------------
600        // Object
601        // ------------------------------------------------------------------------
602    
603        /**
604         * Return a string representation of the artifact.
605         * @return the artifact as a uri
606         */
607        public String toString()
608        {
609             return m_uri.toString();
610        }
611    
612        /**
613         * Compare this artifact with the supplied object for equality.  This method
614         * will return true if the supplied object is an Artifact and has an equal
615         * uri.
616         *
617         * @param other the object to compare with this instance
618         * @return TRUE if this artifact is equal to the supplied object
619         */
620        public boolean equals( Object other )
621        {
622            if( null == other )
623            {
624                return false;
625            }
626            else if( this == other )
627            {
628                return true;
629            }
630            else if( other instanceof Artifact )
631            {
632                Artifact art = (Artifact) other;
633                return m_uri.equals( art.m_uri );
634            }
635            else
636            {
637                return false;
638            }
639        }
640    
641       /**
642        * Return the hashcode for the artifact.
643        * @return the hashcode value
644        */
645        public int hashCode()
646        {
647            return m_uri.hashCode();
648        }
649    }
650