001/*
002 * $RCSfile: Target.java,v $
003 * $Revision: 1.1.2.17.2.2 $ $Date: 2012/08/28 23:48:09 $
004 *
005 * COPYRIGHT
006 *      Copyright 2009-2012 Cloud Software Group, Inc. ALL RIGHTS RESERVED.
007 *      Cloud Software Group, Inc. Confidential Information
008 */
009package com.kabira.platform.management;
010
011import com.kabira.platform.KeyFieldValueList;
012import com.kabira.platform.KeyManager;
013import com.kabira.platform.KeyQuery;
014import com.kabira.platform.LockMode;
015import com.kabira.platform.ManagedObject;
016import com.kabira.platform.ObjectNotUniqueError;
017import com.kabira.platform.OutParameter;
018import com.kabira.platform.switchadmin.AdminReport;
019import com.kabira.platform.switchadmin.CommandStatus;
020import com.kabira.platform.switchadmin.JavaCommand;
021import com.kabira.ktvm.transaction.DeadlockError;
022import java.io.PrintWriter;
023import java.io.StringWriter;
024import java.lang.annotation.Annotation;
025import java.lang.reflect.InvocationTargetException;
026import java.lang.reflect.Method;
027import java.util.HashMap;
028import java.util.logging.Logger;
029
030/**
031 * System management targets allow developers to extend the system
032 * management framework.  System management targets are accessible from
033 * via JMX, from the command line using the "administrator" command, and from
034 * the adminstrative web interface.
035 * System management targets are defined by extending Target.
036 * Note that Target is a managed object, and is always called by the system 
037 * management framework with an active transaction.
038 * <h2>
039 * Management target implementation
040 * </h2>
041 * Targets may return result sets, which consist of a list of column names and
042 * any number of rows.  All rows must contain the same number of m_columns.
043 * setColumnNames() must be called before addRow().  setColumnNames may be
044 * called only once, or a TargetError
045 * will be thrown.  After setColumnNames() is called, addRow() may be called
046 * any number of times.  The number of m_columns in each row must match the
047 * number of column names, or TargetError will be thrown.
048 * Targets are registered by calling Target.register().  A new instance of the
049 * Target is created for every command invocation.  Targets implementing
050 * asynchronous commands may store command-context-specific data in private
051 * members.
052 *
053 * Here's a simple example of an management target with a synchronous
054 * command which returns a result set:
055 * <p>
056 * <pre>
057 *  public class ExampleTarget extends Target {
058 *
059 *    public ExampleTarget() {
060 *        super("exampletarget");
061 *    }
062 *
063 *    &#64;Command
064 *    public void exampleCommand(
065 *            &#64;Parameter(name = "count", description = "number to display",
066 *                defaultValue = &#64;Default(value = "1"))
067 *           int count) {
068 *        String[] names = { "Who", "Where", "What" };
069 *        setColumnNames(names);
070 *
071 *        String[] row = { "Colonel Mustard", "Library", "Candlestick" };
072 *        addRow(row);
073 *        commandComplete();
074 *    }
075 *  }
076 * </pre>
077 *
078 * <h2>Execution JVM</h2>
079 *
080 * All commands associated with an administrative target
081 * execute in the JVM in which the target was registered
082 * using the register() method.
083 *
084 * <h2>Synchronous Commands</h2>
085 *
086 * Any command which calls commandComplete() or commandFailed() before returning
087 * is a synchronous command.
088 *
089 * <h2>Asynchronous commands</h2>
090 *
091 * Commands may return without completing.  In this case, the
092 * command will be held open until commandComplete() or commandFailed() is
093 * called.
094 * Note that asynchronous commands which are abandoned
095 * (e.g. the client never collects the results) will cause a leak in the
096 * underlying framework.
097 *
098 * <h2>Transactions</h2>
099 *
100 * Command methods are always called with a valid transaction.
101 * The transaction is committed after the method returns, unless an exception is
102 * thrown, in which case the transaction is rolled back.  Asynchronous commands
103 * must start a transaction to call back into the Target to complete the
104 * command.
105 *
106 * <h2>Security Configuration</h2>
107 *
108 * Management targets use configuration to define access
109 * control on the methods exposed as commands on the target.
110 *
111 * Here's an example of a security configuration file for ExampleTarget:
112 * <pre>
113 * configuration "ExampleTargetSecurity" version "1" type "security"
114 * {
115 *    configure security
116 *    {
117 *        configure AccessControl
118 *        {
119 *            Rule
120 *            {
121 *                name = "com.kabira.platform.management.ExampleTarget";
122 *                lockAllElements = true;
123 *                accessRules = { };
124 *            }
125 *            Rule
126 *            {
127 *                name = "com.kabira.platform.management.ExampleTarget.examplecommand";
128 *
129 *                accessRules =
130 *                {
131 *                    {
132 *                        roleName = "switchmonitor";
133 *                        permission = Execute;
134 *                    }
135 *                };
136 *            };
137 *        };
138 *    };
139 * };
140 * </pre>
141 */
142
143public class Target extends JavaCommand
144{
145    AdminReport adminReport = null;
146    private CommandStatus m_commandStatus = CommandStatus.CommandInProgress;
147    private long m_columns = 0;
148
149    /**
150     * Register a Target.
151     * <p>
152     * All commands associated with the target being registered will
153     * execute in the JVM in which register was called.
154     * @param targetClass the class of the target, e.g. MyTarget.class
155     * @throws TargetError The target failed registration audit.
156     *
157     */
158    public static void register(Class targetClass)
159            throws TargetError
160    {
161        String targetName = getTargetName(targetClass);
162        
163        Target.audit(targetClass, targetName);
164
165        //
166        // Always unregister the target name before registering it, since
167        // a Factory created in a previous instance of the engine will not
168        // be accessible.
169        //
170        unregister(targetName);
171
172        try
173        {
174            Factory factory = new Factory(targetClass, targetName);
175        }
176        catch (ObjectNotUniqueError ex)
177        {
178            // two callers got here at once. swallow this
179            // error. the other caller won.
180        }
181    }
182
183    /**
184     * Unregister a previously-registered target.
185     * @param targetName the name of the target
186     */
187    public static void unregister(String targetName)
188    {
189        KeyManager<Factory> keyManager = new KeyManager<Factory>();
190        KeyQuery<Factory> keyQuery = keyManager.createKeyQuery(
191                Factory.class, "ByName");
192        KeyFieldValueList keyFieldValueList = new KeyFieldValueList();
193
194        //
195        // Define the query to find the Factory
196        //
197        keyFieldValueList.add("name", targetName);
198        keyQuery.defineQuery(keyFieldValueList);
199
200        Factory factory = keyQuery.getSingleResult(LockMode.WRITELOCK);
201
202        if (factory != null)
203        {
204            ManagedObject.delete(factory);
205        }
206    }
207
208    /**
209     * Indicate that the command has completed successfully.
210     */
211    protected final void commandComplete()
212    {
213        String results = "";
214
215        m_commandStatus = CommandStatus.CommandCompleted;
216
217        if (adminReport != null)
218        {
219                OutParameter<String> out = new OutParameter<String>();
220                adminReport.getReport(out);
221                results = out.getValue();
222
223                ManagedObject.delete(adminReport);
224                adminReport = null;
225
226        }
227        update(m_commandStatus, results, "");
228    }
229
230    /**
231     * Indicate that the command has failed.
232     * The Transaction will be
233     * committed.  To roll back the transaction, throw an exception from the
234     * method implementing the command.
235     * @param reason The reason the command failed.
236     */
237    protected final void commandFailed(String reason)
238    {
239        m_commandStatus = CommandStatus.CommandFailed;
240        update(m_commandStatus, "", reason);
241    }
242
243    /**
244     * Set the names of the m_columns for the result set.
245     * @param names The names of the m_columns for the result set
246     * @throws TargetError The method was called more than once
247     */
248    protected final void setColumnNames(String[] names)
249            throws TargetError
250    {
251        if (adminReport != null)
252        {
253            throw new TargetError("setColumnNames called twice");
254        }
255        if (names.length == 0)
256        {
257            throw new TargetError("zero column names provided");
258        }
259
260        adminReport = new AdminReport();
261
262        adminReport.setHeader(names);
263
264        m_columns = names.length;
265    }
266
267    /**
268     * Add a row to the result set
269     * @param values The list of values for this row
270     * @throws TargetError The number of values does not match
271     * the number of column names, or setColumnNames() has not been called yet.
272     */
273    protected final void addRow(String[] values)
274            throws TargetError
275    {
276        if (adminReport == null)
277        {
278            throw new TargetError(
279                    "addRow called without calling setColumnNames");
280        }
281
282        if (values.length != m_columns)
283        {
284
285            throw new TargetError(
286                    "addRow: number of values (" + values.length +
287                    ") does not match number of columns (" + m_columns +
288                    ")");
289        }
290
291        adminReport.addRecord(values);
292    }
293
294    /**
295     * Execute a command.  This is an internal method which should never
296     * be called.
297     * @param parameters the command parameters
298     */
299    //
300    // unchecked conversion warnings are suppressed for Enum.valueOf(),
301    // which cannot be fixed because generics are not covariant.
302    //
303    @SuppressWarnings("unchecked")
304    @Override
305    protected final void execute(final String[] parameters)
306    {
307        if (commandName.equals("help"))
308        {
309            help();
310            return;
311        }
312
313        //
314        // Check to see if the current user has permission to execute
315        // this command
316        //
317        Boolean ok = false;
318        ok = checkAccess(this.getClass().getName() + "." + commandName);
319                if (!ok)
320                {
321                        String msg = "An access violation occurred while " +
322                                "attempting to execute command [" + commandName +
323                                "] on target [" + targetName + 
324                                "]. The calling principal " +
325                                "does not have execute privileges for this command.";
326                        throw new SecurityException(msg);
327                }
328
329        Method method = null;
330
331        for (Method m : this.getClass().getDeclaredMethods())
332        {
333            if ((m.getAnnotation(Command.class) != null) &&
334                    (m.getName().equals(commandName)))
335            {
336                method = m;
337                break;
338            }
339        }
340
341        if (method == null)
342        {
343            commandFailed("command '" + commandName +
344                    "' not found in target '" + targetName + "'");
345            return;
346        }
347
348        Annotation[][] all = method.getParameterAnnotations();
349        Class pt[] = method.getParameterTypes();
350        Object[] args = new Object[pt.length];
351
352        HashMap<String, String> paramap = new HashMap<String, String>();
353
354        for (int pi = 0; pi < parameters.length;)
355        {
356            paramap.put(parameters[pi++], parameters[pi++]);
357        }
358
359        //
360        // for each of the method's parameters, find the supplied parameter
361        // or use the default (if it isn't required)
362        //
363        for (int i = 0; i < pt.length; i++)
364        {
365            Parameter pp = null;
366            Default defaultValue = null;
367
368            //
369            // find the Parameter annotation for this parameter
370            //
371            for (Annotation a : all[i])
372            {
373                if (a.annotationType() == Parameter.class)
374                {
375                    pp = (Parameter) a;
376                }
377            }
378
379            String name = pp.name();
380            String value = paramap.get(name);
381
382            if (value != null)
383            {
384                paramap.remove(name);
385            }
386            else
387            {
388                if (pp.defaultValue().provided())
389                {
390                    value = pp.defaultValue().value();
391                }
392                else if (pp.required() == true)
393                {
394                    commandFailed("required parameter " +
395                            pp.name() + " missing");
396                    return;
397                }
398            }
399
400            Class type = pt[i];
401
402            try
403            {
404                if (value == null)
405                {
406                    args[i] = null;
407                }
408                else if (type == String.class)
409                {
410                    args[i] = value;
411                }
412                else if (type.isEnum())
413                {
414                    //
415                    // unchecked warnings if not suppressed
416                    //
417                    args[i] = Enum.valueOf(type, value);
418                }
419                else if (type == Boolean.class)
420                {
421                    args[i] = Boolean.valueOf(value);
422                }
423                else if (type == Character.class)
424                {
425                    args[i] = Character.valueOf(value.charAt(0));
426                }
427                else if (type == Integer.class)
428                {
429                    args[i] = Integer.parseInt(value);
430                }
431                else if (type == Byte.class)
432                {
433                    args[i] = Byte.parseByte(value);
434                }
435                else if (type == Short.class)
436                {
437                    args[i] = Short.parseShort(value);
438                }
439                else if (type == Long.class)
440                {
441                    args[i] = Long.parseLong(value);
442                }
443                else if (type == Float.class)
444                {
445                    args[i] = Float.parseFloat(value);
446                }
447                else
448                {
449                    assert (type == Double.class);
450                    args[i] = Double.parseDouble(value);
451                }
452            }
453            catch (NumberFormatException ex)
454            {
455                commandFailed("Invalid numeric format '" + value +
456                    "' for parameter '" + name + ": " +
457                    ex.getMessage());
458                return;
459            }
460            catch (IllegalArgumentException ex)
461            {
462                commandFailed("Illegal argument '" + value +
463                    "' for parameter '" + name + ": " +
464                    ex.getMessage());
465                return;
466            }
467        }
468
469        if (!paramap.isEmpty())
470        {
471            commandFailed("invalid parameter(s): " + paramap.toString());
472            return;
473        }
474
475        try
476        {
477            method.setAccessible(true);
478            method.invoke(this, args);
479        }
480        catch (InvocationTargetException ex)
481        {
482            String reason = "InvocationTargetException: ";
483
484            Throwable cause = ex.getCause();
485
486            if (cause != null)
487            {
488                if (cause instanceof DeadlockError)
489                {
490                    throw (java.lang.Error)cause;
491                }
492
493                reason = cause.getMessage();
494                
495                StringWriter stringWriter = new StringWriter();
496                PrintWriter printWriter = new PrintWriter(stringWriter);
497                cause.printStackTrace(printWriter);
498
499                reason += stringWriter.toString();
500            }
501            else
502            {
503                reason += ex.getMessage();
504            }
505        
506            setAbortFlag();
507            commandFailed(reason);
508        }
509        catch (Exception ex)
510        {
511            ex.printStackTrace();
512            setAbortFlag();
513            commandFailed(ex.getMessage());
514        }
515    }
516
517    /**
518     * Retrieve the name of the currently active Principal. This operation
519     * may be called during command execution to get the name of the Principal
520     * that invoked the command.
521     */
522    protected final String getActivePrincipalName()
523    {
524                String result = getCurrentPrincipalName();
525
526                if ((result == null) || (result.length() == 0))
527                {
528                        return null;
529                }
530
531                return result;
532        }
533                
534    private static String getTargetName(Class<?> targetClass)
535    {
536        ManagementTarget adminTarget = 
537            targetClass.getAnnotation(ManagementTarget.class);
538
539        if (adminTarget == null)
540        {
541            throw new TargetError("Missing ManagementTarget annotation on type "
542                + targetClass.getName() + ". ");
543        }
544        return adminTarget.name();
545    }
546
547    private static void audit(Class targetClass, String targetName)
548            throws TargetError
549    {
550        String error = new String();
551   
552        for (Method m : targetClass.getMethods())
553        {
554            if (m.getAnnotation(Command.class) == null)
555            {
556                continue;
557            }
558
559            Annotation[][] all = m.getParameterAnnotations();
560            Class pt[] = m.getParameterTypes();
561
562            for (int i = 0; i < pt.length; i++)
563            {
564                Parameter pp = null;
565                Default defaultValue = null;
566
567                Class type = pt[i];
568
569                if ((type != String.class) &&
570                        (!type.isEnum()) &&
571                        (type != Boolean.class) &&
572                        (type != Character.class) &&
573                        (type != Integer.class) &&
574                        (type != Byte.class) &&
575                        (type != Short.class) &&
576                        (type != Long.class) &&
577                        (type != Float.class) &&
578                        (type != Double.class))
579                {
580                    error += "Unsupported parameter type " +
581                            type.getName() + " in method " + m.getName() + ". ";
582                }
583
584                //
585                // find the Parameter annotation for this parameter
586                //
587                for (Annotation a : all[i])
588                {
589                    if (a.annotationType() == Parameter.class)
590                    {
591                        pp = (Parameter) a;
592                    }
593                }
594                if (pp == null)
595                {
596                    error += "Missing @Parameter annotation on parameter " +
597                            "of type " + type.getName() + " in method " +
598                            m.getName() + ". ";
599                }
600            }
601        }
602
603        if (!error.isEmpty())
604        {
605            throw new TargetError("Admin target " + targetName +
606                    " failed audit: " + error);
607        }
608    }
609
610    private void help()
611    {
612        String help = new String();
613        
614        help += "  valid commands and parameters for target \"";
615        help += targetName;
616        help += "\":\n\n";
617
618        for (Method m : this.getClass().getDeclaredMethods())
619        {
620            if (m.getName().charAt(0) == '$')
621            {
622                continue;
623            }
624            
625            Command command = m.getAnnotation(Command.class);
626
627            if (command == null)
628            {
629                continue;
630            }
631
632            help += "  " + m.getName() + " " + targetName + "\n";
633
634            Annotation[][] all = m.getParameterAnnotations();
635            Class pt[] = m.getParameterTypes();
636
637            for (int i = 0; i < pt.length; i++)
638            {
639                Parameter pp = null;
640                Class type = pt[i];
641
642                //
643                // find the Parameter annotation for this parameter
644                //
645                for (Annotation a : all[i])
646                {
647                    if (a.annotationType() == Parameter.class)
648                    {
649                        pp = (Parameter) a;
650                    }
651                }
652
653                String ps = pp.name() + "=<" +
654                        type.getName().replace('.', ' ').replaceAll(".* ", "");
655
656                if (pp.defaultValue().provided())
657                {
658                    ps += ", default = " + pp.defaultValue().value();
659                }
660                ps += ">";
661
662                if (!pp.required())
663                {
664                    ps = "[ " + ps + " ]";
665                }
666
667                help += "\t" + ps + "\n";
668                if (!pp.description().equals(""))
669                {
670                    help += "\t\t" + pp.description() + "\n";
671                }
672                help += "\n";
673            }
674
675            if (!command.description().equals(""))
676            {
677                help += "\t" + command.description() + "\n";
678            }
679            help += "\n";
680        }
681
682        ManagementTarget adminTarget =
683                    this.getClass().getAnnotation(ManagementTarget.class);
684
685        if (adminTarget != null)
686        {
687            help += "\nDescription:\n\n" + adminTarget.description() + "\n";
688        }
689
690        update(CommandStatus.CommandCompleted, help, "");
691    }
692}