Commit 54982722 authored by Philip Makedonski's avatar Philip Makedonski
Browse files

+ added servering option for titan compiler

parent 0eb16e91
Loading
Loading
Loading
Loading
+499 −0
Original line number Diff line number Diff line
package de.ugoe.cs.swe.T3Q;
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;

public class HttpFileServer {
    //TODO: experimental pure java boilerplate to handle uploads
    private static final String TEMP_DIR_PREFIX = "fileserver_";
    private final HttpServer server;
    private final ExecutorService executor;
    private final FileProcessor processor;
    
    public HttpFileServer(int port, FileProcessor processor) throws IOException {
        this.server = HttpServer.create(new InetSocketAddress(port), 0);
        this.executor = Executors.newFixedThreadPool(10);
        this.processor = processor;
        this.server.setExecutor(executor);
        
        // Register upload endpoint
        server.createContext("/compile_asn", new UploadHandler(processor));
        server.createContext("/health", new HealthHandler());
    }
    
    public void start() {
        server.start();
        System.out.println("Server started on port " + server.getAddress().getPort());
    }
    
    public void stop() {
        server.stop(0);
        executor.shutdown();
    }
    
    /**
     * Interface for processing uploaded files
     */
    public interface FileProcessor {
        /**
         * Process uploaded files and return path to resulting JSON file
         * @param files Map of field names to temporary file paths
         * @param fields Additional form fields from the request
         * @return Path to the generated JSON file
         */
        Path process(Map<String, Path> files, Map<String, String> fields) throws Exception;
    }
    
    /**
     * Handler for file uploads
     */
    static class UploadHandler implements HttpHandler {
        private final FileProcessor processor;
        
        public UploadHandler(FileProcessor processor) {
            this.processor = processor;
        }
        
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
                sendError(exchange, 405, "Method not allowed");
                return;
            }
            
