All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.dirigible.components.engine.python.PythonEndpoint Maven / Gradle / Ivy

/*
 * Copyright (c) 2023 SAP SE or an SAP affiliate company and Eclipse Dirigible contributors
 *
 * All rights reserved. This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 *
 * SPDX-FileCopyrightText: 2023 SAP SE or an SAP affiliate company and Eclipse Dirigible
 * contributors SPDX-License-Identifier: EPL-2.0
 */
package org.eclipse.dirigible.components.engine.python;

import static org.eclipse.dirigible.graalium.core.graal.ValueTransformer.transformValue;
import java.io.File;
import java.nio.file.Path;
import org.eclipse.dirigible.components.base.endpoint.BaseEndpoint;
import org.eclipse.dirigible.graalium.core.modules.DirigibleSourceProvider;
import org.eclipse.dirigible.graalium.core.python.GraalPyCodeRunner;
import org.eclipse.dirigible.repository.api.IRepository;
import org.eclipse.dirigible.repository.api.IRepositoryStructure;
import org.eclipse.dirigible.repository.api.RepositoryNotFoundException;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

@RestController
@RequestMapping({BaseEndpoint.PREFIX_ENDPOINT_SECURED + "py", BaseEndpoint.PREFIX_ENDPOINT_PUBLIC + "py"})
public class PythonEndpoint extends BaseEndpoint {
    private static final String PYTHON = ".py/";
    private static final Logger logger = LoggerFactory.getLogger(PythonEndpoint.class.getCanonicalName());
    private static final String HTTP_PATH_MATCHER = "/{projectName}/{*projectFilePath}";
    private final IRepository repository;

    @Autowired
    public PythonEndpoint(IRepository repository) {
        this.repository = repository;
    }

    @GetMapping(HTTP_PATH_MATCHER)
    public ResponseEntity get(@PathVariable("projectName") String projectName, @PathVariable("projectFilePath") String projectFilePath,
            @Nullable @RequestParam(required = false) MultiValueMap params) {
        return executePython(projectName, projectFilePath, params, null);
    }

    @PostMapping(HTTP_PATH_MATCHER)
    public ResponseEntity post(@PathVariable("projectName") String projectName, @PathVariable("projectFilePath") String projectFilePath,
            @Nullable @RequestParam(required = false) MultiValueMap params) {
        return executePython(projectName, projectFilePath, params, null);
    }

    @PostMapping(value = HTTP_PATH_MATCHER, consumes = "multipart/form-data")
    public ResponseEntity postFile(@PathVariable("projectName") String projectName,
            @PathVariable("projectFilePath") String projectFilePath,
            @Nullable @RequestParam(required = false) MultiValueMap params,
            @Validated @RequestParam("file") MultipartFile[] file) {
        return executePython(projectName, projectFilePath, params, file);
    }

    @PutMapping(HTTP_PATH_MATCHER)
    public ResponseEntity put(@PathVariable("projectName") String projectName, @PathVariable("projectFilePath") String projectFilePath,
            @Nullable @RequestParam(required = false) MultiValueMap params) {
        return executePython(projectName, projectFilePath, params, null);
    }

    @PutMapping(value = HTTP_PATH_MATCHER, consumes = "multipart/form-data")
    public ResponseEntity putFile(@PathVariable("projectName") String projectName,
            @PathVariable("projectFilePath") String projectFilePath,
            @Nullable @RequestParam(required = false) MultiValueMap params,
            @Validated @RequestParam("file") MultipartFile file) {
        return executePython(projectName, projectFilePath, params, new MultipartFile[] {file});
    }

    @PatchMapping(HTTP_PATH_MATCHER)
    public ResponseEntity patch(@PathVariable("projectName") String projectName, @PathVariable("projectFilePath") String projectFilePath,
            @Nullable @RequestParam(required = false) MultiValueMap params) {
        return executePython(projectName, projectFilePath, params, null);
    }

    @DeleteMapping(HTTP_PATH_MATCHER)
    public ResponseEntity delete(@PathVariable("projectName") String projectName,
            @PathVariable("projectFilePath") String projectFilePath,
            @Nullable @RequestParam(required = false) MultiValueMap params) {
        return executePython(projectName, projectFilePath, params, null);
    }

