blob: 9d1feb614bb677208c43641f95f1fd6d013e0241 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2013, 2016 Atos and others.
* 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
* https://www.eclipse.org/legal/epl-2.0/
*
* Contributors:
* Arthur Daussy - initial implementation
*******************************************************************************/
package org.eclipse.ease;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.core.runtime.Platform;
import org.eclipse.ease.modules.IEnvironment;
import org.eclipse.ease.modules.ModuleHelper;
import org.eclipse.ease.modules.ScriptParameter;
import org.eclipse.ease.modules.WrapToScript;
public abstract class AbstractCodeFactory implements ICodeFactory {
/** Default line break character. */
public static final String LINE_DELIMITER = System.getProperty(Platform.PREF_LINE_SEPARATOR);
protected String buildParameterList(List<Parameter> parameters) {
// build parameter string
final StringBuilder parameterList = new StringBuilder();
for (final Parameter parameter : parameters)
parameterList.append(", ").append(toSafeName(parameter.getName()));
if (parameterList.length() > 2)
parameterList.delete(0, 2);
return parameterList.toString();
}
@Override
public String createKeywordHeader(Map<String, String> keywords, String existingHeader) {
final StringBuilder header = new StringBuilder();
// copy existing text before keywords
if (existingHeader == null)
existingHeader = "";
final String[] existingLines = existingHeader.split("\\r?\\n");
int index = 0;
for (; index < existingLines.length; index++) {
final Matcher matcher = AbstractCodeParser.PARAMETER_PATTERN.matcher(existingLines[index]);
if (!matcher.matches())
header.append(existingLines[index]).append(LINE_DELIMITER);
else
break;
}
// add line delimiter before keywords
if ((header.length() > 0) && (!keywords.isEmpty()))
header.append(LINE_DELIMITER);
// add keywords
for (final Entry<String, String> entry : keywords.entrySet()) {
header.append(entry.getKey());
header.append(new String(new char[Math.max(16 - entry.getKey().length(), 1)]).replace('\0', ' '));
header.append(": ");
header.append(entry.getValue()).append(LINE_DELIMITER);
}
// copy existing text after keywords
boolean isFirstLineAfterKeywords = true;
for (; index < existingLines.length; index++) {
final Matcher matcher = AbstractCodeParser.PARAMETER_PATTERN.matcher(existingLines[index]);
if (!matcher.matches()) {
if (isFirstLineAfterKeywords) {
header.append(LINE_DELIMITER);
isFirstLineAfterKeywords = false;
}
header.append(existingLines[index]).append(LINE_DELIMITER);
}
}
return header.toString();
}
@Override
public String getDefaultValue(final Parameter parameter) {
final String defaultStringValue = parameter.getDefaultValue().replaceAll("\\r", "\\\\r").replaceAll("\\n", "\\\\n");
final Class<?> clazz = parameter.getClazz();
// null as default value
if (ScriptParameter.NULL.equals(defaultStringValue))
return getNullString();
// base datatypes
if ((Integer.class.equals(clazz)) || (int.class.equals(clazz))) {
try {
return Integer.toString(Integer.parseInt(defaultStringValue));
} catch (final NumberFormatException e1) {
}
}
if ((Long.class.equals(clazz)) || (long.class.equals(clazz))) {
try {
return Long.toString(Long.parseLong(defaultStringValue));
} catch (final NumberFormatException e1) {
}
}
if ((Float.class.equals(clazz)) || (float.class.equals(clazz))) {
try {
return Float.toString(Float.parseFloat(defaultStringValue));
} catch (final NumberFormatException e1) {
}
}
if ((Double.class.equals(clazz)) || (double.class.equals(clazz))) {
try {
return Double.toString(Double.parseDouble(defaultStringValue));
} catch (final NumberFormatException e1) {
}
}
if ((Boolean.class.equals(clazz)) || (boolean.class.equals(clazz))) {
return Boolean.parseBoolean(defaultStringValue) ? getTrueString() : getFalseString();
}
if (String.class.equals(clazz))
return "\"" + defaultStringValue + "\"";
// undefined resolves to empty constructor
if (ScriptParameter.UNDEFINED.equals(defaultStringValue)) {
// look for empty constructor
try {
clazz.getConstructor();
// empty constructor found, return class
return classInstantiation(clazz, null);
} catch (final SecurityException e) {
} catch (final NoSuchMethodException e) {
}
}
// look for string constructor
try {
clazz.getConstructor(String.class);
// string constructor found, return class
return classInstantiation(clazz, new String[] { "\"" + defaultStringValue + "\"" });
} catch (final SecurityException e) {
} catch (final NoSuchMethodException e) {
}
// special handling for string defaults passed to an Object.class
if (clazz.isAssignableFrom(String.class))
return classInstantiation(String.class, new String[] { "\"" + defaultStringValue + "\"" });
return getNullString();
}
public static Collection<String> getMethodNames(final Method method) {
final Set<String> methodNames = new HashSet<>();
methodNames.add(method.getName());
methodNames.addAll(getMethodAliases(method));
return methodNames;
}
public static Collection<String> getMethodAliases(final Method method) {
final Set<String> methodAliases = new HashSet<>();
final WrapToScript wrapAnnotation = method.getAnnotation(WrapToScript.class);
if (wrapAnnotation != null) {
for (final String name : wrapAnnotation.alias().split(WrapToScript.DELIMITER))
if (!name.trim().isEmpty())
methodAliases.add(name.trim());
}
return methodAliases;
}
/**
* Get string representation for <code>null</code> in target language.
*
* @return <code>null</code> in target language.
*/
protected abstract String getNullString();
/**
* Get string representation for <code>true</code> in target language.
*
* @return <code>true</code> in target language.
*/
protected String getTrueString() {
return Boolean.TRUE.toString();
}
/**
* Get string representation for <code>false</code> in target language.
*
* @return <code>false</code> in target language.
*/
protected String getFalseString() {
return Boolean.FALSE.toString();
}
@Override
public String createFunctionCall(final Method method, final Object... parameters) {
final StringBuilder code = new StringBuilder();
code.append(method.getName()).append('(');
for (final Object parameter : parameters) {
if (parameter instanceof String)
code.append('"').append(((String) parameter).replace("\"", "\\\"").replace("\\", "\\\\")).append('"');
else if (parameter == null)
code.append(getNullString());
else if (parameter instanceof Boolean)
code.append(((Boolean) parameter) ? getTrueString() : getFalseString());
else
code.append(parameter.toString());
code.append(", ");
}
// remove last comma separator
if (parameters.length > 0)
code.delete(code.length() - 2, code.length());
code.append(");");
return code.toString();
}
/**
* As many languages use //, provide a default of // style comments.
*
* @return "// "
*/
protected String getSingleLineCommentToken() {
return "// ";
}
/**
* As many languages use /*, provide a default of /* style comments.
*
* @return "/*"
*/
protected String getMultiLineCommentStartToken() {
return "/*";
}
/**
* Default block comment end token.
*
* @return asterisk, dash
*/
protected String getMultiLineCommentEndToken() {
return "*/";
}
@Override
public String createCommentedString(String comment, boolean addBlockComment) {
if (addBlockComment) {
return getMultiLineCommentStartToken() + comment + getMultiLineCommentEndToken();
} else {
final String token = getSingleLineCommentToken();
final Stream<String> split = Pattern.compile("\r?\n").splitAsStream(comment);
return split.map(s -> token + s).collect(Collectors.joining(System.lineSeparator()));
}
}
@Override
public String createWrapper(IEnvironment environment, Object instance, String identifier, boolean customNamespace, IScriptEngine engine) {
if (!customNamespace) {
// put functions/fields to global namespace
final StringBuilder scriptCode = new StringBuilder();
// create wrappers for methods
for (final Method method : ModuleHelper.getMethods(instance.getClass())) {
if (isSupportedByLanguage(method)) {
final String code = createFunctionWrapper(environment, identifier, method);
if ((code != null) && !code.isEmpty()) {
scriptCode.append(code);
scriptCode.append('\n');
}
}
}
// create wrappers for final fields
for (final Field field : ModuleHelper.getFields(instance.getClass())) {
if (isSupportedByLanguage(field)) {
try {
final Object toBeInjected = field.get(instance);
// only wrap if field is not already declared
if (!engine.hasVariable(getSaveVariableName(field.getName()))) {
engine.setVariable(getSaveVariableName(field.getName()), toBeInjected);
} else {
// see if the defined variable equals the one we want to set
final Object existing = engine.getVariable(getSaveVariableName(field.getName()));
if (((existing != null) && (!existing.equals(toBeInjected))) || ((existing == null) && (toBeInjected != null))) {
Logger.trace(Activator.PLUGIN_ID, ICodeFactory.TRACE_MODULE_WRAPPER, "Skipped wrapping of field \"" + field.getName()
+ "\" (module \"" + instance.getClass().getName() + "\") as variable is already declared.");
}
}
} catch (final IllegalArgumentException | IllegalAccessException e) {
Logger.error(Activator.PLUGIN_ID, "Could not wrap field \"" + field.getName() + " \" of module \"" + instance.getClass() + "\".", e);
}
}
}
return scriptCode.toString();
} else
throw new RuntimeException("Object wrappers not supported by default wrapper");
}
/**
* Verify that a method can be wrapped for the current target language.
*
* @param method
* method to be queried
* @return <code>true</code> when method can be wrapped
*/
protected boolean isSupportedByLanguage(Method method) {
return isSupportedByLanguage(method.getAnnotation(WrapToScript.class));
}
/**
* Verify that a field can be wrapped for the current target language.
*
* @param field
* method to be queried
* @return <code>true</code> when field can be wrapped
*/
protected boolean isSupportedByLanguage(Field field) {
return isSupportedByLanguage(field.getAnnotation(WrapToScript.class));
}
/**
* Verify that an annotation indicates wrapping support for the current target language.
*
* @param annotation
* annotation to be queried
* @return <code>true</code> when annotation indicated wrapping support
*/
private boolean isSupportedByLanguage(WrapToScript annotation) {
if (annotation != null) {
final String languages = annotation.supportedLanguages().trim();
if (!languages.isEmpty()) {
final String[] supportedLanguages = languages.split(",");
if (supportedLanguages[0].startsWith("!")) {
// exclude pattern
for (final String language : supportedLanguages) {
if (language.substring(1).equals(getLanguageIdentifier()))
return false;
}
} else {
// include pattern
for (final String language : supportedLanguages) {
if (language.equals(getLanguageIdentifier()))
return true;
}
return false;
}
}
}
return true;
}
/**
* Get the language identifier for this code factory
*
* @return language identifier as defined in extension point scriptType.name
*/
protected abstract Object getLanguageIdentifier();
/**
* Create code for a wrapper function in the global namespace of the script engine.
*
* @param environment
* environment instance
* @param identifier
* function name to be used
* @param method
* method to refer to
* @return script code to be injected into the script engine
*/
protected abstract String createFunctionWrapper(IEnvironment environment, String identifier, Method method);
/**
* Convert a given name to a script language safe name. To make sure it is safe we check against a list of given keywords. We then append '_' until the
* value is save.
*
* @param name
* name to check
* @return safe name
*/
protected abstract String toSafeName(String name);
}