This is an automated email from the ASF dual-hosted git repository.

npeltier pushed a commit to branch master
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-pipes.git


The following commit(s) were added to refs/heads/master by this push:
     new 9aad072  SLING-9556 introduce command execution service
9aad072 is described below

commit 9aad072373b23ca7531482893376a6b920d95c08
Author: Nicolas Peltier <[email protected]>
AuthorDate: Wed Aug 5 17:54:08 2020 +0200

    SLING-9556 introduce command execution service
    
    - moved GogoCommand facilities to command executor service, that it points 
to,
    - make command executor service also serving servlet,
    - make the servlet accepting command as parameter or file, considering only 
'run' commands
---
 .../org/apache/sling/pipes/CommandExecutor.java    |  48 ++
 .../java/org/apache/sling/pipes/PipeBindings.java  |   2 +-
 src/main/java/org/apache/sling/pipes/Plumber.java  |  15 +
 ...{GogoCommands.java => CommandExecutorImpl.java} | 513 +++++++++++----------
 .../apache/sling/pipes/internal/GogoCommands.java  | 394 +---------------
 .../sling/pipes/internal/PipeBuilderImpl.java      |  21 +-
 .../apache/sling/pipes/internal/PlumberImpl.java   |  34 ++
 .../sling/pipes/internal/PlumberServlet.java       |  34 +-
 ...mandsTest.java => CommandExecutorImplTest.java} |  70 ++-
 .../sling/pipes/internal/PipeBuilderImplTest.java  |  68 +++
 src/test/resources/testcommand.txt                 |  19 +
 11 files changed, 550 insertions(+), 668 deletions(-)

