protoj.lang.Command Maven / Gradle / Ivy
Show all versions of protoj-jdk6 Show documentation
/**
* Copyright 2009 Ashley Williams
*
* Licensed under the Apache License, Version 2.0 (the "License"); you
* may not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package protoj.lang;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import protoj.lang.internal.InformationException;
import protoj.lang.internal.ProjectReporter;
import protoj.util.ArgRunnable;
import protoj.util.JavaTask;
/**
* Represents a unit of work to be carried out by the project. A command
* consists of two parts:
*
* - bootstrap: the first part is actually the information used to create a
* new vm and is called the bootstrap. This is so that various vm parameters can
* be configured such as the maximum amount of memory, because it isn't possible
* to reconfigure an already running vm.
* - body: the second part is the actual code that should be executed, called
* the body.
*
* To determine whether the command will bootstrap or whether its body will be
* executed, the {@link #execute(Instruction)} method checks for the existence
* of the {@link #BODY} command line options. If it isn't present the command
* will bootstrap to a new vm, but with the BODY option added. If the BODY
* option does exist then the body will be executed.
*
* @author Ashley Williams
*
*/
public final class Command {
private static final String BODY = "body";
/**
* See {@link #getAliases()}.
*/
private ArrayList aliases = new ArrayList();
/**
* The instance used to create a new vm.
*/
private Runnable bootstrap;
/**
* The functionality associated with this command.
*/
private Runnable body;
/**
* The parent aggregate for this command.
*/
private final CommandStore store;
/**
* See {@link #getParser()}.
*/
private OptionParser parser;
/**
* See {@link #getOptions()}.
*/
private OptionSet options;
/**
* See {@link #getMemory()}.
*/
private String memory;
/**
* The command line snippet that is being used to invoke this command.
*/
private Instruction instruction;
/**
* See {@link #initBootstrapCurrentVm()}.
*/
private boolean bootstrapCurrentVm;
/**
* See {@link #getStartVmConfig()}.
*/
private ArgRunnable startVmConfig;
/**
* See {@link #initHelpResource(String)}.
*/
private String helpResource;
/**
* See {@link #initHelpString(String)}
*/
private String helpString;
/**
* Creates a command with an associated body that will execute in another
* instance of the current vm.
*
* @param store
* the owning instance
* @param name
* the name of this command as called at the command line
* @param memory
* how much memory this command should be lauched in
* @param body
* the plugged in command funtionality
*/
public Command(CommandStore store, String name, String memory, Runnable body) {
this.store = store;
this.memory = memory;
this.bootstrap = createBootstrap();
this.parser = createOptionParser();
this.body = body;
this.bootstrapCurrentVm = false;
initAliases(name);
}
/**
* Useful for commands that only have very short descriptions that may not
* require resource files.
*
* @param helpString
*/
public void initHelpString(String helpString) {
this.helpString = helpString;
}
/**
* Provide the path to the resource that contains the help for this command.
* That resource can contain velocity markup.
*
* @param helpResource
*/
public void initHelpResource(String helpResource) {
this.helpResource = helpResource;
}
/**
* See {@link #getParser()}.
*
* @return
*/
private OptionParser createOptionParser() {
OptionParser parser = new OptionParser();
parser.accepts(BODY);
return parser;
}
/**
* Constructor support that creates the vm wrapper used to delegate the
* command body functionality.
*
* @return
*/
private Runnable createBootstrap() {
Runnable bootstrap = new Runnable() {
public void run() {
startVm();
}
};
return bootstrap;
}
/**
* This method must not be inlined into the anonymous inner class as the
* advice in ShellConfig.aj requires this join-point.
*
* @param memory
*/
private void startVm() {
StandardProject parent = store.getParent();
InstructionChain chain = parent.getInstructionChain();
String optsWithBody = (instruction.getOpts() + " -" + BODY).trim();
DispatchFeature dispatchFeature = parent.getDispatchFeature();
String mainClass;
if (bootstrapCurrentVm) {
mainClass = dispatchFeature.getCurrentMainClass();
} else {
mainClass = dispatchFeature.getMainClass();
}
String[] args = chain.createMainArgs(getName(), optsWithBody);
dispatchFeature.startVm(mainClass, args, memory, getStartVmConfig());
}
/**
* See {@link #setStartVmConfig(ArgRunnable)}.
*
* @param startVmConfig
*/
public void setStartVmConfig(ArgRunnable startVmConfig) {
this.startVmConfig = startVmConfig;
}
/**
* The instance used to provide additional configuration before the vm is
* launched inside the {@link #startVm()} method.
*
* @return
*/
public ArgRunnable getStartVmConfig() {
return startVmConfig;
}
/**
* Optional method, specify alternate names for this command.
*
* @param aliases
*/
public void initAliases(String... aliases) {
this.aliases.addAll(Arrays.asList(aliases));
}
/**
* There is almost no reason to use this method since by default most
* commands should bootstrap to the project main. This was created for the
* compile task that must bootstrap to the currently running vm since the
* project main classes may not have been compiled.
*/
public void initBootstrapCurrentVm() {
this.bootstrapCurrentVm = true;
}
/**
* Executes this command using the given specified instruction that usually
* originates from the command line. The --body option is used to determine
* what should happen when this method is called:
*
* - if the --body option is not specified then a new vm is started
* - if the --body option is specified then the command body is executed.
*
*
* @param instruction
*/
public void execute(Instruction instruction) {
this.instruction = instruction;
this.options = parser.parse(instruction.getOpts().split(" "));
boolean executeBody = options.has(BODY);
if (executeBody) {
ProjectReporter reporter = store.getParent().getDispatchFeature()
.getReporter();
reporter.beginCommand(getName());
body.run();
} else {
bootstrap.run();
}
}
/**
* The maximum amount of memory that the forked vm will be able to claim.
*
* @return
*/
public String getMemory() {
return memory;
}
/**
* See {@link #getMemory()}.
*
* @param memory
*/
public void setMemory(String memory) {
this.memory = memory;
}
/**
* The unique name of this command. Also appears in the list of aliases as
* the very first element.
*
* @return
*/
public String getName() {
return aliases.get(0);
}
/**
* A read-only list of all the names this command is known by.
*
* @return
*/
public List getAliases() {
return Collections.unmodifiableList(aliases);
}
/**
* A description useful for help text. This is obtained by loading the
* helpResource resource name as specified in the constructor, on the
* classpath. The resources can contain velocity markup. If the resource
* name hasn't been initialized then the helpString is checked. If that
* isn't found then a default string is used.
*
* @return
*/
public String getDescription() {
String description;
if (helpResource != null) {
ResourceFeature feature = store.getParent().getResourceFeature();
description = feature.filterResourceToString(helpResource);
} else if (helpString != null) {
description = helpString;
} else {
description = "no description provided";
}
return description;
}
/**
* Use in order to configure the command line options for this command. The
* returned parser can be used as in the following example that expects a
* mandatory file path to be passed via the -f option:
*
*
* OptionSpec<File> fileOpt = parser.accepts("f").withRequiredArg().ofType(
* File.class);
*
*
* The returned parser is already configured to look for the "--body" option
* that when specified indicates the command body is to be executed. When
* not specified, a new vm is to be created.
*
* @return
*/
public OptionParser getParser() {
return parser;
}
/**
* Reports the aliases available, useful for help text. An example return
* value would be: "foo (f, -foo, --foo)"
*
* @return
*/
public String getAliasText() {
StringBuilder builder = new StringBuilder();
String name = getName();
builder.append(name);
if (aliases.size() > 1) {
builder.append(" (");
for (String alias : aliases) {
if (!alias.equals(name)) {
builder.append(alias);
builder.append(", ");
}
}
builder.delete(builder.length() - 2, builder.length());
builder.append(")");
}
return builder.toString();
}
/**
* The helper responsible for parsing the command line arguments.
*
* @return
*/
public OptionSet getOptions() {
return options;
}
/**
* Convenience method that checks whether or not the given alias is known to
* this command.
*
* @param alias
* @return
*/
public boolean containsAlias(String alias) {
return aliases.contains(alias);
}
/**
* Includes both the command name and arguments, if any, that make up the
* full definition of the command.
*
* @return
*/
public String getInstructionText() {
return instruction.getText();
}
/**
* A reference to the owning store.
*
* @return
*/
public CommandStore getStore() {
return store;
}
/**
* Builds a message suitable for displaying in response to a help command.
*
* @return
*/
public String getHelpText() {
StringBuilder builder = new StringBuilder();
builder.append("Name: ");
builder.append(getAliasText());
builder.append("\n\n");
builder.append(getDescription());
builder.append("\n");
return builder.toString();
}
/**
* Throws an InformationException with standardized error information.
* Useful for calling from a command that has badly specified options. The
* help description is included in the detailed message along with an
* additional hint that gives more information as to what the problem is.
*
* @param hint
* may be null or empty if no hint is available
*/
public void throwBadOptionsException(String hint) {
StringBuilder builder = new StringBuilder();
builder
.append("The following command contained unrecognized options (ignore any --body switch):\n\n \"");
builder.append(getInstructionText());
boolean isHintSpecified = hint != null && hint.length() != 0;
if (isHintSpecified) {
builder.append("\" - ");
builder.append(hint);
}
builder.append("\n\nPlease review the command help as follows:\n");
builder.append(getHelpText());
throw new InformationException(builder.toString());
}
}