All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.enterprise.v3.admin.CommandRunnerImpl Maven / Gradle / Ivy

There is a newer version: 4.1.2.181
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2008-2013 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
// Portions Copyright [2017] [Payara Foundation and/or its affiliates]

package com.sun.enterprise.v3.admin;

import com.sun.enterprise.admin.event.AdminCommandEventBrokerImpl;
import com.sun.enterprise.admin.util.*;
import com.sun.enterprise.config.serverbeans.Cluster;
import com.sun.enterprise.config.serverbeans.Domain;
import com.sun.enterprise.universal.collections.ManifestUtils;
import com.sun.enterprise.universal.glassfish.AdminCommandResponse;
import com.sun.enterprise.util.AnnotationUtil;
import com.sun.enterprise.util.LocalStringManagerImpl;
import com.sun.enterprise.util.StringUtils;
import com.sun.enterprise.v3.common.XMLContentActionReporter;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Scope;
import javax.inject.Singleton;
import javax.security.auth.Subject;
import javax.validation.*;
import org.glassfish.admin.payload.PayloadFilesManager;
import org.glassfish.api.ActionReport;
import org.glassfish.api.Param;
import org.glassfish.api.admin.*;
import org.glassfish.api.admin.AdminCommandEventBroker.AdminCommandListener;
import org.glassfish.api.admin.Payload;
import org.glassfish.api.admin.ProcessEnvironment;
import org.glassfish.api.admin.SupplementalCommandExecutor.SupplementalCommand;
import org.glassfish.api.logging.LogHelper;
import org.glassfish.common.util.admin.CommandModelImpl;
import org.glassfish.common.util.admin.ManPageFinder;
import org.glassfish.common.util.admin.MapInjectionResolver;
import org.glassfish.common.util.admin.UnacceptableValueException;
import org.glassfish.config.support.CommandTarget;
import org.glassfish.config.support.GenericCrudCommand;
import org.glassfish.config.support.TargetType;
import org.glassfish.hk2.api.MultiException;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.internal.api.*;
import org.glassfish.internal.deployment.DeploymentTargetResolver;
import org.glassfish.kernel.KernelLoggerInfo;
import org.jvnet.hk2.annotations.Service;
import org.jvnet.hk2.component.*;
import org.jvnet.hk2.config.InjectionManager;
import org.jvnet.hk2.config.InjectionResolver;
import org.jvnet.hk2.config.MessageInterpolatorImpl;
import org.jvnet.hk2.config.UnsatisfiedDependencyException;

/**
 * Encapsulates the logic needed to execute a server-side command (for example,
 * a descendant of AdminCommand) including injection of argument values into the
 * command.
 *
 * @author dochez
 * @author tjquinn
 * @author Bill Shannon
 */
@Service
public class CommandRunnerImpl implements CommandRunner {

    private static final Logger logger = KernelLoggerInfo.getLogger();
    // This is used only for backword compatibility with old behavior
    private static final String OLD_PASSWORD_PARAM_PREFIX = "AS_ADMIN_";

    private static final InjectionManager injectionMgr = new InjectionManager();
    @Inject
    private ServiceLocator habitat;
    @Inject
    private ServerContext sc;
    @Inject
    private Domain domain;
    @Inject
    private ServerEnvironment serverEnv;
    @Inject
    private ProcessEnvironment processEnv;
    @Inject
    private InstanceStateService state;
    @Inject
    private AdminCommandLock adminLock;
    @Inject @Named("SupplementalCommandExecutorImpl")
    SupplementalCommandExecutor supplementalExecutor;


    //private final Map, String> commandModelEtagMap = new WeakHashMap, String>();
    private final Map commandModelEtagMap = new IdentityHashMap();

    @Inject
    private CommandSecurityChecker commandSecurityChecker;
    
    private static final LocalStringManagerImpl adminStrings =
            new LocalStringManagerImpl(CommandRunnerImpl.class);
    private static volatile Validator beanValidator = null;

    /**
     * Returns an initialized ActionReport instance for the passed type or
     * null if it cannot be found.
     *
     * @param name action report type name
     * @return uninitialized action report or null
     */
    @Override
    public ActionReport getActionReport(String name) {
        return habitat.getService(ActionReport.class, name);
    }

    /**
     * Returns the command model for a command name.
     *
     * @param commandName command name
     * @param logger logger to log any error messages
     * @return model for this command (list of parameters,etc...),
     *          or null if command is not found
     */
    @Override
    public CommandModel getModel(String commandName, Logger logger) {
        return getModel(null, commandName, logger);
    }
    
