From 9ae8498d33ca86b8bdb8fd0f7c505eec6e521d0e Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti Date: Thu, 7 Nov 2024 16:48:32 +0100 Subject: [PATCH] [Fix #460] Implementing input, output and context Signed-off-by: Francisco Javier Tirado Sarti --- ...tils.java => DefaultWorkflowPosition.java} | 42 ++- .../serverlessworkflow/impl/TaskContext.java | 68 +++++ .../impl/WorkflowContext.java | 70 +++++ .../impl/WorkflowDefinition.java | 43 ++- .../impl/WorkflowExecutionListener.java | 5 +- ...{Expression.java => WorkflowPosition.java} | 11 +- .../impl/executors/AbstractTaskExecutor.java | 117 ++++++++ .../DefaultTaskExecutorFactory.java | 5 +- .../impl/{ => executors}/HttpExecutor.java | 101 ++++--- .../impl/{ => executors}/TaskExecutor.java | 8 +- .../{ => executors}/TaskExecutorFactory.java | 2 +- .../Expression.java} | 27 +- .../{ => expressions}/ExpressionFactory.java | 8 +- .../impl/expressions/ExpressionUtils.java | 78 +++++ .../ExpressionValidationException.java | 14 + .../{jq => expressions}/JQExpression.java | 15 +- .../JQExpressionFactory.java | 5 +- .../impl/expressions/ProxyMap.java | 278 ++++++++++++++++++ .../impl/{ => json}/JsonUtils.java | 8 +- .../impl/{ => json}/MergeUtils.java | 2 +- .../impl/WorkflowDefinitionTest.java | 13 +- .../resources/call-http-query-parameters.yaml | 23 ++ .../{callHttp.yaml => callGetHttp.yaml} | 0 impl/src/test/resources/callPostHttp.yaml | 28 ++ 24 files changed, 847 insertions(+), 124 deletions(-) rename impl/src/main/java/io/serverlessworkflow/impl/{ExpressionUtils.java => DefaultWorkflowPosition.java} (50%) create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java rename impl/src/main/java/io/serverlessworkflow/impl/{Expression.java => WorkflowPosition.java} (79%) create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java rename impl/src/main/java/io/serverlessworkflow/impl/{ => executors}/DefaultTaskExecutorFactory.java (90%) rename impl/src/main/java/io/serverlessworkflow/impl/{ => executors}/HttpExecutor.java (51%) rename impl/src/main/java/io/serverlessworkflow/impl/{ => executors}/TaskExecutor.java (74%) rename impl/src/main/java/io/serverlessworkflow/impl/{ => executors}/TaskExecutorFactory.java (94%) rename impl/src/main/java/io/serverlessworkflow/impl/{AbstractTaskExecutor.java => expressions/Expression.java} (57%) rename impl/src/main/java/io/serverlessworkflow/impl/{ => expressions}/ExpressionFactory.java (83%) create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java rename impl/src/main/java/io/serverlessworkflow/impl/{jq => expressions}/JQExpression.java (94%) rename impl/src/main/java/io/serverlessworkflow/impl/{jq => expressions}/JQExpressionFactory.java (90%) create mode 100644 impl/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java rename impl/src/main/java/io/serverlessworkflow/impl/{ => json}/JsonUtils.java (97%) rename impl/src/main/java/io/serverlessworkflow/impl/{ => json}/MergeUtils.java (98%) create mode 100644 impl/src/test/resources/call-http-query-parameters.yaml rename impl/src/test/resources/{callHttp.yaml => callGetHttp.yaml} (100%) create mode 100644 impl/src/test/resources/callPostHttp.yaml diff --git a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java similarity index 50% rename from impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java rename to impl/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java index 45000931..2e51f6a6 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/DefaultWorkflowPosition.java @@ -15,26 +15,38 @@ */ package io.serverlessworkflow.impl; -public class ExpressionUtils { +public class DefaultWorkflowPosition implements WorkflowPosition { - private static final String EXPR_PREFIX = "${"; - private static final String EXPR_SUFFIX = "}"; + private StringBuilder sb = new StringBuilder(""); - private ExpressionUtils() {} + @Override + public WorkflowPosition addIndex(int index) { + sb.append('/').append(index); + return this; + } - public static String trimExpr(String expr) { - expr = expr.trim(); - if (expr.startsWith(EXPR_PREFIX)) { - expr = trimExpr(expr, EXPR_PREFIX, EXPR_SUFFIX); - } - return expr.trim(); + @Override + public WorkflowPosition addProperty(String prop) { + sb.append('/').append(prop); + return this; + } + + @Override + public String jsonPointer() { + return sb.toString(); + } + + @Override + public String toString() { + return "DefaultWorkflowPosition [sb=" + sb + "]"; } - private static String trimExpr(String expr, String prefix, String suffix) { - expr = expr.substring(prefix.length()); - if (expr.endsWith(suffix)) { - expr = expr.substring(0, expr.length() - suffix.length()); + @Override + public WorkflowPosition back() { + int indexOf = sb.lastIndexOf("/"); + if (indexOf != -1) { + sb.substring(0, indexOf); } - return expr; + return this; } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java b/impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java new file mode 100644 index 00000000..c9e28f12 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/TaskContext.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed 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 io.serverlessworkflow.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.TaskBase; + +public class TaskContext { + + private final JsonNode rawInput; + private final T task; + + private JsonNode input; + private JsonNode output; + private JsonNode rawOutput; + + public TaskContext(JsonNode rawInput, T task) { + this.rawInput = rawInput; + this.input = rawInput; + this.task = task; + } + + public void input(JsonNode input) { + this.input = input; + } + + public JsonNode input() { + return input; + } + + public JsonNode rawInput() { + return rawInput; + } + + public T task() { + return task; + } + + public void rawOutput(JsonNode output) { + this.rawOutput = output; + this.output = output; + } + + public void output(JsonNode output) { + this.output = output; + } + + public JsonNode output() { + return output; + } + + public JsonNode rawOutput() { + return rawOutput; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java new file mode 100644 index 00000000..6982cfd6 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed 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 io.serverlessworkflow.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.impl.json.JsonUtils; + +public class WorkflowContext { + + private final WorkflowPosition position; + private JsonNode context; + private final JsonNode input; + + private WorkflowContext(WorkflowPosition position, JsonNode input) { + this.position = position; + this.input = input; + this.context = JsonUtils.mapper().createObjectNode(); + } + + public static Builder builder(JsonNode input) { + return new Builder(input); + } + + public static class Builder { + private WorkflowPosition position = new DefaultWorkflowPosition(); + private JsonNode input; + + private Builder(JsonNode input) { + this.input = input; + } + + public Builder position(WorkflowPosition position) { + this.position = position; + return this; + } + + public WorkflowContext build() { + return new WorkflowContext(position, input); + } + } + + public WorkflowPosition position() { + return position; + } + + public JsonNode context() { + return context; + } + + public void context(JsonNode context) { + this.context = context; + } + + public JsonNode rawInput() { + return input; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java index f926a755..ec39c90b 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java @@ -15,13 +15,16 @@ */ package io.serverlessworkflow.impl; -import static io.serverlessworkflow.impl.JsonUtils.*; +import static io.serverlessworkflow.impl.json.JsonUtils.*; -import com.fasterxml.jackson.core.JsonPointer; import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.TaskBase; import io.serverlessworkflow.api.types.TaskItem; import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.executors.DefaultTaskExecutorFactory; +import io.serverlessworkflow.impl.executors.TaskExecutor; +import io.serverlessworkflow.impl.executors.TaskExecutorFactory; +import io.serverlessworkflow.impl.json.JsonUtils; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -43,7 +46,7 @@ private WorkflowDefinition( private final Workflow workflow; private final Collection listeners; private final TaskExecutorFactory taskFactory; - private final Map> taskExecutors = + private final Map> taskExecutors = new ConcurrentHashMap<>(); public static class Builder { @@ -94,40 +97,32 @@ enum State { public class WorkflowInstance { - private final JsonNode input; private JsonNode output; private State state; - - private JsonPointer currentPos; + private WorkflowContext context; private WorkflowInstance(TaskExecutorFactory factory, JsonNode input) { - this.input = input; - this.output = object(); + this.output = input; this.state = State.STARTED; - this.currentPos = JsonPointer.compile("/"); + this.context = WorkflowContext.builder(input).build(); processDo(workflow.getDo()); } private void processDo(List tasks) { - currentPos = currentPos.appendProperty("do"); + context.position().addProperty("do"); int index = 0; for (TaskItem task : tasks) { - currentPos = currentPos.appendIndex(index).appendProperty(task.getName()); - listeners.forEach(l -> l.onTaskStarted(currentPos, task.getTask())); + context.position().addIndex(++index).addProperty(task.getName()); + listeners.forEach(l -> l.onTaskStarted(context.position(), task.getTask())); this.output = - MergeUtils.merge( - taskExecutors - .computeIfAbsent(currentPos, k -> taskFactory.getTaskExecutor(task.getTask())) - .apply(input), - output); - listeners.forEach(l -> l.onTaskEnded(currentPos, task.getTask())); - currentPos = currentPos.head().head(); + taskExecutors + .computeIfAbsent( + context.position().jsonPointer(), + k -> taskFactory.getTaskExecutor(task.getTask())) + .apply(context, output); + listeners.forEach(l -> l.onTaskEnded(context.position(), task.getTask())); + context.position().back().back(); } - currentPos = currentPos.head(); - } - - public String currentPos() { - return currentPos.toString(); } public State state() { diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java index 700c6aa9..ce72c70e 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java @@ -15,12 +15,11 @@ */ package io.serverlessworkflow.impl; -import com.fasterxml.jackson.core.JsonPointer; import io.serverlessworkflow.api.types.Task; public interface WorkflowExecutionListener { - void onTaskStarted(JsonPointer currentPos, Task task); + void onTaskStarted(WorkflowPosition currentPos, Task task); - void onTaskEnded(JsonPointer currentPos, Task task); + void onTaskEnded(WorkflowPosition currentPos, Task task); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/Expression.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java similarity index 79% rename from impl/src/main/java/io/serverlessworkflow/impl/Expression.java rename to impl/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java index b5bbfc0b..c43d4b2f 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/Expression.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java @@ -15,8 +15,13 @@ */ package io.serverlessworkflow.impl; -import com.fasterxml.jackson.databind.JsonNode; +public interface WorkflowPosition { -public interface Expression { - JsonNode eval(JsonNode input); + String jsonPointer(); + + WorkflowPosition addProperty(String prop); + + WorkflowPosition addIndex(int index); + + WorkflowPosition back(); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java new file mode 100644 index 00000000..36dbbf4f --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java @@ -0,0 +1,117 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed 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 io.serverlessworkflow.impl.executors; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.Export; +import io.serverlessworkflow.api.types.Input; +import io.serverlessworkflow.api.types.Output; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.expressions.Expression; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.ExpressionUtils; +import io.serverlessworkflow.impl.json.JsonUtils; +import java.util.Map; +import java.util.Optional; + +public abstract class AbstractTaskExecutor implements TaskExecutor { + + protected final T task; + protected final ExpressionFactory exprFactory; + + private interface TaskFilter { + JsonNode apply(WorkflowContext workflow, TaskContext task, JsonNode node); + } + + private final Optional> inputProcessor; + private final Optional> outputProcessor; + private final Optional> contextProcessor; + + protected AbstractTaskExecutor(T task, ExpressionFactory exprFactory) { + this.task = task; + this.exprFactory = exprFactory; + this.inputProcessor = Optional.ofNullable(getInputProcessor()); + this.outputProcessor = Optional.ofNullable(getOutputProcessor()); + this.contextProcessor = Optional.ofNullable(getContextProcessor()); + } + + private TaskFilter getInputProcessor() { + if (task.getInput() != null) { + Input input = task.getInput(); + // TODO add schema validator + if (input.getFrom() != null) { + return getTaskFilter(input.getFrom().getString(), input.getFrom().getObject()); + } + } + return null; + } + + private TaskFilter getOutputProcessor() { + if (task.getOutput() != null) { + Output output = task.getOutput(); + // TODO add schema validator + if (output.getAs() != null) { + return getTaskFilter(output.getAs().getString(), output.getAs().getObject()); + } + } + return null; + } + + private TaskFilter getContextProcessor() { + if (task.getExport() != null) { + Export export = task.getExport(); + // TODO add schema validator + if (export.getAs() != null) { + return getTaskFilter(export.getAs().getString(), export.getAs().getObject()); + } + } + return null; + } + + private TaskFilter getTaskFilter(String str, Object object) { + if (str != null) { + Expression expression = exprFactory.getExpression(str); + return expression::eval; + } else { + Object exprObj = ExpressionUtils.buildExpressionObject(object, exprFactory); + return exprObj instanceof Map + ? (w, t, n) -> + JsonUtils.fromValue( + ExpressionUtils.evaluateExpressionMap((Map) exprObj, w, t, n)) + : (w, t, n) -> JsonUtils.fromValue(object); + } + } + + @Override + public JsonNode apply(WorkflowContext workflowContext, JsonNode rawInput) { + TaskContext taskContext = new TaskContext<>(rawInput, task); + inputProcessor.ifPresent( + p -> taskContext.input(p.apply(workflowContext, taskContext, taskContext.rawInput()))); + taskContext.rawOutput(internalExecute(workflowContext, taskContext, taskContext.input())); + outputProcessor.ifPresent( + p -> taskContext.output(p.apply(workflowContext, taskContext, taskContext.rawOutput()))); + contextProcessor.ifPresent( + p -> + workflowContext.context( + p.apply(workflowContext, taskContext, workflowContext.context()))); + return taskContext.output(); + } + + protected abstract JsonNode internalExecute( + WorkflowContext workflow, TaskContext task, JsonNode node); +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java similarity index 90% rename from impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java rename to impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java index fab07d8c..cf49657e 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.executors; import io.serverlessworkflow.api.types.CallTask; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; -import io.serverlessworkflow.impl.jq.JQExpressionFactory; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.JQExpressionFactory; public class DefaultTaskExecutorFactory implements TaskExecutorFactory { diff --git a/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java similarity index 51% rename from impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java rename to impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java index e2c2c42f..60da619c 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.executors; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; @@ -22,8 +22,12 @@ import io.serverlessworkflow.api.types.EndpointUri; import io.serverlessworkflow.api.types.HTTPArguments; import io.serverlessworkflow.api.types.UriTemplate; -import io.serverlessworkflow.api.types.WithHTTPHeaders; -import io.serverlessworkflow.api.types.WithHTTPQuery; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.expressions.Expression; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.ExpressionUtils; +import io.serverlessworkflow.impl.json.JsonUtils; import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; @@ -33,44 +37,71 @@ import java.net.URI; import java.util.Map; import java.util.Map.Entry; -import java.util.function.Function; public class HttpExecutor extends AbstractTaskExecutor { private static final Client client = ClientBuilder.newClient(); - private final Function targetSupplier; + private final TargetSupplier targetSupplier; + private final Map headersMap; + private final Map queryMap; + private final RequestSupplier requestFunction; - public HttpExecutor(CallHTTP task, ExpressionFactory factory) { - super(task, factory); - this.targetSupplier = getTargetSupplier(task.getWith().getEndpoint()); + @FunctionalInterface + private interface TargetSupplier { + WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node); } - @Override - protected JsonNode internalExecute(JsonNode node) { + @FunctionalInterface + private interface RequestSupplier { + JsonNode apply(Builder request, WorkflowContext workflow, TaskContext task, JsonNode node); + } + + public HttpExecutor(CallHTTP task, ExpressionFactory factory) { + super(task, factory); HTTPArguments httpArgs = task.getWith(); - WithHTTPQuery query = httpArgs.getQuery(); - WebTarget target = targetSupplier.apply(node); - if (query != null) { - for (Entry entry : query.getAdditionalProperties().entrySet()) { - target = target.queryParam(entry.getKey(), entry.getValue()); - } - } - Builder request = target.request(); - WithHTTPHeaders headers = httpArgs.getHeaders(); - if (headers != null) { - headers.getAdditionalProperties().forEach(request::header); - } + this.targetSupplier = getTargetSupplier(httpArgs.getEndpoint()); + this.headersMap = + httpArgs.getHeaders() != null + ? ExpressionUtils.buildExpressionMap( + httpArgs.getHeaders().getAdditionalProperties(), factory) + : Map.of(); + this.queryMap = + httpArgs.getQuery() != null + ? ExpressionUtils.buildExpressionMap( + httpArgs.getQuery().getAdditionalProperties(), factory) + : Map.of(); switch (httpArgs.getMethod().toUpperCase()) { + case HttpMethod.POST: + Object body = ExpressionUtils.buildExpressionObject(httpArgs.getBody(), factory); + this.requestFunction = + (request, workflow, context, node) -> + request.post( + Entity.json( + ExpressionUtils.evaluateExpressionObject(body, workflow, context, node)), + JsonNode.class); + break; case HttpMethod.GET: default: - return request.get(JsonNode.class); - case HttpMethod.POST: - return request.post(Entity.json(httpArgs.getBody()), JsonNode.class); + this.requestFunction = (request, w, t, n) -> request.get(JsonNode.class); } } - private Function getTargetSupplier(Endpoint endpoint) { + @Override + protected JsonNode internalExecute( + WorkflowContext workflow, TaskContext taskContext, JsonNode input) { + WebTarget target = targetSupplier.apply(workflow, taskContext, input); + for (Entry entry : + ExpressionUtils.evaluateExpressionMap(queryMap, workflow, taskContext, input).entrySet()) { + target = target.queryParam(entry.getKey(), entry.getValue()); + } + Builder request = target.request(); + ExpressionUtils.evaluateExpressionMap(headersMap, workflow, taskContext, input) + .forEach(request::header); + return requestFunction.apply(request, workflow, taskContext, input); + } + + private TargetSupplier getTargetSupplier(Endpoint endpoint) { if (endpoint.getEndpointConfiguration() != null) { EndpointUri uri = endpoint.getEndpointConfiguration().getUri(); if (uri.getLiteralEndpointURI() != null) { @@ -86,7 +117,7 @@ private Function getTargetSupplier(Endpoint endpoint) { throw new IllegalArgumentException("Invalid endpoint definition " + endpoint); } - private Function getURISupplier(UriTemplate template) { + private TargetSupplier getURISupplier(UriTemplate template) { if (template.getLiteralUri() != null) { return new URISupplier(template.getLiteralUri()); } else if (template.getLiteralUriTemplate() != null) { @@ -95,7 +126,7 @@ private Function getURISupplier(UriTemplate template) { throw new IllegalArgumentException("Invalid uritemplate definition " + template); } - private class URISupplier implements Function { + private class URISupplier implements TargetSupplier { private final URI uri; public URISupplier(URI uri) { @@ -103,12 +134,12 @@ public URISupplier(URI uri) { } @Override - public WebTarget apply(JsonNode input) { + public WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node) { return client.target(uri); } } - private class URITemplateSupplier implements Function { + private class URITemplateSupplier implements TargetSupplier { private final String uri; public URITemplateSupplier(String uri) { @@ -116,15 +147,15 @@ public URITemplateSupplier(String uri) { } @Override - public WebTarget apply(JsonNode input) { + public WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node) { return client .target(uri) .resolveTemplates( - JsonUtils.mapper().convertValue(input, new TypeReference>() {})); + JsonUtils.mapper().convertValue(node, new TypeReference>() {})); } } - private class ExpressionURISupplier implements Function { + private class ExpressionURISupplier implements TargetSupplier { private Expression expr; public ExpressionURISupplier(String expr) { @@ -132,8 +163,8 @@ public ExpressionURISupplier(String expr) { } @Override - public WebTarget apply(JsonNode input) { - return client.target(expr.eval(input).asText()); + public WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node) { + return client.target(expr.eval(workflow, task, node).asText()); } } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java similarity index 74% rename from impl/src/main/java/io/serverlessworkflow/impl/TaskExecutor.java rename to impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java index 83c4bd18..8c896385 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.executors; import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.TaskBase; -import java.util.function.UnaryOperator; +import io.serverlessworkflow.impl.WorkflowContext; +import java.util.function.BiFunction; -public interface TaskExecutor extends UnaryOperator {} +public interface TaskExecutor + extends BiFunction {} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java similarity index 94% rename from impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java rename to impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java index 69eaa0a0..3a9068c3 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.executors; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; diff --git a/impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java similarity index 57% rename from impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java rename to impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java index 13181603..37206712 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java @@ -13,29 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.expressions; import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; -public abstract class AbstractTaskExecutor implements TaskExecutor { - - protected final T task; - protected final ExpressionFactory exprFactory; - - protected AbstractTaskExecutor(T task, ExpressionFactory exprFactory) { - this.task = task; - this.exprFactory = exprFactory; - } - - @Override - public JsonNode apply(JsonNode node) { - - // do input filtering - return internalExecute(node); - // do output filtering - - } - - protected abstract JsonNode internalExecute(JsonNode node); +public interface Expression { + JsonNode eval( + WorkflowContext workflowContext, TaskContext context, JsonNode node); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java similarity index 83% rename from impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java rename to impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java index 8f9c1dd1..4d07d5af 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java @@ -13,9 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.expressions; public interface ExpressionFactory { - + /** + * @throws ExpressionValidationException + * @param expression + * @return + */ Expression getExpression(String expression); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java new file mode 100644 index 00000000..7f776322 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed 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 io.serverlessworkflow.impl.expressions; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.json.JsonUtils; +import java.util.Map; + +public class ExpressionUtils { + + private static final String EXPR_PREFIX = "${"; + private static final String EXPR_SUFFIX = "}"; + + private ExpressionUtils() {} + + public static Map buildExpressionMap( + Map origMap, ExpressionFactory factory) { + return new ProxyMap(origMap, o -> isExpr(o) ? factory.getExpression(o.toString()) : o); + } + + public static Map evaluateExpressionMap( + Map origMap, WorkflowContext workflow, TaskContext task, JsonNode n) { + return new ProxyMap( + origMap, + o -> + o instanceof Expression + ? JsonUtils.toJavaValue(((Expression) o).eval(workflow, task, n)) + : o); + } + + public static Object buildExpressionObject(Object obj, ExpressionFactory factory) { + return obj instanceof Map + ? ExpressionUtils.buildExpressionMap((Map) obj, factory) + : obj; + } + + public static Object evaluateExpressionObject( + Object obj, WorkflowContext workflow, TaskContext task, JsonNode node) { + return obj instanceof Map + ? ExpressionUtils.evaluateExpressionMap((Map) obj, workflow, task, node) + : obj; + } + + public static boolean isExpr(Object expr) { + return expr instanceof String && ((String) expr).startsWith(EXPR_PREFIX); + } + + public static String trimExpr(String expr) { + expr = expr.trim(); + if (expr.startsWith(EXPR_PREFIX)) { + expr = trimExpr(expr, EXPR_PREFIX, EXPR_SUFFIX); + } + return expr.trim(); + } + + private static String trimExpr(String expr, String prefix, String suffix) { + expr = expr.substring(prefix.length()); + if (expr.endsWith(suffix)) { + expr = expr.substring(0, expr.length() - suffix.length()); + } + return expr; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java new file mode 100644 index 00000000..16fe144f --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java @@ -0,0 +1,14 @@ +package io.serverlessworkflow.impl.expressions; + +public class ExpressionValidationException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ExpressionValidationException(String message) { + super(message); + } + + public ExpressionValidationException(String message, Throwable ex) { + super(message, ex); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java similarity index 94% rename from impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java rename to impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java index b77f34a2..2e64e17a 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java @@ -13,12 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl.jq; +package io.serverlessworkflow.impl.expressions; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; -import io.serverlessworkflow.impl.Expression; -import io.serverlessworkflow.impl.JsonUtils; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.json.JsonUtils; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; @@ -176,14 +178,15 @@ public JsonNode getResult() { } @Override - public JsonNode eval(JsonNode context) { + public JsonNode eval( + WorkflowContext workflow, TaskContext task, JsonNode node) { TypedOutput output = output(JsonNode.class); try { - internalExpr.apply(this.scope.get(), context, output); + internalExpr.apply(this.scope.get(), node, output); return output.getResult(); } catch (JsonQueryException e) { throw new IllegalArgumentException( - "Unable to evaluate content " + context + " using expr " + expr, e); + "Unable to evaluate content " + node + " using expr " + expr, e); } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java similarity index 90% rename from impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java rename to impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java index 787842d6..0375224a 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java @@ -13,11 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl.jq; +package io.serverlessworkflow.impl.expressions; -import io.serverlessworkflow.impl.Expression; -import io.serverlessworkflow.impl.ExpressionFactory; -import io.serverlessworkflow.impl.ExpressionUtils; import java.util.function.Supplier; import net.thisptr.jackson.jq.BuiltinFunctionLoader; import net.thisptr.jackson.jq.Scope; diff --git a/impl/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java new file mode 100644 index 00000000..bf4464b2 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java @@ -0,0 +1,278 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed 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 io.serverlessworkflow.impl.expressions; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.function.UnaryOperator; + +public class ProxyMap implements Map { + + private final Map map; + private final UnaryOperator function; + + public ProxyMap(Map map, UnaryOperator function) { + this.map = map; + this.function = function; + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public Object get(Object key) { + return processValue(map.get(key)); + } + + @Override + public Object put(String key, Object value) { + return map.put(key, processValue(value)); + } + + @Override + public Object remove(Object key) { + return map.remove(key); + } + + @Override + public void putAll(Map m) { + map.putAll(m); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Set keySet() { + return map.keySet(); + } + + @Override + public Collection values() { + return new ProxyCollection(map.values()); + } + + @Override + public Set> entrySet() { + return new ProxyEntrySet(map.entrySet()); + } + + private abstract class AbstractProxyCollection { + + protected Collection values; + + protected AbstractProxyCollection(Collection values) { + this.values = values; + } + + public int size() { + return values.size(); + } + + public boolean isEmpty() { + return values.isEmpty(); + } + + public boolean contains(Object o) { + return values.contains(o); + } + + public boolean remove(Object o) { + return values.remove(o); + } + + public boolean containsAll(Collection c) { + return values.containsAll(c); + } + + public boolean retainAll(Collection c) { + return values.retainAll(c); + } + + public boolean removeAll(Collection c) { + return values.removeAll(c); + } + + public void clear() { + values.clear(); + } + + public boolean addAll(Collection c) { + return values.addAll(c); + } + + public boolean add(T e) { + return values.add(e); + } + } + + private class ProxyEntrySet extends AbstractProxyCollection> + implements Set> { + + public ProxyEntrySet(Set> entrySet) { + super(entrySet); + } + + @Override + public Iterator> iterator() { + return new ProxyEntryIterator(values.iterator()); + } + + @Override + public Object[] toArray() { + return processEntries(values.toArray()); + } + + @Override + public T[] toArray(T[] a) { + return processEntries(values.toArray(a)); + } + + private T[] processEntries(T[] array) { + for (int i = 0; i < array.length; i++) { + array[i] = (T) new ProxyEntry((Entry) array[i]); + } + return array; + } + } + + private class ProxyCollection extends AbstractProxyCollection + implements Collection { + + public ProxyCollection(Collection values) { + super(values); + } + + @Override + public Iterator iterator() { + return new ProxyIterator(values.iterator()); + } + + @Override + public Object[] toArray() { + return processArray(values.toArray()); + } + + @Override + public T[] toArray(T[] a) { + return processArray(values.toArray(a)); + } + + private S[] processArray(S[] array) { + for (int i = 0; i < array.length; i++) { + array[i] = (S) processValue(array[i]); + } + return array; + } + } + + private class ProxyEntry implements Entry { + + private Entry entry; + + private ProxyEntry(Entry entry) { + this.entry = entry; + } + + @Override + public String getKey() { + return entry.getKey(); + } + + @Override + public Object getValue() { + return processValue(entry.getValue()); + } + + @Override + public Object setValue(Object value) { + return entry.setValue(value); + } + } + + private class ProxyIterator implements Iterator { + + private Iterator iter; + + public ProxyIterator(Iterator iter) { + this.iter = iter; + } + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public Object next() { + return processValue(iter.next()); + } + + @Override + public void remove() { + iter.remove(); + } + } + + private class ProxyEntryIterator implements Iterator> { + + private Iterator> iter; + + public ProxyEntryIterator(Iterator> iter) { + this.iter = iter; + } + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public Entry next() { + return new ProxyEntry(iter.next()); + } + + @Override + public void remove() { + iter.remove(); + } + } + + private Object processValue(T obj) { + return function.apply(obj); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/JsonUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java similarity index 97% rename from impl/src/main/java/io/serverlessworkflow/impl/JsonUtils.java rename to impl/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java index b00b14f1..a13c8313 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/JsonUtils.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.json; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -87,6 +87,10 @@ public static JsonNode fromValue(Object value) { } } + public static Object toJavaValue(Object object) { + return object instanceof JsonNode ? toJavaValue((JsonNode) object) : object; + } + public static JsonNode fromString(String value) { String trimmedValue = value.trim(); if (trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) { @@ -201,7 +205,7 @@ private static ArrayNode mapToArray(Collection collection, ArrayNode arrayNod return arrayNode; } - static ObjectNode object() { + public static ObjectNode object() { return mapper.createObjectNode(); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/MergeUtils.java b/impl/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java similarity index 98% rename from impl/src/main/java/io/serverlessworkflow/impl/MergeUtils.java rename to impl/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java index 8c1ec1de..a3615d35 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/MergeUtils.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.json; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; diff --git a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java index 66ef5d86..ba842e4e 100644 --- a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -46,7 +46,16 @@ private static Stream provideParameters() { new Condition<>( o -> ((Map) o).containsKey("photoUrls"), "callHttpCondition"); return Stream.of( - Arguments.of("callHttp.yaml", petInput, petCondition), - Arguments.of("call-http-endpoint-interpolation.yaml", petInput, petCondition)); + Arguments.of("callGetHttp.yaml", petInput, petCondition), + Arguments.of("call-http-endpoint-interpolation.yaml", petInput, petCondition), + Arguments.of( + "call-http-query-parameters.yaml", + Map.of("searchQuery", "R2-D2"), + new Condition<>( + o -> ((Map) o).get("count").equals(1), "R2D2Condition")), + Arguments.of( + "callPostHttp.yaml", + Map.of("name", "Javierito", "status", "available"), + new Condition<>(o -> o.equals("Javierito"), "CallHttpPostCondition"))); } } diff --git a/impl/src/test/resources/call-http-query-parameters.yaml b/impl/src/test/resources/call-http-query-parameters.yaml new file mode 100644 index 00000000..75f33378 --- /dev/null +++ b/impl/src/test/resources/call-http-query-parameters.yaml @@ -0,0 +1,23 @@ +document: + dsl: 1.0.0-alpha2 + namespace: examples + name: http-query-params + version: 1.0.0-alpha2 +input: + schema: + document: + type: object + required: + - searchQuery + properties: + searchQuery: + type: string +do: + - searchStarWarsCharacters: + call: http + with: + method: get + endpoint: https://swapi.dev/api/people/ + query: + search: ${.searchQuery} + diff --git a/impl/src/test/resources/callHttp.yaml b/impl/src/test/resources/callGetHttp.yaml similarity index 100% rename from impl/src/test/resources/callHttp.yaml rename to impl/src/test/resources/callGetHttp.yaml diff --git a/impl/src/test/resources/callPostHttp.yaml b/impl/src/test/resources/callPostHttp.yaml new file mode 100644 index 00000000..d898dbf7 --- /dev/null +++ b/impl/src/test/resources/callPostHttp.yaml @@ -0,0 +1,28 @@ +document: + dsl: 1.0.0-alpha1 + namespace: default + name: http-call-with-response-output + version: 1.0.0 +do: + - postPet: + call: http + with: + method: post + endpoint: + uri: https://petstore.swagger.io/v2/pet + body: + name: ${.name} + status: ${.status} + output: + as: .id + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + input: + from: + petId: ${.} + output: + as: .name \ No newline at end of file pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy