001/*
002 * $RCSfile: Client.java,v $
003 * $Revision: 1.1.2.5.14.2 $ $Date: 2015/05/07 23:56:02 $
004 *
005 * Copyright 2009 Kabira Technologies, Inc. All rights reserved.
006 */
007package com.kabira.test.management;
008
009import java.io.BufferedReader;
010import java.io.IOException;
011import java.io.InputStream;
012import java.io.InputStreamReader;
013import java.net.InetAddress;
014import java.net.MalformedURLException;
015import java.net.URL;
016import java.net.UnknownHostException;
017import java.util.Map;
018import java.util.HashMap;
019import java.util.Map.Entry;
020import java.lang.Throwable;
021import java.util.logging.Level;
022import java.util.logging.Logger;
023import javax.management.AttributeList;
024import javax.management.InstanceNotFoundException;
025import javax.management.MBeanException;
026import javax.management.MBeanServerConnection;
027import javax.management.MalformedObjectNameException;
028import javax.management.ObjectName;
029import javax.management.ReflectionException;
030import javax.management.RuntimeErrorException;
031import javax.management.RuntimeMBeanException;
032import javax.management.remote.JMXConnector;
033import javax.management.remote.JMXConnectorFactory;
034import javax.management.remote.JMXServiceURL;
035import com.kabira.platform.Transaction;
036import com.kabira.platform.Transaction.InvalidTransactionState;
037
038/**
039 * Provides a mechanism for calling administration targets from test code.
040 */
041public class Client
042{
043    /**
044     * Create a new Client instance using the given userName and password.
045     * @param userName a configured principal on the node
046     * @param password the text credential for the principal
047     */
048    public Client(String userName, String password)
049    {
050        m_userName = userName;
051        m_password = password;
052    }
053
054    /**
055     * Execute an administration command.
056     * All administration targets and commands are supported
057     * except "load configuration", which is supported by the
058     * nested Configuration class, and "loadactivate configuration", which is
059     * not supported.
060     * The administration target will always run on the same node as
061     * the application calling runCommand.
062     * 
063     * @param command the command name
064     * @param target the target name
065     * @param parameters a map containing parameter names and values
066     * @return The output of the command
067     * @throws CommandFailed an unsupported command was specified or the 
068     * invocation of the command failed.
069     * @throws InvalidTransactionState called from a transactional context.
070     */
071    public String[] runCommand(
072            final String command,
073            final String target,
074            final HashMap<String, String> parameters)
075            throws CommandFailed, InvalidTransactionState
076    {
077        if (target.equals("configuration"))
078        {
079            if (command.equals("load"))
080            {
081                throw new CommandFailed("load configuration is not supported " +
082                        "by runCommand.  Use loadConfigurationData() or " +
083                        "loadConfigurationResource() instead.");
084            }
085            else if (command.equals("loadactivate"))
086            {
087                throw new CommandFailed(
088                        "loadactivate configuration is not supported");
089            }
090        }
091
092        jmxConnect();
093        
094        String[] params = null;
095
096        if (parameters != null)
097        {
098            params = new String[parameters.size()];
099            int i = 0;
100
101            for (Entry entry : parameters.entrySet())
102            {
103                params[i++] = (String) entry.getKey() + "=" + (String) entry.
104                        getValue();
105            }
106        }
107
108        return invoke(command, target, params);
109    }
110        /**
111         * performs cleanup action.
112         * closes jmx connection, if not done already
113         * this is generally called by finalize method which is called by garbage
114         * collector to do cleanup;  
115         * this is made public so that calling class can do cleanup to release 
116         * resources without having to wait for garbage collector.
117         */
118        public  void tearDown()
119        {
120                try
121                {
122                        if(m_jmxConnector != null)
123                        {
124                                m_jmxConnector.close();
125                        }
126                }
127                catch (IOException ex)
128                {
129                Logger l = Logger.getLogger("");
130                l.log(Level.WARNING,
131                        "Failed to close jmx connector file", ex);
132                }
133        }
134        @Override
135        protected void finalize() throws Throwable
136        {
137                super.finalize();
138                tearDown();
139        }
140    /**
141     * Provides a mechanism for loading and managing configuration data.
142     */
143    public final class Configuration
144    {
145        /**
146         * Create a new Configuration instance to manage the supplied
147         * configuration data 
148         * @param url the URL of the configuration data.
149         */
150        public Configuration(final URL url)
151        {
152            m_url = url;
153        }
154
155        /**
156         * Load configuration data.
157         * @throws CommandFailed the URL could not be opened, or the
158         * configuration failed audit.
159         * @throws InvalidTransactionState called from a transactional context.
160         */
161        public void load() throws CommandFailed, InvalidTransactionState
162        {
163            jmxConnect();
164
165            InputStream is;
166
167            try
168            {
169                is = m_url.openStream();
170            }
171            catch (IOException ex)
172            {
173                throw new CommandFailed("failed to open URL " +
174                        m_url.toString() + ": " + ex.getMessage());
175            }
176
177            String data = readInputStream(is);
178
179            Object[] params = new Object[]
180                {
181                    m_url.toString(),
182                    data,
183                    new Boolean(false),
184                    new Boolean(false),
185                };
186
187            String[] sig = new String[]
188                {
189                    "java.lang.String",
190                    "java.lang.String",
191                    "java.lang.Boolean",
192                    "java.lang.Boolean",
193                };
194
195            String[] results = doInvoke(m_configurationName,
196                        "loadNamedResource", params, sig);
197
198            m_versionInfo = new HashMap<String, String>();
199
200            for (String r : results)
201            {
202                String[]nv = r.replaceAll("\\s", "").split("=");
203                m_versionInfo.put(nv[0], nv[1]);
204            }
205
206            assert(m_versionInfo.get("type") != null);
207            assert(m_versionInfo.get("name") != null);
208            assert(m_versionInfo.get("version") != null);
209
210            m_parameters = new String[]
211                {
212                    "type=" + m_versionInfo.get("type"),
213                    "name=" + m_versionInfo.get("name"),
214                    "version=" + m_versionInfo.get("version")
215                };
216        }
217
218        /**
219         * Activate the current configuration.  Configuration must have been
220         * loaded by this instance of Configuration.
221         * @throws CommandFailed the configuration was not loaded by this
222         * Configuration instance or the configuration is already active.
223         * @throws InvalidTransactionState called from a transactional context.
224         */
225        public void activate() throws CommandFailed, InvalidTransactionState
226        {
227            checkLoaded();
228            invoke("activate", "configuration", m_parameters);
229        }
230
231        /**
232         * Deactivate the current configuration.  Configuration must have been
233         * loaded by this instance of Configuration.
234         * @throws CommandFailed the configuration was not loaded by this
235         * Configuration instance or the configuration is not active.
236         * @throws InvalidTransactionState called from a transactional context.
237         */
238        public void deactivate() throws CommandFailed, InvalidTransactionState
239        {
240            checkLoaded();
241            invoke("deactivate", "configuration", m_parameters);
242        }
243
244        /**
245         * Remove the current configuration. Configuration must have been
246         * loaded by this instance of Configuration.
247         * @throws CommandFailed the configuration was not loaded by this
248         * Configuration instance or the configuration has already been
249         * removed.
250         * @throws InvalidTransactionState called from a transactional context.
251         */
252        public void remove() throws CommandFailed, InvalidTransactionState
253        {
254            checkLoaded();
255            invoke("remove", "configuration", m_parameters);
256        }
257
258        /**
259         *
260         * Display the current Configuration. Configuration must have been
261         * loaded by this instance of Configuration.
262         * @return All available information about the configuration.
263         * @throws CommandFailed the configuration was not loaded by this
264         * Configuration instance.
265         * @throws InvalidTransactionState called from a transactional context.
266         */
267        public String[] display() throws CommandFailed, InvalidTransactionState
268        {
269            checkLoaded();
270            return invoke("display", "configuration", m_parameters);
271        }
272
273        /**
274         * Export the current configuration.  Configuration must have been
275         * loaded by this instance of Configuration.
276         * @return The configuration data formatted as a kcs file.
277         * @throws CommandFailed the configuration was not loaded by this
278         * Configuration instance.
279         * @throws InvalidTransactionState called from a transactional context.
280         */
281        public String[] export() throws CommandFailed, InvalidTransactionState
282        {
283            checkLoaded();
284            return invoke("export", "configuration", m_parameters);
285        }
286
287        /**
288         * Get the type of the currently-loaded configuration.
289         * @return the type of the currently-loaded configuration.
290         * @throws CommandFailed the configuration was not loaded by this
291         * Configuration instance.
292         */
293        public String getType() throws CommandFailed
294        {
295            checkLoaded();
296            return m_versionInfo.get("type");
297        }
298
299        /**
300         * Get the name of the currently-loaded configuration.
301         * @return the name of the currently-loaded configuration
302         * @throws CommandFailed the configuration was not loaded by this
303         * Configuration instance.
304         */
305        public String getName() throws CommandFailed
306        {
307            checkLoaded();
308            return m_versionInfo.get("name");
309        }
310
311        /**
312         * Get the version of the currently-loaded configuration.
313         * @return the version of the currently-loaded configuration
314         * @throws CommandFailed the configuration was not loaded by this
315         * Configuration instance.
316         */
317        public String getVersion() throws CommandFailed
318        {
319            checkLoaded();
320            return m_versionInfo.get("version");
321        }
322
323        /**
324         * Get the state of the currently-loaded configuration.
325         * @return the state of the currently-loaded configuration
326         * @throws CommandFailed the configuration was not loaded by this
327         * Configuration instance.
328         * @throws InvalidTransactionState called from a transactional context.
329         */
330        public ConfigurationState getState()
331            throws CommandFailed, InvalidTransactionState
332        {
333            checkLoaded();
334
335            assert(m_parameters.length == 3);
336
337            String[] parameters = new String[]
338                    {
339                        m_parameters[0],
340                        m_parameters[1],
341                        m_parameters[2],
342                        "delimiter=|"
343                    };
344
345            String[] display = invoke("display", "configuration", parameters);
346
347            assert(display.length == 2);
348
349            String[] names = display[0].split("\\|");
350            String[] values = display[1].split("\\|");
351
352            assert(names.length == values.length);
353
354            ConfigurationState state = null;
355
356            for (int i = 0; i < names.length; i++)
357            {
358                if (names[i].equals("State"))
359                {
360                    if (values[i].equals("Active"))
361                    {
362                        state = ConfigurationState.Active;
363                    }
364                    else
365                    {
366                        state = ConfigurationState.Inactive;
367                    }
368                    break;
369                }
370            }
371
372            return state;
373        }
374
375        private void checkLoaded() throws CommandFailed
376        {
377            if (m_parameters == null)
378            {
379                throw new CommandFailed("Configuration is not loaded");
380            }
381        }
382
383        private URL m_url;
384        private String[] m_parameters = null;
385        private HashMap<String, String> m_versionInfo;
386    }
387
388    private String[] invoke(String command, String target,
389            String[] parameters)
390            throws CommandFailed, InvalidTransactionState
391    {
392        int numArgs = 2;
393
394        if (parameters != null)
395        {
396            numArgs += 1;
397        }
398
399        Object result = null;
400        Object[] params = new Object[numArgs];
401        String[] sig = new String[numArgs];
402
403        params[0] = new String(command);
404        params[1] = new String(target);
405
406        if (parameters != null)
407        {
408            String allargs = new String();
409
410            for (String p : parameters)
411            {
412                if (!allargs.isEmpty())
413                {
414                    allargs += " ";
415                }
416                allargs += p;
417            }
418            params[2] = allargs;
419        }
420
421        for (int i = 0; i < numArgs; i++)
422        {
423            sig[i] = new String("java.lang.String");
424        }
425
426        return doInvoke(m_pluginServiceName, "execute", params, sig);
427
428    }
429
430    private String[] doInvoke(ObjectName name,
431            String command, Object[] args, String[] sig)
432            throws CommandFailed, InvalidTransactionState
433    {
434        //
435        // fail if there is an active transaction
436        //
437        checkInTransaction();
438
439        try
440        {
441            return (String[]) m_mbsc.invoke(name, command, args, sig);
442        }
443        catch (Exception ex)
444        {
445            throw new CommandFailed(ex);
446        }
447    }
448
449    private String readInputStream(InputStream stream) throws CommandFailed
450    {
451        BufferedReader reader = new BufferedReader(
452                new InputStreamReader(stream));
453
454        StringBuffer stringBuffer = new StringBuffer();
455
456        char[] buffer = new char[1024];
457        int bytesRead;
458        CommandFailed commandFailed = null;
459
460        try
461        {
462            while ((bytesRead = reader.read(buffer, 0, buffer.length)) != -1)
463            {
464                stringBuffer.append(buffer, 0, bytesRead);
465            }
466        }
467        catch (IOException ex)
468        {
469            commandFailed = new CommandFailed(ex);
470        }
471
472        try
473        {
474            stream.close();
475        }
476        catch (IOException ex)
477        {
478            if (commandFailed == null)
479            {
480                commandFailed = new CommandFailed(ex);
481            }
482        }
483
484        if (commandFailed != null)
485        {
486            throw commandFailed;
487        }
488
489        return stringBuffer.toString();
490    }
491
492    private void jmxConnect() throws CommandFailed
493    {
494        if (m_mbsc != null)
495        {
496            return;
497        }
498
499        String host;
500        String port = System.getProperty(
501                        "com.kabira.management.jmxregistryport");
502
503        if (port == null)
504        {
505            throw new CommandFailed("System property " +
506                    "com.kabira.management.jmxregistryport is not available " +
507                    "in this engine - is the adminservice component " +
508                    "installed?");
509        }
510
511        try
512        {
513            host = InetAddress.getLocalHost().getHostName();
514        }
515        catch (UnknownHostException ex)
516        {
517            throw new CommandFailed(ex);
518        }
519
520        String addr = "service:jmx:rmi:///jndi/rmi://" +
521                host + ":" + port + "/jmxrmi";
522
523        JMXServiceURL url = null;
524        try
525        {
526            url = new JMXServiceURL(addr);
527        }
528        catch (MalformedURLException ex)
529        {
530            throw new CommandFailed(ex);
531        }
532
533        HashMap<String, String[]> env = new HashMap<String, String[]>();
534
535        env.put("jmx.remote.credentials",
536            new String[] { m_userName, m_password });
537
538        try
539        {
540            m_jmxConnector = JMXConnectorFactory.connect(url, env);
541            m_mbsc = m_jmxConnector.getMBeanServerConnection();
542        }
543        catch (java.io.IOException ex)
544        {
545            throw new CommandFailed(ex);
546        }
547
548        try
549        {
550            m_pluginServiceName = new ObjectName(
551                    "com.kabira.platform.management", "type", "Management");
552            m_configurationName = new ObjectName(
553                    "com.kabira.platform.management", "type", "Configuration");
554        }
555        catch (MalformedObjectNameException ex)
556        {
557            throw new CommandFailed(ex);
558        }
559        catch (NullPointerException ex)
560        {
561            throw new CommandFailed(ex);
562        }
563    }
564
565    private void checkInTransaction() throws InvalidTransactionState
566    {
567        new Transaction()
568        {
569            public void run() throws Transaction.Rollback
570            {
571            }
572        }.execute();
573    }
574  
575    private String m_userName;
576    private String m_password;
577    private MBeanServerConnection m_mbsc = null;
578    private ObjectName m_pluginServiceName = null;
579    private ObjectName m_configurationName = null;
580        private JMXConnector m_jmxConnector = null;
581}
582