SLING-9986 scripting improvements - add possibility to use dates, - rework internals of command tokenization for broader acceptation, and quoting of sub tokens, - add a purge service
diff --git a/src/main/java/org/apache/sling/pipes/PipeBindings.java b/src/main/java/org/apache/sling/pipes/PipeBindings.java index 789e1cb..52c9c1d 100644 --- a/src/main/java/org/apache/sling/pipes/PipeBindings.java +++ b/src/main/java/org/apache/sling/pipes/PipeBindings.java
@@ -17,6 +17,7 @@ package org.apache.sling.pipes; import org.apache.commons.io.IOUtils; +import org.apache.commons.jexl3.JexlException; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; @@ -232,7 +233,7 @@ if (computed != null) { return getEngine() != null ? engine.eval(computed, scriptContext) : internalEvaluate(computed); } - } catch (ScriptException e) { + } catch (ScriptException | JexlException e) { throw new IllegalArgumentException(e); } return expr;
diff --git a/src/main/java/org/apache/sling/pipes/Plumber.java b/src/main/java/org/apache/sling/pipes/Plumber.java index 097539c..c625c6c 100644 --- a/src/main/java/org/apache/sling/pipes/Plumber.java +++ b/src/main/java/org/apache/sling/pipes/Plumber.java
@@ -168,4 +168,9 @@ * @return referenced resource, null otherwise */ @Nullable Resource getReferencedResource(Resource referrer, String reference); + + /* + * Generates unique pipe path for persistence sake + */ + String generateUniquePath(); }
diff --git a/src/main/java/org/apache/sling/pipes/internal/CommandExecutorImpl.java b/src/main/java/org/apache/sling/pipes/internal/CommandExecutorImpl.java index b877b00..b1858bf 100644 --- a/src/main/java/org/apache/sling/pipes/internal/CommandExecutorImpl.java +++ b/src/main/java/org/apache/sling/pipes/internal/CommandExecutorImpl.java
@@ -60,6 +60,7 @@ import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_NOT_ACCEPTABLE; import static javax.servlet.http.HttpServletResponse.SC_OK; +import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.apache.sling.pipes.internal.CommandUtil.keyValuesToArray; import static org.apache.sling.pipes.internal.CommandUtil.writeToMap; @@ -82,8 +83,11 @@ static final String WHITE_SPACE_SEPARATOR = "\\s"; static final String COMMENT_PREFIX = "#"; static final String SEPARATOR = "|"; + static final String PIPE_SEPARATOR = WHITE_SPACE_SEPARATOR + "*\\" + SEPARATOR + WHITE_SPACE_SEPARATOR + "*"; static final String LINE_SEPARATOR = " "; static final String PARAMS = "@"; + static final String PARAMS_SEPARATOR = WHITE_SPACE_SEPARATOR + "+" + PARAMS + WHITE_SPACE_SEPARATOR + "*"; + static final Pattern SUB_TOKEN_PATTERN = Pattern.compile("(([^\"]\\S*)|\"([^\"]+)\")\\s*"); static final String KEY_NAME = "name"; static final String KEY_PATH = "path"; static final String KEY_EXPR = "expr"; @@ -171,7 +175,7 @@ for (String command : cmds) { if (StringUtils.isNotBlank(command)) { currentCommand = command; - PipeBuilder pipeBuilder = parse(resolver, command.split(WHITE_SPACE_SEPARATOR)); + PipeBuilder pipeBuilder = parse(resolver, command); Pipe pipe = pipeBuilder.build(); bindings.put(CMD_LINE_PREFIX + idxLine++, pipe.getResource().getPath()); plumber.execute(resolver, pipe, bindings, pipeWriter, true); @@ -247,21 +251,6 @@ } /** - * 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 */ @@ -314,22 +303,18 @@ */ 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); + String currentKey = null; + List<String> currentList = new ArrayList<>(); + for (String subToken : getSpaceSeparatedTokens(optionToken)) { + if (currentKey == null) { + currentKey = subToken; + } else { + currentList.add(subToken); + } } + finishOption(currentKey, currentList, optionMap); } - finishOption(currentKey, currentList, optionMap); for (Map.Entry<String, Object> entry : optionMap.entrySet()){ switch (entry.getKey()) { case Pipe.PN_NAME : @@ -354,7 +339,6 @@ throw new IllegalArgumentException(String.format("%s is an unknown option", entry.getKey())); } } - } /** @@ -415,39 +399,38 @@ } } + List<String> getSpaceSeparatedTokens(String token) { + List<String> subTokens = new ArrayList<>(); + Matcher matcher = SUB_TOKEN_PATTERN.matcher(token); + while (matcher.find()){ + subTokens.add(matcher.group(2) != null ? matcher.group(2) : matcher.group(3)); + } + return subTokens; + } + /** * @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); + String cat = String.join(EMPTY, commands); + for (String token : cat.split(PIPE_SEPARATOR)){ + CommandExecutorImpl.Token currentToken = new CommandExecutorImpl.Token(); + String[] options = token.split(PARAMS_SEPARATOR); + if (options.length > 1) { + currentToken.options = getOptions(Arrays.copyOfRange(options, 1, options.length)); + } + List<String> subTokens = getSpaceSeparatedTokens(options[0]); + if (subTokens.size() > 0) { + currentToken.pipeKey = subTokens.get(0); + if (subTokens.size() > 1) { + currentToken.args = subTokens.subList(1, subTokens.size()); } } + log.trace("generated following token {}", currentToken); + returnValue.add(currentToken); } - finishToken(currentToken, currentList); return returnValue; }
diff --git a/src/main/java/org/apache/sling/pipes/internal/CommandUtil.java b/src/main/java/org/apache/sling/pipes/internal/CommandUtil.java index 40f0c68..c21004c 100644 --- a/src/main/java/org/apache/sling/pipes/internal/CommandUtil.java +++ b/src/main/java/org/apache/sling/pipes/internal/CommandUtil.java
@@ -38,11 +38,12 @@ static final String FIRST_TOKEN = "first"; static final String SECOND_TOKEN = "second"; private static final Pattern UNEMBEDDEDSCRIPT_PATTERN = Pattern.compile("^(\\d+(\\.\\d+)?)|" + //21.42 - "(\\[.*])|" + //['one','two'] - "(\\w[\\w_\\-\\d]+\\.[\\w_\\-\\d]+)|" + //map.field + "(\\[.*]$)|" + //['one','two'] + "(\\w[\\w_\\-\\d]+\\..+)|" + //map.field... "(\\w[\\w_\\-\\d]+\\['.+'])|" + //map['field'] - "(true|false)|" + //boolean - "'.*'$"); // 'string' + "(true$|false$)|" + //boolean + "(new .*)|" + //instantiation + "(.*'$)"); // 'string' static final String CONFIGURATION_TOKEN = "(?<" + FIRST_TOKEN + ">[\\w/\\:]+)\\s*" + KEY_VALUE_SEP + "(?<" + SECOND_TOKEN + ">[(\\w*)|" + INJECTED_SCRIPT_REGEXP + "]+)"; public static final Pattern CONFIGURATION_PATTERN = Pattern.compile(CONFIGURATION_TOKEN); @@ -109,11 +110,13 @@ */ public static 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)); + if (o != null) { + 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()]);
diff --git a/src/main/java/org/apache/sling/pipes/internal/JsonWriter.java b/src/main/java/org/apache/sling/pipes/internal/JsonWriter.java index e5d3d3f..37d7f11 100644 --- a/src/main/java/org/apache/sling/pipes/internal/JsonWriter.java +++ b/src/main/java/org/apache/sling/pipes/internal/JsonWriter.java
@@ -71,9 +71,8 @@ jsonGenerator.writeStartObject(); jsonGenerator.write(PATH_KEY, resource.getPath()); for (Map.Entry<String, Object> entry : customOutputs.entrySet()) { - Object o = null; try { - o = pipe.getBindings().instantiateObject((String) entry.getValue()); + Object o = pipe.getBindings().instantiateObject((String) entry.getValue()); if (o instanceof JsonValue) { jsonGenerator.write(entry.getKey(), (JsonValue) o); } else { @@ -81,7 +80,7 @@ } } catch (RuntimeException e) { LOGGER.error("unable to write entry {}, will write empty value", entry, e); - jsonGenerator.write(StringUtils.EMPTY); + jsonGenerator.write(entry.getKey(), StringUtils.EMPTY); } } jsonGenerator.writeEnd();
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 d272faa..b4d8196 100644 --- a/src/main/java/org/apache/sling/pipes/internal/PipeBuilderImpl.java +++ b/src/main/java/org/apache/sling/pipes/internal/PipeBuilderImpl.java
@@ -43,12 +43,10 @@ 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; @@ -64,8 +62,6 @@ public class PipeBuilderImpl implements PipeBuilder { private static final Logger logger = LoggerFactory.getLogger(PipeBuilderImpl.class); - public static final String PIPES_REPOSITORY_PATH = "/var/pipes"; - private static final String[] DEFAULT_NAMES = new String[]{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"}; List<Step> steps; @@ -319,16 +315,6 @@ } /** - * build a time + random based path under /var/pipes - * @return full path of future Pipe - */ - String buildRandomPipePath() { - final Calendar now = Calendar.getInstance(); - return PIPES_REPOSITORY_PATH + '/' + now.get(Calendar.YEAR) + '/' + now.get(Calendar.MONTH) + '/' + now.get(Calendar.DAY_OF_MONTH) + "/" - + UUID.randomUUID().toString(); - } - - /** * Create a configuration resource * @param resolver current resolver * @param path path of the resource @@ -364,7 +350,7 @@ @Override public Pipe build() throws PersistenceException { - return build(buildRandomPipePath()); + return build(plumber.generateUniquePath()); } /**
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 dd9eacd..f51155a 100644 --- a/src/main/java/org/apache/sling/pipes/internal/PlumberImpl.java +++ b/src/main/java/org/apache/sling/pipes/internal/PlumberImpl.java
@@ -21,6 +21,7 @@ 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.AbstractResourceVisitor; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.ModifiableValueMap; import org.apache.sling.api.resource.PersistenceException; @@ -69,6 +70,8 @@ import java.lang.management.ManagementFactory; import java.lang.reflect.Method; import java.security.AccessControlException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -80,6 +83,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.UUID; import static org.apache.sling.api.resource.ResourceResolverFactory.SUBSERVICE; import static org.apache.sling.pipes.BasePipe.PN_STATUS; @@ -91,11 +95,11 @@ /** * implements plumber interface, registers default pipes, and provides execution facilities */ -@Component(service = {Plumber.class, JobConsumer.class}, property = { +@Component(service = {Plumber.class, JobConsumer.class, PlumberMXBean.class, Runnable.class}, property = { JobConsumer.PROPERTY_TOPICS +"="+PlumberImpl.SLING_EVENT_TOPIC }) @Designate(ocd = PlumberImpl.Configuration.class) -public class PlumberImpl implements Plumber, JobConsumer, PlumberMXBean { +public class PlumberImpl implements Plumber, JobConsumer, PlumberMXBean, Runnable { private final Logger log = LoggerFactory.getLogger(this.getClass()); public static final int DEFAULT_BUFFER_SIZE = 1000; @@ -110,6 +114,8 @@ static final String PERMISSION_EXECUTION = "/system/sling/permissions/pipes/exec"; + public static final String PIPES_REPOSITORY_PATH = "/var/pipes"; + @ObjectClassDefinition(name="Apache Sling Pipes : Plumber configuration") public @interface Configuration { @AttributeDefinition(description="Number of iterations after which plumber should saves a pipe execution") @@ -129,6 +135,12 @@ @AttributeDefinition(description = "Paths to search for references in") String[] referencesPaths() default {}; + + @AttributeDefinition(description = "max age (in days) of automatically generated pipe persistence") + int maxAge() default 31; + + @AttributeDefinition(description = "schedule of purge process") + String scheduler_expression() default "0 0 12 */7 * ?"; } @Reference(policy= ReferencePolicy.DYNAMIC, cardinality= ReferenceCardinality.OPTIONAL) @@ -545,4 +557,60 @@ } return beans; } + + /** + * build a time + random based path under /var/pipes + * @return full path of future Pipe + */ + public String generateUniquePath() { + final Calendar now = Calendar.getInstance(); + return PIPES_REPOSITORY_PATH + '/' + now.get(Calendar.YEAR) + '/' + now.get(Calendar.MONTH) + '/' + now.get(Calendar.DAY_OF_MONTH) + "/" + + UUID.randomUUID().toString(); + } + + void cleanResourceAndEmptyParents(Resource resource) throws PersistenceException { + log.debug("starting removal of {}", resource); + Resource parent = resource.getParent(); + resource.getResourceResolver().delete(resource); + if (!parent.hasChildren() && !PIPES_REPOSITORY_PATH.equals(parent.getPath())) { + cleanResourceAndEmptyParents(parent); + } + } + + void purge(ResourceResolver resolver, Instant now, int maxDays) throws PersistenceException { + final Collection<String> pipesToRemove = new ArrayList<>(); + AbstractResourceVisitor visitor = new AbstractResourceVisitor() { + @Override + protected void visit(Resource res) { + Calendar cal = res.getValueMap().get(PN_STATUS_MODIFIED, Calendar.class); + if (cal != null && ChronoUnit.DAYS.between(cal.toInstant(), now) > maxDays) { + pipesToRemove.add(res.getPath()); + } + } + }; + visitor.accept(resolver.getResource(PIPES_REPOSITORY_PATH)); + if (pipesToRemove.size() > 0) { + log.info("about to remove {} pipe instances", pipesToRemove.size()); + for (String path : pipesToRemove) { + cleanResourceAndEmptyParents(resolver.getResource(path)); + } + resolver.commit(); + log.info("purge done."); + } + } + + @Override + public void run() { + if (serviceUser == null) { + log.warn("no service user configured, will not be able to purge old pipe instances"); + } else { + try (ResourceResolver resolver = factory.getServiceResourceResolver(serviceUser)) { + log.info("Starting pipe purge based on a max age of {} days", configuration.maxAge()); + purge(resolver, Instant.now(), configuration.maxAge()); + resolver.commit(); + } catch (LoginException | PersistenceException e) { + log.error("unable purge {}", PIPES_REPOSITORY_PATH, e); + } + } + } } \ No newline at end of file
diff --git a/src/main/java/org/apache/sling/pipes/internal/bindings/JxltEngine.java b/src/main/java/org/apache/sling/pipes/internal/bindings/JxltEngine.java index ff84801..220d801 100644 --- a/src/main/java/org/apache/sling/pipes/internal/bindings/JxltEngine.java +++ b/src/main/java/org/apache/sling/pipes/internal/bindings/JxltEngine.java
@@ -16,6 +16,8 @@ */ package org.apache.sling.pipes.internal.bindings; +import java.text.DateFormat; +import java.util.Date; import java.util.Map; import org.apache.commons.jexl3.JexlBuilder; @@ -28,10 +30,12 @@ JexlEngine jexl; JexlContext jc; + static final String KEY_TIME = "timeutil"; public JxltEngine(Map<String, Object> context) { jexl = new JexlBuilder().create(); jc = new MapContext(context); + jc.set(KEY_TIME, new TimeUtil()); } public Object parse(String expression) {
diff --git a/src/main/java/org/apache/sling/pipes/internal/bindings/TimeUtil.java b/src/main/java/org/apache/sling/pipes/internal/bindings/TimeUtil.java new file mode 100644 index 0000000..22455e6 --- /dev/null +++ b/src/main/java/org/apache/sling/pipes/internal/bindings/TimeUtil.java
@@ -0,0 +1,51 @@ +/* + * 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.bindings; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; + +/** + * Utility class to be used in bindings + */ +public class TimeUtil { + + /** + * @param date string date following <code>ISO_LOCAL_DATE</code> + * @return Gregorian calendar impl of that time + */ + public Calendar ofDate(String date) { + LocalDate localDate = LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE); + return GregorianCalendar.from(localDate.atStartOfDay(ZoneId.systemDefault())); + } + + /** + * @param time string date following <code>ISO_DATE_TIME</code> + * @return Gregorian calendar impl of that time + */ + public Calendar of(String time){ + OffsetDateTime dt = OffsetDateTime.parse(time); + return GregorianCalendar.from(dt.toZonedDateTime()); + } +}
diff --git a/src/test/java/org/apache/sling/pipes/AbstractPipeTest.java b/src/test/java/org/apache/sling/pipes/AbstractPipeTest.java index 711a07a..105ec2f 100644 --- a/src/test/java/org/apache/sling/pipes/AbstractPipeTest.java +++ b/src/test/java/org/apache/sling/pipes/AbstractPipeTest.java
@@ -102,7 +102,7 @@ } protected ExecutionResult execute(ResourceResolver resolver, String command) throws InvocationTargetException, IllegalAccessException { - PipeBuilder builder = commandsExecutor.parse(resolver, command.trim().split("\\s")); + PipeBuilder builder = commandsExecutor.parse(resolver, command); return builder.run(); }
diff --git a/src/test/java/org/apache/sling/pipes/internal/CommandExecutorImplTest.java b/src/test/java/org/apache/sling/pipes/internal/CommandExecutorImplTest.java index 7362286..85e44c9 100644 --- a/src/test/java/org/apache/sling/pipes/internal/CommandExecutorImplTest.java +++ b/src/test/java/org/apache/sling/pipes/internal/CommandExecutorImplTest.java
@@ -38,6 +38,7 @@ import java.util.Map; import java.util.stream.Collectors; +import static org.apache.sling.pipes.internal.CommandExecutorImpl.PARAMS_SEPARATOR; import static org.apache.sling.pipes.internal.CommandUtil.keyValuesToArray; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -60,20 +61,37 @@ } @Test - public void testParseTokens(){ - List<CommandExecutorImpl.Token> tokens = commands.parseTokens("some", "isolated", "items"); + public void getSublist() { + assertArrayEquals(new String[]{"check", "this"}, commands.getSpaceSeparatedTokens("check this").toArray()); + assertArrayEquals(new String[]{"and", "now","check this"}, commands.getSpaceSeparatedTokens("and now \"check this\"").toArray()); + } + + @Test + public void testParseTokens() { + List<CommandExecutorImpl.Token> tokens = commands.parseTokens("some isolated items"); assertEquals("there should be 1 token", 1, tokens.size()); 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); + assertEquals("pipe key should be 'some'", "some", token.pipeKey); + assertEquals("pipe args should be isolated, items", Arrays.asList("isolated", "items"), token.args); + } + @Test + public void testParseTokensWithQuotes() { String tokenString = "first arg | second firstarg secondarg @ name second | third blah"; - tokens = commands.parseTokens(tokenString.split("\\s")); + List<CommandExecutorImpl.Token> tokens = commands.parseTokens(tokenString); assertEquals("there should be 3 tokens", 3, tokens.size()); assertEquals("keys check", Arrays.asList("first","second", "third"), tokens.stream().map(t -> t.pipeKey).collect(Collectors.toList())); assertEquals("params check", "second", tokens.get(1).options.name); } @Test + public void testQuotedTokens() { + List<CommandExecutorImpl.Token> tokens = commands.parseTokens("some isolated items \"with quotes\""); + assertEquals("there should be 1 token", 1, tokens.size()); + CommandExecutorImpl.Token token = tokens.get(0); + assertEquals("pipe args should be isolated, items", Arrays.asList("isolated", "items", "with quotes"), token.args); + } + + @Test public void testKeyValueToArray() { assertArrayEquals(new String[]{"one","two","three","four"}, keyValuesToArray(Arrays.asList("one=two","three=four"))); assertArrayEquals(new String[]{"one","two","three","${four}"}, keyValuesToArray(Arrays.asList("one=two","three=${four}"))); @@ -84,13 +102,13 @@ @Test public void testSimpleExpression() throws Exception { - PipeBuilder builder = commands.parse(context.resourceResolver(),"echo","/content/fruits"); + PipeBuilder builder = commands.parse(context.resourceResolver(),"echo /content/fruits"); assertTrue("there should be a resource", builder.build().getOutput().hasNext()); } @Test public void testSimpleChainedConf() throws Exception { - PipeBuilder builder = commands.parse(context.resourceResolver(),"echo /content/fruits | write some=test key=value".split("\\s")); + PipeBuilder builder = commands.parse(context.resourceResolver(),"echo /content/fruits | write some=test key=value"); assertNotNull("there should be a resource", builder.run()); ValueMap props = context.currentResource(PATH_FRUITS).getValueMap(); assertEquals("there should some=test", "test", props.get("some")); @@ -100,8 +118,8 @@ @Test public void testOptions() { String expected = "works"; - String optionString = "@ name works @ path works @ expr works @ with one=works two=works @ outputs one=works two=works"; - CommandExecutorImpl.Options options = commands.getOptions(optionString.split("\\s")); + String optionString = "name works @ path works @ expr works @ with one=works two=works @ outputs one=works two=works"; + CommandExecutorImpl.Options options = commands.getOptions(optionString.split(PARAMS_SEPARATOR)); assertEquals("check name", expected, options.name); assertEquals("check expr", expected, options.expr); assertEquals("check path", expected, options.path); @@ -118,8 +136,8 @@ @Test public void testOptionsListsWithOneItem() { String expected = "works"; - String optionString = "@ with one=works @ outputs one=works"; - CommandExecutorImpl.Options options = commands.getOptions(optionString.split("\\s")); + String optionString = "with one=works @ outputs one=works"; + CommandExecutorImpl.Options options = commands.getOptions(optionString.split(PARAMS_SEPARATOR)); Map bindings = new HashMap(); CommandUtil.writeToMap(bindings, true, options.with); assertEquals("check with first", expected, bindings.get("one")); @@ -131,7 +149,7 @@ @Test public void testChainedConfWithInternalOptions() throws Exception { PipeBuilder builder = commands.parse(context.resourceResolver(), - "echo /content/fruits @ name fruits | write some=${path.fruits} key=value".split("\\s")); + "echo /content/fruits @ name fruits | write some=${path.fruits} key=value"); assertNotNull("there should be a resource", builder.run()); ValueMap props = context.currentResource(PATH_FRUITS).getValueMap(); assertEquals("there should some=/content/fruits", PATH_FRUITS, props.get("some")); @@ -142,7 +160,7 @@ public void adaptToDemoTest() throws Exception { String url = "'http://99-bottles-of-beer.net/lyrics.html'"; String cmd = "egrep " + url + " @ name bottles @ with pattern=(?<number>\\d(\\d)?) | mkdir /var/bottles/${bottles.number}"; - PipeBuilder builder = commands.parse(context.resourceResolver(), cmd.split("\\s")); + PipeBuilder builder = commands.parse(context.resourceResolver(), cmd); ContainerPipe pipe = (ContainerPipe)builder.build(); ValueMap regexp = pipe.getResource().getChild("conf/bottles").getValueMap(); assertEquals("we expect expr to be the url", url, regexp.get("expr")); @@ -152,7 +170,8 @@ 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.execute(context.resourceResolver(), path, "@ outputs title=jcr:title desc=jcr:description @ with node=fruits"); + ExecutionResult result = commands.execute(context.resourceResolver(), path, "outputs title=two['jcr:title'] desc=two['jcr:description']", "with node=fruits"); + assertNotNull(result); } String testServlet(Map<String,Object> params) throws ServletException, IOException {
diff --git a/src/test/java/org/apache/sling/pipes/internal/CommandUtilTest.java b/src/test/java/org/apache/sling/pipes/internal/CommandUtilTest.java index fc3f202..7d79464 100644 --- a/src/test/java/org/apache/sling/pipes/internal/CommandUtilTest.java +++ b/src/test/java/org/apache/sling/pipes/internal/CommandUtilTest.java
@@ -28,6 +28,7 @@ assertEquals("/path/left/0/un-touc_hed", CommandUtil.embedIfNeeded("/path/left/0/un-touc_hed")); assertEquals("/content/json/array/${json.test}", CommandUtil.embedIfNeeded("/content/json/array/${json.test}")); assertEquals("${vegetables['jcr:title']}", CommandUtil.embedIfNeeded("vegetables['jcr:title']")); + assertEquals("${new Date(\"2018-05-05T11:50:55\")}", CommandUtil.embedIfNeeded("new Date(\"2018-05-05T11:50:55\")")); assertEquals("${some + wellformed + script}", CommandUtil.embedIfNeeded("${some + wellformed + script}")); assertEquals("${['one','two']}", CommandUtil.embedIfNeeded("['one','two']")); assertEquals("${true}", CommandUtil.embedIfNeeded("true"));
diff --git a/src/test/java/org/apache/sling/pipes/internal/PlumberImplTest.java b/src/test/java/org/apache/sling/pipes/internal/PlumberImplTest.java new file mode 100644 index 0000000..fc4d0af --- /dev/null +++ b/src/test/java/org/apache/sling/pipes/internal/PlumberImplTest.java
@@ -0,0 +1,43 @@ +/* + * 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.sling.api.resource.PersistenceException; +import org.apache.sling.pipes.AbstractPipeTest; +import org.junit.Test; + +import java.lang.reflect.InvocationTargetException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class PlumberImplTest extends AbstractPipeTest { + + @Test + public void testPurge() throws InvocationTargetException, IllegalAccessException, PersistenceException { + execute("mkdir /var/pipes/this/is/going/away | write statusModified=timeutil.of('2018-05-05T11:50:55+01:00')"); + execute("mkdir /var/pipes/this/is/also/going/away| write statusModified=timeutil.of('2018-05-05T11:50:55+01:00')"); + String recentDate = Instant.now().minus(15, ChronoUnit.DAYS).toString(); + execute("mkdir /var/pipes/this/should/stay| write statusModified=timeutil.of('" + recentDate + "')"); + assertNotNull("checking that part of the tree has been created", context.resourceResolver().getResource("/var/pipes/this/is")); + ((PlumberImpl)plumber).purge(context.resourceResolver(), Instant.now(), 30); + assertNull("there should be no more /var/pipes/this/is resource", context.resourceResolver().getResource("/var/pipes/this/is")); + assertNotNull("there should still be /var/pipes/this/should/stay resource", context.resourceResolver().getResource("/var/pipes/this/should/stay")); + } +} \ No newline at end of file
diff --git a/src/test/java/org/apache/sling/pipes/internal/WritePipeTest.java b/src/test/java/org/apache/sling/pipes/internal/WritePipeTest.java index 43981ce..a65d632 100644 --- a/src/test/java/org/apache/sling/pipes/internal/WritePipeTest.java +++ b/src/test/java/org/apache/sling/pipes/internal/WritePipeTest.java
@@ -30,6 +30,11 @@ import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.RepositoryException; +import java.lang.reflect.InvocationTargetException; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalUnit; +import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; @@ -185,4 +190,13 @@ testIfNode("some random string", true); testIfNode(true, true); } + + @Test + public void testWriteDate() throws InvocationTargetException, IllegalAccessException { + execute("echo /content | write date=timeutil.of('2018-05-05T11:50:55+02:00')"); + ValueMap props = context.resourceResolver().getResource("/content").getValueMap(); + Calendar cal = props.get("date", Calendar.class); + assertNotNull(cal); + assertEquals(2018, cal.get(GregorianCalendar.YEAR)); + } }
diff --git a/src/test/java/org/apache/sling/pipes/internal/bindings/TimeUtilTest.java b/src/test/java/org/apache/sling/pipes/internal/bindings/TimeUtilTest.java new file mode 100644 index 0000000..d72a7d7 --- /dev/null +++ b/src/test/java/org/apache/sling/pipes/internal/bindings/TimeUtilTest.java
@@ -0,0 +1,45 @@ +/* + * 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.bindings; + +import org.apache.sling.pipes.AbstractPipeTest; +import org.junit.Test; + +import java.util.Calendar; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class TimeUtilTest extends AbstractPipeTest { + + @Test + public void testOfDate() { + TimeUtil timeUtil = new TimeUtil(); + Calendar cal = timeUtil.ofDate("2012-12-02"); + assertNotNull(cal); + assertEquals(2012, cal.get(Calendar.YEAR)); + } + + @Test + public void testOf() { + TimeUtil timeUtil = new TimeUtil(); + Calendar cal = timeUtil.of("2012-12-02T12:30:20+02:00"); + assertNotNull(cal); + assertEquals(2012, cal.get(Calendar.YEAR)); + assertNotNull(timeUtil.of("2012-12-30T09:20:26Z")); + } +} \ No newline at end of file