    /**
     * Returns the command model for a command name.
     *
     * @param commandName command name
     * @param logger logger to log any error messages
     * @return model for this command (list of parameters,etc...),
     *          or null if command is not found
     */
    @Override
    public CommandModel getModel(String scope, String commandName, Logger logger) {
        AdminCommand command;
        try {
            String commandServiceName = (scope != null) ? scope + commandName : commandName;
            command = habitat.getService(AdminCommand.class, commandServiceName);
        } catch (MultiException e) {
            LogHelper.log(logger, Level.SEVERE, KernelLoggerInfo.cantInstantiateCommand, 
                    e, commandName);
            return null;
        }
        return command == null ? null : getModel(command);
    }
    
    @Override
    public boolean validateCommandModelETag(AdminCommand command, String eTag) {
        if (command == null) {
            return true; //Everithing is ok for unexisting command
        }
        if (eTag == null || eTag.isEmpty()) {
            return false;
        }
        CommandModel model = getModel(command);
        return validateCommandModelETag(model, eTag);
    }
    
    @Override
    public boolean validateCommandModelETag(CommandModel model, String eTag) {
        if (model == null) {
            return true; //Unexisting model => it is ok (but weard in fact)
        }
        if (eTag == null || eTag.isEmpty()) {
            return false;
        }
        String actualETag = CachedCommandModel.computeETag(model);
        return eTag.equals(actualETag);
    }

    /**
     * Obtain and return the command implementation defined by
     * the passed commandName for the null scope.
     *
     * @param commandName command name as typed by users
     * @param report report used to communicate command status back to the user
     * @param logger logger to log
     * @return command registered under commandName or null if not found
     */
    @Override
    public AdminCommand getCommand(String commandName, 
            ActionReport report, Logger logger) {
        return getCommand(null, commandName, report, logger);
    }
    
    private static Class getScope(Class onMe) {
        for (Annotation anno : onMe.getAnnotations()) {
            if (anno.annotationType().isAnnotationPresent(Scope.class)) {
                return anno.annotationType();
            }
        
        }
        
        return null;
    }

    /**
     * Obtain and return the command implementation defined by
     * the passed commandName.
     *
     * @param commandName command name as typed by users
     * @param report report used to communicate command status back to the user
     * @param logger logger to log
     * @return command registered under commandName or null if not found
     */
    @Override
    public AdminCommand getCommand(String scope, String commandName, 
            ActionReport report, Logger logger) {

        AdminCommand command = null;
        String commandServiceName = (scope != null) ? scope + commandName : commandName;
        
        try {
            command = habitat.getService(AdminCommand.class, commandServiceName);
        } catch (MultiException e) {
            report.setFailureCause(e);
        }
        if (command == null) {
            String msg;

            if (!ok(commandName)) {
                msg = adminStrings.getLocalString("adapter.command.nocommand",
                        "No command was specified.");
            } else {
                // this means either a non-existent command or
                // an ill-formed command
                if (habitat.getServiceHandle(AdminCommand.class, commandServiceName)
                        == null) // somehow it's in habitat
                {
                    msg = adminStrings.getLocalString("adapter.command.notfound", "Command {0} not found", commandName);
                } else {
                    msg = adminStrings.getLocalString("adapter.command.notcreated",
                            "Implementation for the command {0} exists in "
                            + "the system, but it has some errors, "
                            + "check server.log for details", commandName);
                }
            }
            report.setMessage(msg);
            report.setActionExitCode(ActionReport.ExitCode.FAILURE);
            KernelLoggerInfo.getLogger().fine(msg);
            return null;
        }

        Class myScope = getScope(command.getClass());
        if (myScope == null) {
            String msg = adminStrings.getLocalString("adapter.command.noscope",
                    "Implementation for the command {0} exists in the "
                    + "system,\nbut it has no @Scoped annotation", commandName);
            report.setMessage(msg);
            report.setActionExitCode(ActionReport.ExitCode.FAILURE);
            KernelLoggerInfo.getLogger().fine(msg);
            command = null;
        } else if (Singleton.class.equals(myScope)) {
            // check that there are no parameters for this command
            CommandModel model = getModel(command);
            if (model.getParameters().size() > 0) {
                String msg =
                        adminStrings.getLocalString("adapter.command.hasparams",
                        "Implementation for the command {0} exists in the "
                        + "system,\nbut it's a singleton that also has "
                        + "parameters", commandName);
                report.setMessage(msg);
                report.setActionExitCode(ActionReport.ExitCode.FAILURE);
                KernelLoggerInfo.getLogger().fine(msg);
                command = null;
            }
        }

        return command;
    }

