diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/AbstractGraphQLHttpServlet.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/AbstractGraphQLHttpServlet.java new file mode 100644 index 0000000000..2adbd21ae7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/AbstractGraphQLHttpServlet.java @@ -0,0 +1,96 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +import graphql.ExecutionResult; +import graphql.schema.GraphQLFieldDefinition; +import jakarta.servlet.Servlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + + +public abstract class AbstractGraphQLHttpServlet extends HttpServlet + implements Servlet, GraphQLMBean { + + protected abstract GraphQLConfiguration getConfiguration(); + + public void addListener(GraphQLServletListener servletListener) { + getConfiguration().add(servletListener); + } + + public void removeListener(GraphQLServletListener servletListener) { + getConfiguration().remove(servletListener); + } + + @Override + public String[] getQueries() { + return getConfiguration() + .getInvocationInputFactory() + .getSchemaProvider() + .getSchema() + .getQueryType() + .getFieldDefinitions() + .stream() + .map(GraphQLFieldDefinition::getName) + .toArray(String[]::new); + } + + @Override + public String[] getMutations() { + return getConfiguration() + .getInvocationInputFactory() + .getSchemaProvider() + .getSchema() + .getMutationType() + .getFieldDefinitions() + .stream() + .map(GraphQLFieldDefinition::getName) + .toArray(String[]::new); + } + + @Override + public String executeQuery(String query) { + try { + GraphQLRequest graphQLRequest = createQueryOnlyRequest(query); + GraphQLSingleInvocationInput invocationInput = + getConfiguration().getInvocationInputFactory().create(graphQLRequest); + ExecutionResult result = + getConfiguration().getGraphQLInvoker().query(invocationInput).getResult(); + return getConfiguration().getObjectMapper().serializeResultAsJson(result); + } catch (Exception e) { + return e.getMessage(); + } + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + doRequest(req, resp); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + doRequest(req, resp); + } + + private void doRequest(HttpServletRequest request, HttpServletResponse response) { + try { + getConfiguration().getHttpRequestHandler().handle(request, response); + } catch (Exception t) { + } + } +} diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/FileDataFetcher.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/FileDataFetcher.java new file mode 100644 index 0000000000..fbacc43318 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/FileDataFetcher.java @@ -0,0 +1,32 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; + +import java.io.InputStream; + +public class FileDataFetcher implements DataFetcher { + + @Override + public Object get(DataFetchingEnvironment environment) { + InputStream fileContent = environment.getArgument("file"); + // do save here (no real implementation) + return new FileMetadata("file-id", "file-name"); + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/FileMetadata.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/FileMetadata.java new file mode 100644 index 0000000000..60b65a7867 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/FileMetadata.java @@ -0,0 +1,35 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +public class FileMetadata { + private String id; + private String name; + + public FileMetadata(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLConfiguration.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLConfiguration.java new file mode 100644 index 0000000000..ba2816ce4a --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLConfiguration.java @@ -0,0 +1,286 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +import graphql.schema.GraphQLSchema; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class GraphQLConfiguration { + + private final GraphQLInvocationInputFactory invocationInputFactory; + private final Supplier batchInputPreProcessor; + private final GraphQLInvoker graphQLInvoker; + private final GraphQLObjectMapper objectMapper; + private final List listeners; + private final long subscriptionTimeout; + private final long asyncTimeout; + private final ContextSetting contextSetting; + private final GraphQLResponseCacheManager responseCacheManager; + private final Executor asyncExecutor; + private HttpRequestHandler requestHandler; + + private GraphQLConfiguration( + GraphQLInvocationInputFactory invocationInputFactory, + GraphQLInvoker graphQLInvoker, + GraphQLQueryInvoker queryInvoker, + GraphQLObjectMapper objectMapper, + List listeners, + long subscriptionTimeout, + long asyncTimeout, + ContextSetting contextSetting, + Supplier batchInputPreProcessor, + GraphQLResponseCacheManager responseCacheManager, + Executor asyncExecutor) { + this.invocationInputFactory = invocationInputFactory; + this.asyncExecutor = asyncExecutor; + this.graphQLInvoker = graphQLInvoker != null ? graphQLInvoker : queryInvoker.toGraphQLInvoker(); + this.objectMapper = objectMapper; + this.listeners = listeners; + this.subscriptionTimeout = subscriptionTimeout; + this.asyncTimeout = asyncTimeout; + this.contextSetting = contextSetting; + this.batchInputPreProcessor = batchInputPreProcessor; + this.responseCacheManager = responseCacheManager; + } + + public static Builder with(GraphQLSchema schema) { + return with(new DefaultGraphQLSchemaServletProvider(schema)); + } + + public static Builder with(GraphQLSchemaServletProvider schemaProvider) { + return new Builder(GraphQLInvocationInputFactory.newBuilder(schemaProvider)); + } + + public static Builder with( + GraphQLInvocationInputFactory invocationInputFactory) { + return new Builder(invocationInputFactory); + } + + public GraphQLInvocationInputFactory getInvocationInputFactory() { + return invocationInputFactory; + } + + public GraphQLInvoker getGraphQLInvoker() { + return graphQLInvoker; + } + + public GraphQLObjectMapper getObjectMapper() { + return objectMapper; + } + + public List getListeners() { + return new ArrayList<>(listeners); + } + + public void add(GraphQLServletListener listener) { + listeners.add(listener); + } + + public boolean remove(GraphQLServletListener listener) { + return listeners.remove(listener); + } + + public long getSubscriptionTimeout() { + return subscriptionTimeout; + } + + public ContextSetting getContextSetting() { + return contextSetting; + } + + public BatchInputPreProcessor getBatchInputPreProcessor() { + return batchInputPreProcessor.get(); + } + + public GraphQLResponseCacheManager getResponseCacheManager() { + return responseCacheManager; + } + + public HttpRequestHandler getHttpRequestHandler() { + if (requestHandler == null) { + requestHandler = createHttpRequestHandler(); + } + return requestHandler; + } + + private HttpRequestHandler createHttpRequestHandler() { + if (responseCacheManager == null) { + return new HttpRequestHandlerImpl(this); + } else { + return new HttpRequestHandlerImpl(this, new CachingHttpRequestInvoker(this)); + } + } + + public static class Builder { + + private GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder; + private GraphQLInvocationInputFactory invocationInputFactory; + private GraphQLInvoker graphQLInvoker; + private GraphQLQueryInvoker queryInvoker = GraphQLQueryInvoker.newBuilder().build(); + private GraphQLObjectMapper objectMapper = GraphQLObjectMapper.newBuilder().build(); + private List listeners = new ArrayList<>(); + private long subscriptionTimeout = 0; + private long asyncTimeout = 30000; + private ContextSetting contextSetting = ContextSetting.PER_QUERY_WITH_INSTRUMENTATION; + private Supplier batchInputPreProcessorSupplier = + NoOpBatchInputPreProcessor::new; + private GraphQLResponseCacheManager responseCacheManager; + private int asyncCorePoolSize = 10; + private int asyncMaxPoolSize = 200; + private Executor asyncExecutor; + private AsyncTaskDecorator asyncTaskDecorator; + + private Builder(GraphQLInvocationInputFactory.Builder invocationInputFactoryBuilder) { + this.invocationInputFactoryBuilder = invocationInputFactoryBuilder; + } + + private Builder(GraphQLInvocationInputFactory invocationInputFactory) { + this.invocationInputFactory = invocationInputFactory; + } + + public Builder with(GraphQLInvoker graphQLInvoker) { + this.graphQLInvoker = graphQLInvoker; + return this; + } + + public Builder with(GraphQLQueryInvoker queryInvoker) { + if (queryInvoker != null) { + this.queryInvoker = queryInvoker; + } + return this; + } + + public Builder with(GraphQLObjectMapper objectMapper) { + if (objectMapper != null) { + this.objectMapper = objectMapper; + } + return this; + } + + public Builder with(List listeners) { + if (listeners != null) { + this.listeners = listeners; + } + return this; + } + + public Builder with(GraphQLServletContextBuilder contextBuilder) { + this.invocationInputFactoryBuilder.withGraphQLContextBuilder(contextBuilder); + return this; + } + + public Builder with(GraphQLServletRootObjectBuilder rootObjectBuilder) { + this.invocationInputFactoryBuilder.withGraphQLRootObjectBuilder(rootObjectBuilder); + return this; + } + + public Builder with(long subscriptionTimeout) { + this.subscriptionTimeout = subscriptionTimeout; + return this; + } + + public Builder asyncTimeout(long asyncTimeout) { + this.asyncTimeout = asyncTimeout; + return this; + } + + public Builder with(Executor asyncExecutor) { + this.asyncExecutor = asyncExecutor; + return this; + } + + public Builder asyncCorePoolSize(int asyncCorePoolSize) { + this.asyncCorePoolSize = asyncCorePoolSize; + return this; + } + + public Builder asyncMaxPoolSize(int asyncMaxPoolSize) { + this.asyncMaxPoolSize = asyncMaxPoolSize; + return this; + } + + public Builder with(ContextSetting contextSetting) { + if (contextSetting != null) { + this.contextSetting = contextSetting; + } + return this; + } + + public Builder with(BatchInputPreProcessor batchInputPreProcessor) { + if (batchInputPreProcessor != null) { + this.batchInputPreProcessorSupplier = () -> batchInputPreProcessor; + } + return this; + } + + public Builder with(Supplier batchInputPreProcessor) { + if (batchInputPreProcessor != null) { + this.batchInputPreProcessorSupplier = batchInputPreProcessor; + } + return this; + } + + public Builder with(GraphQLResponseCacheManager responseCache) { + this.responseCacheManager = responseCache; + return this; + } + + public Builder with(AsyncTaskDecorator asyncTaskDecorator) { + this.asyncTaskDecorator = asyncTaskDecorator; + return this; + } + + private Executor getAsyncExecutor() { + if (asyncExecutor != null) { + return asyncExecutor; + } + return new ThreadPoolExecutor( + asyncCorePoolSize, + asyncMaxPoolSize, + 60, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(Integer.MAX_VALUE)); + } + + private Executor getAsyncTaskExecutor() { + return new AsyncTaskExecutor(getAsyncExecutor(), asyncTaskDecorator); + } + + public GraphQLConfiguration build() { + return new GraphQLConfiguration( + this.invocationInputFactory != null + ? this.invocationInputFactory + : invocationInputFactoryBuilder.build(), + graphQLInvoker, + queryInvoker, + objectMapper, + listeners, + subscriptionTimeout, + asyncTimeout, + contextSetting, + batchInputPreProcessorSupplier, + responseCacheManager, + getAsyncTaskExecutor()); + } + } +} diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLInvocationInputFactory.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLInvocationInputFactory.java new file mode 100644 index 0000000000..5bbbb7fe5b --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLInvocationInputFactory.java @@ -0,0 +1,186 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +import graphql.schema.GraphQLSchema; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.websocket.Session; +import jakarta.websocket.server.HandshakeRequest; + +import java.util.List; +import java.util.function.Supplier; + +public class GraphQLInvocationInputFactory implements GraphQLSubscriptionInvocationInputFactory { + + private final Supplier schemaProviderSupplier; + private final Supplier contextBuilderSupplier; + private final Supplier rootObjectBuilderSupplier; + + protected GraphQLInvocationInputFactory( + Supplier schemaProviderSupplier, + Supplier contextBuilderSupplier, + Supplier rootObjectBuilderSupplier) { + this.schemaProviderSupplier = schemaProviderSupplier; + this.contextBuilderSupplier = contextBuilderSupplier; + this.rootObjectBuilderSupplier = rootObjectBuilderSupplier; + } + + public static Builder newBuilder(GraphQLSchema schema) { + return new Builder(new DefaultGraphQLSchemaServletProvider(schema)); + } + + public static Builder newBuilder(GraphQLSchemaServletProvider schemaProvider) { + return new Builder(schemaProvider); + } + + public static Builder newBuilder(Supplier schemaProviderSupplier) { + return new Builder(schemaProviderSupplier); + } + + public GraphQLSchemaProvider getSchemaProvider() { + return schemaProviderSupplier.get(); + } + + public GraphQLSingleInvocationInput create( + GraphQLRequest graphQLRequest, HttpServletRequest request, HttpServletResponse response) { + return create(graphQLRequest, request, response, false); + } + + public GraphQLBatchedInvocationInput create( + ContextSetting contextSetting, + List graphQLRequests, + HttpServletRequest request, + HttpServletResponse response) { + return create(contextSetting, graphQLRequests, request, response, false); + } + + public GraphQLSingleInvocationInput createReadOnly( + GraphQLRequest graphQLRequest, HttpServletRequest request, HttpServletResponse response) { + return create(graphQLRequest, request, response, true); + } + + public GraphQLBatchedInvocationInput createReadOnly( + ContextSetting contextSetting, + List graphQLRequests, + HttpServletRequest request, + HttpServletResponse response) { + return create(contextSetting, graphQLRequests, request, response, true); + } + + public GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest) { + return new GraphQLSingleInvocationInput( + graphQLRequest, + schemaProviderSupplier.get().getSchema(), + contextBuilderSupplier.get().build(), + rootObjectBuilderSupplier.get().build()); + } + + private GraphQLSingleInvocationInput create( + GraphQLRequest graphQLRequest, + HttpServletRequest request, + HttpServletResponse response, + boolean readOnly) { + return new GraphQLSingleInvocationInput( + graphQLRequest, + readOnly + ? schemaProviderSupplier.get().getReadOnlySchema(request) + : schemaProviderSupplier.get().getSchema(request), + contextBuilderSupplier.get().build(request, response), + rootObjectBuilderSupplier.get().build(request)); + } + + private GraphQLBatchedInvocationInput create( + ContextSetting contextSetting, + List graphQLRequests, + HttpServletRequest request, + HttpServletResponse response, + boolean readOnly) { + return contextSetting.getBatch( + graphQLRequests, + readOnly + ? schemaProviderSupplier.get().getReadOnlySchema(request) + : schemaProviderSupplier.get().getSchema(request), + () -> contextBuilderSupplier.get().build(request, response), + rootObjectBuilderSupplier.get().build(request)); + } + + @Override + public GraphQLSingleInvocationInput create( + GraphQLRequest graphQLRequest, SubscriptionSession session) { + HandshakeRequest request = + (HandshakeRequest) session.getUserProperties().get(HandshakeRequest.class.getName()); + return new GraphQLSingleInvocationInput( + graphQLRequest, + schemaProviderSupplier.get().getSchema(request), + contextBuilderSupplier.get().build((Session) session.unwrap(), request), + rootObjectBuilderSupplier.get().build(request)); + } + + public GraphQLBatchedInvocationInput create( + ContextSetting contextSetting, List graphQLRequest, Session session) { + HandshakeRequest request = + (HandshakeRequest) session.getUserProperties().get(HandshakeRequest.class.getName()); + return contextSetting.getBatch( + graphQLRequest, + schemaProviderSupplier.get().getSchema(request), + () -> contextBuilderSupplier.get().build(session, request), + rootObjectBuilderSupplier.get().build(request)); + } + + public static class Builder { + + private final Supplier schemaProviderSupplier; + private Supplier contextBuilderSupplier = + DefaultGraphQLServletContextBuilder::new; + private Supplier rootObjectBuilderSupplier = + DefaultGraphQLRootObjectBuilder::new; + + public Builder(GraphQLSchemaServletProvider schemaProvider) { + this(() -> schemaProvider); + } + + public Builder(Supplier schemaProviderSupplier) { + this.schemaProviderSupplier = schemaProviderSupplier; + } + + public Builder withGraphQLContextBuilder(GraphQLServletContextBuilder contextBuilder) { + return withGraphQLContextBuilder(() -> contextBuilder); + } + + public Builder withGraphQLContextBuilder( + Supplier contextBuilderSupplier) { + this.contextBuilderSupplier = contextBuilderSupplier; + return this; + } + + public Builder withGraphQLRootObjectBuilder(GraphQLServletRootObjectBuilder rootObjectBuilder) { + return withGraphQLRootObjectBuilder(() -> rootObjectBuilder); + } + + public Builder withGraphQLRootObjectBuilder( + Supplier rootObjectBuilderSupplier) { + this.rootObjectBuilderSupplier = rootObjectBuilderSupplier; + return this; + } + + public GraphQLInvocationInputFactory build() { + return new GraphQLInvocationInputFactory( + schemaProviderSupplier, contextBuilderSupplier, rootObjectBuilderSupplier); + } + } +} diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLMBean.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLMBean.java new file mode 100644 index 0000000000..d9b3bef940 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLMBean.java @@ -0,0 +1,26 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +public interface GraphQLMBean { + + String[] getQueries(); + + String[] getMutations(); + + String executeQuery(String query); +} diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLServlet.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLServlet.java new file mode 100644 index 0000000000..e07e4dfc7c --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLServlet.java @@ -0,0 +1,87 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +import graphql.schema.GraphQLSchema; + +import javax.servlet.annotation.MultipartConfig; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; +import java.util.Map; + + +@WebServlet(name = "GraphQLServlet", urlPatterns = "/graphql") +@MultipartConfig +public class GraphQLServlet extends SimpleGraphQLHttpServlet { + + private final GraphQLConfiguration configuration; + + public GraphQLServlet() { + this.configuration = GraphQLConfiguration.with(buildSchema()).build(); + } + + private GraphQLSchema buildSchema() { + String schema = "type Query{hello: String}\n" + + "type Mutation {\n" + + " singleUpload(file: Upload!): File!\n" + + "}\n" + + "type File {\n" + + " id: ID!\n" + + " name: String!\n" + + "}\n" + + "scalar Upload"; + + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); + + RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring() + .scalar(UploadScalar.Upload) + .type("Mutation", builder -> builder.dataFetcher("singleUpload", new FileUploadDataFetcher())) + .build(); + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + return schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); + } + + @Override + protected GraphQLConfiguration getConfiguration() { + return this.configuration; + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) { + boolean isMultipart = request.getContentType().startsWith("multipart/form-data"); + + if (isMultipart) { + Map fileMap = MultipartFileHandler.parseMultipartRequest(request); + + // Пример обработки файлов: + for (Map.Entry entry : fileMap.entrySet()) { + String fileName = entry.getKey(); + byte[] fileContent = entry.getValue(); + + // Обработка файла (например, сохранение на диск) + // saveFile(fileName, fileContent); + } + } else { + super.doPost(request, response); + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLServletListener.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLServletListener.java new file mode 100644 index 0000000000..85754021bc --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLServletListener.java @@ -0,0 +1,78 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public interface GraphQLServletListener { + + /** + * Called this method when the request started processing. + * @param request http request + * @param response http response + * @return request callback or {@literal null} + */ + default RequestCallback onRequest(HttpServletRequest request, HttpServletResponse response) { + return null; + } + + /** + * The callback which used to add additional listeners for GraphQL request execution. + */ + interface RequestCallback { + + /** + * Called when failed to parse InvocationInput and the response was not written. + * @param request http request + * @param response http response + */ + default void onParseError( + HttpServletRequest request, HttpServletResponse response, Throwable throwable) {} + + /** + * Called right before the response will be written and flushed. Can be used for applying some + * changes to the response object, like adding response headers. + * @param request http request + * @param response http response + */ + default void beforeFlush(HttpServletRequest request, HttpServletResponse response) {} + + /** + * Called when GraphQL invoked successfully and the response was written already. + * @param request http request + * @param response http response + */ + default void onSuccess(HttpServletRequest request, HttpServletResponse response) {} + + /** + * Called when GraphQL was failed and the response was written already. + * @param request http request + * @param response http response + */ + default void onError( + HttpServletRequest request, HttpServletResponse response, Throwable throwable) {} + + /** + * Called finally once on both success and failed GraphQL invocation. The response is also + * already written. + * @param request http request + * @param response http response + */ + default void onFinally(HttpServletRequest request, HttpServletResponse response) {} + } +} diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLSubscriptionInvocationInputFactory.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLSubscriptionInvocationInputFactory.java new file mode 100644 index 0000000000..7eefcc8af1 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/GraphQLSubscriptionInvocationInputFactory.java @@ -0,0 +1,22 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +public interface GraphQLSubscriptionInvocationInputFactory { + + GraphQLSingleInvocationInput create(GraphQLRequest graphQLRequest, SubscriptionSession session); +} diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/MultipartFileHandler.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/MultipartFileHandler.java new file mode 100644 index 0000000000..51e5543e80 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/MultipartFileHandler.java @@ -0,0 +1,64 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +import jakarta.servlet.http.HttpServletRequest; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +public class MultipartFileHandler { + public static Map parseMultipartRequest(HttpServletRequest request) { + Map fileMap = new HashMap<>(); + + try { + String boundary = request.getContentType().split("boundary=")[1]; + InputStream inputStream = request.getInputStream(); + byte[] bytes = inputStream.readAllBytes(); + String content = new String(bytes); + + String[] parts = content.split("--" + boundary); + for (String part : parts) { + if (part.contains("Content-Disposition: form-data; name=\"file\"")) { + String[] fileData = part.split("\r\n\r\n"); + String fileName = fileData[0].split("filename=")[1].replaceAll("\"", "").trim(); + byte[] fileBytes = fileData[1].getBytes(); + fileMap.put(fileName, fileBytes); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + return fileMap; + } + + private static String getBoundary(String contentType) { + return contentType.split("boundary=")[1]; + } + + private static String getFileName(String contentDisposition) { + String[] elements = contentDisposition.split(";"); + for (String element : elements) { + if (element.trim().startsWith("filename")) { + return element.split("=")[1].replaceAll("\"", "").trim(); + } + } + return null; + } +} diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/SimpleGraphQLHttpServlet.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/SimpleGraphQLHttpServlet.java new file mode 100644 index 0000000000..521436f3ed --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/SimpleGraphQLHttpServlet.java @@ -0,0 +1,39 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +import graphql.schema.GraphQLSchema; + +@WebServlet(name = "SimpleGraphQLHttpServlet", urlPatterns = "/graphql") +public class SimpleGraphQLHttpServlet extends AbstractGraphQLHttpServlet { + + private final GraphQLConfiguration configuration; + + protected SimpleGraphQLHttpServlet() { + this.configuration = GraphQLConfiguration.with(GraphQLInvocationInputFactory.newBuilder() + .with(GraphQLSchema.newSchema().build()) + .build()) + .with(GraphQLQueryInvoker.newBuilder().build()) + .with(GraphQLObjectMapper.newBuilder().build()) + .build(); + } + + @Override + protected GraphQLConfiguration getConfiguration() { + return this.configuration; + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/SubscriptionSession.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/SubscriptionSession.java new file mode 100644 index 0000000000..b6987aa787 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/SubscriptionSession.java @@ -0,0 +1,67 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +import graphql.ExecutionResult; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; + +import java.util.Map; + +public interface SubscriptionSession { + + void subscribe(String id, Publisher data); + + void add(String id, Subscription subscription); + + void unsubscribe(String id); + + void send(String message); + + void sendMessage(Object payload); + + void sendDataMessage(String id, Object payload); + + void sendErrorMessage(String id, Object payload); + + void sendCompleteMessage(String id); + + void close(String reason); + + /** + * While the session is open, this method returns a Map that the developer may use to store + * application specific information relating to this session instance. The developer may retrieve + * information from this Map at any time between the opening of the session and during the + * onClose() method. But outside that time, any information stored using this Map may no longer be + * kept by the container. Web socket applications running on distributed implementations of the + * web container should make any application specific objects stored here java.io.Serializable, or + * the object may not be recreated after a failover. + * + * @return an editable Map of application data. + */ + Map getUserProperties(); + + boolean isOpen(); + + String getId(); + + SessionSubscriptions getSubscriptions(); + + Object unwrap(); + + Publisher getPublisher(); +} diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/UploadScalar.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/UploadScalar.java new file mode 100644 index 0000000000..67d451d80d --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/UploadScalar.java @@ -0,0 +1,43 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.data.transfer.graphql.upload; + +import graphql.schema.Coercing; +import graphql.schema.GraphQLScalarType; + +public class UploadScalar { + public static GraphQLScalarType Upload = GraphQLScalarType.newScalar() + .name("Upload") + .description("A file part in a multipart request") + .coercing(new Coercing<>() { + @Override + public Object serialize(Object dataFetcherResult) { + return dataFetcherResult; + } + + @Override + public Object parseValue(Object input) { + return input; + } + + @Override + public Object parseLiteral(Object input) { + return input; + } + }) + .build(); +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/schema/schema.graphqls b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/schema/schema.graphqls new file mode 100644 index 0000000000..c322215557 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/graphql/upload/schema/schema.graphqls @@ -0,0 +1,10 @@ +type Mutation { + singleUpload(file: Upload!): File! +} + +type File { + id: ID! + name: String! +} + +scalar Upload