    protected String extractProjectFilePath(String projectFilePath) {
        if (projectFilePath.indexOf(PYTHON) > 0) {
            projectFilePath = projectFilePath.substring(0, projectFilePath.indexOf(PYTHON) + PYTHON.length() + 1);
        }
        return projectFilePath;
    }

    protected String extractPathParam(String projectFilePath) {
        String projectFilePathParam = "";
        if (projectFilePath.indexOf(PYTHON) > 0) {
            projectFilePathParam = projectFilePath.substring(projectFilePath.indexOf(PYTHON) + PYTHON.length() + 1);
        }
        return projectFilePathParam;
    }

    private ResponseEntity executePython(String projectName, String projectFilePath, MultiValueMap params,
            MultipartFile[] files) {
        String projectFilePathParam = extractPathParam(projectFilePath);
        projectFilePath = extractProjectFilePath(projectFilePath);
        return executePython(projectName, projectFilePath, projectFilePathParam, params, files);
    }

    protected ResponseEntity executePython(String projectName, String projectFilePath, String projectFilePathParam,
            MultiValueMap params, MultipartFile[] files) {
        try {
            if (isNotValid(projectName) || isNotValid(projectFilePath)) {
                throw new ResponseStatusException(HttpStatus.FORBIDDEN);
            }

            Object result = handleRequest(projectName, normalizePath(projectFilePath), normalizePath(projectFilePathParam),
                    params.get("debug") != null);
            return ResponseEntity.ok(result);
        } catch (RepositoryNotFoundException e) {
            String message = e.getMessage() + ". Try to publish the service before execution.";
            throw new RepositoryNotFoundException(message, e);
        }
    }

    private Object handleRequest(String projectName, String projectFilePath, String projectFilePathParam, boolean debug) {
        Path absoluteSourcePath = getAbsolutePathIfValidProjectFile(projectName, projectFilePath);
        Path workingDir = getDirigibleWorkingDirectory();
        Path projectDir = workingDir.resolve(projectName);
        Path pythonMods = getDirigiblePythonModulesDirectory();

        try (var runner = new GraalPyCodeRunner(workingDir, projectDir, pythonMods, debug)) {
            Source source = runner.prepareSource(absoluteSourcePath);
            Value value = runner.run(source);
            return transformValue(value);
        }
    }

    private static Path getAbsolutePathIfValidProjectFile(String projectName, String projectFilePath) {
        var sourceProvider = new DirigibleSourceProvider();
        String sourceFilePath = Path.of(projectName, projectFilePath)
                                    .toString();
        String maybePythonCode = sourceProvider.getSource(sourceFilePath);
        if (maybePythonCode == null) {
            throw new RuntimeException("Python source code for project name '" + projectName + "' and file name '" + projectFilePath
                    + "' could not be found, consider publishing it.");
        }

        return sourceProvider.getAbsoluteSourcePath(projectName, projectFilePath);
    }

    private boolean isNotValid(String inputPath) {
        String registryPath = getDirigibleWorkingDirectory().toString();
        String normalizedInputPath = Path.of(inputPath)
                                         .normalize()
                                         .toString();
        File file = new File(registryPath, normalizedInputPath);
        try {
            return !file.toPath()
                        .normalize()
                        .startsWith(registryPath);
        } catch (Exception e) {
            return true;
        }
    }

    private Path getDirigibleWorkingDirectory() {
        String publicRegistryPath = repository.getInternalResourcePath(IRepositoryStructure.PATH_REGISTRY_PUBLIC);
        return Path.of(publicRegistryPath);
    }

    private Path getDirigiblePythonModulesDirectory() {
        String publicRegistryPath = repository.getInternalResourcePath(IRepositoryStructure.PATH_REGISTRY_PUBLIC);
        return Path.of(publicRegistryPath)
                   .resolve("python-modules");
    }

    private String normalizePath(String path) {
        if (path != null) {
            if (path.startsWith(IRepository.SEPARATOR)) {
                return path.substring(1);
            }
        }
        return path;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy