/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.waveprotocol.box.server.robots.agent;
import static org.waveprotocol.box.server.robots.agent.RobotAgentUtil.appendLine;
import static org.waveprotocol.box.server.robots.agent.RobotAgentUtil.lastEnteredLineOf;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.name.Names;
import com.google.wave.api.Blip;
import com.google.wave.api.event.DocumentChangedEvent;
import com.google.wave.api.event.WaveletSelfAddedEvent;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.waveprotocol.box.server.CoreSettings;
import org.waveprotocol.box.server.persistence.AccountStore;
import org.waveprotocol.box.server.robots.register.RobotRegistrar;
import org.waveprotocol.box.server.robots.register.RobotRegistrarImpl;
import org.waveprotocol.wave.model.id.TokenGenerator;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* The base for robot agents that run on the WIAB server and interact with users
* by entering commands as text in the blips.
*
* @author yurize@apache.org (Yuri Zelikov)
*/
@SuppressWarnings("serial")
public abstract class AbstractCliRobotAgent extends AbstractBaseRobotAgent {
/** The options for the command. */
private final Options options;
private final CommandLineParser parser;
private final HelpFormatter helpFormatter;
/**
* Constructor. Initializes the agent to serve on the URI provided by
* {@link #getRobotUri()} and ensures that the agent is registered in the
* Account store.
*
* @param injector the injector instance.
*/
public AbstractCliRobotAgent(Injector injector) {
this(injector.getInstance(Key.get(String.class, Names.named(CoreSettings.WAVE_SERVER_DOMAIN))),
injector.getInstance(TokenGenerator.class), injector
.getInstance(ServerFrontendAddressHolder.class), injector
.getInstance(AccountStore.class), injector.getInstance(RobotRegistrarImpl.class),
injector.getInstance(Key.get(Boolean.class, Names.named(CoreSettings.ENABLE_SSL))));
}
/**
* Constructor. Initializes the agent to serve on the URI provided by
* {@link #getRobotUri()} and ensures that the agent is registered in the
* Account store.
*/
AbstractCliRobotAgent(String waveDomain, TokenGenerator tokenGenerator,
ServerFrontendAddressHolder frontendAddressHolder, AccountStore accountStore,
RobotRegistrar robotRegistrar, Boolean sslEnabled) {
super(waveDomain, tokenGenerator, frontendAddressHolder, accountStore, robotRegistrar, sslEnabled);
parser = new PosixParser();
helpFormatter = new HelpFormatter();
options = initOptions();
}
/**
* Displays a short description when the robot is added to a wave.
*/
@Override
public void onWaveletSelfAdded(WaveletSelfAddedEvent event) {
String robotAddress = event.getWavelet().getRobotAddress();
// Display a short description.
appendLine(event.getBlip(), "\n" + robotAddress + ": I am listening.\n" + getShortDescription()
+ "\nFor help type " + "\"" + getCommandName()
+ " -help\" on a new line and hit \"Enter\".");
}
@Override
public void onDocumentChanged(DocumentChangedEvent event) {
Blip blip = event.getBlip();
String modifiedBy = event.getModifiedBy();
CommandLine commandLine = null;
try {
commandLine = preprocessCommand(blip.getContent());
} catch (IllegalArgumentException e) {
appendLine(blip, e.getMessage());
}
if (commandLine != null) {
if (commandLine.hasOption("help")
// Or if only options.
|| (commandLine.getArgs().length - commandLine.getOptions().length <= 1)) {
appendLine(blip, getFullDescription());
} else {
String robotMessage = maybeExecuteCommand(commandLine, modifiedBy);
appendLine(blip, robotMessage);
}
}
}
/**
* Validates and parses the input for the command.
*
* @param blipContent the blip contents.
* @return the command line {@link CommandLine} object with parsed data from
* the blip contents or null in case the content doesn't contain a
* command.
* @throws IllegalArgumentException if illegal arguments passed to the
* command.
*/
protected CommandLine preprocessCommand(String blipContent) throws IllegalArgumentException {
CommandLine commandLine = null;
String lastLine = lastEnteredLineOf(blipContent);
if (lastLine != null) {
try {
commandLine = parse(lastLine.split(" "));
} catch (ParseException e) {
throw new IllegalArgumentException(e);
}
String[] args = commandLine.getArgs();
if (!args[0].equals(getCommandName())) {
return null;
}
int argsNum = args.length - commandLine.getOptions().length - 1;
// If there are only options in the command - then it is also invalid and
// have to display usage anyway.
if ((argsNum > 0)
&& (argsNum < getMinNumOfArguments() || argsNum > getMaxNumOfArguments())) {
String message = null;
if (getMinNumOfArguments() == getMaxNumOfArguments()) {
message =
String.format("Invalid number of arguments. Expected: %d , actual: %d %s",
getMinNumOfArguments(), argsNum, getUsage());
} else {
message =
String.format(
"Invalid number of arguments. Expected between %d and %d, actual: %d. %s",
getMinNumOfArguments(), getMaxNumOfArguments(), argsNum, getUsage());
}
throw new IllegalArgumentException(message);
}
}
return commandLine;
}
@Override
protected String getRobotProfilePageUrl() {
return null;
}
/**
* Returns the command options usage.
*/
public String getUsage() {
StringWriter stringWriter = new StringWriter();
PrintWriter pw = new PrintWriter(stringWriter);
// HelpFormatter doesn't provide other ways to access defaultWidth, so we
// forced to access it in a deprecated way.
// TODO (user) Update this code to remove access of deprecated fields when
// it will be possible.
helpFormatter.printHelp(pw, helpFormatter.defaultWidth, getCommandName() + " "
+ getCmdLineSyntax() + " \n", null, options, helpFormatter.defaultLeftPad,
helpFormatter.defaultDescPad, "", false);
pw.flush();
String usageStr = stringWriter.toString();
return usageStr;
}
/**
* Initializes basic options. Override if more options needed.
*
* @return the command options.
*/
protected Options initOptions() {
// Create Options.
Options options = new Options();
// The robot has only "help" option.
@SuppressWarnings({"static-access", "static"})
Option help = OptionBuilder.withDescription("Displays help for the command.").create("help");
options.addOption(help);
return options;
}
protected CommandLine parse(String... args) throws ParseException {
return getParser().parse(getOptions(), args);
}
/**
* Returns the command line parser.
*/
protected CommandLineParser getParser() {
return parser;
}
/**
* Returns the command options.
*/
protected Options getOptions() {
return options;
}
/**
* Attempts to execute the command.
*
* @param commandLine the commandLine with arguments and/or options entered by
* the user.
* @param modifiedBy the user that entered the content.
* @return the result message: success or failure.
*/
protected abstract String maybeExecuteCommand(CommandLine commandLine, String modifiedBy);
/**
* Returns the short description of the robot.
*/
public abstract String getShortDescription();
/**
* Returns the full robot description.
*/
public abstract String getFullDescription();
/**
* Returns the command name for the robot.
*/
public abstract String getCommandName();
/**
* Returns the command line syntax.
*/
public abstract String getCmdLineSyntax();
/**
* Returns the command use example.
*/
public abstract String getExample();
/**
* Returns the minimum number of arguments this command accepts. Should be
* greater than zero and less or equal to {@link #getMaxNumOfArguments()}.
*/
public abstract int getMinNumOfArguments();
/**
* Returns the maximum number of arguments this command accepts.
*/
public abstract int getMaxNumOfArguments();
}