/* * Copyright (c) 2015 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.cst.functions.groovy.helper.extension; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nullable; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.Platform; import com.google.common.base.Splitter; import de.fhg.igd.eclipse.util.extension.ExtensionUtil; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.cst.functions.groovy.helper.Category; import eu.esdihumboldt.cst.functions.groovy.helper.ContextAwareHelperFunction; import eu.esdihumboldt.cst.functions.groovy.helper.HelperContext; import eu.esdihumboldt.cst.functions.groovy.helper.HelperFunction; import eu.esdihumboldt.cst.functions.groovy.helper.HelperFunctionOrCategory; import eu.esdihumboldt.cst.functions.groovy.helper.HelperFunctionsService; import eu.esdihumboldt.cst.functions.groovy.helper.spec.Specification; import eu.esdihumboldt.hale.common.align.model.Cell; import eu.esdihumboldt.hale.common.align.transformation.function.ExecutionContext; import eu.esdihumboldt.hale.common.core.service.ServiceProvider; /** * Groovy script helper functions extension point. * * @author Simon Templer */ public class HelperFunctionsExtension implements HelperFunctionsService { private static final String EXTENSION_ID = "eu.esdihumboldt.cst.functions.groovy.helper"; private static final ALogger log = ALoggerFactory.getLogger(HelperFunctionsExtension.class); private final Map<Category, Map<String, HelperFunctionOrCategory>> children = new HashMap<>(); private final AtomicBoolean initialized = new AtomicBoolean(); private static final String SPEC_END = "_spec"; private final ServiceProvider serviceProvider; private final HelperContext defaultContext = new HelperContext() { @Override public Cell getTypeCell() { return null; } @Override public ServiceProvider getServiceProvider() { return serviceProvider; } @Override public ExecutionContext getExecutionContext() { return null; } @Override public Cell getContextCell() { return null; } }; /** * Create a helper function extension instance. * * @param serviceProvider the service provider if available */ public HelperFunctionsExtension(@Nullable ServiceProvider serviceProvider) { super(); this.serviceProvider = serviceProvider; } /** * Initialize the extension point from the registered extensions (if not * already done). */ protected void init() { if (initialized.compareAndSet(false, true)) { synchronized (children) { IConfigurationElement[] elements = Platform.getExtensionRegistry() .getConfigurationElementsFor(EXTENSION_ID); for (IConfigurationElement element : elements) { if ("helper".equals(element.getName())) { String category = element.getAttribute("category"); if ("ROOT".equals(category)) { category = ""; } String customName = element.getAttribute("name"); Class<?> helperClass = ExtensionUtil.loadClass(element, "class"); try { Iterable<HelperFunctionWrapper> functions = loadFunctions(helperClass, customName); addToCategory(category, functions); } catch (Exception e) { log.error("Failed loading Groovy helper functions", e); } } } } } } /** * Add the given functions to the given category. * * @param category the category path * @param functions the functions to add to the category */ private void addToCategory(String category, Iterable<HelperFunctionWrapper> functions) { Iterable<String> path = Splitter.on('.').omitEmptyStrings().split(category); Category cat = new Category(path); Map<String, HelperFunctionOrCategory> catMap = children.get(cat); if (catMap == null) { catMap = new HashMap<>(); children.put(cat, catMap); } for (HelperFunctionWrapper function : functions) { Object previous = catMap.put(function.getName(), function); if (previous != null) { log.error(MessageFormat.format("Duplicate helper function {0}.{1}", cat, function.getName())); } } // make sure category is listed while (cat != null) { Category parent = cat.getParent(); Map<String, HelperFunctionOrCategory> parentMap = children.get(parent); if (parentMap == null) { parentMap = new HashMap<>(); children.put(parent, parentMap); } parentMap.put(cat.getName(), cat); // check parent category cat = parent; } } /** * Load helper functions from a class that defines them. * * @param helperClass the helper class, either a {@link HelperFunction} or a * class that defines helper functions by convention * @param customName the custom name for a helper function, only applicable * for {@link HelperFunction} classes * @return the functions that were loaded from the class * @throws Exception if loading the functions failed */ private Iterable<HelperFunctionWrapper> loadFunctions(final Class<?> helperClass, String customName) throws Exception { if (HelperFunction.class.isAssignableFrom(helperClass)) { // name must be defined if (customName == null) { throw new IllegalStateException( "Function name must be specified for HelperFunction implementations"); } // is already a helper function HelperFunction<?> function = (HelperFunction<?>) helperClass.newInstance(); return Collections.singleton(new HelperFunctionWrapper(function, customName)); } else { // determine functions via reflection List<HelperFunctionWrapper> functions = new ArrayList<>(); for (Method method : helperClass.getMethods()) { int modifiers = method.getModifiers(); if (method.getName().startsWith("_") && !method.getName().startsWith( "__") /* exclude __$swapInit and the like */ && !Modifier.isAbstract(modifiers) && !method.getName().endsWith(SPEC_END)) { HelperFunctionWrapper function = loadFunction(method, helperClass); if (function != null) { functions.add(function); } } } return functions; } } /** * Load helper function via reflection from a method. * * @param callMethod the method (probably) defining a helper function * @param helperClass the class defining the method * @return the loaded helper function or <code>null</code> */ @Nullable protected HelperFunctionWrapper loadFunction(final Method callMethod, Class<?> helperClass) { int modifiers = callMethod.getModifiers(); // a candidate -> check parameters Class<?>[] params = callMethod.getParameterTypes(); if (params != null && params.length <= 2) { // has maximum two parameters // last parameter may be context parameter final boolean hasContextParam = params.length >= 1 && params[params.length - 1].equals(HelperContext.class); // check if there is an actual main parameter final boolean hasMainParam = (hasContextParam && params.length == 2) || (!hasContextParam && params.length == 1); final boolean isStatic = Modifier.isStatic(modifiers); // Get the specification from field String specFieldOrMethodName = callMethod.getName() + SPEC_END; Object fieldV = null; try { Field field = helperClass.getField(specFieldOrMethodName); int fieldModifiers = field.getModifiers(); if (Modifier.isStatic(fieldModifiers) && Modifier.isFinal(fieldModifiers)) { fieldV = field.get(null); } } catch (Exception e) { // do nothing } final Object fieldValue = fieldV; // Get spec from method Method meth = null; boolean isSpecStatic = false; try { meth = helperClass.getMethod(specFieldOrMethodName, new Class[] { String.class }); int specModifier = meth.getModifiers(); isSpecStatic = Modifier.isStatic(specModifier); } catch (Exception e) { // do nothing } final Method specMethod = meth; final boolean isSpecMethodStatic = isSpecStatic; HelperFunction<Object> function = new ContextAwareHelperFunction<Object>() { @Override public Object call(Object arg, HelperContext context) throws Exception { Object helper = null; if (!isStatic) { helper = helperClass.newInstance(); } if (hasMainParam) { if (hasContextParam) { return callMethod.invoke(helper, arg, context); } else { return callMethod.invoke(helper, arg); } } else { if (hasContextParam) { return callMethod.invoke(helper, context); } else { return callMethod.invoke(helper); } } } @Override public Specification getSpec(String name) throws Exception { if (fieldValue != null && fieldValue instanceof Specification) { return ((Specification) fieldValue); } else if (specMethod != null) { if (isSpecMethodStatic) { return (Specification) specMethod.invoke(null, name); } else { Object helper = helperClass.newInstance(); return (Specification) specMethod.invoke(helper, name); } } return null; } }; // method name String name = callMethod.getName().substring(1); return new HelperFunctionWrapper(function, name); } return null; } @Override public Iterable<HelperFunctionOrCategory> getChildren(Category cat, HelperContext context) { init(); final HelperContext theContext = extendContext(context); synchronized (children) { final Map<String, HelperFunctionOrCategory> catMap = children.get(cat); if (catMap == null) { return Collections.emptyList(); } else { return () -> catMap.values().stream().map(fc -> injectContext(fc, theContext)) .iterator(); } } } @Override public HelperFunctionOrCategory get(Category cat, String name, HelperContext context) { init(); context = extendContext(context); synchronized (children) { Map<String, HelperFunctionOrCategory> catMap = children.get(cat); if (catMap == null) { return null; } else { return injectContext(catMap.get(name), context); } } } /** * Inject the helper context if applicable. * * @param helperFunctionOrCategory the helper function or category * @param context the helper context to inject * @return the adapted helper function or the unchanged category */ protected HelperFunctionOrCategory injectContext( HelperFunctionOrCategory helperFunctionOrCategory, HelperContext context) { if (context != null) { HelperFunction<?> function = helperFunctionOrCategory.asFunction(); if (function != null && function instanceof ContextAwareHelperFunction<?>) { return new HelperFunctionContextWrapper<>((ContextAwareHelperFunction<?>) function, helperFunctionOrCategory.getName(), context); } } return helperFunctionOrCategory; } /** * Extend the given helper context w/ additional information if possible. * * @param context the context to extend * @return the extended context information */ protected HelperContext extendContext(final HelperContext context) { if (context == null) { return defaultContext; } else if (serviceProvider != null && context.getServiceProvider() == null) { // extend w/ service provider return new HelperContext() { @Override public Cell getTypeCell() { return context.getTypeCell(); } @Override public ServiceProvider getServiceProvider() { return serviceProvider; } @Override public ExecutionContext getExecutionContext() { return context.getExecutionContext(); } @Override public Cell getContextCell() { return context.getContextCell(); } }; } else { return context; } } }