    @Override
    public CommandInvocation getCommandInvocation(String name, ActionReport report, Subject subject) {
        return getCommandInvocation(name,report,subject,false);
    }

    @Override
    public CommandInvocation getCommandInvocation(String scope, String name, ActionReport report, Subject subject) {
        return getCommandInvocation(scope,name,report,subject,false);
    }

    /**
     * Obtain a new command invocation object for the null scope.
     * Command invocations can be configured and used
     * to trigger a command execution.
     *
     * @param name name of the requested command to invoke
     * @param report where to place the status of the command execution
     * @param subject the Subject under which to execute the command
     * @return a new command invocation for that command name
     */
    @Override
    public CommandInvocation getCommandInvocation(String name,
            ActionReport report, Subject subject,boolean isNotify) {
        return getCommandInvocation(null, name, report, subject,false);
    }

    /**
     * Obtain a new command invocation object.
     * Command invocations can be configured and used
     * to trigger a command execution.
     *
     * @param scope the scope (or name space) for the command
     * @param name name of the requested command to invoke
     * @param report where to place the status of the command execution
     * @param subject the Subject under which to execute the command
     * @param isNotify  Should notification be enabled
     * @return a new command invocation for that command name
     */
    @Override
    public CommandInvocation getCommandInvocation(String scope, String name,
            ActionReport report, Subject subject, boolean isNotify) {
        return new ExecutionContext(scope, name, report, subject, isNotify);
    }

    public static boolean injectParameters(final CommandModel model, final Object injectionTarget,
            final InjectionResolver injector,
            final ActionReport report) {

        if (injectionTarget instanceof GenericCrudCommand) {
            GenericCrudCommand c = GenericCrudCommand.class.cast(injectionTarget);
            c.setInjectionResolver(injector);
        }

        // inject
        try {
            injectionMgr.inject(injectionTarget, injector);
        } catch (UnsatisfiedDependencyException e) {
            Param param = e.getAnnotation(Param.class);
            CommandModel.ParamModel paramModel = null;
            for (CommandModel.ParamModel pModel : model.getParameters()) {
                if (pModel.getParam().equals(param)) {
                    paramModel = pModel;
                    break;
                }
            }
            String errorMsg;
            final String usage = getUsageText(model);
            if (paramModel != null) {
                String paramName = paramModel.getName();
                String paramDesc = paramModel.getLocalizedDescription();

                if (param.primary()) {
                    errorMsg = adminStrings.getLocalString("commandrunner.operand.required",
                            "Operand required.");
                } else if (param.password()) {
                    errorMsg = adminStrings.getLocalString("adapter.param.missing.passwordfile",
                            "{0} command requires the passwordfile "
                            + "parameter containing {1} entry.",
                            model.getCommandName(), paramName);
                } else if (paramDesc != null) {
                    errorMsg = adminStrings.getLocalString("admin.param.missing",
                            "{0} command requires the {1} parameter ({2})",
                            model.getCommandName(), paramName, paramDesc);

                } else {
                    errorMsg = adminStrings.getLocalString("admin.param.missing.nodesc",
                            "{0} command requires the {1} parameter",
                            model.getCommandName(), paramName);
                }
            } else {
                errorMsg = adminStrings.getLocalString("admin.param.missing.nofound",
                        "Cannot find {1} in {0} command model, file a bug",
                        model.getCommandName(), e.getUnsatisfiedName());
            }
            report.setActionExitCode(ActionReport.ExitCode.FAILURE);
            report.setMessage(errorMsg);
            report.setFailureCause(e);
            ActionReport.MessagePart childPart =
                    report.getTopMessagePart().addChild();
            childPart.setMessage(usage);
            return false;
        }
        catch (MultiException e) {
            // If the cause is UnacceptableValueException -- we want the message
            // from it.  It is wrapped with a less useful Exception.
            
            Exception exception = null;
            for (Throwable th : e.getErrors()) {
                Throwable cause = th;
                while (cause != null) {
                    if ((cause instanceof UnacceptableValueException) ||
                            (cause instanceof IllegalArgumentException)) {
                        exception = (Exception) th;
                        break;
                    }
                    
                    cause = cause.getCause();
                }
            }
            
            if (exception == null) {
                // Not an UnacceptableValueException or IllegalArgumentException
                exception = e;
            }

            logger.log(Level.SEVERE, KernelLoggerInfo.invocationException, exception);
            report.setActionExitCode(ActionReport.ExitCode.FAILURE);
            report.setMessage(exception.getMessage());
            report.setFailureCause(exception);
            ActionReport.MessagePart childPart =
                    report.getTopMessagePart().addChild();
            childPart.setMessage(getUsageText(model));
            return false;
        }
        
        checkAgainstBeanConstraints(injectionTarget, model.getCommandName());
        return true;
    }

