Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class MetaSchemaHolder {
public static final String DIAL_APPLICATION_TYPE_BUCKET_COPY = "dial:applicationTypeBucketCopy";
public static final String DIAL_APPLICATION_TYPE_INTERCEPTORS = "dial:applicationTypeInterceptors";
public static final String DIAL_APPLICATION_TYPE_ASSISTANT_ATTACHMENTS_IN_REQUEST = "dial:applicationTypeAssistantAttachmentsInRequestSupported";
public static final String DIAL_APPLICATION_TYPE_SCHEMA_ENDPOINT = "dial:applicationTypeSchemaEndpoint";

public static final String APPLICATION_TYPE_ROUTES = "dial:applicationTypeRoutes";

Expand Down Expand Up @@ -61,6 +62,7 @@ public static JsonMetaSchema.Builder getMetaschemaBuilder() {
.keyword(new NonValidationKeyword(DIAL_APPLICATION_TYPE_BUCKET_COPY))
.keyword(new NonValidationKeyword(DIAL_APPLICATION_TYPE_INTERCEPTORS))
.keyword(new NonValidationKeyword(DIAL_APPLICATION_TYPE_ASSISTANT_ATTACHMENTS_IN_REQUEST))
.keyword(new NonValidationKeyword(DIAL_APPLICATION_TYPE_SCHEMA_ENDPOINT))
.keyword(new NonValidationKeyword("$defs"))
.format(new DialFileFormat());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
"definitions": {
"dialRootSchema": {
"properties": {
"dial:applicationTypeSchemaEndpoint": {
"type": "string",
"format": "uri",
"description": "URL to the application JSON schema endpoint of the custom application of given type"
},
"dial:applicationTypeEditorUrl": {
"type": "string",
"format": "uri",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ void start() throws Exception {
ApiKeyStore apiKeyStore = new ApiKeyStore(taskExecutor, redis, storage.getPrefix(), settings("perRequestApiKey"));
ConfigStore configStore = new FileConfigStore(vertx, settings("config"), apiKeyStore);
ApplicationOperatorService operatorService = new ApplicationOperatorService(client, settings("applications"));
ApplicationSchemaService applicationSchemaService = new ApplicationSchemaService(resourceService, configStore, encryptionService);
ApplicationSchemaService applicationSchemaService = new ApplicationSchemaService(resourceService, configStore, encryptionService, httpProxySelector);

TimeProvider timeProvider = new TimeProvider();
TokenRefreshStrategyFactory tokenRefreshStrategyFactory = new TokenRefreshStrategyFactory(timeProvider);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ private List<JsonNode> listSchemas() throws JsonProcessingException {
if (schemaNode.has(MetaSchemaHolder.DIAL_APPLICATION_TYPE_BUCKET_COPY)) {
filteredNode.set(MetaSchemaHolder.DIAL_APPLICATION_TYPE_BUCKET_COPY, schemaNode.get(MetaSchemaHolder.DIAL_APPLICATION_TYPE_BUCKET_COPY));
}
if (schemaNode.has(MetaSchemaHolder.DIAL_APPLICATION_TYPE_SCHEMA_ENDPOINT)) {
filteredNode.set(MetaSchemaHolder.DIAL_APPLICATION_TYPE_SCHEMA_ENDPOINT, schemaNode.get(MetaSchemaHolder.DIAL_APPLICATION_TYPE_SCHEMA_ENDPOINT));
}

filteredSchemas.add(filteredNode);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ private void validateCustomApplication(Application application) {
try {
checkCreateCodeApp(application);
if (application.getApplicationProperties() != null) {
List<ResourceDescriptor> files = proxy.getApplicationSchemaService().getFiles(application);
List<ResourceDescriptor> files = proxy.getApplicationSchemaService().getFiles(application, true);
files.stream().filter(resource -> !(accessService.hasReadAccess(resource, context)))
.findAny().ifPresent(file -> {
throw new HttpException(FORBIDDEN, "No read access to file: " + file.getUrl());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
import com.epam.aidial.core.server.validation.DialMetaKeyword;
import com.epam.aidial.core.server.validation.DialResourceKeyKeyword;
import com.epam.aidial.core.server.validation.ListCollector;
import com.epam.aidial.core.storage.http.HttpException;
import com.epam.aidial.core.storage.resource.ResourceDescriptor;
import com.epam.aidial.core.storage.resource.ResourceType;
import com.epam.aidial.core.storage.resource.ResourceTypes;
import com.epam.aidial.core.storage.service.ResourceService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import com.networknt.schema.CollectorContext;
import com.networknt.schema.InputFormat;
import com.networknt.schema.JsonMetaSchema;
Expand All @@ -34,14 +37,22 @@
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.Type;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nullable;

import static com.epam.aidial.core.metaschemas.MetaSchemaHolder.APPLICATION_TYPE_COMPLETION_ENDPOINT;
Expand All @@ -56,7 +67,6 @@
import static com.epam.aidial.core.metaschemas.MetaSchemaHolder.getMetaschemaBuilder;

@Slf4j
@AllArgsConstructor
public class ApplicationSchemaService {

private static final JsonMetaSchema DIAL_META_SCHEMA = getMetaschemaBuilder()
Expand Down Expand Up @@ -96,7 +106,34 @@ public Type getType() {

private final EncryptionService encryptionService;

String getCustomApplicationSchemaOrThrow(Application application) {
private final HttpClient httpClient;

private final ConcurrentHashMap<URI, String> schemaCache = new ConcurrentHashMap<>();

public ApplicationSchemaService(ResourceService resourceService, ConfigStore configStore,
EncryptionService encryptionService, @Nullable ProxySelector proxySelector) {
this.resourceService = resourceService;
this.configStore = configStore;
this.encryptionService = encryptionService;
HttpClient.Builder builder = HttpClient.newBuilder();
builder.connectTimeout(Duration.of(5, ChronoUnit.SECONDS));
if (proxySelector != null) {
builder.proxy(proxySelector);
}
this.httpClient = builder.build();
}

@VisibleForTesting
ApplicationSchemaService(ResourceService resourceService, ConfigStore configStore,
EncryptionService encryptionService, HttpClient httpClient) {
this.resourceService = resourceService;
this.configStore = configStore;
this.encryptionService = encryptionService;
this.httpClient = httpClient;
}

@SneakyThrows
String getCustomApplicationSchemaOrThrow(Application application, boolean forceReload) {
URI schemaId = application.getApplicationTypeSchemaId();
if (schemaId == null) {
return null;
Expand All @@ -105,7 +142,51 @@ String getCustomApplicationSchemaOrThrow(Application application) {
if (customApplicationSchema == null) {
throw new ApplicationTypeSchemaValidationException("Custom application schema not found: " + schemaId);
}
return customApplicationSchema;
JsonNode schema = ProxyUtil.MAPPER.readTree(customApplicationSchema);
if (schema.has(MetaSchemaHolder.DIAL_APPLICATION_TYPE_SCHEMA_ENDPOINT)) {
String url = schema.get(MetaSchemaHolder.DIAL_APPLICATION_TYPE_SCHEMA_ENDPOINT).textValue();
if (forceReload || !schemaCache.containsKey(schemaId)) {
ObjectNode appSchema = downloadAppSchema(url);
merge(appSchema, schema);
String result = appSchema.toString();
schemaCache.put(schemaId, result);
return result;
} else {
return schemaCache.get(schemaId);
}
} else {
return customApplicationSchema;
}
}

private void merge(ObjectNode appSchema, JsonNode schema) {
Iterator<String> fieldNames = schema.fieldNames();
while (fieldNames.hasNext()) {
String field = fieldNames.next();
appSchema.set(field, schema.get(field));
}
}

@SneakyThrows
private ObjectNode downloadAppSchema(String url) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(java.time.Duration.ofSeconds(5))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
String body = response.body();
if (status != 200) {
log.debug("Error of downloading application schema {}: status {}, response {}, headers: {}",
request.uri(), response.statusCode(), response.body(), response.headers());
throw new HttpException(status, "Application runner returned error on downloading application schema");
}
JsonNode tree = ProxyUtil.MAPPER.readTree(body);
if (!tree.isObject()) {
throw new ApplicationTypeSchemaProcessingException("Application schema is not JSON object");
}
return (ObjectNode) tree;
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -141,7 +222,7 @@ public interface MetadataPropertiesConsumer {
}

public void consumeMetadataProperties(Application application, MetadataPropertiesConsumer consumer) {
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application);
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application, false);
if (customApplicationSchema == null) {
return;
}
Expand Down Expand Up @@ -169,7 +250,7 @@ private interface EndpointConsumer {

private void consumeCustomApplicationEndpoints(Application application, EndpointConsumer consumer) {
try {
String schema = getCustomApplicationSchemaOrThrow(application);
String schema = getCustomApplicationSchemaOrThrow(application, false);
JsonNode schemaNode = ProxyUtil.MAPPER.readTree(schema);

String completionEndpoint = getEndpoint(schemaNode, APPLICATION_TYPE_COMPLETION_ENDPOINT, true);
Expand Down Expand Up @@ -231,7 +312,7 @@ public Application modifyEndpointsForCustomApplication(Application application)
}

public Application filterCustomClientProperties(Application application) {
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application);
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application, false);
if (customApplicationSchema == null) {
return application;
}
Expand All @@ -246,17 +327,21 @@ public Application filterCustomClientProperties(Application application) {
}

public List<ResourceDescriptor> getServerFiles(Application application) {
return getFiles(application, ListCollector.ResourceCollectorType.ONLY_SERVER_RESOURCES);
return getFiles(application, ListCollector.ResourceCollectorType.ONLY_SERVER_RESOURCES, false);
}

public List<ResourceDescriptor> getFiles(Application application) {
return getFiles(application, ListCollector.ResourceCollectorType.ALL_RESOURCES);
return getFiles(application, ListCollector.ResourceCollectorType.ALL_RESOURCES, false);
}

public List<ResourceDescriptor> getFiles(Application application, boolean forceReload) {
return getFiles(application, ListCollector.ResourceCollectorType.ALL_RESOURCES, forceReload);
}

@SuppressWarnings("unchecked")
private List<ResourceDescriptor> getFiles(Application application, ListCollector.ResourceCollectorType collectorName) {
private List<ResourceDescriptor> getFiles(Application application, ListCollector.ResourceCollectorType collectorName, boolean forceReload) {
try {
ListCollector<String> propsCollector = (ListCollector<String>) getCollector(application, collectorName.getValue());
ListCollector<String> propsCollector = (ListCollector<String>) getCollector(application, collectorName.getValue(), forceReload);
if (propsCollector == null) {
return Collections.emptyList();
}
Expand Down Expand Up @@ -287,7 +372,7 @@ private List<ResourceDescriptor> getFiles(Application application, ListCollector
public List<ResourceDescriptor> getDeployments(Application application) {
try {
ListCollector<String> propsCollector = (ListCollector<String>) getCollector(application,
ListCollector.ResourceCollectorType.ALL_RESOURCES.getValue());
ListCollector.ResourceCollectorType.ALL_RESOURCES.getValue(), false);
if (propsCollector == null) {
return Collections.emptyList();
}
Expand Down Expand Up @@ -315,8 +400,8 @@ public List<ResourceDescriptor> getDeployments(Application application) {
}

@Nullable
private Object getCollector(Application application, String collectorName) throws JsonProcessingException {
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application);
private Object getCollector(Application application, String collectorName, boolean forceReload) throws JsonProcessingException {
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application, forceReload);
if (customApplicationSchema == null) {
return null;
}
Expand Down Expand Up @@ -357,7 +442,7 @@ public Application modifySchemaRichApplication(Application application, boolean
@Nullable
@SneakyThrows
public Map<String, Route> getRoutes(Application application) {
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application);
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application, false);
if (customApplicationSchema == null) {
return null;
}
Expand All @@ -371,7 +456,7 @@ public Map<String, Route> getRoutes(Application application) {

@SneakyThrows
public CopyAppBucketOptions getCopyAppBucketOptions(Application application) {
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application);
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application, false);
if (customApplicationSchema == null) {
return CopyAppBucketOptions.DISABLED;
}
Expand All @@ -385,7 +470,7 @@ public CopyAppBucketOptions getCopyAppBucketOptions(Application application) {

@SneakyThrows
public List<String> getInterceptors(Application application) {
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application);
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application, false);
if (customApplicationSchema == null) {
return List.of();
}
Expand All @@ -399,7 +484,7 @@ public List<String> getInterceptors(Application application) {

@SneakyThrows
private boolean getBooleanProperty(Application application, String propName) {
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application);
String customApplicationSchema = getCustomApplicationSchemaOrThrow(application, false);
if (customApplicationSchema == null) {
return false;
}
Expand Down
Loading
Loading