diff --git a/src/main/java/org/apache/sling/pipes/CommandExecutor.java 
b/src/main/java/org/apache/sling/pipes/CommandExecutor.java
new file mode 100644
index 0000000..e8135c4
--- /dev/null
+++ b/src/main/java/org/apache/sling/pipes/CommandExecutor.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import java.lang.reflect.InvocationTargetException;
+
+import org.apache.sling.api.resource.ResourceResolver;
+
+public interface CommandExecutor {
+
+    /**
+     * internal execution command handler
+     * @param resolver resolver with which pipe will be executed
+     * @param path pipe path to execute
+     * @param options different options tokens
+     * @return Execution results
+     */
+    ExecutionResult execute(ResourceResolver resolver, String path, String... 
options);
+
+    /**
+     * @param resolver resource resolver with which pipe will build the pipe
+     * @param commands 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
+     */
+    PipeBuilder parse(ResourceResolver resolver, String... commands) throws 
InvocationTargetException, IllegalAccessException;
+
+    /**
+     * @return help string
+     */
+    String help();
+}
diff --git a/src/main/java/org/apache/sling/pipes/PipeBindings.java 
b/src/main/java/org/apache/sling/pipes/PipeBindings.java
index 16362a2..889e289 100644
--- a/src/main/java/org/apache/sling/pipes/PipeBindings.java
+++ b/src/main/java/org/apache/sling/pipes/PipeBindings.java
@@ -66,7 +66,7 @@ public class PipeBindings {
      */
     public static final String NAME_BINDING = "name";
 
-    private static final String INJECTED_SCRIPT_REGEXP = 
"\\$\\{(([^\\{^\\}]*(\\{[0-9,]+\\})?)*)\\}";
+    public static final String INJECTED_SCRIPT_REGEXP = 
"\\$\\{(([^\\{^\\}]*(\\{[0-9,]+\\})?)*)\\}";
     private static final Pattern INJECTED_SCRIPT = 
Pattern.compile(INJECTED_SCRIPT_REGEXP);
     protected static final String IF_PREFIX = "$if";
     protected static final Pattern CONDITIONAL_STRING =  Pattern.compile("^\\" 
+ IF_PREFIX + INJECTED_SCRIPT_REGEXP);
diff --git a/src/main/java/org/apache/sling/pipes/Plumber.java 
b/src/main/java/org/apache/sling/pipes/Plumber.java
index 9a3091c..b3be41e 100644
--- a/src/main/java/org/apache/sling/pipes/Plumber.java
+++ b/src/main/java/org/apache/sling/pipes/Plumber.java
@@ -16,11 +16,17 @@
  */
 package org.apache.sling.pipes;
 
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.request.RequestParameter;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.event.jobs.Job;
+import org.apache.sling.pipes.internal.JsonUtil;
 import org.osgi.annotation.versioning.ProviderType;
 
+import java.io.IOException;
+import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -145,6 +151,15 @@ public interface Plumber {
     boolean isRunning(Resource pipeResource);
 
     /**
+     * Extract pipe bindings from the request
+     * @param request from where to extract bindings
+     * @param writeAllowed should we consider this execution is about to 
modify content
+     * @return map of bindings
+     * @throws IOException in case something turns wrong with an input stream
+     */
+    Map<String, Object> getBindingsFromRequest(SlingHttpServletRequest 
request, boolean writeAllowed) throws IOException;
+
+    /**
      * @return service user that has been configured for executing pipes;
      */
     Map<String, Object> getServiceUser();
diff --git a/src/main/java/org/apache/sling/pipes/internal/GogoCommands.java 
b/src/main/java/org/apache/sling/pipes/internal/CommandExecutorImpl.java
similarity index 62%
copy from src/main/java/org/apache/sling/pipes/internal/GogoCommands.java
copy to src/main/java/org/apache/sling/pipes/internal/CommandExecutorImpl.java
index 54824ca..d34278a 100644
--- a/src/main/java/org/apache/sling/pipes/internal/GogoCommands.java
+++ b/src/main/java/org/apache/sling/pipes/internal/CommandExecutorImpl.java
@@ -14,120 +14,155 @@
  * 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 java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 import org.apache.commons.lang3.StringUtils;
-import org.apache.sling.api.resource.LoginException;
-import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
 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.api.servlets.ServletResolverConstants;
+import org.apache.sling.api.servlets.SlingAllMethodsServlet;
+import org.apache.sling.pipes.CommandExecutor;
 import org.apache.sling.pipes.ExecutionResult;
 import org.apache.sling.pipes.OutputWriter;
 import org.apache.sling.pipes.Pipe;
 import org.apache.sling.pipes.PipeBuilder;
 import org.apache.sling.pipes.PipeExecutor;
 import org.apache.sling.pipes.Plumber;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
 import org.osgi.service.component.annotations.Reference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-import java.io.PrintStream;
-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 javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static org.apache.sling.pipes.PipeBindings.INJECTED_SCRIPT_REGEXP;
 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 static final String SEPARATOR = "/";
-    protected static final String PARAMS = "@";
-    protected static final String INPUT = "-";
-    protected static final String KEY_VALUE_SEP = "=";
-    protected static final String KEY_NAME = "name";
-    protected static final String KEY_PATH = "path";
-    protected static final String KEY_EXPR = "expr";
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+
+@Component(service = {Servlet.class, CommandExecutor.class}, property= {
+    ServletResolverConstants.SLING_SERVLET_RESOURCE_TYPES + "=" + 
CommandExecutorImpl.RESOURCE_TYPE,
+    ServletResolverConstants.SLING_SERVLET_METHODS + "=POST",
+    ServletResolverConstants.SLING_SERVLET_EXTENSIONS + "=txt"
+})
+public class CommandExecutorImpl extends SlingAllMethodsServlet implements 
CommandExecutor {
+    final Logger log = LoggerFactory.getLogger(CommandExecutorImpl.class);
+    public static final String RESOURCE_TYPE = "slingPipes/exec";
+    static final String REQ_PARAM_FILE = "pipe_cmdfile";
+    static final String REQ_PARAM_CMD = "pipe_cmd";
+    static final String WHITE_SPACE_SEPARATOR = "\\s";
+    static final String COMMENT_PREFIX = "#";
+    static final String SEPARATOR = "/";
+    static final String PARAMS = "@";
+    static final String KEY_VALUE_SEP = "=";
+    static final String FIRST_TOKEN = "first";
+    static final String SECOND_TOKEN = "second";
+    static final String CONFIGURATION_TOKEN = "(?<" + FIRST_TOKEN + 
">[\\w/\\:]+)\\s*" + KEY_VALUE_SEP
+        + "(?<" + SECOND_TOKEN + ">[(\\w*)|" + INJECTED_SCRIPT_REGEXP + "]+)";
+    static final Pattern CONFIGURATION_PATTERN = 
Pattern.compile(CONFIGURATION_TOKEN);
+    static final String KEY_NAME = "name";
+    static final String KEY_PATH = "path";
+    static final String KEY_EXPR = "expr";
+
+    private static final String HELP_START =
+        "\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";
 
+    Map<String, Method> methodMap;
+    Map<String, PipeExecutor> executorMap;
 
-    @Reference
-    ResourceResolverFactory factory;
+    String help;
 
     @Reference
     Plumber plumber;
 
-    Map<String, Method> methodMap;
-
-    Map<String, PipeExecutor> executorMap;
-    
-    
-    PrintStream print() {
-        return System.out;
+    @Activate
+    @Modified
+    public void activate(){
+        methodMap = null;
+        executorMap = null;
+        help = null;
     }
 
-    /**
-     * run command handler
-     * @param cmds string tokens coming with run command
-     * @throws Exception in case anything went wrong
-     */
-    public void run(String... cmds) throws LoginException, 
InvocationTargetException, IllegalAccessException {
-        try (ResourceResolver resolver = 
factory.getServiceResourceResolver(plumber.getServiceUser())) {
-            PipeBuilder builder = parse(resolver, cmds);
-            print().println(builder.run());
-        }
+    boolean isCommandCandidate(String line) {
+        return StringUtils.isNotBlank(line) && 
!line.startsWith(COMMENT_PREFIX);
     }
 
-    /**
-     * build command handler
-     * @param cmds string tokens coming with build command
-     * @throws Exception in case anything went wrong
-     */
-    public void build(String... cmds) throws LoginException, 
InvocationTargetException, IllegalAccessException, PersistenceException {
-        try (ResourceResolver resolver = 
factory.getServiceResourceResolver(plumber.getServiceUser())) {
-            PipeBuilder builder = parse(resolver, cmds);
-            print().println(builder.build().getResource().getPath());
+    List<String> getCommandList(SlingHttpServletRequest request) throws 
IOException {
+        List<String> cmds = new ArrayList<>();
+        if (request.getParameterMap().containsKey(REQ_PARAM_CMD)) {
+            cmds.add(request.getParameter(REQ_PARAM_CMD));
+        } else if (request.getParameterMap().containsKey(REQ_PARAM_FILE)) {
+            InputStream is = 
request.getRequestParameter(REQ_PARAM_FILE).getInputStream();
+            BufferedReader reader = new BufferedReader(new 
InputStreamReader(is, StandardCharsets.UTF_8));
+            String line;
+            while ((line = reader.readLine()) != null) {
+                if (isCommandCandidate(line)) {
+                    cmds.add(line);
+                }
+            }
         }
+        return cmds;
     }
 
-    /**
-     * 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 IOException, 
LoginException {
-        String computedPath = INPUT.equals(path) ? 
IOUtils.toString(System.in).trim() : path;
-        try (ResourceResolver resolver = 
factory.getServiceResourceResolver(plumber.getServiceUser())) {
-            print().println(executeInternal(resolver, computedPath, options));
+    @Override
+    protected void doPost(@NotNull SlingHttpServletRequest request, @NotNull 
SlingHttpServletResponse response) throws ServletException, IOException {
+        String currentCommand = null;
+        PrintWriter writer = response.getWriter();
+        try {
+            ResourceResolver resolver = request.getResourceResolver();
+            Map<String, Object> bindings = 
plumber.getBindingsFromRequest(request, true);
+            List<String> cmds = getCommandList(request);
+            if (cmds.isEmpty()) {
+                writer.println("No command to execute!");
+            }
+            for (String command : cmds) {
+                if (StringUtils.isNotBlank(command)) {
+                    currentCommand = command;
+                    PipeBuilder pipeBuilder = parse(resolver, 
command.split(WHITE_SPACE_SEPARATOR));
+                    writer.println(pipeBuilder.run(bindings));
+                }
+            }
+            response.setStatus(SC_OK);
+        } catch (IllegalAccessException | InvocationTargetException e) {
+            writer.println("Error executing " + currentCommand);
+            e.printStackTrace(writer);
+            response.sendError(SC_INTERNAL_SERVER_ERROR);
+            writer.println(help());
         }
     }
 
-    /**
-     * 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) {
+    @Override
+    public ExecutionResult execute(ResourceResolver resolver, String path, 
String... optionTokens) {
         Resource resource = resolver.getResource(path);
         if (resource == null){
             throw new IllegalArgumentException(String.format("%s resource does 
not exist", path));
@@ -146,36 +181,8 @@ public class GogoCommands {
         return plumber.execute(resolver, path, bMap, writer, true);
     }
 
-    /**
-     * help command handler
-     */
-    public void help(){
-        print().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()){
-            print().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 {
+    @Override
+    public 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);
@@ -199,114 +206,6 @@ public class GogoCommands {
     }
 
     /**
-     * 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<String> 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
@@ -322,24 +221,6 @@ public class GogoCommands {
     }
 
     /**
-     * Pipe token, used to hold information of a "sub pipe" configuration
-     */
-    protected class Token {
-        String pipeKey;
-        List<String> args;
-        Options options;
-
-        @Override
-        public String toString() {
-            return "Token{" +
-                    "pipeKey='" + pipeKey + '\'' +
-                    ", args=" + args +
-                    ", options=" + options +
-                    '}';
-        }
-    }
-
-    /**
      * @param tokens array of tokens
      * @return options from array
      */
@@ -361,7 +242,6 @@ public class GogoCommands {
     protected class Options {
         String name;
         String path;
-
         String expr;
         String[] with;
         OutputWriter writer;
@@ -369,16 +249,14 @@ public class GogoCommands {
         @Override
         public String toString() {
             return "Options{" +
-                    "name='" + name + '\'' +
-                    ", path='" + path + '\'' +
-                    ", expr='" + expr + '\'' +
-                    ", with=" + Arrays.toString(with) +
-                    ", writer=" + writer +
-                    '}';
+                "name='" + name + '\'' +
+                ", path='" + path + '\'' +
+                ", expr='" + expr + '\'' +
+                ", with=" + Arrays.toString(with) +
+                ", writer=" + writer +
+                '}';
         }
 
-
-
         void setOutputs(List<String> values) {
             this.writer = new JsonWriter();
             String[] list = keyValuesToArray(values);
@@ -469,4 +347,149 @@ public class GogoCommands {
             }
         }
     }
+
+    /**
+     * Pipe token, used to hold information of a "sub pipe" configuration
+     */
+    protected class Token {
+        String pipeKey;
+        List<String> args;
+        CommandExecutorImpl.Options options;
+
+        @Override
+        public String toString() {
+            return "Token{" +
+                "pipeKey='" + pipeKey + '\'' +
+                ", args=" + args +
+                ", options=" + options +
+                '}';
+        }
+    }
+
+    /**
+     * @param commands full list of command tokens
+     * @return Token list corresponding to the string ones
+     */
+    protected List<CommandExecutorImpl.Token> parseTokens(String... commands) {
+        List<CommandExecutorImpl.Token> returnValue = new ArrayList<>();
+        CommandExecutorImpl.Token currentToken = new 
CommandExecutorImpl.Token();
+        returnValue.add(currentToken);
+        List<String> currentList = new ArrayList<>();
+        for (String token : commands){
+            if (currentToken.pipeKey == null){
+                currentToken.pipeKey = token;
+            } else {
+                switch (token){
+                    case CommandExecutorImpl.SEPARATOR:
+                        finishToken(currentToken, currentList);
+                        currentList = new ArrayList<>();
+                        currentToken = new CommandExecutorImpl.Token();
+                        returnValue.add(currentToken);
+                        break;
+                    case CommandExecutorImpl.PARAMS:
+                        if (currentToken.args == null){
+                            currentToken.args = currentList;
+                            currentList = new ArrayList<>();
+                        }
+                        currentList.add(PARAMS);
+                        break;
+                    default:
+                        currentList.add(token);
+                }
+            }
+        }
+        finishToken(currentToken, currentList);
+        return returnValue;
+    }
+
+    /**
+     * 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
+     */
+    String[] keyValuesToArray(List<String> o) {
+        List<String> args = new ArrayList<>();
+        for (String pair : o){
+            Matcher matcher = CONFIGURATION_PATTERN.matcher(pair.trim());
+            if (matcher.matches()) {
+                args.add(matcher.group(FIRST_TOKEN));
+                args.add(matcher.group(SECOND_TOKEN));
+            }
+        }
+        return args.toArray(new String[args.size()]);
+    }
+
+    /**
+     * help command handler
+     */
+    public String help(){
+        if (StringUtils.isBlank(help)) {
+            StringBuilder builder = new StringBuilder();
+            builder.append(HELP_START);
+            for (Map.Entry<String, PipeExecutor> entry : 
getExecutorMap().entrySet()) {
+                builder.append(String.format("\t%s\t\t:\t%s%n", 
entry.getKey(), entry.getValue().description()));
+            }
+            help = builder.toString();
+        }
+        return help;
+    }
+
 }
diff --git a/src/main/java/org/apache/sling/pipes/internal/GogoCommands.java 
b/src/main/java/org/apache/sling/pipes/internal/GogoCommands.java
index 54824ca..d137edf 100644
--- a/src/main/java/org/apache/sling/pipes/internal/GogoCommands.java
+++ b/src/main/java/org/apache/sling/pipes/internal/GogoCommands.java
@@ -17,34 +17,20 @@
 package org.apache.sling.pipes.internal;
 
 import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.sling.api.resource.LoginException;
 import org.apache.sling.api.resource.PersistenceException;
-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.Pipe;
+
+import org.apache.sling.pipes.CommandExecutor;
 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.IOException;
 import java.io.PrintStream;
 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,
@@ -56,27 +42,23 @@ import static 
org.apache.sling.pipes.internal.CommandUtil.writeToMap;
             "osgi.command.function=help"
         })
 public class GogoCommands {
-    final Logger log = LoggerFactory.getLogger(GogoCommands.class);
 
-    protected static final String SEPARATOR = "/";
-    protected static final String PARAMS = "@";
-    protected static final String INPUT = "-";
-    protected static final String KEY_VALUE_SEP = "=";
-    protected static final String KEY_NAME = "name";
-    protected static final String KEY_PATH = "path";
-    protected static final String KEY_EXPR = "expr";
+    static final String INPUT = "-";
 
+    static final String COMMAND_HELP_START = "\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";
 
     @Reference
     ResourceResolverFactory factory;
 
     @Reference
-    Plumber plumber;
-
-    Map<String, Method> methodMap;
+    CommandExecutor commandExecutor;
 
-    Map<String, PipeExecutor> executorMap;
-    
+    @Reference
+    Plumber plumber;
     
     PrintStream print() {
         return System.out;
@@ -89,7 +71,7 @@ public class GogoCommands {
      */
     public void run(String... cmds) throws LoginException, 
InvocationTargetException, IllegalAccessException {
         try (ResourceResolver resolver = 
factory.getServiceResourceResolver(plumber.getServiceUser())) {
-            PipeBuilder builder = parse(resolver, cmds);
+            PipeBuilder builder = commandExecutor.parse(resolver, cmds);
             print().println(builder.run());
         }
     }
@@ -101,372 +83,28 @@ public class GogoCommands {
      */
     public void build(String... cmds) throws LoginException, 
InvocationTargetException, IllegalAccessException, PersistenceException {
         try (ResourceResolver resolver = 
factory.getServiceResourceResolver(plumber.getServiceUser())) {
-            PipeBuilder builder = parse(resolver, cmds);
+            PipeBuilder builder = commandExecutor.parse(resolver, cmds);
             print().println(builder.build().getResource().getPath());
         }
     }
 
     /**
      * execute command handler
-     * @param path pipe path
+     * @param path pipe path, {@code INPUT} for getting last token's output as 
path for things like build some / pipe | execute -
      * @param options string tokens coming with run command
      * @throws Exception in case anything went wrong
      */
     public void execute(String path, String... options) throws IOException, 
LoginException {
         String computedPath = INPUT.equals(path) ? 
IOUtils.toString(System.in).trim() : path;
         try (ResourceResolver resolver = 
factory.getServiceResourceResolver(plumber.getServiceUser())) {
-            print().println(executeInternal(resolver, computedPath, options));
+            print().println(commandExecutor.execute(resolver, computedPath, 
options));
         }
     }
 
     /**
-     * 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) {
-        Resource resource = resolver.getResource(path);
-        if (resource == null){
-            throw new IllegalArgumentException(String.format("%s resource does 
not exist", path));
-        }
-        Options options = getOptions(optionTokens);
-        Map<String, Object> 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(){
-        print().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()){
-            print().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<String> 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<String> 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 tokens
-     * @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 +
-                    '}';
-        }
-
-
-
-        void setOutputs(List<String> values) {
-            this.writer = new JsonWriter();
-            String[] list = keyValuesToArray(values);
-            Map<String, Object> outputs = new HashMap<>();
-            CommandUtil.writeToMap(outputs, list);
-            this.writer.setCustomOutputs(outputs);
-        }
-
-        /**
-         * 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 Pipe.PN_NAME :
-                        this.name = (String)entry.getValue();
-                        break;
-                    case Pipe.PN_PATH :
-                        this.path = (String)entry.getValue();
-                        break;
-                    case Pipe.PN_EXPR :
-                        this.expr = (String)entry.getValue();
-                        break;
-                    case "with" :
-                        this.with = 
keyValuesToArray((List<String>)entry.getValue());
-                        break;
-                    case "outputs" :
-                        setOutputs((List<String>)entry.getValue());
-                        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);
-            }
-        }
+        print().println(COMMAND_HELP_START + commandExecutor.help());
     }
 }
diff --git a/src/main/java/org/apache/sling/pipes/internal/PipeBuilderImpl.java 
b/src/main/java/org/apache/sling/pipes/internal/PipeBuilderImpl.java
index e0e89b4..852d629 100644
--- a/src/main/java/org/apache/sling/pipes/internal/PipeBuilderImpl.java
+++ b/src/main/java/org/apache/sling/pipes/internal/PipeBuilderImpl.java
@@ -43,14 +43,17 @@ import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
+import static org.apache.commons.lang3.StringUtils.EMPTY;
 import static 
org.apache.sling.jcr.resource.JcrResourceConstants.NT_SLING_FOLDER;
 import static 
org.apache.sling.jcr.resource.JcrResourceConstants.NT_SLING_ORDERED_FOLDER;
 import static 
org.apache.sling.jcr.resource.JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY;
+import static org.apache.sling.pipes.BasePipe.SLASH;
 import static org.apache.sling.pipes.internal.CommandUtil.checkArguments;
 import static org.apache.sling.pipes.internal.CommandUtil.writeToMap;
 import static org.apache.sling.pipes.internal.ManifoldPipe.PN_NUM_THREADS;
@@ -320,8 +323,22 @@ public class PipeBuilderImpl implements PipeBuilder {
      * @throws PersistenceException in case configuration resource couldn't be 
persisted
      * @return resource created
      */
-    protected Resource createResource(ResourceResolver resolver, String path, 
String type, Map<String, Object> data) throws PersistenceException {
-        return ResourceUtil.getOrCreateResource(resolver, path, data, type, 
false);
+    Resource createResource(ResourceResolver resolver, String path, String 
type, Map<String, Object> data) throws PersistenceException {
+        if (! data.keySet().stream().anyMatch(k -> k.contains(SLASH))) {
+            return ResourceUtil.getOrCreateResource(resolver, path, data, 
type, false);
+        }
+        String returnPath = EMPTY;
+        for (Map.Entry<String, Object> entry : data.entrySet()) {
+            if (entry.getKey().contains(SLASH)) {
+                String deepPath = String.join(SLASH, path, 
StringUtils.substringBeforeLast(entry.getKey(), SLASH));
+                createResource(resolver, deepPath, type, 
Collections.singletonMap(
+                    StringUtils.substringAfterLast(entry.getKey(), SLASH), 
entry.getValue()));
+                if (returnPath.isEmpty() || returnPath.length() > 
deepPath.length()) {
+                    returnPath = deepPath;
+                }
+            }
+        }
+        return resolver.getResource(returnPath);
     }
 
     @Override
diff --git a/src/main/java/org/apache/sling/pipes/internal/PlumberImpl.java 
b/src/main/java/org/apache/sling/pipes/internal/PlumberImpl.java
index 034bed4..27dcc3a 100644
--- a/src/main/java/org/apache/sling/pipes/internal/PlumberImpl.java
+++ b/src/main/java/org/apache/sling/pipes/internal/PlumberImpl.java
@@ -19,6 +19,8 @@ package org.apache.sling.pipes.internal;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.sling.api.SlingConstants;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.request.RequestParameter;
 import org.apache.sling.api.resource.LoginException;
 import org.apache.sling.api.resource.ModifiableValueMap;
 import org.apache.sling.api.resource.PersistenceException;
@@ -33,6 +35,7 @@ import 
org.apache.sling.distribution.SimpleDistributionRequest;
 import org.apache.sling.event.jobs.Job;
 import org.apache.sling.event.jobs.JobManager;
 import org.apache.sling.event.jobs.consumer.JobConsumer;
+import org.apache.sling.pipes.AbstractInputStreamPipe;
 import org.apache.sling.pipes.BasePipe;
 import org.apache.sling.pipes.ExecutionResult;
 import org.apache.sling.pipes.OutputWriter;
@@ -58,6 +61,8 @@ import javax.jcr.RepositoryException;
 import javax.jcr.query.Query;
 import javax.management.MBeanServer;
 import javax.management.ObjectName;
+
+import java.io.IOException;
 import java.lang.management.ManagementFactory;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
@@ -94,6 +99,10 @@ public class PlumberImpl implements Plumber, JobConsumer, 
PlumberMXBean {
 
     protected static final String MBEAN_NAME_FORMAT = 
"org.apache.sling.pipes:name=%s";
 
+    protected static final String PARAM_BINDINGS = "bindings";
+
+    protected static final String PARAM_FILE = "pipes_inputFile";
+
     @ObjectClassDefinition(name="Apache Sling Pipes : Plumber configuration")
     public @interface Configuration {
         @AttributeDefinition(description="Number of iterations after which 
plumber should saves a pipe execution")
@@ -211,6 +220,31 @@ public class PlumberImpl implements Plumber, JobConsumer, 
PlumberMXBean {
     }
 
     @Override
+    public Map<String, Object> getBindingsFromRequest(SlingHttpServletRequest 
request, boolean writeAllowed) throws IOException
+    {
+        Map<String, Object> bindings = new HashMap<>();
+        String dryRun = request.getParameter(BasePipe.DRYRUN_KEY);
+        if (StringUtils.isNotBlank(dryRun) && 
!dryRun.equals(Boolean.FALSE.toString())) {
+            bindings.put(BasePipe.DRYRUN_KEY, true);
+        }
+        String paramBindings = request.getParameter(PARAM_BINDINGS);
+        if (StringUtils.isNotBlank(paramBindings)){
+            try {
+                
bindings.putAll(JsonUtil.unbox(JsonUtil.parseObject(paramBindings)));
+            } catch (Exception e){
+                log.error("Unable to retrieve bindings information", e);
+            }
+        }
+        RequestParameter fileParameter = 
request.getRequestParameter(PARAM_FILE);
+        if (fileParameter != null){
+            bindings.put(AbstractInputStreamPipe.BINDING_IS, 
fileParameter.getInputStream());
+        }
+
+        bindings.put(BasePipe.READ_ONLY, !writeAllowed);
+        return bindings;
+    }
+
+    @Override
     public Job executeAsync(ResourceResolver resolver, String path, 
Map<String, Object> bindings) {
         if (allowedUsers.contains(resolver.getUserID())) {
             return executeAsync(path, bindings);
diff --git a/src/main/java/org/apache/sling/pipes/internal/PlumberServlet.java 
b/src/main/java/org/apache/sling/pipes/internal/PlumberServlet.java
index d174058..4ebbaa6 100644
--- a/src/main/java/org/apache/sling/pipes/internal/PlumberServlet.java
+++ b/src/main/java/org/apache/sling/pipes/internal/PlumberServlet.java
@@ -68,8 +68,6 @@ public class PlumberServlet extends SlingAllMethodsServlet {
 
     protected static final String PARAM_ASYNC = "async";
 
-    protected static final String PARAM_FILE = "pipes_inputFile";
-
     @Reference
     Plumber plumber;
 
@@ -100,7 +98,7 @@ public class PlumberServlet extends SlingAllMethodsServlet {
             if (StringUtils.isBlank(path)) {
                 throw new IllegalArgumentException("path should be provided");
             }
-            Map<String, Object> bindings = getBindingsFromRequest(request, 
writeAllowed);
+            Map<String, Object> bindings = 
plumber.getBindingsFromRequest(request, writeAllowed);
             String asyncParam = request.getParameter(PARAM_ASYNC);
             if (StringUtils.isNotBlank(asyncParam) && 
asyncParam.equals(Boolean.TRUE.toString())){
                 Job job = plumber.executeAsync(request.getResourceResolver(), 
path, bindings);
@@ -120,36 +118,6 @@ public class PlumberServlet extends SlingAllMethodsServlet 
{
     }
 
     /**
-     * Converts request into pipe bindings
-     * @param request from where to extract bindings
-     * @param writeAllowed should we consider this execution is about to 
modify content
-     * @return map of bindings
-     * @throws IOException in case something turns wrong with an input stream
-     */
-    protected Map<String, Object> 
getBindingsFromRequest(SlingHttpServletRequest request, boolean writeAllowed) 
throws IOException{
-        Map<String, Object> bindings = new HashMap<>();
-        String dryRun = request.getParameter(BasePipe.DRYRUN_KEY);
-        if (StringUtils.isNotBlank(dryRun) && 
!dryRun.equals(Boolean.FALSE.toString())) {
-            bindings.put(BasePipe.DRYRUN_KEY, true);
-        }
-        String paramBindings = request.getParameter(PARAM_BINDINGS);
-        if (StringUtils.isNotBlank(paramBindings)){
-            try {
-                
bindings.putAll(JsonUtil.unbox(JsonUtil.parseObject(paramBindings)));
-            } catch (Exception e){
-                log.error("Unable to retrieve bindings information", e);
-            }
-        }
-        RequestParameter fileParameter = 
request.getRequestParameter(PARAM_FILE);
-        if (fileParameter != null){
-            bindings.put(AbstractInputStreamPipe.BINDING_IS, 
fileParameter.getInputStream());
-        }
-
-        bindings.put(BasePipe.READ_ONLY, !writeAllowed);
-        return bindings;
-    }
-
-    /**
      * Retrieve an output writer depending on the request
      * @param request original request against which writers will be tested
      * @param response response writers will point to
diff --git 
a/src/test/java/org/apache/sling/pipes/internal/GogoCommandsTest.java 
b/src/test/java/org/apache/sling/pipes/internal/CommandExecutorImplTest.java
similarity index 62%
rename from src/test/java/org/apache/sling/pipes/internal/GogoCommandsTest.java
rename to 
src/test/java/org/apache/sling/pipes/internal/CommandExecutorImplTest.java
index 6bf25b6..4464978 100644
--- a/src/test/java/org/apache/sling/pipes/internal/GogoCommandsTest.java
+++ b/src/test/java/org/apache/sling/pipes/internal/CommandExecutorImplTest.java
@@ -16,41 +16,51 @@
  */
 package org.apache.sling.pipes.internal;
 
+import org.apache.commons.io.IOUtils;
 import org.apache.sling.api.resource.PersistenceException;
 import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.pipes.AbstractPipeTest;
 import org.apache.sling.pipes.ExecutionResult;
 import org.apache.sling.pipes.PipeBuilder;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
+import 
org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
+import java.io.IOException;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-public class GogoCommandsTest extends AbstractPipeTest {
+import javax.servlet.ServletException;
 
-    GogoCommands commands;
+public class CommandExecutorImplTest extends AbstractPipeTest {
+
+    CommandExecutorImpl commands;
 
     @Before
     public void setup() throws PersistenceException {
         super.setup();
-        commands = new GogoCommands();
+        context = new SlingContext(ResourceResolverType.JCR_OAK);
+        context.load().json("/initial-content/content/fruits.json", 
PATH_FRUITS);
+        commands = new CommandExecutorImpl();
         commands.plumber = plumber;
     }
 
     @Test
     public void testParseTokens(){
-        List<GogoCommands.Token> tokens = commands.parseTokens("some", 
"isolated", "items");
+        List<CommandExecutorImpl.Token> tokens = commands.parseTokens("some", 
"isolated", "items");
         assertEquals("there should be 1 token", 1, tokens.size());
-        GogoCommands.Token token = tokens.get(0);
+        CommandExecutorImpl.Token token = tokens.get(0);
         assertEquals("pipe key should be 'some'","some", token.pipeKey);
         assertEquals("pipe args should be isolated, items", 
Arrays.asList("isolated","items"), token.args);
         String tokenString = "first arg / second firstarg secondarg @ name 
second / third blah";
@@ -61,6 +71,15 @@ public class GogoCommandsTest extends AbstractPipeTest {
     }
 
     @Test
+    public void testKeyValueToArray() {
+        assertArrayEquals(new String[]{"one","two","three","four"}, 
commands.keyValuesToArray(Arrays.asList("one=two","three=four")));
+        assertArrayEquals(new String[]{"one","two","three","${four}"}, 
commands.keyValuesToArray(Arrays.asList("one=two","three=${four}")));
+        assertArrayEquals(new String[]{"one","two","three","${four == 'blah' ? 
'five' : 'six'}"},
+            commands.keyValuesToArray(Arrays.asList("one=two","three=${four == 
'blah' ? 'five' : 'six'}")));
+        assertArrayEquals(new String[]{"jcr:content/singer","${'ringo' == one 
? false : true}"}, 
commands.keyValuesToArray(Arrays.asList("jcr:content/singer=${'ringo' == one ? 
false : true}")));
+    }
+
+    @Test
     public void testSimpleExpression() throws Exception {
         PipeBuilder builder = 
commands.parse(context.resourceResolver(),"echo","/content/fruits");
         assertTrue("there should be a resource", 
builder.build().getOutput().hasNext());
@@ -79,7 +98,7 @@ public class GogoCommandsTest extends AbstractPipeTest {
     public void testOptions() {
         String expected = "works";
         String optionString = "@ name works @ path works @ expr works @ with 
one=works two=works @ outputs one=works two=works";
-        GogoCommands.Options options = 
commands.getOptions(optionString.split("\\s"));
+        CommandExecutorImpl.Options options = 
commands.getOptions(optionString.split("\\s"));
         assertEquals("check name", expected, options.name);
         assertEquals("check expr", expected, options.expr);
         assertEquals("check path", expected, options.path);
@@ -97,7 +116,7 @@ public class GogoCommandsTest extends AbstractPipeTest {
     public void testOptionsListsWithOneItem() {
         String expected = "works";
         String optionString = "@ with one=works @ outputs one=works";
-        GogoCommands.Options options = 
commands.getOptions(optionString.split("\\s"));
+        CommandExecutorImpl.Options options = 
commands.getOptions(optionString.split("\\s"));
         Map bindings = new HashMap();
         CommandUtil.writeToMap(bindings, options.with);
         assertEquals("check with first", expected, bindings.get("one"));
@@ -130,6 +149,39 @@ public class GogoCommandsTest extends AbstractPipeTest {
     public void testExecuteWithWriter() throws Exception {
         PipeBuilder builder = 
plumber.newPipe(context.resourceResolver()).echo("/content/${node}").$("nt:base");
         String path = builder.build().getResource().getPath();
-        ExecutionResult result = 
commands.executeInternal(context.resourceResolver(), path, "@ outputs 
title=jcr:title desc=jcr:description @ with node=fruits");
+        ExecutionResult result = commands.execute(context.resourceResolver(), 
path, "@ outputs title=jcr:title desc=jcr:description @ with node=fruits");
+    }
+
+    String testServlet(Map<String,Object> params) throws ServletException, 
IOException {
+        MockSlingHttpServletRequest request = context.request();
+        MockSlingHttpServletResponse response = context.response();
+        request.setParameterMap(params);
+        request.setMethod("POST");
+        commands.doPost(request, response);
+        if (response.getStatus() != 200) {
+            System.out.println(response.getOutputAsString());
+        }
+        assertEquals(200, response.getStatus());
+        return response.getOutputAsString();
+    }
+
+    @Test
+    public void testSimpleCommandServlet() throws IOException, 
ServletException {
+        Map<String, Object> params = new HashMap<>();
+        params.put(CommandExecutorImpl.REQ_PARAM_CMD, "echo /content / mkdir 
foo / write type=bar");
+        String response = testServlet(params);
+        assertEquals("{\"items\":[\"/content/foo\"],\"size\":1}\n", response);
+    }
+
+    @Test
+    public void testFileCommandServlet() throws IOException, ServletException {
+        Map<String, Object> params = new HashMap<>();
+        params.put(CommandExecutorImpl.REQ_PARAM_FILE, 
IOUtils.toString(getClass().getResourceAsStream("/testcommand"
+            + ".txt"), "UTF-8"));
+        String response = testServlet(params);
+        
assertEquals("{\"items\":[\"/content/beatles/john\",\"/content/beatles/paul\","
+            + "\"/content/beatles/georges\",\"/content/beatles/ringo\"],"
+            + "\"size\":4}\n"
+            + 
"{\"items\":[\"/content/beatles/ringo/jcr:content\"],\"size\":1}\n", response);
     }
 }
\ No newline at end of file
diff --git 
a/src/test/java/org/apache/sling/pipes/internal/PipeBuilderImplTest.java 
b/src/test/java/org/apache/sling/pipes/internal/PipeBuilderImplTest.java
new file mode 100644
index 0000000..bd6475a
--- /dev/null
+++ b/src/test/java/org/apache/sling/pipes/internal/PipeBuilderImplTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.pipes.AbstractPipeTest;
+import org.junit.Test;
+
+public class PipeBuilderImplTest extends AbstractPipeTest {
+    boolean fetchBooleanResource(String path) {
+        Resource res = context.resourceResolver().getResource(path);
+        if (res != null) {
+            Boolean bool = res.adaptTo(Boolean.class);
+            if (bool != null) {
+                return bool;
+            }
+        }
+        return false;
+    }
+    @Test
+    public void testsimpleCreateResource() throws PersistenceException {
+        PipeBuilderImpl impl = new PipeBuilderImpl(context.resourceResolver(), 
plumber);
+        Map<String, Object> map = new HashMap<>();
+        String rootPath = "/content/test";
+        map.put("simple", true);
+        Resource resource = impl.createResource(context.resourceResolver(), 
rootPath, "nt:unstructured", map);
+        context.resourceResolver().commit();
+        assertTrue(fetchBooleanResource(rootPath + "/simple"));
+    }
+
+    @Test
+    public void testcreateResource() throws PersistenceException {
+        PipeBuilderImpl impl = new PipeBuilderImpl(context.resourceResolver(), 
plumber);
+        Map<String, Object> map = new HashMap<>();
+        String rootPath = "/content/test";
+        map.put("one/levelDepth", true);
+        map.put("one/anotherLevel/depth", true);
+        Resource resource = impl.createResource(context.resourceResolver(), 
rootPath, "nt:unstructured", map);
+        assertNotNull(resource);
+        assertEquals("returned resource should be the shallower resource where 
we wrote", rootPath + "/one", resource.getPath());
+        context.resourceResolver().commit();
+        assertTrue(fetchBooleanResource(rootPath + "/one/levelDepth"));
+        assertTrue(fetchBooleanResource(rootPath + "/one/anotherLevel/depth"));
+    }
+}
diff --git a/src/test/resources/testcommand.txt 
b/src/test/resources/testcommand.txt
new file mode 100644
index 0000000..940c815
--- /dev/null
+++ b/src/test/resources/testcommand.txt
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+mkdir /content / json ['john','paul','georges','ringo'] @ name beatle / mkdir 
beatles/${beatle} / write jcr:content/isBeatle=true 
jcr:content/firstName=${beatle} jcr:content/singer=${'ringo'==beatle?false:true}
+
+echo /content / xpath /jcr:root//element(*,nt:base)[@singer='false']
\ No newline at end of file

Reply via email to