| /* |
| * 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 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.security.AccessControlException; |
| 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.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.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 static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; |
| 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; |
| |
| 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 CMD_LINE_PREFIX = "cmd_line_"; |
| 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; |
| |
| String help; |
| |
| @Reference |
| Plumber plumber; |
| |
| @Activate |
| @Modified |
| public void activate(){ |
| methodMap = null; |
| executorMap = null; |
| help = null; |
| } |
| |
| boolean isCommandCandidate(String line) { |
| return StringUtils.isNotBlank(line) && !line.startsWith(COMMENT_PREFIX); |
| } |
| |
| 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; |
| } |
| |
| @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!"); |
| } |
| short idx_line = 0; |
| for (String command : cmds) { |
| if (StringUtils.isNotBlank(command)) { |
| JsonWriter pipeWriter = new JsonWriter(); |
| pipeWriter.starts(); |
| currentCommand = command; |
| PipeBuilder pipeBuilder = parse(resolver, command.split(WHITE_SPACE_SEPARATOR)); |
| Pipe pipe = pipeBuilder.build(); |
| bindings.put(CMD_LINE_PREFIX + idx_line++, pipe.getResource().getPath()); |
| writer.println(plumber.execute(resolver, pipe, bindings, pipeWriter, true)); |
| } |
| } |
| response.setStatus(SC_OK); |
| } |
| catch (AccessControlException e) { |
| response.sendError(SC_FORBIDDEN); |
| } |
| catch (IllegalAccessException | InvocationTargetException e) { |
| writer.println("Error executing " + currentCommand); |
| e.printStackTrace(writer); |
| response.sendError(SC_INTERNAL_SERVER_ERROR); |
| writer.println(help()); |
| } |
| } |
| |
| @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)); |
| } |
| 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); |
| } |
| |
| @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); |
| 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; |
| |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * @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); |
| } |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| |
| } |