blob: b03c7d9cac051003f0d080333f3e62894228f226 [file] [log] [blame]
/*
* 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.apache.sling.pipes.internal;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.pipes.ExecutionResult;
import org.apache.sling.pipes.OutputWriter;
import org.apache.sling.pipes.PipeBuilder;
import org.apache.sling.pipes.PipeExecutor;
import org.apache.sling.pipes.Plumber;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.StringReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.apache.sling.pipes.internal.CommandUtil.writeToMap;
@Component(immediate = true,
service = GogoCommands.class,
property = {
"osgi.command.scope=pipe",
"osgi.command.function=build",
"osgi.command.function=run",
"osgi.command.function=execute",
"osgi.command.function=help"
})
public class GogoCommands {
final Logger log = LoggerFactory.getLogger(GogoCommands.class);
protected final static String SEPARATOR = "/";
protected final static String PARAMS = "@";
protected final static String INPUT = "-";
protected final static String KEY_VALUE_SEP = "=";
protected final static String KEY_NAME = "name";
protected final static String KEY_PATH = "path";
protected final static String KEY_EXPR = "expr";
@Reference
ResourceResolverFactory factory;
@Reference
Plumber plumber;
Map<String, Method> methodMap;
Map<String, PipeExecutor> executorMap;
/**
* run command handler
* @param cmds string tokens coming with run command
* @throws Exception in case anything went wrong
*/
public void run(String... cmds) throws Exception {
try (ResourceResolver resolver = factory.getServiceResourceResolver(plumber.getServiceUser())) {
PipeBuilder builder = parse(resolver, cmds);
System.out.println(builder.run());
}
}
/**
* build command handler
* @param cmds string tokens coming with build command
* @throws Exception in case anything went wrong
*/
public void build(String... cmds) throws Exception {
try (ResourceResolver resolver = factory.getServiceResourceResolver(plumber.getServiceUser())) {
PipeBuilder builder = parse(resolver, cmds);
System.out.println(builder.build().getResource().getPath());
}
}
/**
* execute command handler
* @param path pipe path
* @param options string tokens coming with run command
* @throws Exception in case anything went wrong
*/
public void execute(String path, String... options) throws Exception {
String computedPath = INPUT.equals(path) ? IOUtils.toString(System.in).trim() : path;
try (ResourceResolver resolver = factory.getServiceResourceResolver(plumber.getServiceUser())) {
System.out.println(executeInternal(resolver, computedPath, options));
} catch(Exception e){
log.error("Unable to execute {}", path, e);
throw(e);
}
}
/**
* internal execution command handler
* @param resolver
* @param path
* @param optionTokens
* @return Execution results
* @throws Exception
*/
protected ExecutionResult executeInternal(ResourceResolver resolver, String path, String... optionTokens) throws Exception {
Resource resource = resolver.getResource(path);
if (resource == null){
throw new IllegalArgumentException(String.format("%s resource does not exist", path));
}
Options options = getOptions(optionTokens);
Map bMap = null;
if (options.bindings != null) {
bMap = new HashMap();
writeToMap(bMap, options.bindings);
}
OutputWriter writer = new NopWriter();
if (options.writer != null){
writer = options.writer;
}
writer.starts();
return plumber.execute(resolver, path, bMap, writer, true);
}
/**
* help command handler
*/
public void help(){
System.out.format("\nSling Pipes Help\nAvailable commands are \n\n- execute <path> <options>(execute a pipe already built at a given path), if path is '-' then previous pipe token is used," +
"\n- build (build pipe as configured in arguments)" +
"\n- run (run pipe as configured in arguments)" +
"\n- help (print this help)" +
"\n\nfor pipe configured in argument, do 'pipe:<run|build|runAsync> <pipe token> (/ <pipe token> )*\n" +
"\n a <pipe token> is <pipe> <expr|conf>? (<options>)?" +
"\n <options> are (@ <option>)* form with <option> being either" +
"\n\t'name pipeName' (used in bindings), " +
"\n\t'expr pipeExpression' (when not directly as <args>)" +
"\n\t'path pipePath' (when not directly as <args>)" +
"\n\t'bindings key=value ...'" +
"\n\t'outputs key=value ...'" +
"\n and <pipe> is one of the following :\n");
for (Map.Entry<String, PipeExecutor> entry : getExecutorMap().entrySet()){
System.out.format("\t%s : %s\n", entry.getKey(), entry.getValue().description() );
}
}
/**
* @param resolver resource resolver with which pipe will build the pipe
* @param cmds list of commands for building the pipe
* @return PipeBuilder instance (that can be used to finalize the command)
* @throws InvocationTargetException can happen in case the mapping with PB api went wrong
* @throws IllegalAccessException can happen in case the mapping with PB api went wrong
*/
protected PipeBuilder parse(ResourceResolver resolver, String...cmds) throws InvocationTargetException, IllegalAccessException {
PipeBuilder builder = plumber.newPipe(resolver);
for (Token token : parseTokens(cmds)){
Method method = getMethodMap().get(token.pipeKey);
if (method == null){
throw new IllegalArgumentException(token.pipeKey + " is not a valid pipe");
}
if (isExpressionExpected(method)){
method.invoke(builder, token.args.get(0));
} else if (isConfExpected(method)){
method.invoke(builder, (Object)keyValuesToArray(token.args));
} else if (isWithoutExpectedParameter(method)){
method.invoke(builder);
}
if (token.options != null){
token.options.writeToBuilder(builder);
}
}
return builder;
}
/**
* builds utility maps
*/
protected void computeMaps(){
executorMap = new HashMap<>();
methodMap = new HashMap<>();
for (Method method : PipeBuilder.class.getDeclaredMethods()) {
PipeExecutor executor = method.getAnnotation(PipeExecutor.class);
if (executor != null) {
methodMap.put(executor.command(), method);
executorMap.put(executor.command(), executor);
}
}
}
/**
* @return map of command to PB api method
*/
protected Map<String, Method> getMethodMap() {
if (methodMap == null) {
computeMaps();
}
return methodMap;
}
/**
* @return map of command to Annotation information around the PB api
*/
protected Map<String, PipeExecutor> getExecutorMap() {
if (executorMap == null) {
computeMaps();
}
return executorMap;
}
/**
* @param method corresponding PB api
* @return true if the api does expect an expression (meaning a string)
*/
protected boolean isExpressionExpected(Method method) {
return method.getParameterCount() == 1 && method.getParameterTypes()[0].equals(String.class);
}
/**
* @param method corresponding PB api
* @return true if the api does expect a configuration (meaning a list of key value pairs)
*/
protected boolean isConfExpected(Method method) {
return method.getParameterCount() == 1 && method.getParameterTypes()[0].equals(Object[].class);
}
/**
* @param method corresponding PB api
* @return true if the api does not expect parameters
*/
protected boolean isWithoutExpectedParameter(Method method){
return method.getParameterCount() == 0;
}
/**
* @param o list of key value strings key1:value1,key2:value2,...
* @return String [] key1,value1,key2,value2,... corresponding to the pipe builder API
*/
private String[] keyValuesToArray(List<String> o) {
List<String> args = new ArrayList<>();
for (String pair : o){
args.addAll(Arrays.asList(pair.split(KEY_VALUE_SEP)));
}
return args.toArray(new String[args.size()]);
}
/**
* @param commands full list of command tokens
* @return Token list corresponding to the string ones
*/
protected List<Token> parseTokens(String... commands) {
List<Token> returnValue = new ArrayList();
Token currentToken = new Token();
returnValue.add(currentToken);
List currentList = new ArrayList();
for (String token : commands){
if (currentToken.pipeKey == null){
currentToken.pipeKey = token;
} else {
switch (token){
case GogoCommands.SEPARATOR:
finishToken(currentToken, currentList);
currentList = new ArrayList();
currentToken = new Token();
returnValue.add(currentToken);
break;
case GogoCommands.PARAMS:
currentToken.args = currentList;
currentList = new ArrayList();
currentList.add(PARAMS);
break;
default:
currentList.add(token);
}
}
}
finishToken(currentToken, currentList);
return returnValue;
}
/**
* ends up processing of current token
* @param currentToken token being processed
* @param currentList list of argument that have been collected so far
*/
protected void finishToken(Token currentToken, List<String> currentList){
if (currentToken.args != null){
//it means we have already parse args here, so we need to set current list as options
currentToken.options = getOptions(currentList);
} else {
currentToken.args = currentList;
}
log.debug("current token : {}", currentToken);
}
/**
* Pipe token, used to hold information of a "sub pipe" configuration
*/
protected class Token {
String pipeKey;
List args;
Options options;
@Override
public String toString() {
return "Token{" +
"pipeKey='" + pipeKey + '\'' +
", args=" + args +
", options=" + options +
'}';
}
}
/**
* @param tokens array of tokens
* @return options from array
*/
protected Options getOptions(String[] tokens){
return getOptions(Arrays.asList(tokens));
}
/**
* @param tokens list of toekns
* @return options from token list
*/
protected Options getOptions(List<String> tokens){
return new Options(tokens);
}
/**
* Options for a pipe execution
*/
protected class Options {
String name;
String path;
String expr;
String[] bindings;
OutputWriter writer;
@Override
public String toString() {
return "Options{" +
"name='" + name + '\'' +
", path='" + path + '\'' +
", expr='" + expr + '\'' +
", bindings=" + Arrays.toString(bindings) +
", writer=" + writer +
'}';
}
/**
* Constructor
* @param options string list from where options will be built
*/
protected Options(List<String> options){
Map<String, Object> optionMap = new HashMap<>();
String currentKey = null;
List<String> currentList = null;
for (String optionToken : options) {
if (PARAMS.equals(optionToken)){
finishOption(currentKey, currentList, optionMap);
currentList = new ArrayList<>();
currentKey = null;
} else if (currentKey == null){
currentKey = optionToken;
} else {
currentList.add(optionToken);
}
}
finishOption(currentKey, currentList, optionMap);
for (Map.Entry<String, Object> entry : optionMap.entrySet()){
switch (entry.getKey()) {
case "name" : {
this.name = (String)entry.getValue();
break;
}
case "path" : {
this.path = (String)entry.getValue();
break;
}
case "expr" : {
this.expr = (String)entry.getValue();
break;
}
case "with" : {
this.bindings = keyValuesToArray((List<String>)entry.getValue());
break;
}
case "outputs" : {
this.writer = new JsonWriter();
String[] list = keyValuesToArray((List<String>)entry.getValue());
Map outputs = new HashMap();
CommandUtil.writeToMap(outputs, list);
this.writer.setCustomOutputs(outputs);
break;
}
default: {
throw new IllegalArgumentException(String.format("%s is an unknown option", entry.getKey()));
}
}
}
}
/**
* wrap up current option
* @param currentKey option key
* @param currentList list being processed
* @param optionMap option map
*/
protected void finishOption(String currentKey, List<String> currentList, Map<String, Object> optionMap){
if (currentList != null){
if (currentKey.equals(KEY_NAME) || currentKey.equals(KEY_EXPR) || currentKey.equals(KEY_PATH)) {
optionMap.put(currentKey, currentList.get(0));
} else {
optionMap.put(currentKey, currentList);
}
}
}
/**
* write options to current builder
* @param builder current builder
* @throws IllegalAccessException
*/
void writeToBuilder(PipeBuilder builder) throws IllegalAccessException {
if (StringUtils.isNotBlank(name)){
builder.name(name);
}
if (StringUtils.isNotBlank(path)){
builder.path(path);
}
if (StringUtils.isNotBlank(expr)){
builder.expr(expr);
}
if (bindings != null){
builder.with(bindings);
}
}
}
}