| /* |
| * 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.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 resolver with which pipe will be executed |
| * @param path pipe path to execute, {@code INPUT} for getting last token's output as path for things like build some / pipe | execute - |
| * @param optionTokens different options tokens |
| * @return Execution results |
| * @throws Exception exception in case something goes wrong |
| */ |
| 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.with != null) { |
| bMap = new HashMap(); |
| writeToMap(bMap, options.with); |
| } |
| 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'with 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\t\t:\t%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: |
| if (currentToken.args == null){ |
| 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[] with; |
| OutputWriter writer; |
| |
| @Override |
| public String toString() { |
| return "Options{" + |
| "name='" + name + '\'' + |
| ", path='" + path + '\'' + |
| ", expr='" + expr + '\'' + |
| ", with=" + Arrays.toString(with) + |
| ", 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.with = 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 (with != null){ |
| builder.with(with); |
| } |
| } |
| } |
| } |