org.eclipse.scada.configuration.recipe.lib.internal.DefaultExecutableFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.scada.configuration.recipe.lib.internal.DefaultExecutableFactory.java

Source

/*******************************************************************************
 * Copyright (c) 2013 IBH SYSTEMS GmbH and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBH SYSTEMS GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.scada.configuration.recipe.lib.internal;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;

import javax.inject.Named;
import javax.inject.Singleton;

import org.apache.commons.beanutils.PropertyUtils;
import org.eclipse.emf.common.util.EList;
import org.eclipse.scada.configuration.recipe.CaptureOutput;
import org.eclipse.scada.configuration.recipe.Execute;
import org.eclipse.scada.configuration.recipe.InputReference;
import org.eclipse.scada.configuration.recipe.InputValue;
import org.eclipse.scada.configuration.recipe.MapInput;
import org.eclipse.scada.configuration.recipe.lib.Executable;
import org.eclipse.scada.configuration.recipe.lib.ExecutableFactory;
import org.eclipse.scada.configuration.recipe.lib.Output;
import org.eclipse.scada.utils.inject.InjectHelper;
import org.eclipse.scada.utils.inject.Optional;
import org.eclipse.scada.utils.reflect.Reflections;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefaultExecutableFactory implements ExecutableFactory {

    private final static Logger logger = LoggerFactory.getLogger(DefaultExecutableFactory.class);

    @Override
    public Executable create(final Execute execute, final RunnerContext ctx) {
        if (execute == null) {
            return null;
        }

        final String name = execute.getName();

        // lookup by URI

        Executable exec;

        exec = lookupByUri(name, execute, ctx);

        if (exec != null) {
            return exec;
        }

        throw new IllegalArgumentException(String.format("Executable '%s' is not registered", name));
    }

    private Executable lookupByUri(final String name, final Execute execute, final RunnerContext ctx) {
        logger.debug("Looking up by uri - name: {}, execute: {}", name, execute);

        try {
            final URI uri = new URI(name);
            if (!"bundle-class".equals(uri.getScheme())) {
                logger.debug("Wrong URI scheme: {}", uri.getScheme());
                return null;
            }

            final String host = uri.getHost();

            String clazzName = uri.getPath();
            if (clazzName.startsWith("/")) {
                // cut of first slash
                clazzName = clazzName.substring(1);
            }

            if (clazzName.startsWith(".")) {
                clazzName = host + clazzName;
            }

            final Class<?> clazz = findBundle(host, clazzName);

            if (Executable.class.isAssignableFrom(clazz)) {
                logger.debug("Return by Executable interface");
                if (clazz.isAnnotationPresent(Singleton.class)) {
                    if (!ctx.getSingletons().containsKey(clazz)) {
                        ctx.getSingletons().put(clazz, clazz.newInstance());
                    }
                    return (Executable) ctx.getSingletons().get(clazz);
                } else {
                    return (Executable) clazz.newInstance();
                }
            } else {
                String fragment = uri.getFragment();
                if (fragment == null) {
                    fragment = "execute";
                }

                logger.debug("Return by call wrapper: #{}", fragment);
                return createCallWrapper(name, clazz, fragment, execute, ctx);
            }
        } catch (final Exception e) {
            logger.info("Failed to lookup", e);
            return null;
        }
    }

    private Executable createCallWrapper(final String name, final Class<?> clazz, final String methodName,
            final Execute execute, final RunnerContext ctx) throws Exception {
        logger.debug("Creating call wrapper for class: {}, method: {}", clazz, methodName);

        final Object o;

        if (clazz.isAnnotationPresent(Singleton.class)) {
            if (!ctx.getSingletons().containsKey(clazz)) {
                ctx.getSingletons().put(clazz, clazz.newInstance());
            }
            o = ctx.getSingletons().get(clazz);
        } else {
            o = clazz.newInstance();
        }

        final Method m = gatherMethods(clazz, methodName);

        return new Executable() {

            @Override
            public void run(final RunnerContext ctx) {
                try {
                    final RunnerContext localContext = makeLocalContext(ctx, execute);
                    applyContext(o, localContext);
                    final Object[] args = makeArgs(m, localContext);
                    m.invoke(o, args);
                    captureOutput(o, ctx, execute.getOutput());
                } catch (final Throwable e) {
                    logger.info("Method call failed", e);
                    throw new RuntimeException(String.format("Failed to call method: %s", methodName), e);
                }
            }
        };
    }

    private Method gatherMethods(final Class<?> clazz, final String methodName) {
        final LinkedList<Method> methods = new LinkedList<>();
        for (final Method m : clazz.getMethods()) {
            if (m.getName().equals(methodName)) {
                methods.add(m);
            }
        }
        if (methods.size() == 1) {
            return methods.peek();
        } else if (methods.isEmpty()) {
            throw new IllegalStateException(
                    String.format("Method '%s' not found on class '%s'", methodName, clazz.getName()));
        } else {
            throw new IllegalStateException(String.format(
                    "Method '%s' of class '%s' is polymorphic. This is now allowed for the recipe target classes.",
                    methodName, clazz.getName()));
        }
    }

    protected void captureOutput(final Object o, final RunnerContext ctx, final EList<CaptureOutput> outputs)
            throws Exception {
        here: for (final CaptureOutput output : outputs) {
            final String name = output.getLocalName();
            final String storeName = output.getContextName() == null ? name : output.getContextName();

            logger.debug("Capture output: {} -> {}", name, storeName);

            final BeanInfo bi = Introspector.getBeanInfo(o.getClass());
            for (final PropertyDescriptor pd : bi.getPropertyDescriptors()) {
                if (pd.getName().equals(name)) {
                    final Object value = pd.getReadMethod().invoke(o);
                    ctx.getMap().put(storeName, value);
                    logger.debug("By BeanInfo - value: {}", value);
                    continue here;
                }
            }

            final Field field = Reflections.findField(o.getClass(), name);
            if (field != null && field.getAnnotation(Output.class) != null) {
                final Object value = InjectHelper.getField(o, field);
                ctx.getMap().put(storeName, value);
                logger.debug("By field - value: {}", value);
                continue;
            }
            if (field != null) {
                throw new IllegalStateException(String.format(
                        "Unable to capture output. Field '%s' of class '%s' is not marked with @Output and is not accessible using a getter."));
            }

            throw new IllegalStateException(
                    String.format("Unable to capture output '%s' of class '%s'", name, o.getClass()));
        }
    }

    protected static RunnerContext makeLocalContext(final RunnerContext ctx, final Execute execute) {
        final Map<String, Object> map = new HashMap<>(ctx.getMap());

        // apply all mappings (from context to local)

        for (final MapInput mapInput : execute.getMapInput()) {
            if (!map.containsKey(mapInput.getContextName())) {
                throw new IllegalStateException(String.format(
                        "Invalid input mapping - context entry '%s' does not exists", mapInput.getContextName()));
            }
            map.put(mapInput.getLocalName(), map.remove(mapInput.getContextName()));
        }

        // set all plain values

        for (final InputValue value : execute.getInputValue()) {
            map.put(value.getName(), value.getValue());
        }

        for (final InputReference ref : execute.getInputReference()) {
            map.put(ref.getName(), getReference(ctx.getMap(), ref.getExpression()));
        }

        final RunnerContext result = new RunnerContext(ctx.getProperties());
        result.getMap().putAll(map);
        return result;
    }

    private static Object getReference(final Map<String, Object> map, final String expression) {
        try {
            return PropertyUtils.getProperty(map, expression);
        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }

    protected Object[] makeArgs(final Method m, final RunnerContext localContext) {
        final Object[] args = new Object[m.getParameterTypes().length];

        for (int i = 0; i < m.getParameterTypes().length; i++) {
            final Annotation[] an = m.getParameterAnnotations()[i];

            final boolean optional = isOptional(an);

            final String name = makeName(an);
            if (name != null) {
                args[i] = localContext.getMap().get(name);
            } else {
                final Set<Object> values = new HashSet<>(localContext.getMap().values());
                values.add(localContext);

                final Class<?> type = m.getParameterTypes()[i];
                args[i] = findForType(values, type, i, m.getName(), m.getDeclaringClass(), optional);
            }
        }

        return args;
    }

    private boolean isOptional(final Annotation[] an) {
        for (final Annotation a : an) {
            if (a instanceof Optional) {
                return true;
            }
        }
        return false;
    }

    private Object findForType(final Set<Object> values, final Class<?> type, final int index,
            final String methodName, final Class<?> clazz, final boolean optional) {
        for (final Object value : values) {
            if (type.isAssignableFrom(value.getClass())) {
                return value;
            }
        }
        if (!optional) {
            throw new IllegalStateException(String.format(
                    "Unable to find context value for parameter #%s (type: %s) of method '%s' on class '%s'", index,
                    type.getName(), methodName, clazz.getName()));
        } else {
            return null;
        }
    }

    private String makeName(final Annotation[] an) {
        for (final Annotation a : an) {
            if (a instanceof Named) {
                return ((Named) a).value();
            }
        }
        return null;
    }

    protected static void applyContext(final Object o, final RunnerContext ctx) {
        final Collection<Field> fields = Reflections.findAllFields(o.getClass());
        InjectHelper.injectFields(o, fields, ctx.getMap(), Arrays.<Object>asList(ctx));
    }

    private Class<?> findBundle(final String symbolicName, final String clazzName) {
        logger.debug("Find bundle with class - symbolicName: {}, className: {}", symbolicName, clazzName);

        final BundleContext context = FrameworkUtil.getBundle(DefaultExecutableFactory.class).getBundleContext();

        for (final Bundle bundle : context.getBundles()) {
            if (!symbolicName.equals(bundle.getSymbolicName())) {
                continue;
            }

            logger.debug("Checking bundle: {}", bundle.getSymbolicName());

            Class<?> clazz;
            try {
                clazz = bundle.loadClass(clazzName);
            } catch (final ClassNotFoundException e) {
                logger.debug("Class could not be loaded", e);
                // we continue, since we might have multiple versions
                continue;
            }

            logger.debug("Success");
            return clazz;
        }

        return null;
    }
}