using System.CommandLine;
using System.CommandLine.Invocation;
using com.github.javaparser.resolution;
using com.github.javaparser.symbolsolver.resolution.typesolvers;
using JavaToCSharp;
using Microsoft.Extensions.Logging;
namespace JavaToCSharpCli;
///
/// The main JavaToCSharpCli program class.
///
public class Program
{
private static readonly ILoggerFactory _loggerFactory;
private static readonly ILogger _logger;
private static readonly Option _preserveDirectoryStructure = new(
name: "--preserve-directory-structure",
description: "Keeps original directory structure in output",
getDefaultValue: () => true);
private static readonly Option _includeSubdirectoriesOption = new(
name: "--include-subdirectories",
description: "When the command is dir, converts files in all subdirectories",
getDefaultValue: () => true);
private static readonly Option _includeUsingsOption = new(
name: "--include-usings",
description: "Include using directives in output",
getDefaultValue: () => true);
private static readonly Option _includeNamespaceOption = new(
name: "--include-namespace",
description: "Include namespace in output",
getDefaultValue: () => true);
private static readonly Option _includeCommentsOption = new(
name: "--include-comments",
description: "Include comments in output",
getDefaultValue: () => true);
private static readonly Option _useDebugAssertOption = new(
name: "--use-debug-assert",
description: "Use Debug.Assert for asserts",
getDefaultValue: () => false);
private static readonly Option _startInterfaceNamesWithIOption = new(
name: "--start-interface-names-with-i",
description: "Prefix interface names with the letter I",
getDefaultValue: () => true);
private static readonly Option _endAnnotationNamesWithAttributeOption = new(
name: "--end-annotation-names-with-attribute",
description: "Postfix attribute names with the Attribute",
getDefaultValue: () => true);
private static readonly Option _commentUnrecognizedCodeOption = new(
name: "--comment-unrecognized-code",
description: "Include unrecognized code in output as commented-out code",
getDefaultValue: () => true);
private static readonly Option _systemOutToConsoleOption = new(
name: "--system-out-to-console",
description: "Convert System.out calls to Console",
getDefaultValue: () => false);
private static readonly Option _fileScopedNamespacesOption = new(
name: "--file-scoped-namespaces",
description: "Use file-scoped namespaces in C# output",
getDefaultValue: () => false);
private static readonly Option _clearDefaultUsingsOption = new(
name: "--clear-usings",
description: "Remove all default usings provided by this app",
getDefaultValue: () => false);
private static readonly Option> _addUsingsOption = new(
name: "--add-using",
description: "Adds a using directive to the collection of usings")
{
ArgumentHelpName = "namespace"
};
private static readonly Option> _sourceRoots = new(
name: "--source-roots",
description: "Adds a source root to the solver")
{
AllowMultipleArgumentsPerToken = true
};
private static readonly Option _mappingsFileNameOption = new(
name: "--mappings-file",
description: "A yaml file with syntax mappings from imports, methods and annotations");
static Program()
{
_loggerFactory = LoggerFactory.Create(builder =>
builder.AddSimpleConsole().SetMinimumLevel(LogLevel.Information));
_logger = _loggerFactory.CreateLogger();
}
public static async Task Main(string[] args)
{
var rootCommand = new RootCommand("Java to C# Converter")
{
Description = "A syntactic transformer of source code from Java to C#."
};
rootCommand.AddCommand(CreateFileCommand());
rootCommand.AddCommand(CreateDirectoryCommand());
rootCommand.AddGlobalOption(_preserveDirectoryStructure);
rootCommand.AddGlobalOption(_includeSubdirectoriesOption);
rootCommand.AddGlobalOption(_includeUsingsOption);
rootCommand.AddGlobalOption(_includeNamespaceOption);
rootCommand.AddGlobalOption(_includeCommentsOption);
rootCommand.AddGlobalOption(_useDebugAssertOption);
rootCommand.AddGlobalOption(_startInterfaceNamesWithIOption);
rootCommand.AddGlobalOption(_endAnnotationNamesWithAttributeOption);
rootCommand.AddGlobalOption(_commentUnrecognizedCodeOption);
rootCommand.AddGlobalOption(_systemOutToConsoleOption);
rootCommand.AddGlobalOption(_fileScopedNamespacesOption);
rootCommand.AddGlobalOption(_clearDefaultUsingsOption);
rootCommand.AddGlobalOption(_addUsingsOption);
rootCommand.AddGlobalOption(_mappingsFileNameOption);
rootCommand.AddGlobalOption(_sourceRoots);
await rootCommand.InvokeAsync(args);
// flush logs
_loggerFactory.Dispose();
}
private static Command CreateFileCommand()
{
var inputArgument = new Argument(
name: "input",
description: "A Java source code file to convert");
var outputArgument = new Argument(
name: "output",
description: "Path to place the C# output file, or stdout if omitted",
getDefaultValue: () => null);
var fileCommand = new Command("file", "Convert a Java file to C#");
fileCommand.AddArgument(inputArgument);
fileCommand.AddArgument(outputArgument);
fileCommand.SetHandler(context =>
{
var input = context.ParseResult.GetValueForArgument(inputArgument);
var output = context.ParseResult.GetValueForArgument(outputArgument);
var options = GetJavaConversionOptions(context);
ConvertToCSharpFile(input, output, options, solver: CreateSolver(options));
});
return fileCommand;
}
private static JavaConversionOptions GetJavaConversionOptions(InvocationContext context)
{
var options = new JavaConversionOptions
{
PreserverDirectoryStructure = context.ParseResult.GetValueForOption(_preserveDirectoryStructure),
IncludeSubdirectories = context.ParseResult.GetValueForOption(_includeSubdirectoriesOption),
IncludeUsings = context.ParseResult.GetValueForOption(_includeUsingsOption),
IncludeComments = context.ParseResult.GetValueForOption(_includeCommentsOption),
IncludeNamespace = context.ParseResult.GetValueForOption(_includeNamespaceOption),
ConvertSystemOutToConsole = context.ParseResult.GetValueForOption(_systemOutToConsoleOption),
StartInterfaceNamesWithI = context.ParseResult.GetValueForOption(_startInterfaceNamesWithIOption),
EndAnnotationNamesWithAttribute = context.ParseResult.GetValueForOption(_endAnnotationNamesWithAttributeOption),
UseDebugAssertForAsserts = context.ParseResult.GetValueForOption(_useDebugAssertOption),
UseUnrecognizedCodeToComment = context.ParseResult.GetValueForOption(_commentUnrecognizedCodeOption),
UseFileScopedNamespaces = context.ParseResult.GetValueForOption(_fileScopedNamespacesOption),
SourceRoots = context.ParseResult.GetValueForOption(_sourceRoots) ?? new List(),
};
if (context.ParseResult.GetValueForOption(_clearDefaultUsingsOption))
{
options.ClearUsings();
}
else
{
options.AddUsing("System");
options.AddUsing("System.Collections.Generic");
options.AddUsing("System.Collections.ObjectModel");
options.AddUsing("System.Linq");
options.AddUsing("System.Text");
}
foreach (string ns in context.ParseResult.GetValueForOption(_addUsingsOption) ?? new List())
{
options.AddUsing(ns);
}
var mappingsFile = context.ParseResult.GetValueForOption(_mappingsFileNameOption);
if (!string.IsNullOrEmpty(mappingsFile))
{
options.SyntaxMappings = ReadMappingsFile(mappingsFile);
}
return options;
}
private static SyntaxMapping ReadMappingsFile(string mappingsFile)
{
// Let fail if cannot be read or deserialized to display the exception message in the CLI
var mappingsStr = File.ReadAllText(mappingsFile);
return SyntaxMapping.Deserialize(mappingsStr);
}
private static Command CreateDirectoryCommand()
{
var inputArgument = new Argument(
name: "input",
description: "A directory containing Java source code files to convert");
var outputArgument = new Argument(
name: "output",
description: "Path to place the C# output files");
var dirCommand = new Command("dir", "Convert a directory containing Java files to C#");
dirCommand.AddArgument(inputArgument);
dirCommand.AddArgument(outputArgument);
dirCommand.SetHandler(context =>
{
var input = context.ParseResult.GetValueForArgument(inputArgument);
var output = context.ParseResult.GetValueForArgument(outputArgument);
var options = GetJavaConversionOptions(context);
ConvertToCSharpDir(input, output, options);
});
return dirCommand;
}
private static TypeSolver? CreateSolver(JavaConversionOptions options)
{
TypeSolver? solver = null;
if (options.SourceRoots.Count <= 0)
return solver;
var typeSolverBuilder = new TypeSolverBuilder();
foreach (string sourceRoot in options.SourceRoots)
{
typeSolverBuilder.withSourceCode(sourceRoot);
}
solver = typeSolverBuilder.build();
return solver;
}
private static void ConvertToCSharpDir(DirectoryInfo inputDirectory, DirectoryInfo outputDirectory, JavaConversionOptions options)
{
var typeSolver = CreateSolver(options);
if (inputDirectory.Exists)
{
var searchOption = options.IncludeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
foreach (var f in inputDirectory.GetFiles("*.java", searchOption))
{
string? directoryName = f.DirectoryName;
if (string.IsNullOrWhiteSpace(directoryName))
{
continue;
}
var output = options.PreserverDirectoryStructure ?
new DirectoryInfo(Path.Combine(
outputDirectory.ToString(),
Path.GetRelativePath(inputDirectory.ToString(), directoryName)
))
: outputDirectory;
if (!output.Exists)
{
output.Create();
}
ConvertToCSharpFile(f,
new FileInfo(Path.Combine(output.FullName, Path.ChangeExtension(f.Name, ".cs"))),
options,
false,
typeSolver);
}
}
else
_logger.LogError("Java input folder {path} doesn't exist!", inputDirectory);
}
private static void ConvertToCSharpFile(FileSystemInfo inputFile, FileSystemInfo? outputFile, JavaConversionOptions options, bool overwrite = true, TypeSolver? solver = null)
{
if (!overwrite && outputFile is { Exists: true })
_logger.LogInformation("{outputFilePath} exists, skip to next.", outputFile);
else if (inputFile.Exists)
{
try
{
string javaText = File.ReadAllText(inputFile.FullName);
options.WarningEncountered += (_, eventArgs) =>
{
if (outputFile != null)
{
_logger.LogWarning("Line {JavaLineNumber}: {Message}", eventArgs.JavaLineNumber,
eventArgs.Message);
}
OutputFileOrPrint(outputFile != null ? Path.ChangeExtension(outputFile.FullName, ".warning") : null,
eventArgs.Message + Environment.NewLine);
};
string? parsed = JavaToCSharpConverter.ConvertText(javaText, options, solver);
OutputFileOrPrint(outputFile?.FullName, parsed ?? string.Empty);
if (outputFile != null)
{
_logger.LogInformation("{filePath} converted!", inputFile.Name);
}
}
catch (Exception ex)
{
_logger.LogError("{filePath} failed! {type}: {message}", inputFile.Name, ex.GetType().Name, ex);
if (outputFile != null)
{
File.WriteAllText(Path.ChangeExtension(outputFile.FullName, ".error"), ex.ToString());
}
}
}
else
_logger.LogError("Java input file {filePath} doesn't exist!", inputFile.FullName);
}
private static void OutputFileOrPrint(string? fileName, string contents)
{
if (fileName == null)
{
Console.Out.WriteLine(contents);
}
else
{
File.WriteAllText(fileName, contents);
}
}
}