001    /*
002     * Copyright 2005 Stephen 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.station.exec;
020    
021    import java.io.IOException;
022    import java.net.URI;
023    import java.rmi.registry.Registry;
024    import java.rmi.registry.LocateRegistry;
025    import java.util.Properties;
026    import java.util.Iterator;
027    import java.util.Set;
028    
029    import net.dpml.component.Component;
030    import net.dpml.component.Model;
031    import net.dpml.component.Composition;
032    import net.dpml.component.ActivationPolicy;
033    
034    import net.dpml.station.Station;
035    import net.dpml.station.Callback;
036    import net.dpml.station.ApplicationException;
037    
038    import net.dpml.transit.Artifact;
039    import net.dpml.util.PropertyResolver;
040    
041    import net.dpml.lang.PID;
042    import net.dpml.lang.Part;
043    
044    import net.dpml.util.Logger;
045    
046    import net.dpml.cli.Option;
047    import net.dpml.cli.Group;
048    import net.dpml.cli.CommandLine;
049    import net.dpml.cli.commandline.Parser;
050    import net.dpml.cli.util.HelpFormatter;
051    import net.dpml.cli.DisplaySetting;
052    import net.dpml.cli.builder.ArgumentBuilder;
053    import net.dpml.cli.builder.GroupBuilder;
054    import net.dpml.cli.builder.DefaultOptionBuilder;
055    import net.dpml.cli.builder.CommandBuilder;
056    import net.dpml.cli.option.PropertyOption;
057    import net.dpml.cli.validation.URIValidator;
058    import net.dpml.cli.validation.NumberValidator;
059    
060    /**
061     * Generic application handler that establishes a dedicated handler based 
062     * on the type of resource exposed by a codebase uri.
063     *
064     * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
065     * @version 1.0.3
066     */
067    public class ApplicationHandler
068    {
069        private static final PID PROCESS_ID = new PID();
070    
071        private final Logger m_logger;
072        private final Callback m_callback;
073        
074       /**
075        * The application handler class is a generic component handler that delegates
076        * application deployment processes to dedicated handlers based on the codebase 
077        * type of the target application.  Normally this plugin is activated as a 
078        * consequence of invoking the <tt>metro</tt> commandline handler (which is 
079        * the default haviour of the <tt>station</tt> when deployment local processes).
080        * The following command list help about the <tt>metro</tt> commandline handler:
081        * <pre>$ metro help
082    Usage:
083    metro [-uri <uri> | -help]
084    options
085      -uri <uri>               Execute deployment of an application codebase.
086        -key -port
087          -key (-k) <key>      Station callback application key.
088          -port (-p) <port>    Override default RMI registry port selection.
089      -help                    Print command help.
090        * </pre>
091        * A typical example of a commandline and resulting log of an application launch 
092        * using <tt>metro</tt> is show below:
093        * <pre>
094    $ metro -uri link:part:dpml/planet/http/dpml-http-demo
095    [2236 ] [INFO   ] (demo): Starting
096    [2236 ] [INFO   ] (org.mortbay.http.HttpServer): Version Jetty/5.1.x
097    [2236 ] [INFO   ] (org.mortbay.util.Container): Started net.dpml.http.impl.HttpServerImpl@6355dc
098    [2236 ] [INFO   ] (org.mortbay.util.Credential): Checking Resource aliases
099    [2236 ] [INFO   ] (org.mortbay.util.Container): Started HttpContext[/,/]
100    [2236 ] [INFO   ] (org.mortbay.http.SocketListener): Started SocketListener on 0.0.0.0:8080
101        * </pre>
102        *
103        * When invoked in conjuction with the <tt>station</tt> two optional parameters may be 
104        * supplied by the station.  These include <tt>-port</tt> option which directs the implementation 
105        * to locate the system wide <tt>station</tt> from an RMI registry on the selected port.  
106        * The second option is the <tt>-key</tt> argument that the handler uses to to resolve a 
107        * station callback through which the implementation notifies the station of the process 
108        * identity and startup status.  The callback process effectivly hands control over 
109        * management of the JVM process lietime and root component to to the station.
110        * 
111        * @param logger the assigned logging channel
112        * @param args command line arguments
113        * @exception Exception if an error occurs
114        */
115        public ApplicationHandler( 
116          Logger logger, String[] args ) throws Exception
117        {
118            super();
119            m_logger = logger;
120            
121            Thread.currentThread().setContextClassLoader( Composition.class.getClassLoader() );
122            
123            if( logger.isDebugEnabled() )
124            {
125                for( int i=0; i<args.length; i++ )
126                {
127                    logger.debug( "arg: " + ( i + 1 ) + ": " + args[i] );
128                }
129            }
130            
131            Parser parser = new Parser();
132            parser.setGroup( COMMAND_GROUP );
133            CommandLine line = parser.parse( args );
134            
135            if( line.hasOption( HELP_COMMAND ) || !line.hasOption( EXECUTE_COMMAND ) )
136            {
137                processHelp();
138                System.exit( 0 );
139            }
140            
141            URI uri = (URI) line.getValue( EXECUTE_COMMAND, null );
142            if( null == uri )
143            {
144                throw new NullPointerException( "uri" ); // will not happen
145            }
146            
147            String key = (String) line.getValue( KEY_OPTION, null );
148            if( null != key )
149            {
150                int port = Registry.REGISTRY_PORT;
151                if( line.hasOption( PORT_OPTION ) )
152                {
153                    Number number = 
154                      (Number) line.getValue( PORT_OPTION, null );
155                    port = number.intValue();
156                }
157                
158                Station station = getStation( port );
159                m_callback = station.getCallback( key );
160            }
161            else
162            {
163                m_callback = new LocalCallback();
164            }
165            
166            
167            URI categories = getCategoriesURI( line );
168            Properties properties = getCommandLineProperties( line );
169            String raw = System.getProperty( "dpml.station.partition", "" );
170            String partition = PropertyResolver.resolve( raw );
171            Component component = 
172              resolveTargetComponent( 
173                logger, partition, uri, categories, properties );
174            m_callback.started( PROCESS_ID, component );
175        }
176        
177       /**
178        * Return a properties instance composed of the <tt>-C&lt;key&gt;=&lt;value&gt;</tt>
179        * commandline arguments.
180        * @param line the commandline
181        * @return the resolved properties
182        */
183        private Properties getCommandLineProperties( CommandLine line )
184        {
185            Properties properties = new Properties();
186            Set propertyValue = line.getProperties();
187            Iterator iterator = propertyValue.iterator();
188            while( iterator.hasNext() )
189            {
190                String name = (String) iterator.next();
191                String value = line.getProperty( name );
192                properties.setProperty( name, value );
193            }
194            return properties;
195        }
196        
197        //------------------------------------------------------------------------------
198        // internals
199        //------------------------------------------------------------------------------
200        
201        private URI getCategoriesURI( CommandLine line )
202        {
203            return (URI) line.getValue( LOGGING_OPTION, null );
204        }
205        
206        private Component resolveTargetComponent( 
207          Logger logger, String partition, URI uri, URI categories, Properties properties ) throws Exception
208        {
209            if( Artifact.isRecognized( uri ) )
210            {
211                Artifact artifact = Artifact.createArtifact( uri );
212                String type = artifact.getType();
213                if( type.equals( "part" ) )
214                {
215                    Part part = Part.load( uri );
216                    if( part instanceof Composition )
217                    {
218                        Composition composition = (Composition) part;
219                        Model model = composition.getModel();
220                        model.setActivationPolicy( ActivationPolicy.STARTUP );
221                        return composition.newComponent();
222                    }
223                }
224            }
225            
226            final String error = 
227              "URI not supported [" + uri + "].";
228            throw new ApplicationException( error );
229        }
230        
231        private Station getStation( int port ) throws Exception
232        {
233            Registry registry = LocateRegistry.getRegistry( port );
234            return (Station) registry.lookup( Station.STATION_KEY );
235        }
236        
237        private Logger getLogger()
238        {
239            return m_logger;
240        }
241    
242       /**
243        * List general command help to the console.
244        * @exception IOException if an I/O error occurs
245        */
246        private void processHelp() throws IOException
247        {
248            HelpFormatter formatter = new HelpFormatter( 
249              HelpFormatter.DEFAULT_GUTTER_LEFT, 
250              HelpFormatter.DEFAULT_GUTTER_CENTER, 
251              HelpFormatter.DEFAULT_GUTTER_RIGHT, 
252              100 );
253            
254            formatter.getDisplaySettings().add( DisplaySetting.DISPLAY_GROUP_OUTER );
255            formatter.getDisplaySettings().add( DisplaySetting.DISPLAY_PROPERTY_OPTION );
256            formatter.getDisplaySettings().add( DisplaySetting.DISPLAY_ARGUMENT_BRACKETED );
257            
258            formatter.getFullUsageSettings().add( DisplaySetting.DISPLAY_OPTIONAL );
259            formatter.getFullUsageSettings().add( DisplaySetting.DISPLAY_GROUP_OUTER );
260            formatter.getFullUsageSettings().add( DisplaySetting.DISPLAY_PROPERTY_OPTION );
261            formatter.getFullUsageSettings().add( DisplaySetting.DISPLAY_OPTIONAL );
262            formatter.getFullUsageSettings().add( DisplaySetting.DISPLAY_ARGUMENT_BRACKETED );
263            formatter.getFullUsageSettings().remove( DisplaySetting.DISPLAY_PARENT_CHILDREN );
264            
265            formatter.getLineUsageSettings().add( DisplaySetting.DISPLAY_PROPERTY_OPTION );
266            formatter.getLineUsageSettings().add( DisplaySetting.DISPLAY_ARGUMENT_BRACKETED );
267            formatter.getLineUsageSettings().remove( DisplaySetting.DISPLAY_PARENT_CHILDREN );
268            formatter.getLineUsageSettings().remove( DisplaySetting.DISPLAY_GROUP_EXPANDED );
269            
270            formatter.setGroup( COMMAND_GROUP );
271            formatter.setShellCommand( "metro" );
272            formatter.print();
273        }
274        
275        //-----------------------------------------------------------------------------
276        // CLI
277        //-----------------------------------------------------------------------------
278    
279        private static final DefaultOptionBuilder OPTION_BUILDER = new DefaultOptionBuilder();
280        private static final ArgumentBuilder ARGUMENT_BUILDER = new ArgumentBuilder();
281        private static final GroupBuilder GROUP_BUILDER = new GroupBuilder();
282        private static final CommandBuilder COMMAND_BUILDER = new CommandBuilder();
283    
284        private static final PropertyOption CONTEXT_OPTION = 
285          new PropertyOption( "-C", "Set a context entry value.", 'C' );
286        private static final NumberValidator PORT_VALIDATOR = NumberValidator.getIntegerInstance();
287        private static final URIValidator URI_VALIDATOR = new URIValidator();
288        
289        private static final Option LOGGING_OPTION = 
290            OPTION_BUILDER
291              .withShortName( "categories" )
292              .withDescription( "Set logging category priorities." )
293              .withRequired( false )
294              .withArgument(
295                ARGUMENT_BUILDER 
296                  .withDescription( "URI." )
297                  .withName( "uri" )
298                  .withMinimum( 1 )
299                  .withMaximum( 1 )
300                  .withValidator( URI_VALIDATOR )
301                  .create() )
302              .create();
303    
304        private static final Option PORT_OPTION = 
305            OPTION_BUILDER
306              .withShortName( "port" )
307              .withDescription( "Override default RMI registry port selection." )
308              .withRequired( false )
309              .withArgument(
310                ARGUMENT_BUILDER 
311                  .withDescription( "Port." )
312                  .withName( "port" )
313                  .withMinimum( 1 )
314                  .withMaximum( 1 )
315                  .withValidator( PORT_VALIDATOR )
316                  .create() )
317              .create();
318    
319        private static final Option KEY_OPTION = 
320            OPTION_BUILDER
321              .withShortName( "key" )
322              .withShortName( "k" )
323              .withDescription( "Station callback application key." )
324              .withRequired( false )
325              .withArgument(
326                ARGUMENT_BUILDER 
327                  .withDescription( "Key." )
328                  .withName( "key" )
329                  .withMinimum( 1 )
330                  .withMaximum( 1 )
331                  .create() )
332              .create();
333            
334    
335        private static final Option HELP_COMMAND =
336          OPTION_BUILDER
337            .withShortName( "help" )
338            .withShortName( "h" )
339            .withDescription( "Print command help." )
340            .create();
341    
342        private static final Group EXECUTE_GROUP =
343          GROUP_BUILDER
344            .withOption( KEY_OPTION )
345            .withOption( PORT_OPTION )
346            .withOption( CONTEXT_OPTION )
347            .withOption( LOGGING_OPTION )
348            .create();
349        
350        private static final Option EXECUTE_COMMAND =
351          OPTION_BUILDER
352            .withShortName( "uri" )
353            .withDescription( "Execute deployment of an application codebase." )
354            .withChildren( EXECUTE_GROUP )
355            .withArgument(
356              ARGUMENT_BUILDER 
357                .withDescription( "Application codebase uri." )
358                .withName( "uri" )
359                .withMinimum( 1 )
360                .withMaximum( 1 )
361                .withValidator( new URIValidator() )
362                .create() )
363            .create();
364        
365        private static final Group COMMAND_GROUP =
366          GROUP_BUILDER
367            .withName( "options" )
368            .withMinimum( 1 )
369            .withMaximum( 1 )
370            .withOption( EXECUTE_COMMAND )
371            .withOption( HELP_COMMAND )
372            .create();
373    
374       /**
375        * Internal class supporting callback semantics used in cases where the application
376        * handler has to assume management of a target.
377        */
378        private class LocalCallback implements Callback
379        {
380            private PID m_pid;
381            private Component m_handler;
382            
383           /**
384            * Method invoked by a process to signal that the process has started.
385            *
386            * @param pid the process identifier
387            * @param handler optional handler reference
388            * @exception RemoteException if a remote error occurs
389            */
390            public void started( PID pid, Component handler )
391            {
392                m_pid = pid;
393                m_handler = handler;
394                try
395                {
396                    handler.commission();
397                }
398                catch( Exception e )
399                {
400                    final String error = 
401                      "Initiating process deactivation due to an application error.";
402                    getLogger().error( error, e );
403                    try
404                    {
405                        handler.decommission();
406                    }
407                    catch( Exception deactivationError )
408                    {
409                        // ignore
410                    }
411                }
412            }
413        
414            /**
415            * Method invoked by a process to signal that the process has 
416            * encounter an error condition.
417            *
418            * @param throwable the error condition
419            * @param fatal if true the process is requesting termination
420            * @exception RemoteException if a remote error occurs
421            */
422            public void error( Throwable throwable, boolean fatal )
423            {
424                final String error = "Application error.";
425                if( fatal )
426                {
427                    getLogger().error( error, throwable );
428                    try
429                    {
430                        m_handler.decommission();
431                    }
432                    catch( Throwable e )
433                    {
434                    }
435                }
436                else
437                {
438                    getLogger().warn( error, throwable );
439                }
440            }
441        
442            /**
443            * Method invoked by a process to send a arbitary message to the 
444            * the callback handler.
445            *
446            * @param message the message
447            * @exception RemoteException if a remote error occurs
448            */
449            public void info( String message )
450            {
451                getLogger().info( message );
452            }
453        
454            /**
455            * Method invoked by a process to signal its imminent termination.
456            *
457            * @exception RemoteException if a remote error occurs
458            */
459            public void stopped()
460            {
461                Thread thread = new Thread(
462                  new Runnable()
463                  {
464                      public void run()
465                      {
466                        System.exit( 0 );
467                      }
468                  }
469                );
470                thread.start();
471            }
472        }
473    }