    private static synchronized void initBeanValidator() {
        if (beanValidator != null) {
            return;
        }
        ClassLoader cl = System.getSecurityManager() == null ?
                Thread.currentThread().getContextClassLoader():
                AccessController.doPrivileged(new PrivilegedAction() {
                    @Override
                    public ClassLoader run() {
                        return Thread.currentThread().getContextClassLoader();
                    }
                });
        try {
            Thread.currentThread().setContextClassLoader(Validation.class.getClassLoader());
            Configuration configuration = Validation.byDefaultProvider().providerResolver(
                    new HibernateValidationProviderResolver()
            ).configure();
            ValidatorFactory validatorFactory = configuration.buildValidatorFactory();
            ValidatorContext validatorContext = validatorFactory.usingContext();
            validatorContext.messageInterpolator(new MessageInterpolatorImpl());                
            beanValidator = validatorContext.getValidator();
        } finally {
            Thread.currentThread().setContextClassLoader(cl);
        }       
    }
    
    private static void checkAgainstBeanConstraints(Object component, String cname) {
        initBeanValidator();
                
        Set> constraintViolations = beanValidator.validate(component);
        if (constraintViolations == null || constraintViolations.isEmpty()) {
            return;
        }
        StringBuilder msg = new StringBuilder(adminStrings.getLocalString("commandrunner.unacceptableBV",
                "Parameters for command {0} violate the following constraints: ", 
                cname));
        boolean addc = false;
        String violationMsg = adminStrings.getLocalString("commandrunner.unacceptableBV.reason",
                "on parameter [ {1} ] violation reason [ {0} ]");
        for (ConstraintViolation cv : constraintViolations) {
            if (addc) {
                msg.append(", ");
            }
            msg.append(MessageFormat.format(violationMsg, cv.getMessage(), cv.getPropertyPath()));
            addc = true;
        }
        throw new UnacceptableValueException(msg.toString());
    }
    
    /**
     * Executes the provided command object.
     *
     * @param model model of the command (used for logging and reporting)
     * @param command the command service to execute
     * @param context the AdminCommandcontext that has the payload and report
     */
    private ActionReport doCommand(
            final CommandModel model,
            final AdminCommand command,
            final AdminCommandContext context,
            final CommandRunnerProgressHelper progressHelper) {

        ActionReport report = context.getActionReport();
        report.setActionDescription(model.getCommandName() + " AdminCommand");

        // We need to set context CL to common CL before executing
        // the command. See issue #5596
        final Thread thread = Thread.currentThread();
        final ClassLoader origCL = thread.getContextClassLoader();
        final ClassLoader ccl = sc.getCommonClassLoader();
            
        AdminCommand wrappedCommand = new WrappedAdminCommand(command) {
            @Override
            public void execute(final AdminCommandContext context) {
                try {
                    if (origCL != ccl) {
                        thread.setContextClassLoader(ccl);
                    }
                    /*
                     * Execute the command in the security context of the 
                     * previously-authenticated subject.
                     */
                    Subject.doAs(context.getSubject(), 
                            new PrivilegedAction () {

                        @Override
                        public Void run() {
                            command.execute(context);
                            return null;
                        }

                    });
                } finally {
                    if (origCL != ccl) {
                        thread.setContextClassLoader(origCL);
                    }
                }
            }
        };

        // look for other wrappers using CommandAspect annotation
        final AdminCommand otherWrappedCommand = CommandSupport.createWrappers(habitat, model, wrappedCommand, report);
            
        try {
            Subject.doAs(context.getSubject(), 
                            new PrivilegedAction () {

                        @Override
                        public Void run() {
                            try {
                                if (origCL != ccl) {
                                    thread.setContextClassLoader(ccl);
                                }
                                otherWrappedCommand.execute(progressHelper.wrapContext4MainCommand(context));
                                return null;
                            } finally {
                                if (origCL != ccl) {
                                    thread.setContextClassLoader(origCL);
                                }
                            }
                        }

                    });
            
        } catch (Throwable e) {
            logger.log(Level.SEVERE, KernelLoggerInfo.invocationException, e);
            report.setMessage(e.toString());
            report.setActionExitCode(ActionReport.ExitCode.FAILURE);
            report.setFailureCause(e);
            }
        
        return context.getActionReport();
    }

