package cz.habarta.typescript.generator.compiler;
import cz.habarta.typescript.generator.Settings;
import cz.habarta.typescript.generator.util.Pair;
import cz.habarta.typescript.generator.util.Utils;
import java.util.*;
import java.util.regex.Pattern;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
/**
* Name table.
*/
public class SymbolTable {
private final Settings settings;
private final LinkedHashMap<Pair<Class<?>, String>, Symbol> symbols = new LinkedHashMap<>();
private final LinkedHashMap<String, Symbol> syntheticSymbols = new LinkedHashMap<>();
private CustomTypeNamingFunction customTypeNamingFunction;
public SymbolTable(Settings settings) {
this.settings = settings;
}
public Symbol getSymbol(Class<?> cls) {
return getSymbol(cls, null);
}
public Symbol getSymbol(Class<?> cls, String suffix) {
final String suffixString = suffix != null ? suffix : "";
final Pair<Class<?>, String> key = Pair.<Class<?>, String>of(cls, suffixString);
if (!symbols.containsKey(key)) {
symbols.put(key, new Symbol("$" + cls.getName().replace('.', '$') + suffixString + "$"));
}
return symbols.get(key);
}
public Symbol hasSymbol(Class<?> cls, String suffix) {
return symbols.get(Pair.<Class<?>, String>of(cls, suffix));
}
public Class<?> getSymbolClass(Symbol symbol) {
for (Map.Entry<Pair<Class<?>, String>, Symbol> entry : symbols.entrySet()) {
if (entry.getValue() == symbol) {
return entry.getKey().getValue1();
}
}
return null;
}
public Symbol getSyntheticSymbol(String name) {
if (!syntheticSymbols.containsKey(name)) {
syntheticSymbols.put(name, new Symbol(name));
}
return syntheticSymbols.get(name);
}
public Symbol getSyntheticSymbol(String name, String suffix) {
return getSyntheticSymbol(name + (suffix != null ? suffix : ""));
}
public Symbol addSuffixToSymbol(Symbol symbol, String suffix) {
// try symbols
for (Map.Entry<Pair<Class<?>, String>, Symbol> entry : symbols.entrySet()) {
if (entry.getValue() == symbol) {
return getSymbol(entry.getKey().getValue1(), entry.getKey().getValue2() + suffix);
}
}
// syntheticSymbols
return getSyntheticSymbol(symbol.getFullName() + suffix);
}
public void resolveSymbolNames() {
final Map<String, List<Class<?>>> names = new LinkedHashMap<>();
for (Map.Entry<Pair<Class<?>, String>, Symbol> entry : symbols.entrySet()) {
final Class<?> cls = entry.getKey().getValue1();
final String suffix = entry.getKey().getValue2();
final Symbol symbol = entry.getValue();
final String suffixString = suffix != null ? suffix : "";
final String fullName = getMappedFullName(cls) + suffixString;
symbol.setFullName(fullName);
if (!names.containsKey(fullName)) {
names.put(fullName, new ArrayList<Class<?>>());
}
names.get(fullName).add(cls);
}
reportConflicts(names);
}
private static void reportConflicts(Map<String, List<Class<?>>> names) {
boolean conflict = false;
for (Map.Entry<String, List<Class<?>>> entry : names.entrySet()) {
final String name = entry.getKey();
final List<Class<?>> classes = entry.getValue();
if (classes.size() > 1) {
System.out.println(String.format("Multiple classes are mapped to '%s' name. Conflicting classes: %s", name, classes));
conflict = true;
}
}
if (conflict) {
throw new NameConflictException("Multiple classes are mapped to the same name. You can use 'customTypeNaming' or 'customTypeNamingFunction' settings to resolve conflicts or exclude conflicting class if it was added accidentally.");
}
}
public String getMappedFullName(Class<?> cls) {
if (cls == null) {
return null;
}
final String customName = settings.customTypeNaming.get(cls.getName());
if (customName != null) {
return customName;
}
if (settings.customTypeNamingFunction != null) {
try {
final CustomTypeNamingFunction function = getCustomTypeNamingFunction();
final Object getNameResult = function.getName(cls.getName(), cls.getSimpleName());
if (getNameResult != null && !isUndefined(getNameResult)) {
return (String) getNameResult;
}
} catch (ScriptException e) {
throw new RuntimeException("Evaluating 'customTypeNamingFunction' failed.", e);
}
}
String simpleName = cls.getSimpleName();
if (settings.removeTypeNamePrefix != null && simpleName.startsWith(settings.removeTypeNamePrefix)) {
simpleName = simpleName.substring(settings.removeTypeNamePrefix.length(), simpleName.length());
}
if (settings.removeTypeNameSuffix != null && simpleName.endsWith(settings.removeTypeNameSuffix)) {
simpleName = simpleName.substring(0, simpleName.length() - settings.removeTypeNameSuffix.length());
}
if (settings.addTypeNamePrefix != null) {
simpleName = settings.addTypeNamePrefix + simpleName;
}
if (settings.addTypeNameSuffix != null) {
simpleName = simpleName + settings.addTypeNameSuffix;
}
if (settings.mapPackagesToNamespaces) {
final String classNameDotted = cls.getName().replace('$', '.');
final String[] parts = classNameDotted.split(Pattern.quote("."));
final List<String> safeParts = new ArrayList<>();
for (String part : Arrays.asList(parts).subList(0, parts.length - 1)) {
safeParts.add(isReservedWord(part) ? "_" + part : part);
}
safeParts.add(simpleName);
return Utils.join(safeParts, ".");
} else {
return simpleName;
}
}
// https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#221-reserved-words
private static final Set<String> Keywords = new LinkedHashSet<>(Arrays.asList(
"break", "case", "catch", "class",
"const", "continue", "debugger", "default",
"delete", "do", "else", "enum",
"export", "extends", "false", "finally",
"for", "function", "if", "import",
"in", "instanceof", "new", "null",
"return", "super", "switch", "this",
"throw", "true", "try", "typeof",
"var", "void", "while", "with",
"implements", "interface", "let", "package",
"private", "protected", "public", "static",
"yield"
));
private static boolean isReservedWord(String word) {
return Keywords.contains(word);
}
private static boolean isUndefined(Object variable) {
// Java 8
// return ScriptObjectMirror.isUndefined(variable);
// Hack for Java 7, it should match both:
// org.mozilla.javascript.Undefined (Java 7)
// jdk.nashorn.internal.runtime.Undefined (Java 8)
return variable != null && variable.getClass().getSimpleName().equals("Undefined");
}
private CustomTypeNamingFunction getCustomTypeNamingFunction() throws ScriptException {
if (customTypeNamingFunction == null) {
final String engineMimeType = "application/javascript";
final ScriptEngineManager manager = new ScriptEngineManager();
final ScriptEngine engine = manager.getEngineByMimeType(engineMimeType);
if (engine == null) {
System.out.println(String.format("Error: Script engine for '%s' MIME type not found. Available engines: %s", engineMimeType, manager.getEngineFactories().size()));
for (ScriptEngineFactory factory : manager.getEngineFactories()) {
System.out.println(String.format(" %s %s - MIME types: %s", factory.getEngineName(), factory.getEngineVersion(), factory.getMimeTypes()));
}
throw new RuntimeException("Cannot evaluate function specified using 'customTypeNamingFunction' parameter. See log for details.");
}
engine.eval("var getName = " + settings.customTypeNamingFunction);
final Invocable invocable = (Invocable) engine;
customTypeNamingFunction = invocable.getInterface(CustomTypeNamingFunction.class);
}
return customTypeNamingFunction;
}
public static interface CustomTypeNamingFunction {
public Object getName(String className, String classSimpleName);
}
public static class NameConflictException extends RuntimeException {
private static final long serialVersionUID = 1L;
public NameConflictException() {
}
public NameConflictException(String message) {
super(message);
}
public NameConflictException(String message, Throwable cause) {
super(message, cause);
}
public NameConflictException(Throwable cause) {
super(cause);
}
}
}