diff --git a/README.md b/README.md index cb516899c..ceb6bd900 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ try (OutputStream out = new FileOutputStream("simple.json")) { ``` For additional writing helper methods, check [WorkflowWriter](api/src/main/java/org/openworkflow/sdk/api/WorkflowWriter.java) class. -## Jackson dependency +#### Jackson dependency The `openworkflow-api` module depends heavily on Jackson for loading file content into an in-memory POJO. For users who use a JSON handling library other than Jackson, the `openworkflow-types` module contains Java POJOs generated from the OpenWorkflow schema without any Jackson annotations. Using this module as a dependency, they can build a reader/writer module equivalent to `openworkflow-api` with their preferred JSON library. @@ -148,4 +148,14 @@ The reference implementation provides a ready-to-use runtime that supports the O ## License -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fopen-workflow-specification%2Fsdk-java.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fopen-workflow-specification%2Fsdk-java?ref=badge_large) \ No newline at end of file +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fopen-workflow-specification%2Fsdk-java.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fopen-workflow-specification%2Fsdk-java?ref=badge_large) + +## Third-Party Code Attributions + +This project contains source code copied and modified from the following third-party project: + +* **Name:** std-uritemplate +* **Source:** [std-uritemplate GitHub Repository](https://github.com/std-uritemplate/std-uritemplate) +* **File Copied:** `java/src/main/java/io/github/stduritemplate/StdUriTemplate.java` +* **License:** Apache License 2.0 +* **Copyright:** Copyright 2024-2026 std-uritemplate contributors \ No newline at end of file diff --git a/examples/simpleGet/pom.xml b/examples/simpleGet/pom.xml index cb3cd8327..ab0d158ca 100644 --- a/examples/simpleGet/pom.xml +++ b/examples/simpleGet/pom.xml @@ -17,18 +17,6 @@ org.openworkflow.sdk openworkflow-impl-jackson - - org.openworkflow.sdk - openworkflow-impl-http - - - org.glassfish.jersey.media - jersey-media-json-jackson3 - - - org.glassfish.jersey.core - jersey-client - org.slf4j slf4j-simple diff --git a/impl/README.md b/impl/README.md index bff553e2c..01dee15b8 100644 --- a/impl/README.md +++ b/impl/README.md @@ -81,36 +81,43 @@ This SDK is modular by design—pull in only what you need: * **openworkflow-impl-core** Workflow engine & core interfaces. Depends on generated types and CloudEvents SDK. + * **openworkflow-impl-jackson** - 👉 **Most users add this one.** - - Adds Jackson integration, JQ expressions, JSON Schema validation, and CloudEvents (de)serialization. - - Internally, this module is an aggregation of smaller modules to allow part replacement if needed: - * **openworkflow-impl-jq** JQ expression implementation - * **openworkflow-impl-model** Jackson model implementation - * **openworkflow-impl-validation** Schema validation implementation - * **openworkflow-impl-lifecycle-events** Enable publication of lifecycle events as Json cloud events. - * **openworkflow-impl-function** Function support. - * **openworkflow-impl-template-resolver** JaxRS URI template implementation. - * **openworkflow-impl-json** Json common utilities shared by all modules in this list - * **openworkflow-impl-cron** Cron parsing - * **openworkflow-impl-http** HTTP `Call` task handler, based on Jax-RS client - * **openworkflow-impl-jackson-jwt** Optimized entity converter for Jackson model and OAuth2/OIDC helpers for HTTP calls. - * **openworkflow-impl-openapi** OpenAPI support using Jackson - * **openworkflow-impl-openapi-base** Base OpenAPI support intended to be extended for a specific JSON library - * **openworkflow-impl-grpc** gRPC support - * **openworkflow-impl-script-python** Python support - * **openworkflow-impl-script-js** Javascript support - * **openworkflow-impl-container** Docker container support + 👉 **Most users add this one.** Contains implementation of core features using Jackson library, like JQ expressions, JSON Schema validation, and CloudEvents (de)serialization. Internally, it is an aggregation of smaller modules: + + + * **openworkflow-impl-jq** JQ expression implementation + * **openworkflow-impl-model** Jackson model implementation + * **openworkflow-impl-validation** Schema validation implementation + * **openworkflow-impl-lifecycle-events** Enable publication of lifecycle events as Json cloud events. + * **openworkflow-impl-function** Jackson Function support. + * **openworkflow-impl-json** Jackson common utilities shared by all modules in this list + * **openworkflow-impl-cron** Cron parsing for workflow scheduled execution + + Additionally, The OpenWokflow specification defines a set of [call task](https://github.com/open-workflow-specification/specification/blob/main/dsl-reference.md#call), which requires additional dependencies, with the exception of HTTP (see section below). You might add those ones needed depending on the feature you are using in your definition: + + * **openworkflow-impl-openapi** OpenAPI support using Jackson + * **openworkflow-impl-openapi-base** Base OpenAPI support intended to be extended for a specific JSON library + * **openworkflow-impl-grpc** gRPC support + * **openworkflow-impl-script-python** Python support + * **openworkflow-impl-script-js** Javascript support + * **openworkflow-impl-container** Docker container support There are also companion modules/docs for: * **Fluent DSL** (programmatic builder) * **Mermaid** (diagramming workflows) -Links below. +### Http dependencies + +By default, the SDK reference implementation use `java.net.HttpRequest` to support Http calls. Therefore, no dependency needs to be added for using Http Call task or downloading of http resources, like OpenAPI schemas. + +However, for further configuration of Http functionality, users might include the Jakarta based modules, which will allow customizing `jakarta.ws.rs.client.Client` in Jakarta EE environments. + + * **openworkflow-impl-http** HTTP `Call` task handler, based on JAX-RS client + * **openworkflow-impl-jackson-jwt** Optimized entity converter for Jackson model and OAuth2/OIDC helpers for Jakarta HTTP calls. + --- ## Installation diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowApplication.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowApplication.java index 55df748ab..b98915394 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowApplication.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowApplication.java @@ -43,6 +43,7 @@ import java.util.stream.Collectors; import org.openworkflow.sdk.api.types.SchemaInline; import org.openworkflow.sdk.api.types.Workflow; +import org.openworkflow.sdk.impl.additional.NamedSuppliedAdditionalObject; import org.openworkflow.sdk.impl.additional.NamedWorkflowAdditionalObject; import org.openworkflow.sdk.impl.additional.WorkflowAdditionalObject; import org.openworkflow.sdk.impl.auth.AuthProviderFactory; @@ -62,6 +63,10 @@ import org.openworkflow.sdk.impl.executors.TaskExecutorFactory; import org.openworkflow.sdk.impl.expressions.ExpressionFactory; import org.openworkflow.sdk.impl.expressions.RuntimeDescriptor; +import org.openworkflow.sdk.impl.http.DefaultHttpRequestBuilderFactory; +import org.openworkflow.sdk.impl.http.DefaultURITemplateResolver; +import org.openworkflow.sdk.impl.http.URITemplateResolver; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestFactory; import org.openworkflow.sdk.impl.lifecycle.LifeCycleEventType; import org.openworkflow.sdk.impl.lifecycle.WorkflowExecutionCompletableListener; import org.openworkflow.sdk.impl.lifecycle.WorkflowExecutionListener; @@ -71,7 +76,6 @@ import org.openworkflow.sdk.impl.resources.DefaultResourceLoaderFactory; import org.openworkflow.sdk.impl.resources.ExternalResourceHandler; import org.openworkflow.sdk.impl.resources.ResourceLoaderFactory; -import org.openworkflow.sdk.impl.resources.URITemplateResolver; import org.openworkflow.sdk.impl.scheduler.AllStrategyCorrelationInfoFactory; import org.openworkflow.sdk.impl.scheduler.CronResolver; import org.openworkflow.sdk.impl.scheduler.CronResolverFactory; @@ -107,7 +111,7 @@ public class WorkflowApplication implements AutoCloseable { private final ConfigManager configManager; private final SecretManager secretManager; private final SchedulerListener schedulerListener; - private final Optional templateResolver; + private final URITemplateResolver templateResolver; private final Optional functionReader; private final URI defaultCatalogURI; private final WorkflowValueResolver defaultEventSource; @@ -117,6 +121,7 @@ public class WorkflowApplication implements AutoCloseable { private final WorkflowLifeCycleCloudEventFactory lifeCycleCloudEventFactory; private final ScheduledExecutorService schedulerExecutorService; private final Set allowedCommands; + private final WorkflowHttpRequestFactory httpRequestBuilderFactory; private final Map, ServiceLoader> servicesLoaded = new ConcurrentHashMap<>(); private final Set lifeCycleEventTypes; @@ -156,6 +161,7 @@ private WorkflowApplication(Builder builder) { this.schedulerExecutorService = builder.schedulerExecutorService; this.allowedCommands = Collections.unmodifiableSet(builder.allowedCommands); this.lifeCycleEventTypes = builder.lifeCycleEventTypes; + this.httpRequestBuilderFactory = builder.httpRequestBuilderFactory; } public TaskExecutorFactory taskFactory() { @@ -274,7 +280,7 @@ public SchemaValidator getValidator(SchemaInline inline) { private SecretManager secretManager; private ConfigManager configManager; private SchedulerListener schedulerListener; - private Optional templateResolver; + private URITemplateResolver templateResolver; private Optional functionReader; private URI defaultCatalogURI; private WorkflowValueResolver defaultEventSource; @@ -285,10 +291,13 @@ public SchemaValidator getValidator(SchemaInline inline) { private ScheduledExecutorService schedulerExecutorService; private Set allowedCommands = new HashSet<>(); private Set lifeCycleEventTypes; + private WorkflowHttpRequestFactory httpRequestBuilderFactory; private Builder() { ServiceLoader.load(NamedWorkflowAdditionalObject.class) .forEach(a -> additionalObjects.put(a.name(), a)); + ServiceLoader.load(NamedSuppliedAdditionalObject.class) + .forEach(a -> this.additionalObjectSuppliers.put(a.name(), a)); } public Builder withId(String id) { @@ -468,6 +477,17 @@ public Builder withLifeCycleEventTypes(Set lifeCycleEventTyp return this; } + public Builder withHttpRequestBuilderFactory( + WorkflowHttpRequestFactory httpRequestBuilderFactory) { + this.httpRequestBuilderFactory = httpRequestBuilderFactory; + return this; + } + + public Builder withURITemplateResolver(URITemplateResolver templateResolver) { + this.templateResolver = templateResolver; + return this; + } + public WorkflowApplication build() { if (executorFactory == null) { executorFactory = new DefaultExecutorServiceFactory(); @@ -546,7 +566,10 @@ public CronResolver parseCron(String cron) { secretManager = loadFirst(SecretManager.class).orElseGet(() -> new ConfigSecretManager(configManager)); } - templateResolver = loadFirst(URITemplateResolver.class); + if (templateResolver == null) { + templateResolver = + loadFirst(URITemplateResolver.class).orElseGet(() -> new DefaultURITemplateResolver()); + } functionReader = loadFirst(FunctionReader.class); if (cloudEventPredicateFactory == null) { cloudEventPredicateFactory = @@ -578,7 +601,14 @@ public CronResolver parseCron(String cron) { if (lifeCycleEventTypes == null) { lifeCycleEventTypes = EnumSet.of(LifeCycleEventType.V8); } - return new WorkflowApplication(this); + if (httpRequestBuilderFactory == null) { + httpRequestBuilderFactory = + loadFirst(WorkflowHttpRequestFactory.class) + .orElseGet(() -> new DefaultHttpRequestBuilderFactory()); + } + WorkflowApplication app = new WorkflowApplication(this); + httpRequestBuilderFactory.init(app); + return app; } private List loadFromServiceLoader(Class clazz) { @@ -617,6 +647,7 @@ public void close() { } listenersByPriority.clear(); } + safeClose(httpRequestBuilderFactory); } public WorkflowPositionFactory positionFactory() { @@ -668,7 +699,7 @@ SchedulerListener schedulerListener() { return schedulerListener; } - public Optional templateResolver() { + public URITemplateResolver templateResolver() { return templateResolver; } @@ -699,7 +730,8 @@ public Optional additionalObject(String name) { public Optional additionalObject( String name, WorkflowContext workflowContext, TaskContext taskContext) { return Optional.ofNullable(additionalObjects.get(name)) - .map(v -> (T) v.apply(workflowContext, taskContext)); + .map(v -> (T) v.apply(workflowContext, taskContext)) + .or(() -> additionalObject(name)); } public AuthProviderFactory authProviderFactory() { @@ -726,6 +758,10 @@ public Set lifeCycleEventTypes() { return lifeCycleEventTypes; } + public WorkflowHttpRequestFactory httpRequestFactory() { + return httpRequestBuilderFactory; + } + @SuppressWarnings("unchecked") public > List serviceLoadedClasses(Class clazz) { ServiceLoader serviceLoader = servicesLoaded.computeIfAbsent(clazz, ServiceLoader::load); diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowError.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowError.java index 1acbef092..533e659b2 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowError.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowError.java @@ -17,6 +17,7 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Optional; import org.openworkflow.sdk.impl.lifecycle.TaskFailedEvent; import org.openworkflow.sdk.impl.lifecycle.WorkflowFailedEvent; import org.openworkflow.sdk.types.Errors; @@ -39,15 +40,30 @@ public static Builder communication(int status, TaskContext context, Throwable e return communication(status, context, ex.getMessage()); } + public static Builder communication(int status, Optional context, Throwable ex) { + return communication(status, context, ex.getMessage()); + } + public static Builder communication(int status, TaskContext context, String title) { - return new Builder(Errors.COMMUNICATION.toString(), status) - .instance(context.position().jsonPointer()) - .title(title); + return communication(status, context).title(title); + } + + public static Builder communication(int status, Optional context, String title) { + return communication(status, context).title(title); } public static Builder communication(int status, TaskContext context) { - return new Builder(Errors.COMMUNICATION.toString(), status) - .instance(context.position().jsonPointer()); + Builder builder = new Builder(Errors.COMMUNICATION.toString(), status); + if (context != null) { + builder.instance(context.position().jsonPointer()); + } + return builder; + } + + public static Builder communication(int status, Optional context) { + Builder result = new Builder(Errors.COMMUNICATION.toString(), status); + context.ifPresent(ctx -> result.instance(ctx.position().jsonPointer())); + return result; } public static Builder communication(TaskContext context, String title) { diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowModel.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowModel.java index efb817a0d..197cfbf41 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowModel.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowModel.java @@ -34,6 +34,10 @@ public interface WorkflowModel { Optional> asMap(); + default Optional asJson() { + return Optional.empty(); + } + Object asJavaObject(); Class objectClass(); diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowModelFactory.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowModelFactory.java index 916b50910..38ac61b85 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowModelFactory.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowModelFactory.java @@ -19,6 +19,7 @@ import io.cloudevents.CloudEventData; import java.time.OffsetDateTime; import java.util.Map; +import java.util.Optional; public interface WorkflowModelFactory extends ServicePriority { @@ -66,4 +67,8 @@ default WorkflowModel fromAny(Object obj) { default -> fromOther(obj); }; } + + default Optional fromJson(byte[] bytes) { + return Optional.empty(); + } } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowUtils.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowUtils.java index b76a1a85a..412e6feac 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowUtils.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowUtils.java @@ -255,12 +255,14 @@ public static Optional> getTaskTimeout( return Optional.of(WorkflowUtils.fromTimeoutAfter(appl, timeoutDef.getAfter())); } - public static final String secretProp(WorkflowContext context, String secretName, String prop) { - return (String) secret(context, secretName).get(prop); + public static final String secretProp( + WorkflowApplication application, String secretName, String prop) { + return (String) secret(application, secretName).get(prop); } - public static final Map secret(WorkflowContext context, String secretName) { - return context.definition().application().secretManager().secret(secretName); + public static final Map secret( + WorkflowApplication application, String secretName) { + return application.secretManager().secret(secretName); } public static final String checkSecret( @@ -317,10 +319,6 @@ public static WorkflowValueResolver getURISupplier( return (w, t, n) -> application .templateResolver() - .orElseThrow( - () -> - new IllegalStateException( - "Need an uri template resolver to resolve uri template")) .resolveTemplates(template.getLiteralUriTemplate(), w, t, n); } throw new IllegalArgumentException("Invalid uritemplate definition " + template); diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowValueResolver.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowValueResolver.java index a3c0a9363..d823ee173 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowValueResolver.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/WorkflowValueResolver.java @@ -17,4 +17,8 @@ public interface WorkflowValueResolver { T apply(WorkflowContext workflow, TaskContext task, WorkflowModel model); + + default T apply(WorkflowContext workflow, TaskContext task) { + return apply(workflow, task, task != null ? task.input() : null); + } } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/additional/NamedSuppliedAdditionalObject.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/additional/NamedSuppliedAdditionalObject.java new file mode 100644 index 000000000..b1341d4d2 --- /dev/null +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/additional/NamedSuppliedAdditionalObject.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.additional; + +import java.util.function.Supplier; +import org.openworkflow.sdk.impl.NamedObject; + +public interface NamedSuppliedAdditionalObject extends Supplier, NamedObject {} diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AbstractAuthRequestBuilder.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AbstractAuthRequestBuilder.java index a8b853fda..4c5af4b15 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AbstractAuthRequestBuilder.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AbstractAuthRequestBuilder.java @@ -54,7 +54,7 @@ public AbstractAuthRequestBuilder(WorkflowApplication application) { } @Override - public HttpRequestInfo apply(T authenticationData) { + public AuthHttpRequestInfo apply(T authenticationData) { requestEncoding(authenticationData); authenticationURI(authenticationData); audience(authenticationData); @@ -65,7 +65,7 @@ public HttpRequestInfo apply(T authenticationData) { } @Override - public HttpRequestInfo apply(Map secret) { + public AuthHttpRequestInfo apply(Map secret) { requestEncoding(secret); authenticationURI(secret); audience(secret); diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AccessTokenProvider.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AccessTokenProvider.java index c1ef5f730..75cc9c9ab 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AccessTokenProvider.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AccessTokenProvider.java @@ -18,9 +18,7 @@ import java.util.concurrent.CompletableFuture; import org.openworkflow.sdk.impl.TaskContext; import org.openworkflow.sdk.impl.WorkflowContext; -import org.openworkflow.sdk.impl.WorkflowModel; public interface AccessTokenProvider { - CompletableFuture validateAndGet( - WorkflowContext workflow, TaskContext context, WorkflowModel model); + CompletableFuture validateAndGet(WorkflowContext workflow, TaskContext context); } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AccessTokenProviderFactory.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AccessTokenProviderFactory.java index 7eddaaf5c..b87c77274 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AccessTokenProviderFactory.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AccessTokenProviderFactory.java @@ -17,9 +17,15 @@ import java.util.List; import org.openworkflow.sdk.impl.ServicePriority; +import org.openworkflow.sdk.impl.WorkflowApplication; -public interface AccessTokenProviderFactory extends ServicePriority { +public interface AccessTokenProviderFactory extends ServicePriority, AutoCloseable { AccessTokenProvider build( - HttpRequestInfo requestInfo, List issuers, JWTConverter converter); + WorkflowApplication app, + AuthHttpRequestInfo requestInfo, + List issuers, + JWTConverter converter); + + default void close() {} } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/HttpRequestInfo.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthHttpRequestInfo.java similarity index 97% rename from impl/core/src/main/java/org/openworkflow/sdk/impl/auth/HttpRequestInfo.java rename to impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthHttpRequestInfo.java index 5bf459ee9..c3e119eaf 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/HttpRequestInfo.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthHttpRequestInfo.java @@ -20,7 +20,7 @@ import java.util.Optional; import org.openworkflow.sdk.impl.WorkflowValueResolver; -public record HttpRequestInfo( +public record AuthHttpRequestInfo( Map> headers, Map> queryParams, Map> clientAuthParams, diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthProvider.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthProvider.java index ab36fa45d..43202a7b3 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthProvider.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthProvider.java @@ -19,12 +19,11 @@ import java.util.concurrent.CompletableFuture; import org.openworkflow.sdk.impl.TaskContext; import org.openworkflow.sdk.impl.WorkflowContext; -import org.openworkflow.sdk.impl.WorkflowModel; public interface AuthProvider { String scheme(); CompletableFuture content( - WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri); + WorkflowContext workflow, TaskContext task, URI uri, String method); } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthProviderFactory.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthProviderFactory.java index 00c0f93a9..cea617658 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthProviderFactory.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthProviderFactory.java @@ -27,5 +27,5 @@ Optional getAuth( WorkflowDefinition definition, EndpointConfiguration configuration); Optional getAuth( - WorkflowDefinition definition, ReferenceableAuthenticationPolicy auth, String method); + WorkflowDefinition definition, ReferenceableAuthenticationPolicy auth); } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthRequestBuilder.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthRequestBuilder.java index 22e7fedce..8bd2c6ee5 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthRequestBuilder.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/AuthRequestBuilder.java @@ -20,7 +20,7 @@ public interface AuthRequestBuilder { - HttpRequestInfo apply(T authenticationData); + AuthHttpRequestInfo apply(T authenticationData); - HttpRequestInfo apply(Map authenticationData); + AuthHttpRequestInfo apply(Map authenticationData); } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/BasicAuthProvider.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/BasicAuthProvider.java index 780bbfb24..cdb55e779 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/BasicAuthProvider.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/BasicAuthProvider.java @@ -28,7 +28,6 @@ import org.openworkflow.sdk.impl.TaskContext; import org.openworkflow.sdk.impl.WorkflowApplication; import org.openworkflow.sdk.impl.WorkflowContext; -import org.openworkflow.sdk.impl.WorkflowModel; import org.openworkflow.sdk.impl.WorkflowUtils; import org.openworkflow.sdk.impl.WorkflowValueResolver; @@ -51,8 +50,8 @@ public BasicAuthProvider( } else if (authPolicy.getBasic().getBasicAuthenticationPolicySecret() != null) { String secretName = checkSecret(workflow, authPolicy.getBasic().getBasicAuthenticationPolicySecret()); - userFilter = (w, t, m) -> secretProp(w, secretName, USER); - passwordFilter = (w, t, m) -> secretProp(w, secretName, PASSWORD); + userFilter = (w, t, m) -> secretProp(app, secretName, USER); + passwordFilter = (w, t, m) -> secretProp(app, secretName, PASSWORD); } else { throw new IllegalStateException("Both secret and properties are null for authorization"); } @@ -60,14 +59,14 @@ public BasicAuthProvider( @Override public CompletableFuture content( - WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { + WorkflowContext workflow, TaskContext task, URI uri, String method) { return CompletableFuture.completedFuture( Base64.getEncoder() .encodeToString( String.format( USER_PASSWORD, - userFilter.apply(workflow, task, model), - passwordFilter.apply(workflow, task, model)) + userFilter.apply(workflow, task), + passwordFilter.apply(workflow, task)) .getBytes())); } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/BearerAuthProvider.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/BearerAuthProvider.java index 0f02d5568..f966da28b 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/BearerAuthProvider.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/BearerAuthProvider.java @@ -27,7 +27,6 @@ import org.openworkflow.sdk.impl.TaskContext; import org.openworkflow.sdk.impl.WorkflowApplication; import org.openworkflow.sdk.impl.WorkflowContext; -import org.openworkflow.sdk.impl.WorkflowModel; import org.openworkflow.sdk.impl.WorkflowUtils; import org.openworkflow.sdk.impl.WorkflowValueResolver; @@ -45,14 +44,14 @@ public BearerAuthProvider( tokenFilter = WorkflowUtils.buildStringFilter(app, token); } else if (config.getBearerAuthenticationPolicySecret() != null) { String secretName = checkSecret(workflow, config.getBearerAuthenticationPolicySecret()); - tokenFilter = (w, t, m) -> secretProp(w, secretName, TOKEN); + tokenFilter = (w, t, m) -> secretProp(app, secretName, TOKEN); } } @Override public CompletableFuture content( - WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { - return CompletableFuture.completedFuture(tokenFilter.apply(workflow, task, model)); + WorkflowContext workflow, TaskContext task, URI uri, String method) { + return CompletableFuture.completedFuture(tokenFilter.apply(workflow, task)); } @Override diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/CommonOAuthProvider.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/CommonOAuthProvider.java index 4fcf48ddc..768e23424 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/CommonOAuthProvider.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/CommonOAuthProvider.java @@ -28,7 +28,6 @@ import org.openworkflow.sdk.impl.TaskContext; import org.openworkflow.sdk.impl.WorkflowApplication; import org.openworkflow.sdk.impl.WorkflowContext; -import org.openworkflow.sdk.impl.WorkflowModel; import org.openworkflow.sdk.impl.WorkflowValueResolver; public abstract class CommonOAuthProvider implements AuthProvider { @@ -41,11 +40,8 @@ protected CommonOAuthProvider(WorkflowValueResolver tokenPr @Override public CompletableFuture content( - WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { - return tokenProvider - .apply(workflow, task, model) - .validateAndGet(workflow, task, model) - .thenApply(JWT::token); + WorkflowContext workflow, TaskContext task, URI uri, String method) { + return tokenProvider.apply(workflow, task).validateAndGet(workflow, task).thenApply(JWT::token); } @Override @@ -79,6 +75,7 @@ private static WorkflowValueResolver build( AccessTokenProvider tokenProvider = app.serviceLoadedClass(AccessTokenProviderFactory.class) .build( + app, authBuilder.apply(authenticationData), authenticationData.getIssuers(), app.serviceLoadedClass(JWTConverter.class)); @@ -88,10 +85,11 @@ private static WorkflowValueResolver build( private static WorkflowValueResolver build( String secretName, AuthRequestBuilder authBuilder, WorkflowApplication app) { return (w, t, m) -> { - Map secret = secret(w, secretName); + Map secret = secret(app, secretName); String issuers = (String) secret.get("issuers"); return app.serviceLoadedClass(AccessTokenProviderFactory.class) .build( + app, authBuilder.apply(secret), issuers != null ? Arrays.asList(issuers.split(",")) : null, app.serviceLoadedClass(JWTConverter.class)); diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/DefaultAuthProviderFactory.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/DefaultAuthProviderFactory.java index 07b977147..363527a09 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/DefaultAuthProviderFactory.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/DefaultAuthProviderFactory.java @@ -41,23 +41,19 @@ public Optional getAuth( WorkflowDefinition definition, EndpointConfiguration configuration) { return configuration == null ? Optional.empty() - : getAuth(definition, configuration.getAuthentication(), "GET"); + : getAuth(definition, configuration.getAuthentication()); } @Override public Optional getAuth( - WorkflowDefinition definition, ReferenceableAuthenticationPolicy auth, String method) { + WorkflowDefinition definition, ReferenceableAuthenticationPolicy auth) { return OAuthUtils.resolvePolicy(definition.workflow(), auth) .flatMap( - policy -> - buildFromPolicy(definition.application(), definition.workflow(), policy, method)); + policy -> buildFromPolicy(definition.application(), definition.workflow(), policy)); } private Optional buildFromPolicy( - WorkflowApplication app, - Workflow workflow, - AuthenticationPolicyUnion authenticationPolicy, - String method) { + WorkflowApplication app, Workflow workflow, AuthenticationPolicyUnion authenticationPolicy) { if (authenticationPolicy.getBasicAuthenticationPolicy() != null) { return Optional.ofNullable( basicAuthProvider(app, workflow, authenticationPolicy.getBasicAuthenticationPolicy())); @@ -66,8 +62,7 @@ private Optional buildFromPolicy( bearerAuthProvider(app, workflow, authenticationPolicy.getBearerAuthenticationPolicy())); } else if (authenticationPolicy.getDigestAuthenticationPolicy() != null) { return Optional.ofNullable( - digestAuthProvider( - app, workflow, authenticationPolicy.getDigestAuthenticationPolicy(), method)); + digestAuthProvider(app, workflow, authenticationPolicy.getDigestAuthenticationPolicy())); } return OAuthUtils.from(authenticationPolicy) .map( @@ -90,10 +85,9 @@ protected AuthProvider openIdAuthProvider( protected AuthProvider digestAuthProvider( WorkflowApplication app, Workflow workflow, - DigestAuthenticationPolicy digestAuthenticationPolicy, - String method) { + DigestAuthenticationPolicy digestAuthenticationPolicy) { - return new DigestAuthProvider(app, workflow, digestAuthenticationPolicy, method); + return new DigestAuthProvider(app, workflow, digestAuthenticationPolicy); } protected AuthProvider bearerAuthProvider( diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/DigestAuthProvider.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/DigestAuthProvider.java index 26d87ccd2..5d5aefbf4 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/DigestAuthProvider.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/DigestAuthProvider.java @@ -20,8 +20,6 @@ import static org.openworkflow.sdk.impl.auth.AuthUtils.PASSWORD; import static org.openworkflow.sdk.impl.auth.AuthUtils.USER; -import java.io.IOException; -import java.net.HttpURLConnection; import java.net.URI; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -38,9 +36,10 @@ import org.openworkflow.sdk.impl.TaskContext; import org.openworkflow.sdk.impl.WorkflowApplication; import org.openworkflow.sdk.impl.WorkflowContext; -import org.openworkflow.sdk.impl.WorkflowModel; import org.openworkflow.sdk.impl.WorkflowUtils; import org.openworkflow.sdk.impl.WorkflowValueResolver; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestInfo; +import org.openworkflow.sdk.impl.http.WorkflowHttpResponse; import org.openworkflow.sdk.impl.utils.RandomFactory; class DigestAuthProvider implements AuthProvider { @@ -115,14 +114,12 @@ private static enum QOP { private static final String HEX = "%08x"; private final WorkflowValueResolver userFilter; private final WorkflowValueResolver passwordFilter; - private final String method; + private final WorkflowApplication app; private final Random random = RandomFactory.getRandom(); public DigestAuthProvider( - WorkflowApplication app, - Workflow workflow, - DigestAuthenticationPolicy authPolicy, - String method) { + WorkflowApplication app, Workflow workflow, DigestAuthenticationPolicy authPolicy) { + this.app = app; DigestAuthenticationProperties properties = authPolicy.getDigest().getDigestAuthenticationProperties(); if (properties != null) { @@ -131,13 +128,12 @@ public DigestAuthProvider( } else if (authPolicy.getDigest().getDigestAuthenticationPolicySecret() != null) { String secretName = checkSecret(workflow, authPolicy.getDigest().getDigestAuthenticationPolicySecret()); - userFilter = (w, t, m) -> secretProp(w, secretName, USER); - passwordFilter = (w, t, m) -> secretProp(w, secretName, PASSWORD); + userFilter = (w, t, m) -> secretProp(app, secretName, USER); + passwordFilter = (w, t, m) -> secretProp(app, secretName, PASSWORD); } else { throw new IllegalStateException( "Both secret and properties are null for digest authorization"); } - this.method = method; } @Override @@ -147,59 +143,68 @@ public String scheme() { @Override public CompletableFuture content( - WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { - try { - HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); - connection.setRequestMethod(method); - int responseCode = connection.getResponseCode(); - if (responseCode == 401) { - DigestServerInfo serverInfo = - DigestServerInfo.from(connection.getHeaderField("WWW-Authenticate")); - String userName = userFilter.apply(workflow, task, model); - String path = uri.getPath(); - String nonceCount; - String clientNonce; - if (serverInfo.qop.isPresent() || serverInfo.algorithm == Algorithm.MD5SESSS) { - nonceCount = String.format(HEX, nc.getAndIncrement()); - clientNonce = String.format(HEX, random.nextInt()); - } else { - nonceCount = null; - clientNonce = null; - } - final String hash1 = - calculateHash(userName, serverInfo.realm, passwordFilter.apply(workflow, task, model)); - final String ha1 = - serverInfo.algorithm == Algorithm.MD5SESSS - ? calculateHash(hash1, serverInfo.nonce, clientNonce) - : hash1; - final String ha2 = calculateHash(method, path); - String response = - serverInfo - .qop - .map( - qop -> - calculateHash( - ha1, - serverInfo.nonce, - nonceCount, - clientNonce, - qop.toString().toLowerCase(), - ha2)) - .orElseGet(() -> calculateHash(ha1, serverInfo.nonce, ha2)); + WorkflowContext workflow, TaskContext task, URI uri, String method) { + return app.httpRequestFactory() + .request(app, WorkflowHttpRequestInfo.builder(method).build(uri)) + .invoke(workflow, task) + .thenApply( + r -> { + try { + return processResponse(r, uri, method, workflow, task); + } finally { + r.close(); + } + }); + } - return CompletableFuture.completedFuture( - buildResponseInfo(serverInfo, userName, path, clientNonce, nonceCount, response)); + private String processResponse( + WorkflowHttpResponse r, URI uri, String method, WorkflowContext workflow, TaskContext task) { + int responseCode = r.status(); + if (responseCode == 401) { + DigestServerInfo serverInfo = DigestServerInfo.from((String) r.getHeader("WWW-Authenticate")); + String userName = userFilter.apply(workflow, task); + String path = uri.getPath(); + String nonceCount; + String clientNonce; + if (serverInfo.qop.isPresent() || serverInfo.algorithm == Algorithm.MD5SESSS) { + nonceCount = String.format(HEX, nc.getAndIncrement()); + clientNonce = String.format(HEX, random.nextInt()); } else { - return CompletableFuture.failedFuture( - new IllegalStateException( - "URI " - + uri - + " is not digest protected, it returned code " - + responseCode - + " when invoked without authentication header, but it should have returned 401 as per RFC 2617")); + nonceCount = null; + clientNonce = null; } - } catch (IOException io) { - return CompletableFuture.failedFuture(io); + final String hash1 = + calculateHash(userName, serverInfo.realm, passwordFilter.apply(workflow, task)); + final String ha1 = + serverInfo.algorithm == Algorithm.MD5SESSS + ? calculateHash(hash1, serverInfo.nonce, clientNonce) + : hash1; + final String ha2 = calculateHash(method, path); + return buildResponseInfo( + serverInfo, + userName, + path, + clientNonce, + nonceCount, + serverInfo + .qop + .map( + qop -> + calculateHash( + ha1, + serverInfo.nonce, + nonceCount, + clientNonce, + qop.toString().toLowerCase(), + ha2)) + .orElseGet(() -> calculateHash(ha1, serverInfo.nonce, ha2))); + } else { + throw new IllegalStateException( + "URI " + + uri + + " is not digest protected, it returned code " + + responseCode + + " when invoked without authentication header, but it should have returned 401 as per RFC 2617"); } } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/HttpRequestInfoBuilder.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/HttpRequestInfoBuilder.java index f016d0b56..740c8cd16 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/HttpRequestInfoBuilder.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/auth/HttpRequestInfoBuilder.java @@ -114,13 +114,13 @@ HttpRequestInfoBuilder withGrantType(String grantType) { return this; } - HttpRequestInfo build() { + AuthHttpRequestInfo build() { Objects.requireNonNull(uri, "URI must be set before building the request"); Objects.requireNonNull(grantType, "Grant type must be set before building the request"); if (contentType == null) { contentType = APPLICATION_X_WWW_FORM_URLENCODED.value(); } - return new HttpRequestInfo( + return new AuthHttpRequestInfo( headers, queryParams, clientAuthParams, diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/executors/RunScriptExecutorBuilder.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/executors/RunScriptExecutorBuilder.java index 2f4d3de2d..c5640f1e5 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/executors/RunScriptExecutorBuilder.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/executors/RunScriptExecutorBuilder.java @@ -58,7 +58,7 @@ public CallableTask build(RunScript taskConfiguration, WorkflowDefinition defini scriptUnion.getExternalScript(), "External script is required if inline script was not set") .getSource(), - ResourceLoaderUtils::readString, + h -> ResourceLoaderUtils.readString(h, w, t), w, t, m), diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/CallableTaskHttpExecutorBuilder.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/executors/http/CallableTaskHttpExecutorBuilder.java similarity index 83% rename from impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/CallableTaskHttpExecutorBuilder.java rename to impl/core/src/main/java/org/openworkflow/sdk/impl/executors/http/CallableTaskHttpExecutorBuilder.java index 22e83bf19..84b0299d9 100644 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/CallableTaskHttpExecutorBuilder.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/executors/http/CallableTaskHttpExecutorBuilder.java @@ -27,6 +27,7 @@ import org.openworkflow.sdk.impl.WorkflowValueResolver; import org.openworkflow.sdk.impl.executors.CallableTaskBuilder; import org.openworkflow.sdk.impl.executors.CallableTaskFactory; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestInfoBuilderFactory; public class CallableTaskHttpExecutorBuilder implements CallableTaskBuilder { @@ -34,16 +35,16 @@ public class CallableTaskHttpExecutorBuilder implements CallableTaskBuilder uriSupplier = definition.resourceLoader().uriSupplier(endpoint); - if (httpArgs.getHeaders() != null) { builder.withHeaders( buildMapResolver( @@ -53,7 +54,6 @@ public CallableTaskFactory init( ? httpArgs.getHeaders().getHTTPHeaders().getAdditionalProperties() : null)); } - if (httpArgs.getQuery() != null) { builder.withQueryMap( buildMapResolver( @@ -63,11 +63,9 @@ public CallableTaskFactory init( ? httpArgs.getQuery().getHTTPQuery().getAdditionalProperties() : null)); } - - builder.withBody(httpArgs.getBody()); - builder.withMethod(httpArgs.getMethod().toUpperCase()); - builder.redirect(httpArgs.isRedirect()); - return () -> builder.build(uriSupplier); + final WorkflowValueResolver uriSupplier = + definition.resourceLoader().uriSupplier(endpoint); + return () -> new HttpExecutor(builder, uriSupplier); } @Override diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpExecutor.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpExecutor.java new file mode 100644 index 000000000..398deec94 --- /dev/null +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpExecutor.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.executors.http; + +import java.net.URI; +import java.util.concurrent.CompletableFuture; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowContext; +import org.openworkflow.sdk.impl.WorkflowModel; +import org.openworkflow.sdk.impl.WorkflowValueResolver; +import org.openworkflow.sdk.impl.executors.CallableTask; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestHelper; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestInfoBuilderFactory; + +public class HttpExecutor implements CallableTask { + + private final WorkflowHttpRequestInfoBuilderFactory builder; + private final WorkflowValueResolver uriSupplier; + + public HttpExecutor( + WorkflowHttpRequestInfoBuilderFactory builder, WorkflowValueResolver uriSupplier) { + this.builder = builder; + this.uriSupplier = uriSupplier; + } + + public CompletableFuture apply( + WorkflowContext workflow, TaskContext taskContext, WorkflowModel input) { + return WorkflowHttpRequestHelper.executeRequest( + builder + .build(workflow, taskContext, input) + .build(uriSupplier.apply(workflow, taskContext, input)), + workflow, + taskContext); + } +} diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultHttpRequest.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultHttpRequest.java new file mode 100644 index 000000000..0a4e3a6a6 --- /dev/null +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultHttpRequest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.http; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowApplication; +import org.openworkflow.sdk.impl.WorkflowContext; +import org.openworkflow.sdk.impl.WorkflowModel; + +public class DefaultHttpRequest implements WorkflowHttpRequest { + + private final WorkflowHttpRequestInfo httpInfo; + private final HttpClient client; + private final WorkflowApplication app; + + public DefaultHttpRequest( + WorkflowApplication app, HttpClient client, WorkflowHttpRequestInfo httpInfo) { + this.httpInfo = httpInfo; + this.client = client; + this.app = app; + } + + @Override + public CompletableFuture invoke( + WorkflowContext workflowContext, TaskContext taskContext) { + Builder builder = HttpRequest.newBuilder(); + WorkflowModel body = httpInfo.body(); + builder.method( + httpInfo.method(), + body == null || body.isNull() + ? HttpRequest.BodyPublishers.noBody() + : HttpRequest.BodyPublishers.ofByteArray( + body.asJson() + .orElseThrow( + () -> + new IllegalStateException( + "Underlying model needs to support json in order to use http")))); + Map headers = httpInfo.headers(); + + // add default content type, it might be override by specified headers. + builder.header("Content-Type", "application/json"); + headers.forEach((k, v) -> builder.setHeader(k, v.toString())); + URI uri = httpInfo.uri(); + Map queryParams = httpInfo.queryParams(); + if (!queryParams.isEmpty()) { + uri = addQueryParams(uri, queryParams); + } + builder.uri(uri); + String ifModifiedSince = httpInfo.ifModifiedSince(); + if (ifModifiedSince != null) { + builder.header(WorkflowHttpRequestHelper.IF_MODIFIED_SINCE_HEADER, ifModifiedSince); + } + return WorkflowHttpRequestHelper.processAuth( + httpInfo, builder::header, workflowContext, taskContext) + .thenCompose( + __ -> + client + .sendAsync(builder.build(), HttpResponse.BodyHandlers.ofByteArray()) + .exceptionallyCompose( + ex -> + CompletableFuture.failedFuture( + WorkflowHttpRequestHelper.fromError(ex, taskContext)))) + .thenApply(response -> new DefaultHttpResponse(app, taskContext, response)); + } + + private URI addQueryParams(URI uri, Map queryParams) { + String addedQuery = + queryParams.entrySet().stream() + .map( + entry -> + URIDecodedBuilder.encode(entry.getKey()) + + "=" + + URIDecodedBuilder.encode(entry.getValue())) + .collect(Collectors.joining("&")); + String rawQuery = uri.getRawQuery(); + return new URIDecodedBuilder(uri) + .query(rawQuery == null || rawQuery.isEmpty() ? addedQuery : rawQuery + "&" + addedQuery) + .build(); + } +} diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/auth/JaxRSAccessTokenProviderFactory.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultHttpRequestBuilderFactory.java similarity index 52% rename from impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/auth/JaxRSAccessTokenProviderFactory.java rename to impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultHttpRequestBuilderFactory.java index ddbc71bba..a9864927d 100644 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/auth/JaxRSAccessTokenProviderFactory.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultHttpRequestBuilderFactory.java @@ -13,19 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openworkflow.sdk.impl.executors.http.auth; +package org.openworkflow.sdk.impl.http; -import java.util.List; -import org.openworkflow.sdk.impl.auth.AccessTokenProvider; -import org.openworkflow.sdk.impl.auth.AccessTokenProviderFactory; -import org.openworkflow.sdk.impl.auth.HttpRequestInfo; -import org.openworkflow.sdk.impl.auth.JWTConverter; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Redirect; +import org.openworkflow.sdk.impl.WorkflowApplication; -public class JaxRSAccessTokenProviderFactory implements AccessTokenProviderFactory { +public class DefaultHttpRequestBuilderFactory implements WorkflowHttpRequestFactory { + + private HttpClient client = HttpClient.newBuilder().followRedirects(Redirect.NORMAL).build(); + + @Override + public WorkflowHttpRequest request(WorkflowApplication app, WorkflowHttpRequestInfo httpInfo) { + return new DefaultHttpRequest(app, client, httpInfo); + } @Override - public AccessTokenProvider build( - HttpRequestInfo requestInfo, List issuers, JWTConverter converter) { - return new JaxRSAccessTokenProvider(requestInfo, issuers, converter); + public void close() { + client.close(); } } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultHttpResponse.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultHttpResponse.java new file mode 100644 index 000000000..d54fd1363 --- /dev/null +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultHttpResponse.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.http; + +import java.net.http.HttpResponse; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Optional; +import java.util.OptionalLong; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowApplication; +import org.openworkflow.sdk.impl.WorkflowModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DefaultHttpResponse implements WorkflowHttpResponse { + + private static final Logger logger = LoggerFactory.getLogger(DefaultHttpResponse.class); + private final WorkflowApplication application; + private final HttpResponse response; + private final TaskContext context; + + public DefaultHttpResponse( + WorkflowApplication application, TaskContext context, HttpResponse response) { + this.application = application; + this.response = response; + this.context = context; + } + + @Override + public byte[] asByteArray() { + return response.body(); + } + + @Override + public Object getHeader(String name) { + return response.headers().firstValue(name).orElse(null); + } + + @Override + public int status() { + return response.statusCode(); + } + + @Override + public OptionalLong lastModified() { + return response + .headers() + .firstValue("Last-Modified") + .map(DefaultHttpResponse::fromGMTString) + .orElseGet(OptionalLong::empty); + } + + @Override + public String errorDetails() { + byte[] body = response.body(); + return body != null ? new String(body) : "No error information"; + } + + @Override + public Optional asWorkflowModel() { + try { + return application.modelFactory().fromJson(response.body()); + } catch (Exception ex) { + throw WorkflowHttpRequestHelper.fromError(ex, context); + } + } + + @Override + public String toString() { + return "DefaultHttpResponse [response=" + response + "]"; + } + + private static OptionalLong fromGMTString(String str) { + try { + return OptionalLong.of( + ZonedDateTime.parse(str, DateTimeFormatter.RFC_1123_DATE_TIME) + .toInstant() + .toEpochMilli()); + } catch (DateTimeParseException ex) { + logger.warn("Invalid date returned in Last-Modified header", ex); + return OptionalLong.empty(); + } + } +} diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultURITemplateResolver.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultURITemplateResolver.java new file mode 100644 index 000000000..5d2f95f8b --- /dev/null +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/DefaultURITemplateResolver.java @@ -0,0 +1,624 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.http; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowContext; +import org.openworkflow.sdk.impl.WorkflowModel; + +public class DefaultURITemplateResolver implements URITemplateResolver { + + @Override + public URI resolveTemplates( + String uri, WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel model) { + return URI.create(expandImpl(uri, model.asMap().orElse(Map.of()))); + } + + // Private implementation + private enum Operator { + NO_OP, + PLUS, + HASH, + DOT, + SLASH, + SEMICOLON, + QUESTION_MARK, + AMP; + } + + private static void checkVarname(String token, int col) { + if (token.startsWith(".") || token.endsWith(".")) { + throw new IllegalArgumentException( + "Variable name cannot start or end with a dot at col:" + col); + } + if (token.contains("..")) { + throw new IllegalArgumentException( + "Variable name cannot contain consecutive dots at col:" + col); + } + for (int i = 0; i < token.length(); i++) { + if (token.charAt(i) == '%') { + if (i + 2 >= token.length() + || !isHexDigit(token.charAt(i + 1)) + || !isHexDigit(token.charAt(i + 2))) { + throw new IllegalArgumentException( + "Invalid percent-encoding in variable name at col:" + col); + } + } + } + } + + private static boolean isHexDigit(char c) { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + } + + private static void validateLiteral(Character c, int col) { + switch (c) { + case '+': + case '#': + case '/': + case ';': + case '?': + case '&': + case ' ': + case '!': + case '=': + case '$': + case '|': + case '*': + case ':': + case '~': + case '-': + throw new IllegalArgumentException( + "Illegal character identified in the token at col:" + col); + default: + break; + } + } + + private static int getMaxChar(StringBuilder buffer, int col) { + if (buffer == null || buffer.length() == 0) { + return -1; + } else { + String value = buffer.toString(); + + if (value.isEmpty()) { + return -1; + } else { + try { + if (value.charAt(0) == '0') { + throw new IllegalArgumentException( + "Leading zero not allowed in max chars at col:" + col); + } + int result = Integer.parseInt(value); + if (result < 1 || result > 9999) { + throw new IllegalArgumentException("Max chars out of range (1-9999) at col:" + col); + } + return result; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Cannot parse max chars at col:" + col); + } + } + } + } + + private static Operator getOperator(Character c, StringBuilder token, int col) { + switch (c) { + case '+': + return Operator.PLUS; + case '#': + return Operator.HASH; + case '.': + return Operator.DOT; + case '/': + return Operator.SLASH; + case ';': + return Operator.SEMICOLON; + case '?': + return Operator.QUESTION_MARK; + case '&': + return Operator.AMP; + default: + validateLiteral(c, col); + token.append(c); + return Operator.NO_OP; + } + } + + private static String expandImpl(String str, Map substitutions) { + final StringBuilder result = new StringBuilder(str.length() * 2); + + boolean toToken = false; + final StringBuilder token = new StringBuilder(); + + Operator operator = null; + boolean composite = false; + boolean toMaxCharBuffer = false; + final StringBuilder maxCharBuffer = new StringBuilder(3); + boolean firstToken = true; + + for (int i = 0; i < str.length(); i++) { + char character = str.charAt(i); + switch (character) { + case '{': + toToken = true; + token.setLength(0); + firstToken = true; + break; + case '}': + if (toToken) { + if (toMaxCharBuffer && maxCharBuffer.length() == 0) { + throw new IllegalArgumentException("Empty prefix modifier at col:" + i); + } + boolean expanded = + expandToken( + operator, + token.toString(), + composite, + getMaxChar(maxCharBuffer, i), + firstToken, + substitutions, + result, + i); + if (expanded && firstToken) { + firstToken = false; + } + toToken = false; + token.setLength(0); + operator = null; + composite = false; + toMaxCharBuffer = false; + maxCharBuffer.setLength(0); + } else { + throw new IllegalArgumentException("Failed to expand token, invalid at col:" + i); + } + break; + case ',': + if (toToken) { + if (toMaxCharBuffer && maxCharBuffer.length() == 0) { + throw new IllegalArgumentException("Empty prefix modifier at col:" + i); + } + boolean expanded = + expandToken( + operator, + token.toString(), + composite, + getMaxChar(maxCharBuffer, i), + firstToken, + substitutions, + result, + i); + if (expanded && firstToken) { + firstToken = false; + } + token.setLength(0); + composite = false; + toMaxCharBuffer = false; + maxCharBuffer.setLength(0); + break; + } + // Intentional fall-through for commas outside the {} + default: + if (toToken) { + if (operator == null) { + operator = getOperator(character, token, i); + } else if (toMaxCharBuffer) { + if (Character.isDigit(character)) { + maxCharBuffer.append(character); + } else { + throw new IllegalArgumentException( + "Illegal character identified in the token at col:" + i); + } + } else { + if (character == ':') { + toMaxCharBuffer = true; + maxCharBuffer.setLength(0); + } else if (character == '*') { + composite = true; + } else { + validateLiteral(character, i); + token.append(character); + } + } + } else { + if (character > 0x7F || Character.isHighSurrogate(character)) { + String toEncode; + if (Character.isHighSurrogate(character) + && i + 1 < str.length() + && Character.isLowSurrogate(str.charAt(i + 1))) { + toEncode = new String(new char[] {character, str.charAt(++i)}); + } else { + toEncode = Character.toString(character); + } + for (byte b : toEncode.getBytes(StandardCharsets.UTF_8)) { + result.append(String.format("%%%02X", b & 0xFF)); + } + } else { + result.append(character); + } + } + break; + } + } + + if (!toToken) { + return result.toString(); + } else { + throw new IllegalArgumentException("Unterminated token"); + } + } + + private static void addPrefix(Operator op, StringBuilder result) { + switch (op) { + case HASH: + result.append('#'); + break; + case DOT: + result.append('.'); + break; + case SLASH: + result.append('/'); + break; + case SEMICOLON: + result.append(';'); + break; + case QUESTION_MARK: + result.append('?'); + break; + case AMP: + result.append('&'); + break; + default: + return; + } + } + + private static void addSeparator(Operator op, StringBuilder result) { + switch (op) { + case DOT: + result.append('.'); + break; + case SLASH: + result.append('/'); + break; + case SEMICOLON: + result.append(';'); + break; + case QUESTION_MARK: + case AMP: + result.append('&'); + break; + default: + result.append(','); + return; + } + } + + private static void addValue( + Operator op, String token, Object value, StringBuilder result, int maxChar) { + switch (op) { + case PLUS: + case HASH: + addExpandedValue(null, value, result, maxChar, false); + break; + case QUESTION_MARK: + case AMP: + result.append(token + '='); + addExpandedValue(null, value, result, maxChar, true); + break; + case SEMICOLON: + result.append(token); + addExpandedValue("=", value, result, maxChar, true); + break; + case DOT: + case SLASH: + case NO_OP: + addExpandedValue(null, value, result, maxChar, true); + } + } + + private static void addValueElement( + Operator op, String token, Object value, StringBuilder result, int maxChar) { + switch (op) { + case PLUS: + case HASH: + addExpandedValue(null, value, result, maxChar, false); + break; + case QUESTION_MARK: + case AMP: + case SEMICOLON: + case DOT: + case SLASH: + case NO_OP: + addExpandedValue(null, value, result, maxChar, true); + } + } + + private static boolean isSurrogate(char cp) { + return (cp >= 0xD800 && cp <= 0xDFFF); + } + + private static boolean isIprivate(char cp) { + return (0xE000 <= cp && cp <= 0xF8FF); + } + + private static boolean isUcschar(char cp) { + return (0xA0 <= cp && cp <= 0xD7FF) + || (0xF900 <= cp && cp <= 0xFDCF) + || (0xFDF0 <= cp && cp <= 0xFFEF); + } + + private static void addExpandedValue( + String prefix, Object value, StringBuilder result, int maxChar, boolean replaceReserved) { + String stringValue = convertNativeTypes(value); + int cpCount = stringValue.codePointCount(0, stringValue.length()); + int max = (maxChar != -1) ? Math.min(maxChar, cpCount) : cpCount; + result.ensureCapacity(max * 2); // hint to SB + boolean toReserved = false; + final StringBuilder reservedBuffer = new StringBuilder(3); + + if (max > 0 && prefix != null) { + result.append(prefix); + } + + int charCount = 0; + for (int i = 0; i < stringValue.length() && charCount < max; i++) { + char character = stringValue.charAt(i); + charCount++; + + if (character == '%' && !replaceReserved) { + toReserved = true; + reservedBuffer.setLength(0); + } + + String toAppend = Character.toString(character); + try { + if (isSurrogate(character)) { + StringBuilder sb = new StringBuilder(); + sb.append(Character.toChars(stringValue.codePointAt(i++))); + toAppend = URLEncoder.encode(sb.toString(), StandardCharsets.UTF_8.name()); + } else if (replaceReserved || isUcschar(character) || isIprivate(character)) { + toAppend = + URLEncoder.encode(toAppend, StandardCharsets.UTF_8.name()) + .replace("*", "%2A") + .replace("%7E", "~"); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + + if (toReserved) { + reservedBuffer.append(toAppend); + + if (reservedBuffer.length() == 3) { + boolean isEncoded = false; + try { + URLDecoder.decode(reservedBuffer.toString(), StandardCharsets.UTF_8.name()); + isEncoded = true; + } catch (Exception e) { + // ignore + } + + if (isEncoded) { + result.append(reservedBuffer); + } else { + result.append("%25"); + // only if !replaceReserved + result.append(reservedBuffer.substring(1)); + } + toReserved = false; + reservedBuffer.setLength(0); + } + } else { + if (character == ' ') { + result.append("%20"); + } else if (character == '%') { + result.append("%25"); + } else { + result.append(toAppend); + } + } + } + + if (toReserved) { + result.append("%25"); + result.append(reservedBuffer.substring(1)); + } + } + + private static boolean isList(Object value) { + return value instanceof ArrayList + || // checking concrete instances first as it's faster + value instanceof List; + } + + private static boolean isMap(Object value) { + return value instanceof HashMap + || // checking concrete instances first as it's faster + value instanceof Map; + } + + enum SubstitutionType { + EMPTY, + STRING, + LIST, + MAP; + } + + private static SubstitutionType getSubstitutionType(Object value, int col) { + if (value == null) { + return SubstitutionType.EMPTY; + } else if (isNativeType(value)) { + return SubstitutionType.STRING; + } else if (isList(value)) { + return SubstitutionType.LIST; + } else if (isMap(value)) { + return SubstitutionType.MAP; + } else { + throw new IllegalArgumentException( + "Illegal class passed as substitution, found " + value.getClass() + " at col:" + col); + } + } + + private static boolean isEmpty(SubstitutionType substType, Object value) { + if (value == null) { + return true; + } else { + switch (substType) { + case STRING: + return false; + case LIST: + return ((List) value).isEmpty(); + case MAP: + return ((Map) value).isEmpty(); + default: + return true; + } + } + } + + private static boolean isNativeType(Object value) { + if (value instanceof String || value instanceof Boolean || value instanceof Number) { + return true; + } + return false; + } + + private static String convertNativeTypes(Object value) { + if (value instanceof String) { + return (String) value; + } else if (value instanceof Boolean || value instanceof Number) { + return value.toString(); + } + throw new IllegalArgumentException( + "Illegal class passed as substitution, found " + value.getClass()); + } + + // returns true if expansion happened + private static boolean expandToken( + Operator operator, + String token, + boolean composite, + int maxChar, + boolean firstToken, + Map substitutions, + StringBuilder result, + int col) { + if (token.isEmpty()) { + throw new IllegalArgumentException("Found an empty token at col:" + col); + } + + checkVarname(token, col); + + Object value = substitutions.get(token); + SubstitutionType substType = getSubstitutionType(value, col); + if (substType == SubstitutionType.EMPTY || isEmpty(substType, value)) { + return false; + } + + if (firstToken) { + addPrefix(operator, result); + } else { + addSeparator(operator, result); + } + + switch (substType) { + case STRING: + addStringValue(operator, token, (Object) value, result, maxChar); + break; + case LIST: + addListValue(operator, token, (List) value, result, maxChar, composite); + break; + case MAP: + addMapValue(operator, token, (Map) value, result, maxChar, composite); + break; + } + + return true; + } + + private static boolean addStringValue( + Operator operator, String token, Object value, StringBuilder result, int maxChar) { + addValue(operator, token, value, result, maxChar); + return true; + } + + private static boolean addListValue( + Operator operator, + String token, + List value, + StringBuilder result, + int maxChar, + boolean composite) { + boolean first = true; + for (Object v : value) { + if (first) { + addValue(operator, token, v, result, maxChar); + first = false; + } else { + if (composite) { + addSeparator(operator, result); + addValue(operator, token, v, result, maxChar); + } else { + result.append(','); + addValueElement(operator, token, v, result, maxChar); + } + } + } + return !first; + } + + private static boolean addMapValue( + Operator operator, + String token, + Map value, + StringBuilder result, + int maxChar, + boolean composite) { + boolean first = true; + if (maxChar != -1) { + throw new IllegalArgumentException("Value trimming is not allowed on Maps"); + } + for (Map.Entry v : value.entrySet()) { + if (composite) { + if (!first) { + addSeparator(operator, result); + } + addValueElement(operator, token, v.getKey(), result, maxChar); + result.append('='); + } else { + if (first) { + addValue(operator, token, v.getKey(), result, maxChar); + } else { + result.append(','); + addValueElement(operator, token, v.getKey(), result, maxChar); + } + result.append(','); + } + addValueElement(operator, token, v.getValue(), result, maxChar); + first = false; + } + return !first; + } +} diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/http/URIDecodedBuilder.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/URIDecodedBuilder.java new file mode 100644 index 000000000..fb589f701 --- /dev/null +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/URIDecodedBuilder.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.http; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +public class URIDecodedBuilder { + + private final URI originalUri; + + private String path; + private String query; + private String host; + + public URIDecodedBuilder(URI originalUri) { + this.originalUri = originalUri; + this.path = originalUri.getRawPath(); + this.query = originalUri.getRawQuery(); + this.host = originalUri.getHost(); + } + + public URIDecodedBuilder path(String path) { + this.path = path; + return this; + } + + public URIDecodedBuilder query(String query) { + this.query = query; + return this; + } + + public URIDecodedBuilder host(String host) { + this.host = host; + return this; + } + + public URI build() { + StringBuilder sb = new StringBuilder(); + + if (originalUri.getScheme() != null) { + sb.append(originalUri.getScheme()).append("://"); + } + if (originalUri.getRawUserInfo() != null) { + sb.append(originalUri.getRawUserInfo()).append("@"); + } + + if (host != null) { + sb.append(host); + } + + if (originalUri.getPort() != -1) { + sb.append(":").append(originalUri.getPort()); + } + + if (path != null) { + sb.append(path); + } + + if (query != null && !query.isBlank()) { + sb.append("?").append(query); + } + + if (originalUri.getRawFragment() != null) { + sb.append("#").append(originalUri.getRawFragment()); + } + + return URI.create(sb.toString()); + } + + public static String encode(Object part) { + return URLEncoder.encode(part.toString(), StandardCharsets.UTF_8).replace("+", "%20"); + } +} diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/URITemplateResolver.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/URITemplateResolver.java similarity index 95% rename from impl/core/src/main/java/org/openworkflow/sdk/impl/resources/URITemplateResolver.java rename to impl/core/src/main/java/org/openworkflow/sdk/impl/http/URITemplateResolver.java index ace9b68b3..ae36a8ae9 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/URITemplateResolver.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/URITemplateResolver.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openworkflow.sdk.impl.resources; +package org.openworkflow.sdk.impl.http; import java.net.URI; import org.openworkflow.sdk.impl.ServicePriority; diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/RequestExecutor.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequest.java similarity index 68% rename from impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/RequestExecutor.java rename to impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequest.java index f1dcffe3e..372d15a1b 100644 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/RequestExecutor.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequest.java @@ -13,17 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openworkflow.sdk.impl.executors.http; +package org.openworkflow.sdk.impl.http; -import jakarta.ws.rs.client.Invocation.Builder; -import java.net.URI; import java.util.concurrent.CompletableFuture; import org.openworkflow.sdk.impl.TaskContext; import org.openworkflow.sdk.impl.WorkflowContext; -import org.openworkflow.sdk.impl.WorkflowModel; -@FunctionalInterface -interface RequestExecutor { - CompletableFuture apply( - Builder request, URI uri, WorkflowContext workflow, TaskContext task, WorkflowModel model); +public interface WorkflowHttpRequest { + + CompletableFuture invoke( + WorkflowContext workflowContext, TaskContext taskContext); } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestFactory.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestFactory.java new file mode 100644 index 000000000..bbb77d067 --- /dev/null +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.http; + +import org.openworkflow.sdk.impl.ServicePriority; +import org.openworkflow.sdk.impl.WorkflowApplication; + +public interface WorkflowHttpRequestFactory extends ServicePriority, AutoCloseable { + + WorkflowHttpRequest request(WorkflowApplication app, WorkflowHttpRequestInfo httpInfo); + + default void init(WorkflowApplication app) {} + + default void close() {} +} diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestHelper.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestHelper.java new file mode 100644 index 000000000..946da813e --- /dev/null +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestHelper.java @@ -0,0 +1,129 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.http; + +import java.net.SocketTimeoutException; +import java.net.http.HttpTimeoutException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowApplication; +import org.openworkflow.sdk.impl.WorkflowContext; +import org.openworkflow.sdk.impl.WorkflowError; +import org.openworkflow.sdk.impl.WorkflowException; +import org.openworkflow.sdk.impl.WorkflowModel; +import org.openworkflow.sdk.impl.auth.AuthProvider; +import org.openworkflow.sdk.impl.auth.AuthUtils; +import org.openworkflow.sdk.types.Errors; + +public class WorkflowHttpRequestHelper { + + public static final String IF_MODIFIED_SINCE_HEADER = "If-Modified-Since"; + + public static CompletableFuture executeRequest( + WorkflowHttpRequestInfo requestInfo, WorkflowContext workflow, TaskContext taskContext) { + WorkflowApplication app = workflow.definition().application(); + return app.httpRequestFactory() + .request(app, requestInfo) + .invoke(workflow, taskContext) + .thenApply( + response -> { + try { + validateStatus(taskContext, requestInfo, response); + return response + .asWorkflowModel() + .orElseThrow( + () -> + new IllegalStateException( + "Unable to extract WorkflowModel from response " + response)); + } finally { + response.close(); + } + }); + } + + public static WorkflowException fromError(Throwable ex, TaskContext task) { + throw new WorkflowException( + WorkflowError.communication(errorCodeFromException(ex.getCause()), task, ex).build(), ex); + } + + private static int errorCodeFromException(Throwable ex) { + while (ex != null) { + if (ex instanceof TimeoutException + || ex instanceof SocketTimeoutException + || ex instanceof HttpTimeoutException) { + return Errors.TIMEOUT.status(); + } + ex = ex.getCause(); + } + return Errors.COMMUNICATION.status(); + } + + private static void validateStatus( + TaskContext task, WorkflowHttpRequestInfo requestInfo, WorkflowHttpResponse response) { + int status = response.status(); + if (!isSuccessful(status) && (!requestInfo.redirect() || !isRedirection(status))) { + throw new WorkflowException( + WorkflowError.communication(status, task) + .title(response.reasonPhrase()) + .details(response.errorDetails()) + .build()); + } + } + + public static CompletableFuture processAuth( + WorkflowHttpRequestInfo requestInfo, + BiConsumer headerFiller, + WorkflowContext workflow, + TaskContext task) { + return requestInfo.auth() != null + ? addAuthHeader(requestInfo.auth(), requestInfo, headerFiller, workflow, task) + : CompletableFuture.completedFuture(null); + } + + private static CompletableFuture addAuthHeader( + AuthProvider auth, + WorkflowHttpRequestInfo info, + BiConsumer headerFiller, + WorkflowContext workflow, + TaskContext task) { + String scheme = auth.scheme(); + return auth.content(workflow, task, info.uri(), info.method()) + .thenAccept( + parameter -> { + if (task != null) { + task.authorization(scheme, parameter); + } + headerFiller.accept( + AuthUtils.AUTH_HEADER_NAME, AuthUtils.authHeaderValue(scheme, parameter)); + }); + } + + private static boolean isSuccessful(int status) { + return inStatusRange(status, 200, 300); + } + + private static boolean isRedirection(int status) { + return inStatusRange(status, 300, 400); + } + + private static boolean inStatusRange(int status, int lower, int upper) { + return status >= lower && status < upper; + } + + private WorkflowHttpRequestHelper() {} +} diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestInfo.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestInfo.java new file mode 100644 index 000000000..a6be9a960 --- /dev/null +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestInfo.java @@ -0,0 +1,127 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.http; + +import java.net.URI; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; +import org.openworkflow.sdk.impl.WorkflowModel; +import org.openworkflow.sdk.impl.WorkflowUtils; +import org.openworkflow.sdk.impl.auth.AuthProvider; + +public record WorkflowHttpRequestInfo( + URI uri, + String method, + boolean redirect, + Map headers, + Map queryParams, + WorkflowModel body, + AuthProvider auth, + String ifModifiedSince) { + + public static Builder builder(String method) { + return new Builder(method); + } + + public static class Builder { + private final String method; + private boolean redirect; + private Map headers = Map.of(); + private Map queryParams = Map.of(); + private WorkflowModel body; + private AuthProvider auth; + private Long ifModifiedSince; + private URI path; + + public Builder() { + this.method = "GET"; + } + + public Builder(String method) { + this.method = Objects.requireNonNull(method).toUpperCase(); + } + + public Builder redirect(boolean redirect) { + this.redirect = redirect; + return this; + } + + public Builder headers(Map headers) { + this.headers = headers; + return this; + } + + public Builder path(URI path) { + this.path = path; + return this; + } + + public Builder queryParams(Map queryParams) { + this.queryParams = queryParams; + return this; + } + + public Builder body(WorkflowModel body) { + this.body = body; + return this; + } + + public Builder auth(AuthProvider auth) { + this.auth = auth; + return this; + } + + public Builder ifModifiedSince(long millis) { + this.ifModifiedSince = millis; + return this; + } + + public WorkflowHttpRequestInfo build(URI uri) { + return new WorkflowHttpRequestInfo( + path == null ? uri : WorkflowUtils.concatURI(uri, path), + method, + redirect, + headers != null + ? headers.entrySet().stream() + .filter(e -> e.getValue() != null) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)) + : Map.of(), + queryParams != null + ? queryParams.entrySet().stream() + .collect( + Collectors.toMap( + Entry::getKey, e -> e.getValue() == null ? "" : e.getValue())) + : Map.of(), + body, + auth, + ifModifiedSince != null + ? Instant.ofEpochMilli(ifModifiedSince) + .atZone(ZoneOffset.UTC) + .format( + DateTimeFormatter.ofPattern( + "EEE, dd MMM uuuu HH:mm:ss 'GMT'", + Locale.US)) // RFC_1123 skips the 0 in single + // digit days + : null); + } + } +} diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestInfoBuilderFactory.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestInfoBuilderFactory.java new file mode 100644 index 000000000..60bf33a25 --- /dev/null +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpRequestInfoBuilderFactory.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.http; + +import java.net.URI; +import java.util.Map; +import org.openworkflow.sdk.api.types.ReferenceableAuthenticationPolicy; +import org.openworkflow.sdk.api.types.Use; +import org.openworkflow.sdk.api.types.UseAuthentications; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowContext; +import org.openworkflow.sdk.impl.WorkflowDefinition; +import org.openworkflow.sdk.impl.WorkflowFilter; +import org.openworkflow.sdk.impl.WorkflowModel; +import org.openworkflow.sdk.impl.WorkflowUtils; +import org.openworkflow.sdk.impl.WorkflowValueResolver; +import org.openworkflow.sdk.impl.auth.AuthProvider; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestInfo.Builder; + +public class WorkflowHttpRequestInfoBuilderFactory { + + private final WorkflowDefinition definition; + private WorkflowValueResolver pathSupplier; + private WorkflowFilter bodyFilter; + private String method = "GET"; + private AuthProvider auth; + private boolean redirect; + private WorkflowValueResolver> headers; + private WorkflowValueResolver> queryMap; + + public static WorkflowHttpRequestInfoBuilderFactory builder(WorkflowDefinition definition) { + return new WorkflowHttpRequestInfoBuilderFactory(definition); + } + + private WorkflowHttpRequestInfoBuilderFactory(WorkflowDefinition definition) { + this.definition = definition; + } + + public WorkflowHttpRequestInfoBuilderFactory withAuth(ReferenceableAuthenticationPolicy policy) { + if (policy != null) { + if (policy.getAuthenticationPolicyReference() != null) { + String name = policy.getAuthenticationPolicyReference().getUse(); + Use use = definition.workflow().getUse(); + UseAuthentications authentications = use == null ? null : use.getAuthentications(); + if (authentications == null + || !authentications.getAdditionalProperties().containsKey(name)) { + throw new IllegalArgumentException( + String.format( + "Authentication '%s' is referenced but not defined in use.authentications.", + name)); + } + } + auth = + definition.application().authProviderFactory().getAuth(definition, policy).orElse(null); + } + return this; + } + + public WorkflowHttpRequestInfoBuilderFactory withBody(Object body) { + this.bodyFilter = + body == null ? null : WorkflowUtils.buildWorkflowFilter(definition.application(), body); + return this; + } + + public WorkflowHttpRequestInfoBuilderFactory withPath(WorkflowValueResolver pathSupplier) { + this.pathSupplier = pathSupplier; + return this; + } + + public WorkflowHttpRequestInfoBuilderFactory withHeaders(Map headers) { + return withHeaders( + headers == null ? null : WorkflowUtils.buildMapResolver(definition.application(), headers)); + } + + public WorkflowHttpRequestInfoBuilderFactory withQueryMap(Map queryMap) { + return withQueryMap( + queryMap == null + ? null + : WorkflowUtils.buildMapResolver(definition.application(), queryMap)); + } + + public WorkflowHttpRequestInfoBuilderFactory withHeaders( + WorkflowValueResolver> headers) { + this.headers = headers; + return this; + } + + public WorkflowHttpRequestInfoBuilderFactory withQueryMap( + WorkflowValueResolver> queryMap) { + this.queryMap = queryMap; + return this; + } + + public WorkflowHttpRequestInfoBuilderFactory withMethod(String method) { + if (method != null) { + this.method = method.toUpperCase(); + } + return this; + } + + public WorkflowHttpRequestInfoBuilderFactory redirect(boolean redirect) { + this.redirect = redirect; + return this; + } + + public WorkflowHttpRequestInfo.Builder build( + WorkflowContext workflow, TaskContext taskContext, WorkflowModel input) { + Builder builder = WorkflowHttpRequestInfo.builder(method).redirect(redirect); + if (auth != null) { + builder.auth(auth); + } + if (queryMap != null) { + builder.queryParams(queryMap.apply(workflow, taskContext, input)); + } + if (headers != null) { + builder.headers(headers.apply(workflow, taskContext, input)); + } + if (bodyFilter != null) { + builder.body(bodyFilter.apply(workflow, taskContext, input)); + } + if (pathSupplier != null) { + builder.path(pathSupplier.apply(workflow, taskContext, input)); + } + return builder; + } +} diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpResponse.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpResponse.java new file mode 100644 index 000000000..9a02e162d --- /dev/null +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/http/WorkflowHttpResponse.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.http; + +import java.util.Optional; +import java.util.OptionalLong; +import org.openworkflow.sdk.impl.WorkflowModel; + +public interface WorkflowHttpResponse extends AutoCloseable { + + byte[] asByteArray(); + + Optional asWorkflowModel(); + + Object getHeader(String name); + + int status(); + + OptionalLong lastModified(); + + default String reasonPhrase() { + return switch (status()) { + case 100 -> "Continue"; + case 101 -> "Switching Protocols"; + case 102 -> "Processing"; + case 103 -> "Early Hints"; + case 200 -> "OK"; + case 201 -> "Created"; + case 202 -> "Accepted"; + case 204 -> "No Content"; + case 206 -> "Partial Content"; + case 301 -> "Moved Permanently"; + case 302 -> "Found"; + case 304 -> "Not Modified"; + case 307 -> "Temporary Redirect"; + case 308 -> "Permanent Redirect"; + case 400 -> "Bad Request"; + case 401 -> "Unauthorized"; + case 403 -> "Forbidden"; + case 404 -> "Not Found"; + case 405 -> "Method Not Allowed"; + case 408 -> "Request Timeout"; + case 409 -> "Conflict"; + case 429 -> "Too Many Requests"; + case 500 -> "Internal Server Error"; + case 502 -> "Bad Gateway"; + case 503 -> "Service Unavailable"; + case 504 -> "Gateway Timeout"; + default -> "Unknown Status Code"; + }; + } + + String errorDetails(); + + default void close() {} +} diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ClasspathResource.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ClasspathResource.java index 0fa344685..4f51ac41f 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ClasspathResource.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ClasspathResource.java @@ -18,6 +18,8 @@ import java.io.IOException; import java.io.InputStream; import java.util.Objects; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowContext; public class ClasspathResource implements ExternalResourceHandler { @@ -28,7 +30,8 @@ public ClasspathResource(String path) { } @Override - public InputStream open() throws IOException { + public InputStream open(WorkflowContext workflowContext, TaskContext taskContext) + throws IOException { InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path); if (is == null) { throw new IOException("Cannot load resource with path: " + path + " from classpath"); diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/DefaultResourceLoader.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/DefaultResourceLoader.java index 21f43ed46..161592a9c 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/DefaultResourceLoader.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/DefaultResourceLoader.java @@ -15,7 +15,6 @@ */ package org.openworkflow.sdk.impl.resources; -import java.net.MalformedURLException; import java.net.URI; import java.nio.file.Path; import java.time.Instant; @@ -23,7 +22,10 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import org.openworkflow.sdk.impl.TaskContext; import org.openworkflow.sdk.impl.WorkflowApplication; +import org.openworkflow.sdk.impl.WorkflowContext; +import org.openworkflow.sdk.impl.auth.AuthProvider; public class DefaultResourceLoader extends ResourceLoader { @@ -36,21 +38,6 @@ protected DefaultResourceLoader(WorkflowApplication application, Path workflowPa this.workflowPath = Optional.ofNullable(workflowPath); } - @Override - public T loadURI( - URI uri, Function function, Optional auth) { - ExternalResourceHandler resourceHandler = buildFromURI(uri, auth); - return (T) - resourceCache - .compute( - resourceHandler, - (k, v) -> - v == null || k.shouldReload(v.lastReload()) - ? new CachedResource(Instant.now(), function.apply(k)) - : v) - .content(); - } - protected ExternalResourceHandler fileResource(String pathStr) { Path path = Path.of(pathStr); if (path.isAbsolute()) { @@ -62,24 +49,22 @@ protected ExternalResourceHandler fileResource(String pathStr) { } } - protected ExternalResourceHandler httpResource(URI uri, Optional auth) { - try { - return new HttpResource(uri.toURL(), auth); - } catch (MalformedURLException e) { - throw new IllegalArgumentException(e); - } + protected ExternalResourceHandler httpResource( + URI uri, AuthProvider auth, WorkflowContext workflowContext, TaskContext taskContext) { + return new HttpResource(this.application, uri, auth); } protected URI handleURI(URI uri) { return GitHubHelper.handleURL(uri); } - private ExternalResourceHandler buildFromURI(URI uri, Optional auth) { + private ExternalResourceHandler buildFromURI( + URI uri, AuthProvider auth, WorkflowContext workflowContext, TaskContext taskContext) { String scheme = uri.getScheme(); if (scheme == null || scheme.equalsIgnoreCase("file")) { return fileResource(uri.getPath()); } else if (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) { - return httpResource(handleURI(uri), auth); + return httpResource(handleURI(uri), auth, workflowContext, taskContext); } else { throw new UnsupportedOperationException("Unsupported scheme " + scheme); } @@ -89,4 +74,23 @@ private ExternalResourceHandler buildFromURI(URI uri, Optional auth) { public void close() { resourceCache.clear(); } + + @Override + protected T loadURI( + URI uri, + Function function, + AuthProvider auth, + WorkflowContext workflowContext, + TaskContext taskContext) { + ExternalResourceHandler resourceHandler = buildFromURI(uri, auth, workflowContext, taskContext); + return (T) + resourceCache + .compute( + resourceHandler, + (k, v) -> + v == null || k.shouldReload(v.lastReload(), workflowContext, taskContext) + ? new CachedResource(Instant.now(), function.apply(k)) + : v) + .content(); + } } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ExternalResourceHandler.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ExternalResourceHandler.java index 74cffbe98..f665b4f89 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ExternalResourceHandler.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ExternalResourceHandler.java @@ -18,14 +18,21 @@ import java.io.IOException; import java.io.InputStream; import java.time.Instant; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowContext; public interface ExternalResourceHandler { String name(); - InputStream open() throws IOException; + default InputStream open() throws IOException { + return open(null, null); + } + + InputStream open(WorkflowContext workflowContext, TaskContext taskContext) throws IOException; - default boolean shouldReload(Instant lastUpdate) { + default boolean shouldReload( + Instant lastUpdate, WorkflowContext workflowContext, TaskContext taskContext) { return false; } } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/FileResource.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/FileResource.java index 72b0dc4aa..4413915dc 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/FileResource.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/FileResource.java @@ -23,6 +23,8 @@ import java.nio.file.attribute.BasicFileAttributes; import java.time.Instant; import java.util.Objects; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowContext; class FileResource implements ExternalResourceHandler { @@ -33,7 +35,8 @@ public FileResource(Path path) { } @Override - public InputStream open() throws IOException { + public InputStream open(WorkflowContext workflowContext, TaskContext taskContext) + throws IOException { return Files.newInputStream(path); } @@ -43,7 +46,8 @@ public String name() { } @Override - public boolean shouldReload(Instant lastUpdate) { + public boolean shouldReload( + Instant lastUpdate, WorkflowContext workflowContext, TaskContext taskContext) { try { return Files.readAttributes(path, BasicFileAttributes.class) .lastModifiedTime() diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/GitHubHelper.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/GitHubHelper.java index 54ce1773f..065487993 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/GitHubHelper.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/GitHubHelper.java @@ -16,7 +16,7 @@ package org.openworkflow.sdk.impl.resources; import java.net.URI; -import java.net.URISyntaxException; +import org.openworkflow.sdk.impl.http.URIDecodedBuilder; public class GitHubHelper { @@ -26,23 +26,11 @@ private GitHubHelper() {} public static URI handleURL(URI url) { if ("github.com".equals(url.getHost())) { - String path = url.getPath(); + String path = url.getRawPath(); if (path.startsWith(BLOB)) { path = path.substring(BLOB.length()); } - try { - return new URI( - url.getScheme(), - url.getUserInfo(), - "raw.githubusercontent.com", - url.getPort(), - path, - url.getQuery(), - url.getFragment()); - } catch (URISyntaxException e) { - return url; - } - + return new URIDecodedBuilder(url).host("raw.githubusercontent.com").path(path).build(); } else { return url; } diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/HttpResource.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/HttpResource.java index 968c336f9..97d343d02 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/HttpResource.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/HttpResource.java @@ -15,53 +15,91 @@ */ package org.openworkflow.sdk.impl.resources; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.UncheckedIOException; import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLConnection; +import java.net.MalformedURLException; +import java.net.URI; import java.time.Instant; import java.util.Objects; -import java.util.Optional; -import org.openworkflow.sdk.impl.auth.AuthUtils; +import java.util.OptionalLong; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowApplication; +import org.openworkflow.sdk.impl.WorkflowContext; +import org.openworkflow.sdk.impl.auth.AuthProvider; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestInfo; +import org.openworkflow.sdk.impl.http.WorkflowHttpResponse; public class HttpResource implements ExternalResourceHandler { - private final URL url; - private final Optional auth; + private final URI url; + private final AuthProvider auth; + private final WorkflowApplication application; + private volatile String name; - public HttpResource(URL url, Optional auth) { + public HttpResource(WorkflowApplication application, URI url, AuthProvider auth) { + this.application = application; this.url = url; this.auth = auth; } @Override - public InputStream open() throws IOException { - URLConnection connection = url.openConnection(); - auth.ifPresent(s -> connection.setRequestProperty(AuthUtils.AUTH_HEADER_NAME, s)); - return connection.getInputStream(); + public InputStream open(WorkflowContext workflowContext, TaskContext taskContext) + throws IOException { + WorkflowHttpRequestInfo.Builder info = WorkflowHttpRequestInfo.builder("GET").auth(auth); + try (WorkflowHttpResponse response = + application + .httpRequestFactory() + .request(application, info.build(url)) + .invoke(workflowContext, taskContext) + .join()) { + int status = response.status(); + if (status < 200 || status >= 300) { + throw new IOException( + "Failed to download resource " + + url + + ": HTTP " + + status + + " " + + response.reasonPhrase()); + } + return new ByteArrayInputStream(response.asByteArray()); + } } public String name() { - return url.getFile(); + if (name == null) { + try { + name = url.toURL().getFile(); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Cannot extract name from uri " + url, e); + } + } + return name; } @Override - public boolean shouldReload(Instant lastUpdate) { - try { - long millis = lastUpdate.toEpochMilli(); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setUseCaches(true); - connection.setRequestMethod("HEAD"); - connection.setIfModifiedSince(millis); - return connection.getResponseCode() != HttpURLConnection.HTTP_NOT_MODIFIED - && connection.getLastModified() > millis; - } catch (IOException e) { - throw new UncheckedIOException(e); + public boolean shouldReload( + Instant lastUpdate, WorkflowContext workflowContext, TaskContext taskContext) { + long millis = lastUpdate.toEpochMilli(); + WorkflowHttpRequestInfo.Builder info = + WorkflowHttpRequestInfo.builder("HEAD").ifModifiedSince(millis).auth(auth); + try (WorkflowHttpResponse response = + application + .httpRequestFactory() + .request(application, info.build(url)) + .invoke(workflowContext, taskContext) + .join()) { + return response.status() != HttpURLConnection.HTTP_NOT_MODIFIED + && checkLastModified(response.lastModified(), millis); } } + private boolean checkLastModified(OptionalLong lastModified, long millis) { + return lastModified.isEmpty() || lastModified.getAsLong() > millis; + } + @Override public int hashCode() { return Objects.hash(url); diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ResourceLoader.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ResourceLoader.java index 346291c32..6316416a7 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ResourceLoader.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ResourceLoader.java @@ -18,7 +18,6 @@ import static org.openworkflow.sdk.impl.WorkflowUtils.getURISupplier; import java.net.URI; -import java.util.Optional; import java.util.function.Function; import org.openworkflow.sdk.api.types.Endpoint; import org.openworkflow.sdk.api.types.EndpointUri; @@ -29,7 +28,7 @@ import org.openworkflow.sdk.impl.WorkflowContext; import org.openworkflow.sdk.impl.WorkflowModel; import org.openworkflow.sdk.impl.WorkflowValueResolver; -import org.openworkflow.sdk.impl.auth.AuthUtils; +import org.openworkflow.sdk.impl.auth.AuthProvider; import org.openworkflow.sdk.impl.expressions.ExpressionDescriptor; public abstract class ResourceLoader implements AutoCloseable { @@ -110,19 +109,21 @@ public T load( application .authProviderFactory() .getAuth(workflowContext.definition(), endPoint.getEndpointConfiguration()) - .map( - auth -> - AuthUtils.authHeaderValue( - auth.scheme(), - auth.content(workflowContext, taskContext, model, uri).join()))); + .orElse(null), + workflowContext, + taskContext); } public T loadURI(URI uri, Function function) { - return loadURI(uri, function, Optional.empty()); + return loadURI(uri, function, null, null, null); } protected abstract T loadURI( - URI uri, Function function, Optional auth); + URI uri, + Function function, + AuthProvider auth, + WorkflowContext workflowContext, + TaskContext context); private class ExpressionURISupplier implements WorkflowValueResolver { private WorkflowValueResolver expr; diff --git a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ResourceLoaderUtils.java b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ResourceLoaderUtils.java index dfae30c66..aaa340044 100644 --- a/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ResourceLoaderUtils.java +++ b/impl/core/src/main/java/org/openworkflow/sdk/impl/resources/ResourceLoaderUtils.java @@ -18,13 +18,20 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowContext; public class ResourceLoaderUtils { private ResourceLoaderUtils() {} public static String readString(ExternalResourceHandler handler) { - try (InputStream in = handler.open()) { + return readString(handler, null, null); + } + + public static String readString( + ExternalResourceHandler handler, WorkflowContext workflow, TaskContext task) { + try (InputStream in = handler.open(workflow, task)) { return new String(in.readAllBytes()); } catch (IOException io) { throw new UncheckedIOException(io); diff --git a/impl/core/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.executors.CallableTaskBuilder b/impl/core/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.executors.CallableTaskBuilder index 69ad9014d..27dfcf972 100644 --- a/impl/core/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.executors.CallableTaskBuilder +++ b/impl/core/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.executors.CallableTaskBuilder @@ -1 +1,2 @@ -org.openworkflow.sdk.impl.executors.CallFunctionExecutorBuilder \ No newline at end of file +org.openworkflow.sdk.impl.executors.CallFunctionExecutorBuilder +org.openworkflow.sdk.impl.executors.http.CallableTaskHttpExecutorBuilder \ No newline at end of file diff --git a/impl/http/pom.xml b/impl/http/pom.xml index d27dcafb4..fe34db6d2 100644 --- a/impl/http/pom.xml +++ b/impl/http/pom.xml @@ -10,7 +10,11 @@ org.openworkflow.sdk - openworkflow-impl-template-resolver + openworkflow-impl-core + + + jakarta.ws.rs + jakarta.ws.rs-api org.junit.jupiter diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/AbstractRequestExecutor.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/AbstractRequestExecutor.java deleted file mode 100644 index e30257372..000000000 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/AbstractRequestExecutor.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.executors.http; - -import static jakarta.ws.rs.core.Response.Status.Family.REDIRECTION; -import static jakarta.ws.rs.core.Response.Status.Family.SUCCESSFUL; - -import jakarta.ws.rs.ProcessingException; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.client.Invocation.Builder; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status.Family; -import java.net.SocketTimeoutException; -import java.net.URI; -import java.net.http.HttpTimeoutException; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeoutException; -import org.openworkflow.sdk.impl.TaskContext; -import org.openworkflow.sdk.impl.WorkflowContext; -import org.openworkflow.sdk.impl.WorkflowError; -import org.openworkflow.sdk.impl.WorkflowException; -import org.openworkflow.sdk.impl.WorkflowModel; -import org.openworkflow.sdk.impl.auth.AuthProvider; -import org.openworkflow.sdk.impl.auth.AuthUtils; -import org.openworkflow.sdk.types.Errors; - -abstract class AbstractRequestExecutor implements RequestExecutor { - - private final boolean redirect; - private final Optional authProvider; - protected final String method; - - public AbstractRequestExecutor(String method, boolean redirect, Optional auth) { - this.redirect = redirect; - this.method = method; - this.authProvider = auth; - } - - @Override - public CompletableFuture apply( - Builder request, URI uri, WorkflowContext workflow, TaskContext task, WorkflowModel model) { - ExecutorService executorService = workflow.definition().application().executorService(); - return authProvider - .map(auth -> addAuthHeader(auth, uri, request, workflow, task, model)) - .orElse(CompletableFuture.completedFuture(null)) - .thenApplyAsync( - __ -> - doRequest( - request, - HttpConverterResolver.converter(workflow, task), - workflow, - task, - model), - executorService); - } - - private WorkflowModel doRequest( - Builder request, - HttpModelConverter converter, - WorkflowContext workflow, - TaskContext task, - WorkflowModel model) { - try (Response response = invokeRequest(request, converter, workflow, task, model)) { - validateStatus(task, response, converter); - return workflow - .definition() - .application() - .modelFactory() - .fromAny(response.readEntity(converter.responseType())); - } catch (ProcessingException ex) { - throw new WorkflowException( - WorkflowError.communication(errorCodeFromException(ex.getCause()), task, ex).build(), ex); - } catch (WebApplicationException ex) { - throw new WorkflowException( - WorkflowError.communication(ex.getResponse().getStatus(), task, ex).build(), ex); - } - } - - protected int errorCodeFromException(Throwable ex) { - while (ex != null) { - if (ex instanceof TimeoutException - || ex instanceof SocketTimeoutException - || ex instanceof HttpTimeoutException) { - return Errors.TIMEOUT.status(); - } - ex = ex.getCause(); - } - return Errors.COMMUNICATION.status(); - } - - private void validateStatus(TaskContext task, Response response, HttpModelConverter converter) { - Family statusFamily = response.getStatusInfo().getFamily(); - if (statusFamily != SUCCESSFUL && (!this.redirect || statusFamily != REDIRECTION)) { - throw new WorkflowException( - converter - .errorFromResponse(WorkflowError.communication(response.getStatus(), task), response) - .build()); - } - } - - protected abstract Response invokeRequest( - Builder request, - HttpModelConverter converter, - WorkflowContext workflow, - TaskContext task, - WorkflowModel model); - - private CompletableFuture addAuthHeader( - AuthProvider auth, - URI uri, - Builder request, - WorkflowContext workflow, - TaskContext task, - WorkflowModel model) { - String scheme = auth.scheme(); - return auth.content(workflow, task, model, uri) - .thenAccept( - parameter -> { - task.authorization(scheme, parameter); - request.header( - AuthUtils.AUTH_HEADER_NAME, AuthUtils.authHeaderValue(scheme, parameter)); - }); - } -} diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpExecutor.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpExecutor.java deleted file mode 100644 index ba6be80c4..000000000 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpExecutor.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.executors.http; - -import jakarta.ws.rs.client.Invocation.Builder; -import jakarta.ws.rs.client.WebTarget; -import java.net.URI; -import java.util.Collection; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import org.openworkflow.sdk.impl.TaskContext; -import org.openworkflow.sdk.impl.WorkflowContext; -import org.openworkflow.sdk.impl.WorkflowModel; -import org.openworkflow.sdk.impl.WorkflowUtils; -import org.openworkflow.sdk.impl.WorkflowValueResolver; -import org.openworkflow.sdk.impl.executors.CallableTask; - -public class HttpExecutor implements CallableTask { - - private final WorkflowValueResolver uriSupplier; - private final Optional> pathSupplier; - private final Optional>> headersMap; - private final Optional>> queryMap; - private final RequestExecutor requestFunction; - private final Collection requestDecorators; - - HttpExecutor( - WorkflowValueResolver uriSupplier, - Optional>> headersMap, - Optional>> queryMap, - RequestExecutor requestFunction, - Optional> pathSupplier, - Collection requestDecorators) { - this.uriSupplier = uriSupplier; - this.headersMap = headersMap; - this.queryMap = queryMap; - this.requestFunction = requestFunction; - this.pathSupplier = pathSupplier; - this.requestDecorators = requestDecorators; - } - - public CompletableFuture apply( - WorkflowContext workflow, TaskContext taskContext, WorkflowModel input) { - URI uri = - pathSupplier - .map( - p -> - WorkflowUtils.concatURI( - uriSupplier.apply(workflow, taskContext, input), - p.apply(workflow, taskContext, input))) - .orElse(uriSupplier.apply(workflow, taskContext, input)); - - WebTarget target = HttpClientResolver.client(workflow, taskContext).target(uri); - for (Entry entry : - queryMap.map(q -> q.apply(workflow, taskContext, input)).orElse(Map.of()).entrySet()) { - target = target.queryParam(entry.getKey(), entry.getValue()); - } - Builder request = target.request(); - requestDecorators.forEach(d -> d.decorate(request, workflow, taskContext)); - headersMap.ifPresent(h -> h.apply(workflow, taskContext, input).forEach(request::header)); - return requestFunction.apply(request, uri, workflow, taskContext, input); - } -} diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpExecutorBuilder.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpExecutorBuilder.java deleted file mode 100644 index db15d100e..000000000 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpExecutorBuilder.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.executors.http; - -import jakarta.ws.rs.HttpMethod; -import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import org.openworkflow.sdk.api.types.ReferenceableAuthenticationPolicy; -import org.openworkflow.sdk.api.types.Use; -import org.openworkflow.sdk.api.types.UseAuthentications; -import org.openworkflow.sdk.impl.WorkflowDefinition; -import org.openworkflow.sdk.impl.WorkflowUtils; -import org.openworkflow.sdk.impl.WorkflowValueResolver; -import org.openworkflow.sdk.impl.auth.AuthProvider; - -public class HttpExecutorBuilder { - - public static final String HTTP_REQUEST_DECORATOR_KEY = "HttpRequestDecorators"; - private final WorkflowDefinition definition; - private List requestDecorators; - private WorkflowValueResolver pathSupplier; - private Object body; - private String method = HttpMethod.GET; - private ReferenceableAuthenticationPolicy policy; - private boolean redirect; - private WorkflowValueResolver> headersMap; - private WorkflowValueResolver> queryMap; - - private HttpExecutorBuilder(WorkflowDefinition definition) { - this.definition = definition; - this.requestDecorators = - new ArrayList<>(definition.application().serviceLoadedClasses(HttpRequestDecorator.class)); - requestDecorators.addAll( - definition - .application() - .>additionalObject(HTTP_REQUEST_DECORATOR_KEY) - .orElse(List.of())); - Collections.sort(requestDecorators); - } - - public HttpExecutorBuilder withAuth(ReferenceableAuthenticationPolicy policy) { - checkAuthentication(policy); - this.policy = policy; - return this; - } - - private void checkAuthentication(ReferenceableAuthenticationPolicy policy) { - if (policy == null || policy.getAuthenticationPolicyReference() == null) { - return; - } - String name = policy.getAuthenticationPolicyReference().getUse(); - Use use = definition.workflow().getUse(); - UseAuthentications authentications = use == null ? null : use.getAuthentications(); - if (authentications == null || !authentications.getAdditionalProperties().containsKey(name)) { - throw new IllegalArgumentException( - String.format( - "Authentication '%s' is referenced but not defined in use.authentications.", name)); - } - } - - public HttpExecutorBuilder withBody(Object body) { - this.body = body; - return this; - } - - public HttpExecutorBuilder withPath(WorkflowValueResolver pathSupplier) { - this.pathSupplier = pathSupplier; - return this; - } - - public HttpExecutorBuilder withHeaders(Map headersMap) { - return withHeaders(WorkflowUtils.buildMapResolver(definition.application(), headersMap)); - } - - public HttpExecutorBuilder withQueryMap(Map queryMap) { - return withQueryMap(WorkflowUtils.buildMapResolver(definition.application(), queryMap)); - } - - public HttpExecutorBuilder withHeaders(WorkflowValueResolver> headersMap) { - this.headersMap = headersMap; - return this; - } - - public HttpExecutorBuilder withQueryMap(WorkflowValueResolver> queryMap) { - this.queryMap = queryMap; - return this; - } - - public HttpExecutorBuilder withMethod(String method) { - this.method = method; - return this; - } - - public HttpExecutorBuilder redirect(boolean redirect) { - this.redirect = redirect; - return this; - } - - public HttpExecutor build(String uri) { - return build((w, f, n) -> URI.create(uri)); - } - - public HttpExecutor build(WorkflowValueResolver uriSupplier) { - return new HttpExecutor( - uriSupplier, - Optional.ofNullable(headersMap), - Optional.ofNullable(queryMap), - buildRequestExecutor(), - Optional.ofNullable(pathSupplier), - requestDecorators); - } - - public static HttpExecutorBuilder builder(WorkflowDefinition definition) { - return new HttpExecutorBuilder(definition); - } - - private RequestExecutor buildRequestExecutor() { - String httpMethod = method.toUpperCase(); - Optional auth = - definition.application().authProviderFactory().getAuth(definition, policy, httpMethod); - switch (httpMethod) { - case HttpMethod.POST: - case HttpMethod.PUT: - case HttpMethod.PATCH: - return body != null - ? new WithBodyRequestExecutor( - httpMethod, redirect, auth, definition.application(), body) - : new WithoutBodyRequestExecutor(httpMethod, redirect, auth); - case HttpMethod.DELETE: - case HttpMethod.HEAD: - case HttpMethod.OPTIONS: - case HttpMethod.GET: - default: - return new WithoutBodyRequestExecutor(httpMethod, redirect, auth); - } - } -} diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/WithBodyRequestExecutor.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/WithBodyRequestExecutor.java deleted file mode 100644 index 399939288..000000000 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/WithBodyRequestExecutor.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.executors.http; - -import jakarta.ws.rs.client.Invocation.Builder; -import jakarta.ws.rs.core.Response; -import java.util.Optional; -import org.openworkflow.sdk.impl.TaskContext; -import org.openworkflow.sdk.impl.WorkflowApplication; -import org.openworkflow.sdk.impl.WorkflowContext; -import org.openworkflow.sdk.impl.WorkflowFilter; -import org.openworkflow.sdk.impl.WorkflowModel; -import org.openworkflow.sdk.impl.WorkflowUtils; -import org.openworkflow.sdk.impl.auth.AuthProvider; - -class WithBodyRequestExecutor extends AbstractRequestExecutor { - private final WorkflowFilter bodyFilter; - - public WithBodyRequestExecutor( - String method, - boolean redirect, - Optional auth, - WorkflowApplication application, - Object body) { - super(method, redirect, auth); - bodyFilter = WorkflowUtils.buildWorkflowFilter(application, body); - } - - @Override - protected Response invokeRequest( - Builder request, - HttpModelConverter converter, - WorkflowContext workflow, - TaskContext task, - WorkflowModel model) { - return request.method(method, converter.toEntity(bodyFilter.apply(workflow, task, model))); - } -} diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/WithoutBodyRequestExecutor.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/WithoutBodyRequestExecutor.java deleted file mode 100644 index 7fbe835fe..000000000 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/WithoutBodyRequestExecutor.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.executors.http; - -import jakarta.ws.rs.client.Invocation.Builder; -import jakarta.ws.rs.core.Response; -import java.util.Optional; -import org.openworkflow.sdk.impl.TaskContext; -import org.openworkflow.sdk.impl.WorkflowContext; -import org.openworkflow.sdk.impl.WorkflowModel; -import org.openworkflow.sdk.impl.auth.AuthProvider; - -class WithoutBodyRequestExecutor extends AbstractRequestExecutor { - - public WithoutBodyRequestExecutor(String method, boolean redirect, Optional auth) { - super(method, redirect, auth); - } - - @Override - protected Response invokeRequest( - Builder request, - HttpModelConverter converter, - WorkflowContext workflow, - TaskContext task, - WorkflowModel model) { - return request.method(method); - } -} diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/auth/JaxRSAccessTokenProvider.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/auth/JaxRSAccessTokenProvider.java similarity index 81% rename from impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/auth/JaxRSAccessTokenProvider.java rename to impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/auth/JaxRSAccessTokenProvider.java index f7a612fe2..245acd594 100644 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/auth/JaxRSAccessTokenProvider.java +++ b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/auth/JaxRSAccessTokenProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openworkflow.sdk.impl.executors.http.auth; +package org.openworkflow.sdk.impl.jakarta.auth; import static org.openworkflow.sdk.api.types.OAuth2TokenRequest.Oauth2TokenRequestEncoding.APPLICATION_X_WWW_FORM_URLENCODED; @@ -32,34 +32,43 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowApplication; import org.openworkflow.sdk.impl.WorkflowContext; import org.openworkflow.sdk.impl.WorkflowError; import org.openworkflow.sdk.impl.WorkflowException; -import org.openworkflow.sdk.impl.WorkflowModel; import org.openworkflow.sdk.impl.auth.AccessTokenProvider; -import org.openworkflow.sdk.impl.auth.HttpRequestInfo; +import org.openworkflow.sdk.impl.auth.AuthHttpRequestInfo; import org.openworkflow.sdk.impl.auth.JWT; import org.openworkflow.sdk.impl.auth.JWTConverter; -import org.openworkflow.sdk.impl.executors.http.HttpClientResolver; +import org.openworkflow.sdk.impl.jakarta.http.HttpClientResolver; +// TODO this class should be refactor to be included in the core and use WorkflowHttpRequest class JaxRSAccessTokenProvider implements AccessTokenProvider { + private final WorkflowApplication app; + private final Client defaultClient; private final List issuers; - private final HttpRequestInfo requestInfo; + private final AuthHttpRequestInfo requestInfo; private final JWTConverter jwtConverter; JaxRSAccessTokenProvider( - HttpRequestInfo requestInfo, List issuers, JWTConverter converter) { + WorkflowApplication app, + Client client, + AuthHttpRequestInfo requestInfo, + List issuers, + JWTConverter converter) { + this.app = app; + this.defaultClient = client; this.requestInfo = requestInfo; this.issuers = issuers; this.jwtConverter = converter; } - public CompletableFuture validateAndGet( - WorkflowContext workflow, TaskContext context, WorkflowModel model) { + @Override + public CompletableFuture validateAndGet(WorkflowContext workflow, TaskContext context) { return CompletableFuture.supplyAsync( () -> { - Map token = invoke(workflow, context, model); + Map token = invoke(workflow, context); JWT jwt = jwtConverter.fromToken((String) token.get("access_token")); if (issuers != null && !issuers.isEmpty()) { jwt.issuer() @@ -72,12 +81,11 @@ public CompletableFuture validateAndGet( } return jwt; }, - workflow.definition().application().executorService()); + app.executorService()); } - private Map invoke( - WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel model) { - try (Response response = executeRequest(workflowContext, taskContext, model)) { + private Map invoke(WorkflowContext workflowContext, TaskContext taskContext) { + try (Response response = executeRequest(workflowContext, taskContext)) { int status = response.getStatus(); if (status < 200 || status >= 300) { StringBuilder message = new StringBuilder("Failed to obtain token. Error code: " + status); @@ -105,12 +113,12 @@ private Map invoke( } } - private Response executeRequest(WorkflowContext workflow, TaskContext task, WorkflowModel model) { - - Client client = HttpClientResolver.client(workflow, task); - WebTarget target = client.target(requestInfo.uri().apply(workflow, task, model)); - - Invocation.Builder builder = commonHeaders(target, workflow, task, model); + private Response executeRequest(WorkflowContext workflow, TaskContext task) { + Client client = + app.additionalObject(HttpClientResolver.HTTP_CLIENT_PROVIDER, workflow, task) + .orElse(defaultClient); + WebTarget target = client.target(requestInfo.uri().apply(workflow, task)); + Invocation.Builder builder = commonHeaders(target, workflow, task); builder.header("grant_type", requestInfo.grantType()); Entity entity; @@ -119,20 +127,20 @@ private Response executeRequest(WorkflowContext workflow, TaskContext task, Work form.param("grant_type", requestInfo.grantType()); requestInfo .queryParams() - .forEach((key, value) -> form.param(key, value.apply(workflow, task, model))); + .forEach((key, value) -> form.param(key, value.apply(workflow, task))); requestInfo .clientAuthParams() - .forEach((key, value) -> form.param(key, value.apply(workflow, task, model))); + .forEach((key, value) -> form.param(key, value.apply(workflow, task))); entity = Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED); } else { Map jsonData = new HashMap<>(); jsonData.put("grant_type", requestInfo.grantType()); requestInfo .queryParams() - .forEach((key, value) -> jsonData.put(key, value.apply(workflow, task, model))); + .forEach((key, value) -> jsonData.put(key, value.apply(workflow, task))); requestInfo .clientAuthParams() - .forEach((key, value) -> jsonData.put(key, value.apply(workflow, task, model))); + .forEach((key, value) -> jsonData.put(key, value.apply(workflow, task))); entity = Entity.entity(jsonData, MediaType.APPLICATION_JSON); } @@ -140,13 +148,13 @@ private Response executeRequest(WorkflowContext workflow, TaskContext task, Work } private Invocation.Builder commonHeaders( - WebTarget target, WorkflowContext workflow, TaskContext task, WorkflowModel model) { + WebTarget target, WorkflowContext workflow, TaskContext task) { Invocation.Builder builder = target.request(MediaType.APPLICATION_JSON); builder.header("User-Agent", "OAuth2-Client-Credentials/1.0"); builder.header("Accept", MediaType.APPLICATION_JSON); builder.header("Cache-Control", "no-cache"); for (var entry : requestInfo.headers().entrySet()) { - String headerValue = entry.getValue().apply(workflow, task, model); + String headerValue = entry.getValue().apply(workflow, task); if (headerValue != null) { builder.header(entry.getKey(), headerValue); } diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpClientResolver.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/auth/JaxRSAccessTokenProviderFactory.java similarity index 59% rename from impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpClientResolver.java rename to impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/auth/JaxRSAccessTokenProviderFactory.java index c34e28336..b83265f68 100644 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpClientResolver.java +++ b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/auth/JaxRSAccessTokenProviderFactory.java @@ -13,19 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openworkflow.sdk.impl.executors.http; +package org.openworkflow.sdk.impl.jakarta.auth; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; +import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import org.openworkflow.sdk.impl.TaskContext; import org.openworkflow.sdk.impl.WorkflowApplication; -import org.openworkflow.sdk.impl.WorkflowContext; +import org.openworkflow.sdk.impl.auth.AccessTokenProvider; +import org.openworkflow.sdk.impl.auth.AccessTokenProviderFactory; +import org.openworkflow.sdk.impl.auth.AuthHttpRequestInfo; +import org.openworkflow.sdk.impl.auth.JWTConverter; +import org.openworkflow.sdk.impl.jakarta.http.JakartaRequestExecutorFactory; -public class HttpClientResolver { - - public static final String HTTP_CLIENT_PROVIDER = "httpClientProvider"; +public class JaxRSAccessTokenProviderFactory implements AccessTokenProviderFactory { private static class DefaultHolder { @@ -47,11 +49,19 @@ private static final Client client() { } } - public static Client client(WorkflowContext workflowContext, TaskContext taskContext) { - WorkflowApplication appl = workflowContext.definition().application(); - return appl.additionalObject(HTTP_CLIENT_PROVIDER, workflowContext, taskContext) - .orElseGet(() -> DefaultHolder.client()); + @Override + public AccessTokenProvider build( + WorkflowApplication app, + AuthHttpRequestInfo requestInfo, + List issuers, + JWTConverter converter) { + return new JaxRSAccessTokenProvider( + app, + app.httpRequestFactory() instanceof JakartaRequestExecutorFactory factory + ? factory.client() + : DefaultHolder.client(), + requestInfo, + issuers, + converter); } - - private HttpClientResolver() {} } diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpClientResolver.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpClientResolver.java new file mode 100644 index 000000000..75dcde4cb --- /dev/null +++ b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpClientResolver.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.jakarta.http; + +public class HttpClientResolver { + + public static final String HTTP_CLIENT_PROVIDER = "httpClientProvider"; + + private HttpClientResolver() {} +} diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpConverterResolver.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpConverterResolver.java similarity index 79% rename from impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpConverterResolver.java rename to impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpConverterResolver.java index 783b7066d..8b2673d39 100644 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpConverterResolver.java +++ b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpConverterResolver.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openworkflow.sdk.impl.executors.http; +package org.openworkflow.sdk.impl.jakarta.http; import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowApplication; import org.openworkflow.sdk.impl.WorkflowContext; public class HttpConverterResolver { @@ -33,11 +34,9 @@ public Class responseType() { } public static HttpModelConverter converter( - WorkflowContext workflowContext, TaskContext taskContext) { - return workflowContext - .definition() - .application() - .additionalObject(HTTP_MODEL_CONVERTER, workflowContext, taskContext) + WorkflowApplication app, WorkflowContext workflowContext, TaskContext taskContext) { + return app.additionalObject( + HTTP_MODEL_CONVERTER, workflowContext, taskContext) .orElseGet(() -> DefaultHolder.converter); } diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpModelConverter.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpModelConverter.java similarity index 56% rename from impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpModelConverter.java rename to impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpModelConverter.java index 0e9ff3097..25b751e6e 100644 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpModelConverter.java +++ b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpModelConverter.java @@ -13,13 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openworkflow.sdk.impl.executors.http; +package org.openworkflow.sdk.impl.jakarta.http; import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.Response; -import org.openworkflow.sdk.impl.WorkflowError; import org.openworkflow.sdk.impl.WorkflowModel; -import org.slf4j.LoggerFactory; public interface HttpModelConverter { @@ -28,19 +25,4 @@ default Entity toEntity(WorkflowModel model) { } Class responseType(); - - default WorkflowError.Builder errorFromResponse( - WorkflowError.Builder errorBuilder, Response response) { - errorBuilder.title(response.getStatusInfo().getReasonPhrase()); - try { - Object details = response.readEntity(responseType()); - if (details != null) { - errorBuilder.details(details.toString()); - } - } catch (Exception ex) { - LoggerFactory.getLogger(HttpModelConverter.class) - .debug("Problem extracting error from http response", ex); - } - return errorBuilder; - } } diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpRequestDecorator.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpRequestDecorator.java similarity index 82% rename from impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpRequestDecorator.java rename to impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpRequestDecorator.java index e8d46dfc2..c3b40ae99 100644 --- a/impl/http/src/main/java/org/openworkflow/sdk/impl/executors/http/HttpRequestDecorator.java +++ b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/HttpRequestDecorator.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openworkflow.sdk.impl.executors.http; +package org.openworkflow.sdk.impl.jakarta.http; import jakarta.ws.rs.client.Invocation; import org.openworkflow.sdk.impl.WorkflowDecorator; -/** Interface for decorating HTTP requests with additional modifications. */ +/** + * Interface for decorating HTTP requests with additional modifications. Note that TaskContext might + * be null + */ public interface HttpRequestDecorator extends WorkflowDecorator {} diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/JakartaHttpResponse.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/JakartaHttpResponse.java new file mode 100644 index 000000000..07572779a --- /dev/null +++ b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/JakartaHttpResponse.java @@ -0,0 +1,108 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.jakarta.http; + +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.core.Response; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.stream.Collectors; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowApplication; +import org.openworkflow.sdk.impl.WorkflowModel; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestHelper; +import org.openworkflow.sdk.impl.http.WorkflowHttpResponse; + +public class JakartaHttpResponse implements WorkflowHttpResponse { + + private final WorkflowApplication app; + private HttpModelConverter converter; + private Response response; + private TaskContext taskContext; + + public JakartaHttpResponse( + WorkflowApplication app, + TaskContext taskContext, + HttpModelConverter converter, + Response response) { + this.app = app; + this.converter = converter; + this.response = response; + this.taskContext = taskContext; + } + + @Override + public byte[] asByteArray() { + return response.readEntity(byte[].class); + } + + @Override + public Object getHeader(String name) { + return fromListToSingleObject(response.getHeaders().get(name)); + } + + private Object fromListToSingleObject(List list) { + return list == null + ? null + : list.stream().map(Object::toString).collect(Collectors.joining(",")); + } + + @Override + public int status() { + return response.getStatus(); + } + + @Override + public OptionalLong lastModified() { + Date date = response.getLastModified(); + return date == null ? OptionalLong.empty() : OptionalLong.of(date.getTime()); + } + + @Override + public void close() { + response.close(); + } + + @Override + public String reasonPhrase() { + return response.getStatusInfo().getReasonPhrase(); + } + + @Override + public String errorDetails() { + try { + return response.readEntity(String.class); + } catch (ProcessingException ex) { + return ex.getMessage(); + } + } + + @Override + public Optional asWorkflowModel() { + try { + return Optional.of(app.modelFactory().fromAny(response.readEntity(converter.responseType()))); + } catch (ProcessingException ex) { + throw WorkflowHttpRequestHelper.fromError(ex, taskContext); + } + } + + @Override + public String toString() { + return "JakartaHttpResponse [response=" + response + "]"; + } +} diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/JakartaRequestExecutor.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/JakartaRequestExecutor.java new file mode 100644 index 000000000..be573b0a0 --- /dev/null +++ b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/JakartaRequestExecutor.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.jakarta.http; + +import jakarta.ws.rs.ProcessingException; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.Invocation.Builder; +import jakarta.ws.rs.client.WebTarget; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.WorkflowApplication; +import org.openworkflow.sdk.impl.WorkflowContext; +import org.openworkflow.sdk.impl.WorkflowError; +import org.openworkflow.sdk.impl.WorkflowException; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequest; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestHelper; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestInfo; +import org.openworkflow.sdk.impl.http.WorkflowHttpResponse; + +class JakartaRequestExecutor implements WorkflowHttpRequest { + + private final ExecutorService executorService; + private final WorkflowHttpRequestInfo requestInfo; + private final WorkflowApplication application; + private final Client defaultClient; + private final Collection requestDecorators; + + public JakartaRequestExecutor( + WorkflowApplication application, + Client defaultClient, + WorkflowHttpRequestInfo requestInfo, + List requestDecorators) { + this.application = application; + this.defaultClient = defaultClient; + this.executorService = application.executorService(); + this.requestInfo = requestInfo; + this.requestDecorators = requestDecorators; + } + + @Override + public CompletableFuture invoke( + WorkflowContext workflowContext, TaskContext taskContext) { + WebTarget target = + application + .additionalObject( + HttpClientResolver.HTTP_CLIENT_PROVIDER, workflowContext, taskContext) + .orElse(defaultClient) + .target(requestInfo.uri()); + Map queryParams = requestInfo.queryParams(); + for (Entry entry : queryParams.entrySet()) { + target = target.queryParam(entry.getKey(), entry.getValue()); + } + Builder request = target.request(); + String ifModifiedSince = requestInfo.ifModifiedSince(); + if (ifModifiedSince != null) { + request.header(WorkflowHttpRequestHelper.IF_MODIFIED_SINCE_HEADER, ifModifiedSince); + } + requestInfo.headers().forEach(request::header); + if (workflowContext != null) { + requestDecorators.forEach(d -> d.decorate(request, workflowContext, taskContext)); + } + return WorkflowHttpRequestHelper.processAuth( + requestInfo, request::header, workflowContext, taskContext) + .thenApplyAsync( + __ -> + doRequest( + request, + HttpConverterResolver.converter(application, workflowContext, taskContext), + taskContext), + executorService); + } + + private WorkflowHttpResponse doRequest( + Builder request, HttpModelConverter converter, TaskContext task) { + try { + return new JakartaHttpResponse( + application, + task, + converter, + requestInfo.body() == null || requestInfo.body().isNull() + ? request.method(requestInfo.method()) + : request.method(requestInfo.method(), converter.toEntity(requestInfo.body()))); + } catch (ProcessingException ex) { + throw WorkflowHttpRequestHelper.fromError(ex, task); + } catch (WebApplicationException ex) { + throw new WorkflowException( + WorkflowError.communication(ex.getResponse().getStatus(), task, ex).build(), ex); + } + } +} diff --git a/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/JakartaRequestExecutorFactory.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/JakartaRequestExecutorFactory.java new file mode 100644 index 000000000..9083c8212 --- /dev/null +++ b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/http/JakartaRequestExecutorFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020-Present The Open 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 org.openworkflow.sdk.impl.jakarta.http; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.openworkflow.sdk.impl.WorkflowApplication; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequest; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestFactory; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestInfo; + +public class JakartaRequestExecutorFactory implements WorkflowHttpRequestFactory { + + private static final String HTTP_REQUEST_DECORATOR_KEY = "HttpRequestDecorators"; + + private Client client = ClientBuilder.newClient(); + private List requestDecorators; + + public Client client() { + return client; + } + + @Override + public void init(WorkflowApplication app) { + requestDecorators = new ArrayList<>(app.serviceLoadedClasses(HttpRequestDecorator.class)); + requestDecorators.addAll( + app.>additionalObject(HTTP_REQUEST_DECORATOR_KEY) + .orElse(List.of())); + Collections.sort(requestDecorators); + } + + @Override + public WorkflowHttpRequest request(WorkflowApplication app, WorkflowHttpRequestInfo httpInfo) { + return new JakartaRequestExecutor(app, client, httpInfo, requestDecorators); + } + + @Override + public void close() { + client.close(); + } +} diff --git a/impl/template/src/main/java/org/openworkflow/sdk/impl/template/JaxrsURITemplateResolver.java b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/template/JaxrsURITemplateResolver.java similarity index 91% rename from impl/template/src/main/java/org/openworkflow/sdk/impl/template/JaxrsURITemplateResolver.java rename to impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/template/JaxrsURITemplateResolver.java index f2a365282..9db4ab7fd 100644 --- a/impl/template/src/main/java/org/openworkflow/sdk/impl/template/JaxrsURITemplateResolver.java +++ b/impl/http/src/main/java/org/openworkflow/sdk/impl/jakarta/template/JaxrsURITemplateResolver.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openworkflow.sdk.impl.template; +package org.openworkflow.sdk.impl.jakarta.template; import jakarta.ws.rs.core.UriBuilder; import java.net.URI; @@ -21,7 +21,7 @@ import org.openworkflow.sdk.impl.TaskContext; import org.openworkflow.sdk.impl.WorkflowContext; import org.openworkflow.sdk.impl.WorkflowModel; -import org.openworkflow.sdk.impl.resources.URITemplateResolver; +import org.openworkflow.sdk.impl.http.URITemplateResolver; public class JaxrsURITemplateResolver implements URITemplateResolver { @Override diff --git a/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.auth.AccessTokenProviderFactory b/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.auth.AccessTokenProviderFactory index f1dba17de..62abf8f92 100644 --- a/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.auth.AccessTokenProviderFactory +++ b/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.auth.AccessTokenProviderFactory @@ -1 +1 @@ -org.openworkflow.sdk.impl.executors.http.auth.JaxRSAccessTokenProviderFactory \ No newline at end of file +org.openworkflow.sdk.impl.jakarta.auth.JaxRSAccessTokenProviderFactory \ No newline at end of file diff --git a/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.executors.CallableTaskBuilder b/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.executors.CallableTaskBuilder deleted file mode 100644 index 96a53d33d..000000000 --- a/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.executors.CallableTaskBuilder +++ /dev/null @@ -1 +0,0 @@ -org.openworkflow.sdk.impl.executors.http.CallableTaskHttpExecutorBuilder \ No newline at end of file diff --git a/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.http.URITemplateResolver b/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.http.URITemplateResolver new file mode 100644 index 000000000..4e27a7cb9 --- /dev/null +++ b/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.http.URITemplateResolver @@ -0,0 +1 @@ +org.openworkflow.sdk.impl.jakarta.template.JaxrsURITemplateResolver \ No newline at end of file diff --git a/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.http.WorkflowHttpRequestFactory b/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.http.WorkflowHttpRequestFactory new file mode 100644 index 000000000..ead6390a6 --- /dev/null +++ b/impl/http/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.http.WorkflowHttpRequestFactory @@ -0,0 +1 @@ +org.openworkflow.sdk.impl.jakarta.http.JakartaRequestExecutorFactory \ No newline at end of file diff --git a/impl/jackson/pom.xml b/impl/jackson/pom.xml index 0df977746..dcd550d74 100644 --- a/impl/jackson/pom.xml +++ b/impl/jackson/pom.xml @@ -16,10 +16,6 @@ org.openworkflow.sdk openworkflow-impl-jq - - org.openworkflow.sdk - openworkflow-impl-template-resolver - org.openworkflow.sdk openworkflow-impl-model diff --git a/impl/jwt-impl/src/main/java/org/openworkflow/sdk/impl/executors/http/jackson/JacksonModelConverterFactory.java b/impl/jwt-impl/src/main/java/org/openworkflow/sdk/impl/executors/http/jackson/JacksonModelConverterFactory.java index 072cd5ae9..84ca68b3f 100644 --- a/impl/jwt-impl/src/main/java/org/openworkflow/sdk/impl/executors/http/jackson/JacksonModelConverterFactory.java +++ b/impl/jwt-impl/src/main/java/org/openworkflow/sdk/impl/executors/http/jackson/JacksonModelConverterFactory.java @@ -15,15 +15,13 @@ */ package org.openworkflow.sdk.impl.executors.http.jackson; -import org.openworkflow.sdk.impl.TaskContextData; -import org.openworkflow.sdk.impl.WorkflowContextData; -import org.openworkflow.sdk.impl.additional.NamedWorkflowAdditionalObject; -import org.openworkflow.sdk.impl.executors.http.HttpConverterResolver; -import org.openworkflow.sdk.impl.executors.http.HttpModelConverter; +import org.openworkflow.sdk.impl.additional.NamedSuppliedAdditionalObject; +import org.openworkflow.sdk.impl.jakarta.http.HttpConverterResolver; +import org.openworkflow.sdk.impl.jakarta.http.HttpModelConverter; import tools.jackson.databind.JsonNode; public class JacksonModelConverterFactory - implements NamedWorkflowAdditionalObject { + implements NamedSuppliedAdditionalObject { private static class JacksonModelConverterHolder { @@ -37,7 +35,7 @@ public Class responseType() { } @Override - public HttpModelConverter apply(WorkflowContextData t, TaskContextData u) { + public HttpModelConverter get() { return JacksonModelConverterHolder.converter; } diff --git a/impl/jwt-impl/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.additional.NamedWorkflowAdditionalObject b/impl/jwt-impl/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.additional.NamedSuppliedAdditionalObject similarity index 100% rename from impl/jwt-impl/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.additional.NamedWorkflowAdditionalObject rename to impl/jwt-impl/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.additional.NamedSuppliedAdditionalObject diff --git a/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModel.java b/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModel.java index f187cbf38..01c026911 100644 --- a/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModel.java +++ b/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModel.java @@ -128,6 +128,11 @@ public Class objectClass() { return node.getClass(); } + @Override + public Optional asJson() { + return Optional.of(JsonUtils.mapper().writeValueAsBytes(node)); + } + @Override protected Optional convert(Class clazz) { return clazz.isAssignableFrom(node.getClass()) diff --git a/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModelCollection.java b/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModelCollection.java index ed60f6b50..7b89a2d37 100644 --- a/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModelCollection.java +++ b/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModelCollection.java @@ -162,6 +162,11 @@ public Object asJavaObject() { return JsonUtils.toJavaValue(node); } + @Override + public Optional asJson() { + return Optional.of(JsonUtils.mapper().writeValueAsBytes(node)); + } + @Override public Class objectClass() { return ArrayNode.class; diff --git a/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModelFactory.java b/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModelFactory.java index 442f97be0..c654da045 100644 --- a/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModelFactory.java +++ b/impl/model/src/main/java/org/openworkflow/sdk/impl/model/jackson/JacksonModelFactory.java @@ -20,6 +20,7 @@ import java.math.BigDecimal; import java.time.OffsetDateTime; import java.util.Map; +import java.util.Optional; import org.openworkflow.sdk.impl.WorkflowModel; import org.openworkflow.sdk.impl.WorkflowModelCollection; import org.openworkflow.sdk.impl.WorkflowModelFactory; @@ -112,6 +113,13 @@ public WorkflowModel fromOther(Object value) { return new JacksonModel(JsonUtils.fromValue(value)); } + @Override + public Optional fromJson(byte[] value) { + return value == null || value.length == 0 + ? Optional.of(fromNull()) + : Optional.of(new JacksonModel(JsonUtils.mapper().readTree(value))); + } + @Override public WorkflowModel fromNull() { return JacksonModel.NULL; diff --git a/impl/openapi-jackson/src/main/java/org/openworkflow/sdk/impl/executors/openapi/jackson/JacksonUnifiedOpenAPIReader.java b/impl/openapi-jackson/src/main/java/org/openworkflow/sdk/impl/executors/openapi/jackson/JacksonUnifiedOpenAPIReader.java index 39167ff99..de6909a54 100644 --- a/impl/openapi-jackson/src/main/java/org/openworkflow/sdk/impl/executors/openapi/jackson/JacksonUnifiedOpenAPIReader.java +++ b/impl/openapi-jackson/src/main/java/org/openworkflow/sdk/impl/executors/openapi/jackson/JacksonUnifiedOpenAPIReader.java @@ -18,6 +18,10 @@ import java.io.IOException; import java.io.InputStream; import org.openworkflow.sdk.api.WorkflowFormat; +import org.openworkflow.sdk.impl.TaskContext; +import org.openworkflow.sdk.impl.TaskContextData; +import org.openworkflow.sdk.impl.WorkflowContext; +import org.openworkflow.sdk.impl.WorkflowContextData; import org.openworkflow.sdk.impl.executors.openapi.UnifiedOpenAPI; import org.openworkflow.sdk.impl.executors.openapi.UnifiedOpenAPIReader; import org.openworkflow.sdk.impl.resources.ExternalResourceHandler; @@ -25,10 +29,18 @@ public class JacksonUnifiedOpenAPIReader implements UnifiedOpenAPIReader { + private WorkflowContext workflow; + private TaskContext task; + + public JacksonUnifiedOpenAPIReader(WorkflowContextData workflow, TaskContextData task) { + this.workflow = workflow instanceof WorkflowContext ctx ? ctx : null; + this.task = task instanceof TaskContext ctx ? ctx : null; + } + @Override public UnifiedOpenAPI read(ExternalResourceHandler handler) throws IOException { ObjectMapper objectMapper = WorkflowFormat.fromFileName(handler.name()).mapper(); - try (InputStream is = handler.open()) { + try (InputStream is = handler.open(workflow, task)) { return objectMapper.readValue(is, UnifiedOpenAPI.class); } } diff --git a/impl/openapi-jackson/src/main/java/org/openworkflow/sdk/impl/executors/openapi/jackson/JacksonUnifiedOpenAPIReaderFactory.java b/impl/openapi-jackson/src/main/java/org/openworkflow/sdk/impl/executors/openapi/jackson/JacksonUnifiedOpenAPIReaderFactory.java index 11efc844f..0ab9e7ac9 100644 --- a/impl/openapi-jackson/src/main/java/org/openworkflow/sdk/impl/executors/openapi/jackson/JacksonUnifiedOpenAPIReaderFactory.java +++ b/impl/openapi-jackson/src/main/java/org/openworkflow/sdk/impl/executors/openapi/jackson/JacksonUnifiedOpenAPIReaderFactory.java @@ -22,13 +22,9 @@ public class JacksonUnifiedOpenAPIReaderFactory implements NamedWorkflowAdditionalObject { - private static class JacksonUnifiedOpenAPIReaderHolder { - private static JacksonUnifiedOpenAPIReader instance = new JacksonUnifiedOpenAPIReader(); - } - @Override public JacksonUnifiedOpenAPIReader apply(WorkflowContextData t, TaskContextData u) { - return JacksonUnifiedOpenAPIReaderHolder.instance; + return new JacksonUnifiedOpenAPIReader(t, u); } @Override diff --git a/impl/openapi/src/main/java/org/openworkflow/sdk/impl/executors/openapi/OpenAPIExecutor.java b/impl/openapi/src/main/java/org/openworkflow/sdk/impl/executors/openapi/OpenAPIExecutor.java index b7108c4d9..619abc344 100644 --- a/impl/openapi/src/main/java/org/openworkflow/sdk/impl/executors/openapi/OpenAPIExecutor.java +++ b/impl/openapi/src/main/java/org/openworkflow/sdk/impl/executors/openapi/OpenAPIExecutor.java @@ -17,7 +17,7 @@ import java.io.IOException; import java.io.UncheckedIOException; -import java.util.Collection; +import java.net.URI; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -30,8 +30,9 @@ import org.openworkflow.sdk.impl.WorkflowContext; import org.openworkflow.sdk.impl.WorkflowModel; import org.openworkflow.sdk.impl.executors.CallableTask; -import org.openworkflow.sdk.impl.executors.http.HttpExecutor; -import org.openworkflow.sdk.impl.executors.http.HttpExecutorBuilder; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestHelper; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestInfo.Builder; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestInfoBuilderFactory; import org.openworkflow.sdk.impl.resources.ExternalResourceHandler; class OpenAPIExecutor implements CallableTask { @@ -39,13 +40,13 @@ class OpenAPIExecutor implements CallableTask { private final OpenAPIProcessor processor; private final ExternalResource resource; private final Map parameters; - private final HttpExecutorBuilder builder; + private final WorkflowHttpRequestInfoBuilderFactory builder; OpenAPIExecutor( OpenAPIProcessor processor, ExternalResource resource, Map parameters, - HttpExecutorBuilder builder) { + WorkflowHttpRequestInfoBuilderFactory builder) { this.processor = processor; this.resource = resource; this.parameters = parameters; @@ -72,43 +73,41 @@ public CompletableFuture apply( .thenCompose( operationDefinition -> { fillHttpBuilder(workflowContext.definition().application(), operationDefinition); - // One executor per operation, even if the document is the same - // Me may refactor this even further to reuse the same executor (since the base URI is - // the same, - // but the path differs, although some use cases may require different client - // configurations for - // different paths...) - Collection executors = - operationDefinition.getServers().stream().map(builder::build).toList(); + Builder httpInfoBuilder = builder.build(workflowContext, taskContext, input); + Iterator iter = operationDefinition.getServers().iterator(); - Iterator iter = executors.iterator(); if (!iter.hasNext()) { throw new IllegalArgumentException( "List of servers is empty for schema " + resource.getName()); } CompletableFuture future = - iter.next().apply(workflowContext, taskContext, input); + WorkflowHttpRequestHelper.executeRequest( + httpInfoBuilder.build(URI.create(iter.next())), workflowContext, taskContext); while (iter.hasNext()) { - HttpExecutor item = iter.next(); + final String uri = iter.next(); future = future.exceptionallyCompose( - i -> item.apply(workflowContext, taskContext, input)); + i -> + WorkflowHttpRequestHelper.executeRequest( + httpInfoBuilder.build(URI.create(uri)), + workflowContext, + taskContext)); } return future; }); } private void fillHttpBuilder(WorkflowApplication application, OperationDefinition operation) { - Map headersMap = new HashMap<>(); + Map headers = new HashMap<>(); Map queryMap = new HashMap<>(); Map pathParameters = new HashMap<>(); Set missingParams = new HashSet<>(); - + boolean hasBody = operation.getOperation().hasRequestBody(); Map bodyParameters = new HashMap<>(parameters); for (ParameterDefinition parameter : operation.getParameters()) { switch (parameter.in()) { case "header": - param(parameter, bodyParameters, headersMap, missingParams); + param(parameter, bodyParameters, headers, missingParams); break; case "path": param(parameter, bodyParameters, pathParameters, missingParams); @@ -116,9 +115,11 @@ private void fillHttpBuilder(WorkflowApplication application, OperationDefinitio case "query": param(parameter, bodyParameters, queryMap, missingParams); break; + case "body": + hasBody = true; + break; } } - if (!missingParams.isEmpty()) { throw new IllegalArgumentException( "Missing required OpenAPI parameters for operation '" @@ -128,11 +129,11 @@ private void fillHttpBuilder(WorkflowApplication application, OperationDefinitio + missingParams); } builder + .withBody(hasBody || !bodyParameters.isEmpty() ? bodyParameters : null) + .withQueryMap(!queryMap.isEmpty() ? queryMap : null) + .withHeaders(!headers.isEmpty() ? headers : null) .withMethod(operation.getMethod()) - .withPath(new OperationPathResolver(operation.getPath(), application, pathParameters)) - .withBody(bodyParameters) - .withQueryMap(queryMap) - .withHeaders(headersMap); + .withPath(new OperationPathResolver(operation.getPath(), application, pathParameters)); } private void param( @@ -144,7 +145,6 @@ private void param( if (origMap.containsKey(name)) { collectorMap.put(parameter.name(), origMap.remove(name)); } else if (parameter.required()) { - UnifiedOpenAPI.Schema schema = parameter.schema(); Object defaultValue = schema != null ? schema._default() : null; if (defaultValue != null) { diff --git a/impl/openapi/src/main/java/org/openworkflow/sdk/impl/executors/openapi/OpenAPIExecutorBuilder.java b/impl/openapi/src/main/java/org/openworkflow/sdk/impl/executors/openapi/OpenAPIExecutorBuilder.java index 81c2f3b94..31490c4dd 100644 --- a/impl/openapi/src/main/java/org/openworkflow/sdk/impl/executors/openapi/OpenAPIExecutorBuilder.java +++ b/impl/openapi/src/main/java/org/openworkflow/sdk/impl/executors/openapi/OpenAPIExecutorBuilder.java @@ -24,7 +24,7 @@ import org.openworkflow.sdk.impl.WorkflowMutablePosition; import org.openworkflow.sdk.impl.executors.CallableTaskBuilder; import org.openworkflow.sdk.impl.executors.CallableTaskFactory; -import org.openworkflow.sdk.impl.executors.http.HttpExecutorBuilder; +import org.openworkflow.sdk.impl.http.WorkflowHttpRequestInfoBuilderFactory; public class OpenAPIExecutorBuilder implements CallableTaskBuilder { @@ -43,8 +43,8 @@ public CallableTaskFactory init( with.getParameters() != null && with.getParameters().getAdditionalProperties() != null ? with.getParameters().getAdditionalProperties() : Map.of(); - HttpExecutorBuilder builder = - HttpExecutorBuilder.builder(definition) + WorkflowHttpRequestInfoBuilderFactory builder = + WorkflowHttpRequestInfoBuilderFactory.builder(definition) .withAuth(with.getAuthentication()) .redirect(with.isRedirect()); return () -> new OpenAPIExecutor(processor, resource, parameters, builder); diff --git a/impl/pom.xml b/impl/pom.xml index d59448cea..2352763cf 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -32,11 +32,6 @@ openworkflow-impl-http ${project.version} - - org.openworkflow.sdk - openworkflow-impl-template-resolver - ${project.version} - org.openworkflow.sdk openworkflow-impl-json @@ -217,7 +212,6 @@ core - template http jackson jwt-impl diff --git a/impl/template/pom.xml b/impl/template/pom.xml deleted file mode 100644 index 2fc12c832..000000000 --- a/impl/template/pom.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - 4.0.0 - - org.openworkflow.sdk - openworkflow-impl - 8.0.0-SNAPSHOT - - - openworkflow-impl-template-resolver - Open Workflow :: Impl :: Template :: Jersey - - - - jakarta.ws.rs - jakarta.ws.rs-api - - - org.openworkflow.sdk - openworkflow-impl-core - - - \ No newline at end of file diff --git a/impl/template/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.resources.URITemplateResolver b/impl/template/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.resources.URITemplateResolver deleted file mode 100644 index 932a83d34..000000000 --- a/impl/template/src/main/resources/META-INF/services/org.openworkflow.sdk.impl.resources.URITemplateResolver +++ /dev/null @@ -1 +0,0 @@ -org.openworkflow.sdk.impl.template.JaxrsURITemplateResolver \ No newline at end of file diff --git a/impl/test/src/test/java/org/openworkflow/sdk/impl/auth/OAuthRequestBuilderTest.java b/impl/test/src/test/java/org/openworkflow/sdk/impl/auth/OAuthRequestBuilderTest.java index 6f20d6c0c..36fa515c8 100644 --- a/impl/test/src/test/java/org/openworkflow/sdk/impl/auth/OAuthRequestBuilderTest.java +++ b/impl/test/src/test/java/org/openworkflow/sdk/impl/auth/OAuthRequestBuilderTest.java @@ -42,7 +42,7 @@ void resolvesConfiguredEndpointsAndNormalizesLeadingSlash() { .withRevocation("/protocol/openid-connect/revoke") .withIntrospection("protocol/openid-connect/introspect")); - HttpRequestInfo info = build(props); + AuthHttpRequestInfo info = build(props); assertEquals( URI.create("http://localhost:8888/realms/test-realm/protocol/openid-connect/token"), @@ -59,7 +59,7 @@ void resolvesConfiguredEndpointsAndNormalizesLeadingSlash() { @Test void appliesDefaultTokenPathAndLeavesManagementEndpointsUnconfigured() { - HttpRequestInfo info = build(baseProperties()); + AuthHttpRequestInfo info = build(baseProperties()); assertEquals( URI.create("http://localhost:8888/realms/test-realm/oauth2/token"), @@ -77,7 +77,7 @@ private static OAuth2ConnectAuthenticationProperties baseProperties() { return props; } - private static HttpRequestInfo build(OAuth2ConnectAuthenticationProperties props) { + private static AuthHttpRequestInfo build(OAuth2ConnectAuthenticationProperties props) { try (WorkflowApplication app = WorkflowApplication.builder().build()) { return new OAuthRequestBuilder(app).apply(props); } diff --git a/impl/test/src/test/java/org/openworkflow/sdk/impl/test/CustomAuthProviderFactoryOverrideTest.java b/impl/test/src/test/java/org/openworkflow/sdk/impl/test/CustomAuthProviderFactoryOverrideTest.java index 0747221e5..9f6bad210 100644 --- a/impl/test/src/test/java/org/openworkflow/sdk/impl/test/CustomAuthProviderFactoryOverrideTest.java +++ b/impl/test/src/test/java/org/openworkflow/sdk/impl/test/CustomAuthProviderFactoryOverrideTest.java @@ -38,7 +38,6 @@ import org.openworkflow.sdk.impl.WorkflowApplication; import org.openworkflow.sdk.impl.WorkflowContext; import org.openworkflow.sdk.impl.WorkflowDefinition; -import org.openworkflow.sdk.impl.WorkflowModel; import org.openworkflow.sdk.impl.auth.AuthProvider; import org.openworkflow.sdk.impl.auth.AuthProviderFactory; @@ -80,16 +79,12 @@ public void frameworkOverrideSuppliesAuthHeaderWithoutTokenExchange() throws Exc public Optional getAuth( WorkflowDefinition definition, EndpointConfiguration configuration) { return getAuth( - definition, - configuration == null ? null : configuration.getAuthentication(), - "GET"); + definition, configuration == null ? null : configuration.getAuthentication()); } @Override public Optional getAuth( - WorkflowDefinition definition, - ReferenceableAuthenticationPolicy auth, - String method) { + WorkflowDefinition definition, ReferenceableAuthenticationPolicy auth) { factoryInvocations.incrementAndGet(); return Optional.of( new AuthProvider() { @@ -100,7 +95,7 @@ public String scheme() { @Override public CompletableFuture content( - WorkflowContext workflow, TaskContext task, WorkflowModel model, URI uri) { + WorkflowContext workflow, TaskContext task, URI uri, String method) { return CompletableFuture.completedFuture(frameworkToken); } }); diff --git a/impl/test/src/test/java/org/openworkflow/sdk/impl/test/HTTPWorkflowDefinitionTest.java b/impl/test/src/test/java/org/openworkflow/sdk/impl/test/HTTPWorkflowDefinitionTest.java index 5f916e236..77c794a09 100644 --- a/impl/test/src/test/java/org/openworkflow/sdk/impl/test/HTTPWorkflowDefinitionTest.java +++ b/impl/test/src/test/java/org/openworkflow/sdk/impl/test/HTTPWorkflowDefinitionTest.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -35,45 +36,62 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.openworkflow.sdk.impl.WorkflowApplication; import org.openworkflow.sdk.impl.WorkflowError; import org.openworkflow.sdk.impl.WorkflowException; import org.openworkflow.sdk.impl.WorkflowModel; +import org.openworkflow.sdk.impl.executors.CallableTask; +import org.openworkflow.sdk.impl.executors.CallableTaskProxyBuilder; +import org.openworkflow.sdk.impl.http.DefaultHttpRequestBuilderFactory; +import org.openworkflow.sdk.impl.http.DefaultURITemplateResolver; import org.openworkflow.sdk.types.Errors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class HTTPWorkflowDefinitionTest { - private static WorkflowApplication appl; + private static WorkflowApplication defaultApp; + private static WorkflowApplication jakartaApp; private static MockWebServer mockServer; private static final Logger logger = LoggerFactory.getLogger(HTTPWorkflowDefinitionTest.class); + private static class TimeCallableTaskProxyBuilder implements CallableTaskProxyBuilder { + + @Override + public CallableTask build(CallableTask delegate) { + return (w, t, n) -> { + long init = System.currentTimeMillis(); + CompletableFuture result = delegate.apply(w, t, n); + if (logger.isDebugEnabled()) { + result.thenAccept( + x -> + logger.debug( + "Http calls takes {} milliseconds", System.currentTimeMillis() - init)); + } + return result; + }; + } + } + @BeforeAll static void init() { - appl = + defaultApp = WorkflowApplication.builder() - .withCallableProxy( - delegate -> - (w, t, n) -> { - long init = System.currentTimeMillis(); - CompletableFuture result = delegate.apply(w, t, n); - if (logger.isDebugEnabled()) { - result.thenAccept( - x -> - logger.debug( - "Http calls takes {} milliseconds", - System.currentTimeMillis() - init)); - } - return result; - }) + .withHttpRequestBuilderFactory(new DefaultHttpRequestBuilderFactory()) + .withURITemplateResolver(new DefaultURITemplateResolver()) + .withCallableProxy(new TimeCallableTaskProxyBuilder()) .build(); + + jakartaApp = + WorkflowApplication.builder().withCallableProxy(new TimeCallableTaskProxyBuilder()).build(); } @AfterAll static void cleanup() { - appl.close(); + defaultApp.close(); + jakartaApp.close(); } @BeforeEach @@ -87,13 +105,13 @@ void shutdownServer() { mockServer.close(); } - private static boolean httpCondition(WorkflowModel obj) { - Map map = obj.asMap().orElseThrow(); - return map.containsKey("photoUrls") || map.containsKey("petId"); + private static List applications() { + return List.of(defaultApp, jakartaApp); } - @Test - void callHttpGet_should_return_pet_data() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void callHttpGet_should_return_pet_data(WorkflowApplication app) throws Exception { mockServer.enqueue( new MockResponse( 200, @@ -108,7 +126,7 @@ void callHttpGet_should_return_pet_data() throws Exception { } """)); WorkflowModel result = - appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-get.yaml")) + app.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-get.yaml")) .instance(Map.of("petId", 10)) .start() .join(); @@ -128,8 +146,10 @@ void callHttpGet_should_return_pet_data() throws Exception { }); } - @Test - void callHttpGet_with_not_found_petId_should_keep_input_petId() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void callHttpGet_with_not_found_petId_should_keep_input_petId(WorkflowApplication app) + throws Exception { mockServer.enqueue( new MockResponse( 404, @@ -138,7 +158,7 @@ void callHttpGet_with_not_found_petId_should_keep_input_petId() throws Exception {"message": "Pet not found"} """)); WorkflowModel result = - appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-get.yaml")) + app.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-get.yaml")) .instance(Map.of("petId", "-1")) .start() .join(); @@ -152,8 +172,9 @@ void callHttpGet_with_not_found_petId_should_keep_input_petId() throws Exception }); } - @Test - void callHttpEndpointInterpolation_should_work() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void callHttpEndpointInterpolation_should_work(WorkflowApplication app) throws Exception { mockServer.enqueue( new MockResponse( 200, @@ -168,7 +189,7 @@ void callHttpEndpointInterpolation_should_work() throws Exception { } """)); WorkflowModel result = - appl.workflowDefinition( + app.workflowDefinition( readWorkflowFromClasspath( "workflows-samples/call-http-endpoint-interpolation.yaml")) .instance(Map.of("petId", 1994)) @@ -187,8 +208,10 @@ void callHttpEndpointInterpolation_should_work() throws Exception { }); } - @Test - void callHttpQueryParameters_should_find_star_trek_movie() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void callHttpQueryParameters_should_find_star_trek_movie(WorkflowApplication app) + throws Exception { mockServer.enqueue( new MockResponse( 200, @@ -204,7 +227,7 @@ void callHttpQueryParameters_should_find_star_trek_movie() throws Exception { """)); WorkflowModel result = - appl.workflowDefinition( + app.workflowDefinition( readWorkflowFromClasspath("workflows-samples/call-http-query-parameters.yaml")) .instance(Map.of("uid", "MOMA0000092393")) .start() @@ -222,8 +245,10 @@ void callHttpQueryParameters_should_find_star_trek_movie() throws Exception { }); } - @Test - void callHttpFindByStatus_should_return_non_empty_collection() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void callHttpFindByStatus_should_return_non_empty_collection(WorkflowApplication app) + throws Exception { mockServer.enqueue( new MockResponse( 200, @@ -244,7 +269,7 @@ void callHttpFindByStatus_should_return_non_empty_collection() throws Exception """)); WorkflowModel result = - appl.workflowDefinition( + app.workflowDefinition( readWorkflowFromClasspath("workflows-samples/call-http-find-by-status.yaml")) .instance(Map.of()) .start() @@ -259,8 +284,10 @@ void callHttpFindByStatus_should_return_non_empty_collection() throws Exception }); } - @Test - void callHttpQueryParameters_external_schema_should_find_star_trek() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void callHttpQueryParameters_external_schema_should_find_star_trek(WorkflowApplication app) + throws Exception { mockServer.enqueue( new MockResponse( 200, @@ -276,7 +303,7 @@ void callHttpQueryParameters_external_schema_should_find_star_trek() throws Exce """)); WorkflowModel result = - appl.workflowDefinition( + app.workflowDefinition( readWorkflowFromClasspath("workflows-samples/call-http-query-parameters.yaml")) .instance(Map.of("uid", "MOMA0000092393")) .start() @@ -294,8 +321,9 @@ void callHttpQueryParameters_external_schema_should_find_star_trek() throws Exce }); } - @Test - void callHttpPost_should_return_created_firstName() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void callHttpPost_should_return_created_firstName(WorkflowApplication app) throws Exception { mockServer.enqueue( new MockResponse( 200, @@ -306,7 +334,7 @@ void callHttpPost_should_return_created_firstName() throws Exception { } """)); WorkflowModel result = - appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-post.yaml")) + app.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-post.yaml")) .instance(Map.of("name", "Javierito", "surname", "Unknown")) .start() .join(); @@ -324,13 +352,14 @@ void callHttpPost_should_return_created_firstName() throws Exception { }); } - @Test - void callHttpPost_without_body_should_not_throw() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void callHttpPost_without_body_should_not_throw(WorkflowApplication app) throws Exception { mockServer.enqueue(new MockResponse(204, Headers.of(), "")); assertDoesNotThrow( () -> - appl.workflowDefinition( + app.workflowDefinition( readWorkflowFromClasspath("workflows-samples/call-http-post-no-body.yaml")) .instance(Map.of()) .start() @@ -345,13 +374,13 @@ void callHttpPost_without_body_should_not_throw() throws Exception { }); } - @Test - void testCallHttpDelete() throws IOException, InterruptedException { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void testCallHttpDelete(WorkflowApplication app) throws IOException, InterruptedException { mockServer.enqueue(new MockResponse(204, Headers.of(), "")); WorkflowModel model = - appl.workflowDefinition( - readWorkflowFromClasspath("workflows-samples/call-http-delete.yaml")) + app.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-delete.yaml")) .instance(Map.of()) .start() .join(); @@ -365,8 +394,9 @@ void testCallHttpDelete() throws IOException, InterruptedException { }); } - @Test - void callHttpPut_should_contain_firstName_with_john() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void callHttpPut_should_contain_firstName_with_john(WorkflowApplication app) throws Exception { mockServer.enqueue( new MockResponse( 200, @@ -379,7 +409,7 @@ void callHttpPut_should_contain_firstName_with_john() throws Exception { """)); WorkflowModel result = - appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-put.yaml")) + app.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-put.yaml")) .instance(Map.of("firstName", "John")) .start() .join(); @@ -396,13 +426,14 @@ void callHttpPut_should_contain_firstName_with_john() throws Exception { }); } - @Test - void testWrongSchema_should_throw_exception() { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void testWrongSchema_should_throw_exception(WorkflowApplication app) { WorkflowException exception = catchThrowableOfType( WorkflowException.class, () -> - appl.workflowDefinition( + app.workflowDefinition( readWorkflowFromClasspath( "workflows-samples/call-http-query-parameters.yaml")) .instance(Map.of())); @@ -412,8 +443,9 @@ void testWrongSchema_should_throw_exception() { assertThat(error.type()).isEqualTo(Errors.VALIDATION.toString()); } - @Test - void testHeadCall() throws InterruptedException { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void testHeadCall(WorkflowApplication app) throws InterruptedException { mockServer.enqueue( new MockResponse( 200, @@ -429,8 +461,7 @@ void testHeadCall() throws InterruptedException { assertDoesNotThrow( () -> { - appl.workflowDefinition( - readWorkflowFromClasspath("workflows-samples/call-http-head.yaml")) + app.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-head.yaml")) .instance(Map.of()) .start() .join(); @@ -444,10 +475,11 @@ void testHeadCall() throws InterruptedException { }); } - @Test - void testOptionsCall() throws IOException, InterruptedException { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + void testOptionsCall(WorkflowApplication app) throws IOException, InterruptedException { mockServer.enqueue(new MockResponse(200, Headers.of("Allow", "GET, POST, OPTIONS"), "")); - appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-options.yaml")) + app.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-options.yaml")) .instance(Map.of()) .start() .join(); @@ -460,13 +492,14 @@ void testOptionsCall() throws IOException, InterruptedException { }); } - @Test + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") @Disabled( value = "See the following discussion: https://github.com/open-workflow-specification/sdk-java/pull/1013/files#r2562919102 and https://github.com/open-workflow-specification/sdk-java/issues/1024") - void testPatchCall() throws IOException, InterruptedException { + void testPatchCall(WorkflowApplication app) throws IOException, InterruptedException { mockServer.enqueue(new MockResponse(204, Headers.of(), "")); - appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-patch.yaml")) + app.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-patch.yaml")) .instance(Map.of()) .start() .join(); @@ -479,10 +512,12 @@ void testPatchCall() throws IOException, InterruptedException { }); } - @Test + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") @Disabled( "See the following discussion: https://github.com/open-workflow-specification/sdk-java/pull/1013/files#r2566152233 and https://github.com/open-workflow-specification/sdk-java/issues/1024#issue-3680971320") - void testRedirect_should_throws_when_redirect_is_false_and_response_status_is_not_2xx() { + void testRedirect_should_throws_when_redirect_is_false_and_response_status_is_not_2xx( + WorkflowApplication app) { mockServer.enqueue( new MockResponse(301, Headers.of("Location", "http://localhost:9876/redirected"), "")); @@ -490,7 +525,7 @@ void testRedirect_should_throws_when_redirect_is_false_and_response_status_is_no catchThrowableOfType( CompletionException.class, () -> - appl.workflowDefinition( + app.workflowDefinition( readWorkflowFromClasspath( "workflows-samples/call-http-redirect-false.yaml")) .instance(Map.of()) diff --git a/impl/test/src/test/java/org/openworkflow/sdk/impl/test/OpenAPITest.java b/impl/test/src/test/java/org/openworkflow/sdk/impl/test/OpenAPITest.java index 9f923656c..98b3a5cfb 100644 --- a/impl/test/src/test/java/org/openworkflow/sdk/impl/test/OpenAPITest.java +++ b/impl/test/src/test/java/org/openworkflow/sdk/impl/test/OpenAPITest.java @@ -15,6 +15,8 @@ */ package org.openworkflow.sdk.impl.test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -23,7 +25,8 @@ import java.io.IOException; import java.io.InputStream; -import java.util.Date; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Map; import okhttp3.mockwebserver.MockResponse; @@ -34,16 +37,21 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.openworkflow.sdk.api.types.Workflow; import org.openworkflow.sdk.impl.WorkflowApplication; import org.openworkflow.sdk.impl.WorkflowDefinition; import org.openworkflow.sdk.impl.WorkflowException; +import org.openworkflow.sdk.impl.http.DefaultHttpRequestBuilderFactory; +import org.openworkflow.sdk.impl.http.DefaultURITemplateResolver; public class OpenAPITest { - private static WorkflowApplication app; + private static WorkflowApplication defaultApp; + private static WorkflowApplication jakartaApp; private static byte[] yaml; + private static String dateBefore; private MockWebServer authServer; private MockWebServer openApiServer; @@ -98,15 +106,24 @@ public class OpenAPITest { @BeforeAll static void init() throws IOException { + + dateBefore = + OffsetDateTime.now(java.time.ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME); try (InputStream is = OpenAPITest.class.getResourceAsStream("/schema/openapi/openapi.yaml")) { yaml = is.readAllBytes(); } - app = WorkflowApplication.builder().build(); + defaultApp = + WorkflowApplication.builder() + .withHttpRequestBuilderFactory(new DefaultHttpRequestBuilderFactory()) + .withURITemplateResolver(new DefaultURITemplateResolver()) + .build(); + jakartaApp = WorkflowApplication.builder().build(); } @AfterAll static void cleanup() { - app.close(); + defaultApp.close(); + jakartaApp.close(); } @BeforeEach @@ -131,8 +148,14 @@ void tearDown() throws IOException { restServer.shutdown(); } - @Test - public void testOpenAPIBearerQueryInlinedBodyWithPositiveResponse() throws Exception { + private static List applications() { + return List.of(defaultApp, jakartaApp); + } + + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + public void testOpenAPIBearerQueryInlinedBodyWithPositiveResponse(WorkflowApplication app) + throws InterruptedException, IOException { Workflow workflow = readWorkflowFromClasspath("workflows-samples/openapi/project-post-positive.yaml"); @@ -140,6 +163,7 @@ public void testOpenAPIBearerQueryInlinedBodyWithPositiveResponse() throws Excep new MockResponse() .setBody(yamlBuffer) .setHeader("Content-Type", "application/yaml") + .setHeader("last-modified", dateBefore) .setResponseCode(200)); restServer.enqueue( @@ -149,7 +173,7 @@ public void testOpenAPIBearerQueryInlinedBodyWithPositiveResponse() throws Excep .setResponseCode(201)); Map result = - app.workflowDefinition(workflow).instance().start().get().asMap().orElseThrow(); + app.workflowDefinition(workflow).instance().start().join().asMap().orElseThrow(); RecordedRequest restRequest = restServer.takeRequest(); assertEquals("POST", restRequest.getMethod()); @@ -174,8 +198,10 @@ public void testOpenAPIBearerQueryInlinedBodyWithPositiveResponse() throws Excep assertEquals(67890, members.get(1)); } - @Test - public void testOpenAPIBearerQueryInlinedBodyWithNegativeResponse() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + public void testOpenAPIBearerQueryInlinedBodyWithNegativeResponse(WorkflowApplication app) + throws IOException, InterruptedException { Workflow workflow = readWorkflowFromClasspath("workflows-samples/openapi/project-post-positive.yaml"); @@ -183,6 +209,7 @@ public void testOpenAPIBearerQueryInlinedBodyWithNegativeResponse() throws Excep new MockResponse() .setBody(yamlBuffer) .setHeader("Content-Type", "application/yaml") + .setHeader("last-modified", dateBefore) .setResponseCode(200)); restServer.enqueue( @@ -194,22 +221,25 @@ public void testOpenAPIBearerQueryInlinedBodyWithNegativeResponse() throws Excep Exception exception = assertThrows( Exception.class, - () -> app.workflowDefinition(workflow).instance().start().get().asMap().orElseThrow()); + () -> app.workflowDefinition(workflow).instance().start().join().asMap().orElseThrow()); assertInstanceOf(WorkflowException.class, exception.getCause()); - assertTrue(exception.getMessage().contains("status=409")); + assertThat(exception.getMessage()).contains("status=409"); RecordedRequest restRequest = restServer.takeRequest(); - assertEquals("POST", restRequest.getMethod()); - assertTrue(restRequest.getPath().startsWith("/api/v1/projects?")); - assertTrue(restRequest.getPath().contains("notifyMembers=true")); - assertTrue(restRequest.getPath().contains("validateOnly=false")); - assertTrue(restRequest.getPath().contains("lang=en")); - assertEquals("application/json", restRequest.getHeader("Content-Type")); - assertEquals("Bearer eyJhbnNpc2l0b3IuYm9sdXMubWFnbnVz", restRequest.getHeader("Authorization")); + assertThat(restRequest.getMethod()).isEqualTo("POST"); + assertThat(restRequest.getPath()).startsWith("/api/v1/projects"); + assertThat(restRequest.getPath()).contains("notifyMembers=true"); + assertThat(restRequest.getPath()).contains("validateOnly=false"); + assertThat(restRequest.getPath()).contains("lang=en"); + assertThat(restRequest.getHeader("Content-Type")).isEqualTo("application/json"); + assertThat(restRequest.getHeader("Authorization")) + .isEqualTo("Bearer eyJhbnNpc2l0b3IuYm9sdXMubWFnbnVz"); } - @Test - public void testOpenAPIGetWithPositiveResponse() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + public void testOpenAPIGetWithPositiveResponse(WorkflowApplication app) + throws InterruptedException, IOException { Workflow workflow = readWorkflowFromClasspath("workflows-samples/openapi/get-user-get-request.yaml"); @@ -218,11 +248,15 @@ public void testOpenAPIGetWithPositiveResponse() throws Exception { new MockResponse() .setBody(yamlBuffer) .setHeader("Content-Type", "application/yaml") + .setHeader("last-modified", dateBefore) .setResponseCode(200)); openApiServer.enqueue( new MockResponse() - .setHeader("last-modified", new Date().toGMTString()) + .setHeader( + "last-modified", + OffsetDateTime.now(java.time.ZoneOffset.UTC) + .format(DateTimeFormatter.RFC_1123_DATE_TIME)) .setResponseCode(304)); restServer.enqueue( @@ -238,29 +272,18 @@ public void testOpenAPIGetWithPositiveResponse() throws Exception { .setResponseCode(200)); WorkflowDefinition definition = app.workflowDefinition(workflow); - assertData(definition.instance().start().get().asMap().orElseThrow()); + assertData(definition.instance().start().join().asMap().orElseThrow()); RecordedRequest openAPIRequest = openApiServer.takeRequest(); assertEquals("GET", openAPIRequest.getMethod()); - assertData(definition.instance().start().get().asMap().orElseThrow()); + assertData(definition.instance().start().join().asMap().orElseThrow()); openAPIRequest = openApiServer.takeRequest(); assertEquals("HEAD", openAPIRequest.getMethod()); } - private void assertData(Map result) throws InterruptedException { - RecordedRequest restRequest = restServer.takeRequest(); - assertEquals("GET", restRequest.getMethod()); - assertTrue(restRequest.getPath().startsWith("/api/v1/users/40099?")); - - assertTrue(result.containsKey("data")); - Map data = (Map) result.get("data"); - assertEquals(40099, data.get("id")); - assertEquals("Severus Calix", data.get("name")); - assertEquals("severus.calix@hive-terra.example.com", data.get("email")); - } - - @Test - public void testOpenAPIGetWithPositiveResponseAndVars() throws Exception { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("applications") + public void testOpenAPIGetWithPositiveResponseAndVars(WorkflowApplication app) throws Exception { Workflow workflow = readWorkflowFromClasspath("workflows-samples/openapi/get-user-get-request-vars.yaml"); @@ -268,6 +291,7 @@ public void testOpenAPIGetWithPositiveResponseAndVars() throws Exception { new MockResponse() .setBody(yamlBuffer) .setHeader("Content-Type", "application/yaml") + .setHeader("last-modified", dateBefore) .setResponseCode(200)); restServer.enqueue( @@ -298,6 +322,18 @@ public void testOpenAPIGetWithPositiveResponseAndVars() throws Exception { Map result = app.workflowDefinition(workflow).instance(params).start().get().asMap().orElseThrow(); + RecordedRequest restRequest = restServer.takeRequest(); + assertThat(restRequest.getMethod()).isEqualTo("GET"); + assertThat(restRequest.getPath()).startsWith("/api/v1/users/40099"); + + assertThat(result).containsKey("data"); + Map data = (Map) result.get("data"); + assertThat(data.get("id")).isEqualTo(40099); + assertThat(data.get("name")).isEqualTo("Severus Calix"); + assertThat(data.get("email")).isEqualTo("severus.calix@hive-terra.example.com"); + } + + private void assertData(Map result) throws InterruptedException { RecordedRequest restRequest = restServer.takeRequest(); assertEquals("GET", restRequest.getMethod()); assertTrue(restRequest.getPath().startsWith("/api/v1/users/40099?")); diff --git a/types/src/main/resources/schema/workflow.yaml b/types/src/main/resources/schema/workflow.yaml index efb27347a..8f36b99d5 100644 --- a/types/src/main/resources/schema/workflow.yaml +++ b/types/src/main/resources/schema/workflow.yaml @@ -1,4 +1,4 @@ -$id: https://open-workflow-specification.org/schemas/1.0.3/workflow.yaml +$id: https://open-workflow-specification.org/schemas/1.0.4/workflow.yaml $schema: https://json-schema.org/draft/2020-12/schema description: Open Workflow DSL - Workflow Schema. type: object @@ -876,6 +876,10 @@ $defs: type: string title: ScriptLanguage description: The language of the script to run. + stdin: + type: string + title: ScriptStdin + description: A runtime expression, if any, to pipe to the script as standard input (stdin). arguments: type: object title: ScriptArguments @@ -1461,11 +1465,11 @@ $defs: - title: LiteralUriTemplate type: string format: uri-template - pattern: "^(?!\\s*\\$\\{)(?=\\S)(([^:/?#]+):)?(//([^/?#\\s]*))?([^?#\\s]*)(\\?([^#\\s]*))?(#(\\S*))?$" + pattern: "^(?!\\s*\\$\\{)(?=\\S)(?=.*\\{[^}]+\\})(([^:/?#]+):)?(//([^/?#\\s]*))?([^?#\\s]*)(\\?([^#\\s]*))?(#(\\S*))?$" - title: LiteralUri type: string format: uri-reference - pattern: "^(?!\\s*\\$\\{)(?=\\S)(([^:/?#]+):)?(//([^/?#\\s]*))?([^?#\\s]*)(\\?([^#\\s]*))?(#(\\S*))?$" + pattern: "^(?!\\s*\\$\\{)(?=\\S)(?!.*[{}])(([^:/?#]+):)?(//([^/?#\\s]*))?([^?#\\s]*)(\\?([^#\\s]*))?(#(\\S*))?$" endpoint: title: Endpoint description: Represents an endpoint.