    /**
     * Get the usage-text of the command.
     * Check if .usagetext is defined in LocalString.properties.
     * If defined, then use the usagetext from LocalString.properties else
     * generate the usagetext from Param annotations in the command class.
     *
     * @param model command model
     * @return usagetext
     */
    static String getUsageText(CommandModel model) {
        StringBuilder usageText = new StringBuilder();

        String usage;
        if (ok(usage = model.getUsageText())) {
            usageText.append(
                    adminStrings.getLocalString("adapter.usage", "Usage: "));
            usageText.append(usage);
            return usageText.toString();
        } else {
            return generateUsageText(model);
        }
    }

    /**
     * Generate the usage-text from the annotated Param in the command class.
     *
     * @param model command model
     * @return generated usagetext
     */
    private static String generateUsageText(CommandModel model) {
        StringBuffer usageText = new StringBuffer();
        usageText.append(
                adminStrings.getLocalString("adapter.usage", "Usage: "));
        usageText.append(model.getCommandName());
        usageText.append(" ");
        StringBuffer operand = new StringBuffer();
        for (CommandModel.ParamModel pModel : model.getParameters()) {
            final Param param = pModel.getParam();
            final String paramName =
                    pModel.getName().toLowerCase(Locale.ENGLISH);
            // skip "hidden" options
            if (paramName.startsWith("_")) {
                continue;
            }
            // do not want to display password as an option
            if (param.password()) {
                continue;
            }
            // do not want to display obsolete options
            if (param.obsolete()) {
                continue;
            }
            final boolean optional = param.optional();
            final Class ftype = pModel.getType();
            Object fvalue = null;
            String fvalueString = null;
            try {
                fvalue = param.defaultValue();
                if (fvalue != null) {
                    fvalueString = fvalue.toString();
                }
            } catch (Exception e) {
                // just leave it as null...
            }
            // this is a param.
            if (param.primary()) {
                if (optional) {
                    operand.append("[").append(paramName).append("] ");
                } else {
                    operand.append(paramName).append(" ");
                }
                continue;
            }

            if (optional) {
                usageText.append("[");
            }

            usageText.append("--").append(paramName);
            if (ok(param.defaultValue())) {
                usageText.append("=").append(param.defaultValue());
            } else if (ftype.isAssignableFrom(String.class)) {
                // check if there is a default value assigned
                if (ok(fvalueString)) {
                    usageText.append("=").append(fvalueString);
                } else {
                    usageText.append("=").append(paramName);
                }
            } else if (ftype.isAssignableFrom(Boolean.class)) {
                // note: There is no defaultValue for this param.  It might
                // hava  value -- but we don't care -- it isn't an official
                // default value.
                usageText.append("=").append("true|false");
            } else {
                usageText.append("=").append(paramName);
            }

            if (optional) {
                usageText.append("] ");
            } else {
                usageText.append(" ");
            }
        }
        usageText.append(operand);
        return usageText.toString();
    }
    
