001    /*
002     * Copyright 2006 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.lang;
020    
021    import java.io.File;
022    import java.io.IOException;
023    import java.net.URI;
024    import java.net.URL;
025    import java.util.Map;
026    import java.util.Hashtable;
027    import java.lang.ref.WeakReference;
028    
029    import net.dpml.transit.link.ArtifactLinkManager;
030    
031    import net.dpml.util.Logger;
032    import net.dpml.util.DefaultLogger;
033    import net.dpml.util.ElementHelper;
034    import net.dpml.util.DOM3DocumentBuilder;
035    import net.dpml.util.Decoder;
036    import net.dpml.util.DecoderFactory;
037    import net.dpml.util.DecodingException;
038    import net.dpml.util.Resolver;
039    import net.dpml.util.SimpleResolver;
040    
041    import org.w3c.dom.Document;
042    import org.w3c.dom.Element;
043    import org.w3c.dom.TypeInfo;
044    
045    /**
046     * Construct a part.
047     */
048    public final class PartDecoder implements Decoder
049    {
050       /**
051        * Part XSD uri.
052        */
053        public static final String PART_XSD_URI = "link:xsd:dpml/lang/dpml-part#1.0";
054    
055        private static final DOM3DocumentBuilder DOCUMENT_BUILDER = 
056          new DOM3DocumentBuilder();
057        
058        private static final ValueDecoder VALUE_DECODER = new ValueDecoder();
059        
060        private static final PartDecoder DECODER = new PartDecoder();
061        
062        private static final String BASEPATH = setupBasePathSpec();
063        
064       /**
065        * Get the singleton instance of the part decoder.
066        * @return the decoder instance.
067        */
068        public static PartDecoder getInstance()
069        {
070            return DECODER;
071        }
072        
073        private Map m_map = new Hashtable();
074        private Map m_builders = new Hashtable();
075        
076        private Logger m_logger;
077        
078        private PartDecoder()
079        {
080            m_logger = new DefaultLogger( "dpml.lang" );
081        }
082        
083       /**
084        * Load a part from a uri.
085        * @param uri the part uri
086        * @param cache if true parts are cached relative to the requested uri
087        * @return the part definition
088        * @exception IOException if an IO error occurs
089        */
090        public Part loadPart( URI uri, boolean cache ) throws IOException
091        {
092            if( null == uri )
093            {
094                throw new NullPointerException( "uri" );
095            }
096            if( getLogger().isDebugEnabled() )
097            {
098                String path = getPartSpec( uri );
099                if( getLogger().isTraceEnabled() )
100                {
101                    if( cache )
102                    {
103                        getLogger().trace( "loading part (cache enabled): " + path );
104                    }
105                    else
106                    {
107                        getLogger().trace( "loading part (cache disabled): " + path );
108                    }
109                }
110                else
111                {
112                    getLogger().debug( "loading part: " + path );
113                }
114            }
115            String key = buildKey( uri );
116            if( cache )
117            {
118                WeakReference ref = (WeakReference) m_map.get( key );
119                if( null != ref )
120                {
121                    Part part = (Part) ref.get();
122                    if( null != part )
123                    {
124                        if( getLogger().isDebugEnabled() )
125                        {
126                            getLogger().debug( "located part in cache" );
127                        }
128                        return part;
129                    }
130                }
131            }
132            
133            // cache based retrieval was disabled or no cache value present
134            
135            try
136            {
137                final Document document = DOCUMENT_BUILDER.parse( uri );
138                final Element root = document.getDocumentElement();
139                Resolver resolver = new SimpleResolver();
140                Part value = decodePart( uri, root, resolver );
141                if( cache )
142                {
143                    WeakReference reference = new WeakReference( value );
144                    m_map.put( key, reference );
145                    if( getLogger().isTraceEnabled() )
146                    {
147                        getLogger().trace( "caching part" 
148                          + "\n  uri: " + uri
149                          + "\n  key: " + key ); 
150                    }
151                }
152                return value;
153            }
154            catch( Throwable e )
155            {
156                final String error =
157                  "An error while attempting to load a part."
158                  + "\n  uri: " + uri;
159                IOException exception = new IOException( error );
160                exception.initCause( e );
161                throw exception;
162            }
163        }
164        
165        private String buildKey( URI uri ) throws IOException
166        {
167            ClassLoader classloader = getAnchorClassLoader();
168            int n = System.identityHashCode( classloader );
169            return "" + n + "#" + getRealURI( uri ).toASCIIString();
170        }
171        
172        private String getID()
173        {
174            ClassLoader classloader = getAnchorClassLoader();
175            int n = System.identityHashCode( classloader );
176            return "" + n;
177        }
178        
179        private URI getRealURI( URI uri ) throws IOException
180        {
181            if( "link".equals( uri.getScheme() ) )
182            {
183                ArtifactLinkManager manager = new ArtifactLinkManager();
184                return manager.getTargetURI( uri );
185            }
186            else
187            {
188                return uri;
189            }
190        }
191        
192       /**
193        * Resolve a object from a DOM element.
194        * @param element the dom element
195        * @param resolver build-time value resolver
196        * @return the resolved object
197        * @exception IOException if an error occurs during element evaluation
198        */
199        public Object decode( Element element, Resolver resolver ) throws IOException
200        {
201            return decodePart( null, element, resolver );
202        }
203        
204       /**
205        * Resolve a part from a DOM element.
206        * @param uri the part uri
207        * @param element element part definition
208        * @param resolver build-time value resolver
209        * @return the resolved part datastructure
210        * @exception IOException if an error occurs during element evaluation
211        */
212        public Part decodePart( URI uri, Element element, Resolver resolver ) throws IOException
213        {
214            TypeInfo info = element.getSchemaTypeInfo();
215            String namespace = info.getTypeNamespace();
216            if( PART_XSD_URI.equals( namespace ) )
217            {
218                boolean alias = ElementHelper.getBooleanAttribute( element, "alias", false );
219                Info information = getInfo( uri, element );
220                Classpath classpath = getClasspath( element );
221                Element strategy = getStrategyElement( element );
222                return build( m_logger, information, classpath, strategy, resolver );
223            }
224            else
225            {
226                final String error = 
227                  "Part namespace not recognized."
228                  + "\nExpecting: " + PART_XSD_URI
229                  + "\nFound: " + namespace;
230                throw new DecodingException( element, error );
231            }
232        }
233        
234       /**
235        * Resolve a part plugin or resource strategy.
236        * @param logger the logging channel
237        * @param information the part info definition
238        * @param classpath the part classpath definition
239        * @param strategy part deployment strategy definition
240        * @param resolver build-time value resolver
241        * @return the resolved part
242        * @exception IOException if an error occurs during element evaluation
243        */
244        public Part build( 
245          Logger logger, Info information, Classpath classpath, Element strategy, Resolver resolver ) 
246          throws IOException
247        {
248            ClassLoader anchor = getAnchorClassLoader();
249            TypeInfo info = strategy.getSchemaTypeInfo();
250            String namespace = info.getTypeNamespace();
251            if( PART_XSD_URI.equals( namespace ) )
252            {
253                // this is either a plugin or a resource
254                
255                String name = info.getTypeName();
256                if( "plugin".equals( name ) )
257                {
258                    if( logger.isTraceEnabled() )
259                    {
260                        logger.trace( "reading plugin definition" );
261                    }
262                    String classname = ElementHelper.getAttribute( strategy, "class" );
263                    Element[] elements = ElementHelper.getChildren( strategy );
264                    Value[] values = VALUE_DECODER.decodeValues( elements );
265                    Part part = new Plugin( logger, information, classpath, classname, values );
266                    if( logger.isTraceEnabled() )
267                    {
268                        logger.trace( "loaded plugin definition" );
269                    }
270                    return part;
271                }
272                else if( "resource".equals( name ) )
273                {
274                    if( logger.isTraceEnabled() )
275                    {
276                        logger.trace( "reading resource definition" );
277                    }
278                    String urn = ElementHelper.getAttribute( strategy, "urn" );
279                    String path = ElementHelper.getAttribute( strategy, "path" );
280                    Part part = new Resource( logger, information, classpath, urn, path );
281                    if( logger.isTraceEnabled() )
282                    {
283                        logger.trace( "loaded resource definition" );
284                    }
285                    return part;
286                }
287                else
288                {
289                    final String error = 
290                      "Element type name ["
291                      + name
292                      + "] is not a recognized element type within the "
293                      + PART_XSD_URI
294                      + " namespace.";
295                    throw new DecodingException( strategy, error );
296                }
297            }
298            else
299            {
300                // this is a foreign part
301                
302                try
303                {
304                    URI uri = getDecoderURI( strategy );
305                    Builder builder = loadForeignBuilder( uri );
306                    if( logger.isTraceEnabled() )
307                    {
308                        logger.trace( 
309                          "using builder [" 
310                          + builder.getClass().getName() 
311                          + "]" );
312                    }
313                    Part part = builder.build( logger, information, classpath, strategy, resolver );
314                    if( logger.isTraceEnabled() )
315                    {
316                        logger.trace( 
317                          "loaded part ["  
318                          + part.getClass().getName() 
319                          + "]" );
320                    }
321                    return part;
322                }
323                catch( Exception ioe )
324                {
325                    final String error = 
326                      "Internal error while attempting to load foreign part.";
327                    throw new DecodingException( strategy, error, ioe );
328                }
329                finally
330                {
331                    Thread.currentThread().setContextClassLoader( anchor );
332                }
333            }
334        }
335        
336       /**
337        * Resolve the element decoder uri.
338        *
339        * @param element the DOM element
340        * @return the decoder uri
341        * @exception DecodingException if an error occurs
342        */
343        public URI getDecoderURI( Element element ) throws DecodingException
344        {
345            String uri = ElementHelper.getAttribute( element, "handler" );
346            if( null != uri )
347            {
348                try
349                {
350                    return new URI( uri );
351                }
352                catch( Exception e )
353                {
354                    final String error = 
355                      "Internal error while resolving handler attribute (expecting uri value)";
356                    throw new DecodingException( element, error, e );
357                }
358            }
359            TypeInfo info = element.getSchemaTypeInfo();
360            String namespace = info.getTypeNamespace();
361            try
362            {
363                return DecoderFactory.getDecoderURIFromNamespaceURI( namespace );
364            }
365            catch( Exception e )
366            {
367                final String error = 
368                  "Internal error while attempting to resolve default decoder uri.";
369                throw new DecodingException( element, error, e );
370            }
371        }
372        
373       /**
374        * Get the assigned logging channel.
375        * @return the logging channel
376        */
377        protected Logger getLogger()
378        {
379            return m_logger;
380        }
381        
382       /**
383        * Load a forign part builder.  The implementation will attempt to resolve a 
384        * plugin defintion from the supplied uri, caching a reference to
385        * the builder, and returning the plugin instance as a builder instance.
386        *
387        * @param uri the part builder uri
388        * @exception DecodingException if a part decoding error occurs
389        * @exception Exception if part loading error occurs
390        */
391        private Builder loadForeignBuilder( URI uri ) throws DecodingException, Exception
392        {
393            WeakReference ref = (WeakReference) m_builders.get( uri );
394            if( null != ref )
395            {
396                Builder builder = (Builder) ref.get();
397                if( null != builder )
398                {
399                    if( getLogger().isTraceEnabled() )
400                    {
401                        getLogger().trace( "located builder [" + uri + "]" );
402                    }
403                    return builder;
404                }
405                else
406                {
407                    if( getLogger().isTraceEnabled() )
408                    {
409                        getLogger().trace( "reloading builder [" + uri + "]" );
410                    }
411                }
412            }
413            else
414            {
415                if( getLogger().isTraceEnabled() )
416                {
417                    getLogger().trace( "loading builder [" + uri + "]" );
418                }
419            }
420            
421            Part part = loadPart( uri, true );
422            Logger logger = getLogger();
423            Object[] args = new Object[]{logger};
424            Object object = part.instantiate( args );
425            if( Builder.class.isAssignableFrom( object.getClass() ) )
426            {
427                Builder builder = (Builder) object;
428                WeakReference reference = new WeakReference( builder );
429                m_builders.put( uri, reference );
430                return builder;
431            }
432            else
433            {
434                String stack = 
435                  StandardClassLoader.toString(
436                    getClass().getClassLoader(), 
437                    object.getClass().getClassLoader() );
438                String classname = object.getClass().getName();
439                final String error = 
440                  "Plugin instance is not assignable to the "
441                  + Builder.class.getName()
442                  + " interface."
443                  + "\n  URI: " + uri 
444                  + "\n  Class: " + classname
445                  + "\n" + stack;
446                throw new PartException( error );
447            }
448        }
449        
450        private Element getStrategyElement( Element root ) throws DecodingException
451        {
452            Element[] children = ElementHelper.getChildren( root );
453            if( children.length != 3 )
454            {
455                final String error = 
456                  "Illegal number of child elements in <part>. Expecting 3, found " 
457                  + children.length
458                  + ".";
459                throw new DecodingException( root, error );
460            }
461            return children[1];
462        }
463        
464        
465        private Info getInfo( URI uri, Element root )
466        {
467            Element element = ElementHelper.getChild( root, "info" );
468            String title = ElementHelper.getAttribute( element, "title" );
469            Element descriptionElement = ElementHelper.getChild( element, "description" );
470            String description = ElementHelper.getValue( descriptionElement );
471            return new Info( uri, title, description );
472        }
473        
474       /**
475        * Construct the classpath defintion.
476        * @param root the element containing a 'classpath' element.
477        * @return the classpath definition
478        * @exception DecodingException if an error occurs during element evaluation
479        */
480        protected Classpath getClasspath( Element root ) throws DecodingException
481        {
482            Element classpath = ElementHelper.getChild( root, "classpath" );
483            if( null == classpath )
484            {
485                final String error = 
486                  "Required classpath element is not present in plugin descriptor.";
487                throw new DecodingException( root, error );
488            }
489            
490            try
491            {
492                Element[] children = ElementHelper.getChildren( classpath );
493                URI[] sys = buildURIs( classpath, "system" );
494                URI[] pub = buildURIs( classpath, "public" );
495                URI[] prot = buildURIs( classpath, "protected" );
496                URI[] priv = buildURIs( classpath, "private" );
497                Classpath cp = new Classpath( sys, pub, prot, priv );
498                return cp;
499            }
500            catch( Throwable e )
501            {
502                final String error = 
503                  "Unable to decode classpath due to an unexpected error.";
504                throw new DecodingException( classpath, error, e );
505            }
506        }
507        
508        private URI[] buildURIs( Element classpath, String key ) throws Exception
509        {
510            Element category = ElementHelper.getChild( classpath, key );
511            if( null == category )
512            {
513                return new URI[0];
514            }
515            else
516            {
517                Element[] children = ElementHelper.getChildren( category, "uri" );
518                URI[] uris = new URI[ children.length ];
519                for( int i=0; i<children.length; i++ )
520                {
521                    Element child = children[i];
522                    String value = ElementHelper.getValue( child );
523                    uris[i] = new URI( value );
524                }
525                return uris;
526            }
527        }
528        
529        private ClassLoader getAnchorClassLoader()
530        {
531            ClassLoader classloader = Thread.currentThread().getContextClassLoader();
532            if( null == classloader )
533            {
534                return Part.class.getClassLoader();
535            }
536            else
537            {
538                return classloader;
539            }
540        }
541        
542        private static String setupBasePathSpec()
543        {
544            try
545            {
546                String path = System.getProperty( "user.dir" );
547                File file = new File( path );
548                URI uri = file.toURI();
549                URL url = uri.toURL();
550                return url.toString();
551            }
552            catch( Exception e )
553            {   
554                return e.toString();
555            }
556        }
557        
558        static String getPartSpec( URI uri )
559        {
560            String path = uri.toASCIIString();
561            if( path.startsWith( BASEPATH ) )
562            {
563                return "./" + path.substring( BASEPATH.length() );
564            }
565            else
566            {
567                return path;
568            }
569        }
570    }