            Path tempDir = null;
            try {
                // Create temporary directory for uploaded files
                tempDir = Files.createTempDirectory(TEMP_DIR_PREFIX);
                
                // Parse multipart request
                String contentType = exchange.getRequestHeaders().getFirst("Content-Type");
                if (contentType == null || !contentType.startsWith("multipart/form-data")) {
                    sendError(exchange, 400, "Expected multipart/form-data");
                    return;
                }
                
                String boundary = extractBoundary(contentType);
                if (boundary == null) {
                    sendError(exchange, 400, "No boundary found in Content-Type");
                    return;
                }
                
                MultipartParser parser = new MultipartParser(exchange.getRequestBody(), boundary, tempDir);
                MultipartData data = parser.parse();
                
                if (data.files.isEmpty()) {
                    sendError(exchange, 400, "No files uploaded");
                    return;
                }
                
                // Process files with external command
                Path jsonResult = processor.process(data.files, data.fields);
                
                if (jsonResult == null || !Files.exists(jsonResult)) {
                    sendError(exchange, 500, "Processing failed to produce result");
                    return;
                }
                
                // Send JSON response
                byte[] jsonBytes = Files.readAllBytes(jsonResult);
                exchange.getResponseHeaders().set("Content-Type", "application/json");
                exchange.sendResponseHeaders(200, jsonBytes.length);
                try (OutputStream os = exchange.getResponseBody()) {
                    os.write(jsonBytes);
                }
                
                // Cleanup result file
                Files.deleteIfExists(jsonResult);
                
            } catch (Exception e) {
                e.printStackTrace();
                sendError(exchange, 500, "Processing error: " + e.getMessage());
            } finally {
                // Cleanup temp directory
                if (tempDir != null) {
                    cleanupDirectory(tempDir);
                }
            }
        }
        
        private String extractBoundary(String contentType) {
            String[] parts = contentType.split(";");
            for (String part : parts) {
                part = part.trim();
                if (part.startsWith("boundary=")) {
                    return part.substring(9);
                }
            }
            return null;
        }
    }
    
    /**
     * Health check endpoint
     */
    static class HealthHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            String response = "{\"status\":\"ok\"}";
            exchange.getResponseHeaders().set("Content-Type", "application/json");
            exchange.sendResponseHeaders(200, response.length());
            try (OutputStream os = exchange.getResponseBody()) {
                os.write(response.getBytes(StandardCharsets.UTF_8));
            }
        }
    }
    
    /**
     * Multipart form data parser
     */
    static class MultipartParser {
        private final InputStream input;
        private final String boundary;
        private final Path tempDir;
        
        public MultipartParser(InputStream input, String boundary, Path tempDir) {
            this.input = input;
            this.boundary = boundary;
            this.tempDir = tempDir;
        }
        
        public MultipartData parse() throws IOException {
            MultipartData data = new MultipartData();
            BufferedInputStream bis = new BufferedInputStream(input);
            
            System.out.println("DEBUG: Starting parse, boundary=" + boundary);
            
            // Skip preamble and find first boundary (starts with --boundary, not \r\n--boundary)
            String firstBoundary = "--" + boundary;
            if (!findBoundary(bis, firstBoundary.getBytes(StandardCharsets.UTF_8))) {
                System.out.println("DEBUG: Could not find first boundary");
                return data;
            }
            System.out.println("DEBUG: Found first boundary");
            
            while (true) {
                // Check if this is the end boundary (--boundary--)
                bis.mark(4);
                int c1 = bis.read();
                int c2 = bis.read();
                System.out.println("DEBUG: After boundary, next chars: '" + (char)c1 + "' '" + (char)c2 + "'");
                if (c1 == '-' && c2 == '-') {
                    // End of multipart
                    System.out.println("DEBUG: Found end boundary");
                    break;
                }
                bis.reset();
                
                // Skip CRLF after boundary
                skipLine(bis);
                
                // Read headers
                Map<String, String> headers = readHeaders(bis);
                System.out.println("DEBUG: Read headers: " + headers);
                if (headers.isEmpty()) {
                    System.out.println("DEBUG: No headers found, ending parse");
                    break;
                }
                
                // Read part data until next boundary
                byte[] partData = readUntilBoundary(bis);
                System.out.println("DEBUG: Read part data: " + partData.length + " bytes");
                
                // Process this part
                processPart(headers, partData, data);
                System.out.println("DEBUG: Files so far: " + data.files.size() + ", Fields: " + data.fields.size());
            }
            
            System.out.println("DEBUG: Parse complete. Files: " + data.files.size() + ", Fields: " + data.fields.size());
            return data;
        }
        
        private boolean findBoundary(InputStream is, byte[] boundaryBytes) throws IOException {
            List<Integer> window = new ArrayList<>();
            int b;
            
            while ((b = is.read()) != -1) {
                window.add(b);
                
                // Keep window size equal to boundary length
                if (window.size() > boundaryBytes.length) {
                    window.remove(0);
                }
                
                // Check if window matches boundary
                if (window.size() == boundaryBytes.length) {
                    boolean match = true;
                    for (int i = 0; i < boundaryBytes.length; i++) {
                        if (window.get(i) != (boundaryBytes[i] & 0xFF)) {
                            match = false;
                            break;
                        }
                    }
                    if (match) {
                        return true;
                    }
                }
            }
            return false;
        }
        
        private byte[] readUntilBoundary(InputStream is) throws IOException {
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            String boundaryMarker = "\r\n--" + boundary;
            byte[] boundaryBytes = boundaryMarker.getBytes(StandardCharsets.UTF_8);
            
            List<Integer> window = new ArrayList<>();
            int b;
            
            while ((b = is.read()) != -1) {
                window.add(b);
                
                // Keep window size equal to boundary length
                if (window.size() > boundaryBytes.length) {
                    buffer.write(window.remove(0));
                }
                
                // Check if window matches boundary
                if (window.size() == boundaryBytes.length) {
                    boolean match = true;
                    for (int i = 0; i < boundaryBytes.length; i++) {
                        if (window.get(i) != (boundaryBytes[i] & 0xFF)) {
                            match = false;
                            break;
                        }
                    }
                    if (match) {
                        // Found boundary, return data before it
                        return buffer.toByteArray();
                    }
                }
            }
            
            // Flush remaining window
            for (int val : window) {
                buffer.write(val);
            }
            
            return buffer.toByteArray();
        }
        
        private void skipLine(InputStream is) throws IOException {
            int b;
            while ((b = is.read()) != -1) {
                if (b == '\n') {
                    break;
                }
            }
        }
        
        private Map<String, String> readHeaders(BufferedInputStream is) throws IOException {
            Map<String, String> headers = new HashMap<>();
            ByteArrayOutputStream lineBuffer = new ByteArrayOutputStream();
            int prev = 0;
            int b;
            
            while ((b = is.read()) != -1) {
                if (b == '\n' && prev == '\r') {
                    // End of line
                    byte[] lineBytes = lineBuffer.toByteArray();
                    if (lineBytes.length <= 1) {
                        // Empty line marks end of headers
                        break;
                    }
                    
                    // Parse header line (remove trailing \r)
                    String line = new String(lineBytes, 0, lineBytes.length - 1, StandardCharsets.UTF_8);
                    int colon = line.indexOf(':');
                    if (colon > 0) {
                        String name = line.substring(0, colon).trim();
                        String value = line.substring(colon + 1).trim();
                        headers.put(name.toLowerCase(), value);
                    }
                    
                    lineBuffer.reset();
                } else {
                    lineBuffer.write(b);
                }
                prev = b;
            }
            
            return headers;
        }
        
        private void processPart(Map<String, String> headers, byte[] data, MultipartData result) throws IOException {
            String disposition = headers.get("content-disposition");
            if (disposition == null) return;
            
            String name = extractValue(disposition, "name");
            String filename = extractValue(disposition, "filename");
            
            if (filename != null && !filename.isEmpty()) {
                // This is a file
                Path filePath = tempDir.resolve(filename);
                Files.write(filePath, data);
                result.files.put(name, filePath);
            } else if (name != null) {
                // This is a regular field
                result.fields.put(name, new String(data, StandardCharsets.UTF_8).trim());
            }
        }
        
        private String extractValue(String header, String key) {
            int start = header.indexOf(key + "=\"");
            if (start == -1) return null;
            start += key.length() + 2;
            int end = header.indexOf("\"", start);
            if (end == -1) return null;
            return header.substring(start, end);
        }
    }
    
    static class MultipartData {
        Map<String, Path> files = new HashMap<>();
        Map<String, String> fields = new HashMap<>();
    }
    
    private static void sendError(HttpExchange exchange, int code, String message) throws IOException {
        String json = String.format("{\"error\":\"%s\"}", message);
        exchange.getResponseHeaders().set("Content-Type", "application/json");
        exchange.sendResponseHeaders(code, json.length());
        try (OutputStream os = exchange.getResponseBody()) {
            os.write(json.getBytes(StandardCharsets.UTF_8));
        }
    }
    
    private static void cleanupDirectory(Path dir) {
        try {
            Files.walk(dir)
                .sorted(Comparator.reverseOrder())
                .forEach(path -> {
                    try {
                        Files.delete(path);
                    } catch (IOException e) {
                        // Ignore cleanup errors
                    }
                });
        } catch (IOException e) {
            // Ignore cleanup errors
        }
    }
    
    // Example: External command processor
    public static class ExternalCommandProcessor implements FileProcessor {
        private final String command;
        private final Path workDir;
		private List<String> arguments;
        
        public ExternalCommandProcessor(String command, List<String> arguments, Path workDir) {
            this.command = command;
            this.arguments = arguments;
            this.workDir = workDir;
        }
        
        @Override
        public Path process(Map<String, Path> files, Map<String, String> fields) throws Exception {
            // Build command with file paths as arguments
            List<String> cmd = new ArrayList<>();
            cmd.add(command);
            cmd.add("--ttcn2json");
            // Add file paths as arguments
            for (Path file : files.values()) {
                cmd.add(file.toString());
            }
            cmd.add("-");
            String jsonFile = "asn.json";
			cmd.add(jsonFile);
            
            // Execute command
            ProcessBuilder pb = new ProcessBuilder(cmd);
            pb.directory(workDir.toFile());
            pb.redirectErrorStream(true);
            
            Process process = pb.start();
            
            // Read output (assuming the command outputs JSON file path)
            String output;
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()))) {
                output = reader.lines()
                    .reduce((a, b) -> a + "\n" + b)
                    .orElse("");
            }
            
            int exitCode = process.waitFor();
            if (exitCode != 0) {
                throw new RuntimeException("Command failed with exit code " + exitCode + ": " + output);
            }
            
            // The command should output the path to the generated JSON file
            File wd = workDir.toFile();
            String jsonPath = new File(wd, jsonFile).getAbsolutePath();
            Path result = Paths.get(jsonPath);
            
            if (!Files.exists(result)) {
                throw new FileNotFoundException("Expected JSON file not found: " + jsonPath);
            }
            
            return result;
        }
    }
    
    // Example usage
    public static void main(String[] args) {
        try {
            // Example 1: Simple processor that creates a JSON response
            FileProcessor simpleProcessor = (files, fields) -> {
                Path jsonFile = Files.createTempFile("result_", ".json");
                
                // Create simple JSON response
                StringBuilder json = new StringBuilder("{");
                json.append("\"filesProcessed\":").append(files.size()).append(",");
                json.append("\"files\":[");
                
                int i = 0;
                for (Map.Entry<String, Path> entry : files.entrySet()) {
                    if (i++ > 0) json.append(",");
                    json.append("{")
                        .append("\"field\":\"").append(entry.getKey()).append("\",")
                        .append("\"filename\":\"").append(entry.getValue().getFileName()).append("\",")
                        .append("\"size\":").append(Files.size(entry.getValue()))
                        .append("}");
                }
                
                json.append("]}");
                
                Files.write(jsonFile, json.toString().getBytes(StandardCharsets.UTF_8));
                return jsonFile;
            };
            
//            HttpFileServer server = new HttpFileServer(8080, simpleProcessor);
//            server.start();
            
            // Example 2: External command processor
            // Uncomment and modify for your external command
            /*
             */
            Path workDir = Paths.get("./");
            Files.createDirectories(workDir);
            //TODO: expose as parameter
            FileProcessor cmdProcessor = new ExternalCommandProcessor(
                "./compiler",
                List.of(),
                workDir
            );
            HttpFileServer server = new HttpFileServer(3005, cmdProcessor);
            server.start();
            
            System.out.println("Server ready. Test with:");
            System.out.println("curl -X POST -F 'file=@test.txt' http://localhost:3005/compile_asn");
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
 No newline at end of file
+50 −1
Original line number Diff line number Diff line
@@ -6,10 +6,14 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

import org.apache.commons.cli.CommandLine;
@@ -25,6 +29,8 @@ import org.apache.commons.io.filefilter.RegexFileFilter;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;

import de.ugoe.cs.swe.T3Q.HttpFileServer.ExternalCommandProcessor;
import de.ugoe.cs.swe.T3Q.HttpFileServer.FileProcessor;
import de.ugoe.cs.swe.TTCN3Configuration.QualityCheckProfile;
import de.ugoe.cs.swe.common.ConfigTools;
import de.ugoe.cs.swe.common.MiscTools;
@@ -55,6 +61,7 @@ public class T3Q {
	private boolean analyzeUsage = false;
	//TODO: expose in configuration?
	private String schemaFile = "asn.json";
	private boolean serveASN1Compiler;

	// private boolean formattingEnabled = false;

@@ -115,6 +122,11 @@ public class T3Q {
			System.out.println("ERRORING OUT!");
		}

		if (this.isServeASN1Compiler()) {
			serveASNCompiler();
			return;
		}

		if (this.isConvertASN1toJSONSchema()) {
			//TODO: skip if none!
			convertASN1toJSONSchema();
@@ -276,6 +288,31 @@ public class T3Q {
		return true;
	}

	private void serveASNCompiler() {
		//TODO: this may need to be temporary..
        Path workDir = Paths.get("./");
        try {
			Files.createDirectories(workDir);
	        FileProcessor cmdProcessor = new ExternalCommandProcessor(
        		//DONE: expose as parameter
//	            "./compiler",
        		activeProfile.getTitanCompilerPath(),
	            List.of(),
	            workDir
	        );
	        //TODO: send titan output as well
	        HttpFileServer server = new HttpFileServer(3005, cmdProcessor);
	        server.start();
	        System.out.println("Server ready at http://localhost:3005/compile_asn. Test with:");
	        System.out.println("curl -X POST -F 'file=@test.txt' http://localhost:3005/compile_asn");
        } catch (IOException e) {
        	// TODO Auto-generated catch block
        	e.printStackTrace();
        }
        
		
	}

	private CommandLine parseCommandLineArguments(String[] args) {
		CommandLineParser parser = new DefaultParser();
		T3QOptionsHandler optionsHandler = new T3QOptionsHandler();
@@ -331,6 +368,10 @@ public class T3Q {
		if (commandLine.hasOption("local-dependencies")) {
			this.setGenerateLocalDependencies(true);
		}
		if (commandLine.hasOption("serve-asn1-compiler")) {
			this.setServeASN1Compiler(true);
		}

		return commandLine.getArgs();
	}

@@ -573,4 +614,12 @@ public class T3Q {
		this.convertASN1toJSONSchema = convertASN1toJSONSchema;
	}

	public boolean isServeASN1Compiler() {
		return serveASN1Compiler;
	}

	public void setServeASN1Compiler(boolean serveASN1Compiler) {
		this.serveASN1Compiler = serveASN1Compiler;
	}

}
+3 −0
Original line number Diff line number Diff line
@@ -56,6 +56,9 @@ public class OptionsHandler {
		optionWithID = new OptionWithID(500, "convert-asn1-to-schema", "Convert ASN.1 files to JSON schema\n");
		getOptions().addOption(optionWithID);

		optionWithID = new OptionWithID(501, "serve-asn1-compiler", "Start a server for converting ASN.1 files to JSON schema\n");
		getOptions().addOption(optionWithID);
		
		optionWithID = new OptionWithID(510, "convert-schemas", "Convert JSON schemas to TTCN-3\n");
		getOptions().addOption(optionWithID);