    @Override
    public BufferedReader getHelp(CommandModel model) throws CommandNotFoundException {
        BufferedReader manPage = getManPage(model.getCommandName(), model);
        if (manPage != null) {
            return manPage;
        } else {
            StringBuilder hlp = new StringBuilder(256);
            StringBuilder part = new StringBuilder(64);
            hlp.append("NAME").append(ManifestUtils.EOL);
            part.append(model.getCommandName());
            String description = model.getLocalizedDescription();
            if (ok(description)) {
                part.append(" - ").append(model.getLocalizedDescription());
            }
            hlp.append(formatGeneratedManPagePart(part.toString(), 5, 65)).append(ManifestUtils.EOL);
            //Usage
            hlp.append(ManifestUtils.EOL).append("SYNOPSIS").append(ManifestUtils.EOL);
            hlp.append(formatGeneratedManPagePart(getUsageText(model), 5, 65));
            //Options
            hlp.append(ManifestUtils.EOL).append(ManifestUtils.EOL);
            hlp.append("OPTIONS").append(ManifestUtils.EOL);
            CommandModel.ParamModel operand = null;
            for (CommandModel.ParamModel paramModel : model.getParameters()) {
                Param param = paramModel.getParam();
                if (param == null || paramModel.getName().startsWith("_") || 
                        param.password() || param.obsolete()) {
                    continue;
                }
                if (param.primary()) {
                    operand = paramModel;
                    continue;
                }
                hlp.append("     --").append(paramModel.getName().toLowerCase(Locale.ENGLISH));
                hlp.append(ManifestUtils.EOL);
                if (ok(param.shortName())) {
                    hlp.append("      -").append(param.shortName().toLowerCase(Locale.ENGLISH));
                    hlp.append(ManifestUtils.EOL);
                }
                String descr = paramModel.getLocalizedDescription();
                if (ok(descr)) {
                    hlp.append(formatGeneratedManPagePart(descr, 9, 65));
                }
                hlp.append(ManifestUtils.EOL);
            }
            //Operand
            if (operand != null) {
                hlp.append("OPERANDS").append(ManifestUtils.EOL);
                hlp.append("     ").append(operand.getName().toLowerCase(Locale.ENGLISH));
                hlp.append(ManifestUtils.EOL);
                String descr = operand.getLocalizedDescription();
                if (ok(descr)) {
                    hlp.append(formatGeneratedManPagePart(descr, 9, 65));
                }
            }
            return new BufferedReader(new StringReader(hlp.toString()));
        }
    }
    
    private String formatGeneratedManPagePart(String part, int prefix, int lineLength) {
        if (part == null) {
            return null;
        }
        if (prefix < 0) {
            prefix = 0;
        }
        //Prepare prefix
        StringBuilder sb = new StringBuilder(prefix);
        for (int i = 0; i < prefix; i++) {
            sb.append(' ');
        }
        String prfx = sb.toString();
        StringBuilder result = new StringBuilder(part.length() + prefix + 16);
        boolean newLine = true;
        boolean lastWasCR = false;
        int counter = 0;
        for (int i = 0; i < part.length(); i++) {
            boolean addPrefix = newLine;
            char ch = part.charAt(i);
            switch (ch) {
                case '\n':
                    if (!lastWasCR) {
                        newLine = true;
                    } else {
                        lastWasCR = false;
                    }
                    counter = 0;
                    break;
                case '\r':
                    newLine = true;
                    lastWasCR = true;
                    counter = 0;
                    break;
                default:
                    newLine = false;
                    lastWasCR = false;
                    counter++;
            }
            if (addPrefix && !newLine) {
                result.append(prfx);
                counter += prefix;
            }
            result.append(ch);
            if (lineLength > 0 && counter >= lineLength && !newLine) {
                newLine = true;
                result.append(ManifestUtils.EOL);
                counter = 0;
            }
        }
        return result.toString();
    }

    public void getHelp(AdminCommand command, ActionReport report) {

        CommandModel model = getModel(command);
        report.setActionDescription(model.getCommandName() + " help");

        // XXX - this is a hack for now.  if the request mapped to an
        // XMLContentActionReporter, that means we want the command metadata.
        if (report instanceof XMLContentActionReporter) {
            getMetadata(command, model, report);
        } else {
            report.setMessage(model.getCommandName() + " - "
                    + model.getLocalizedDescription());
            report.getTopMessagePart().addProperty("SYNOPSIS",
                    encodeManPage(new BufferedReader(new StringReader(
                    getUsageText(model)))));
            for (CommandModel.ParamModel param : model.getParameters()) {
                addParamUsage(report, param);
            }
            report.setActionExitCode(ActionReport.ExitCode.SUCCESS);
        }
    }

    /**
     * Return the metadata for the command.  We translate the parameter
     * and operand information to parts and properties of the ActionReport,
     * which will be translated to XML elements and attributes by the
     * XMLContentActionReporter.
     *
     * @param command the command
     * @param model the CommandModel describing the command
     * @param report	the (assumed to be) XMLContentActionReporter
     */
    private void getMetadata(AdminCommand command, CommandModel model,
            ActionReport report) {
        ActionReport.MessagePart top = report.getTopMessagePart();
        ActionReport.MessagePart cmd = top.addChild();
        // 
        cmd.setChildrenType("command");
        cmd.addProperty("name", model.getCommandName());
        if (model.unknownOptionsAreOperands()) {
            cmd.addProperty("unknown-options-are-operands", "true");
        }
        String usage = model.getUsageText();
        if (ok(usage)) {
            cmd.addProperty("usage", usage);
        }
        CommandModel.ParamModel primary = null;
        // for each parameter add
        //