/*
 * Copyright 2017 eagle.jfaster.org.
 *

 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *

 */

package eagle.jfaster.org.util;

import com.google.common.base.Strings;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * 反射处理类
 *
 * Created by fangyanpeng1 on 2017/7/28.
 */
public class ReflectUtil {

    public static final String PARAM_CLASS_SPLIT = ",";

    public static final String METHOD_DESC_VOID = "%s()";

    public static final String METHOD_DESC_PARAM = "%s(%s)";

    private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];

    private static final ConcurrentMap<String, Class<?>> name2ClassCache = new ConcurrentHashMap<String, Class<?>>();

    private static final ConcurrentMap<Class<?>, String> class2NameCache = new ConcurrentHashMap<Class<?>, String>();

    private static final String[] PRIMITIVE_NAMES = new String[]{"boolean", "byte", "char", "double", "float", "int",
            "long", "short",
            "void"};

    private static final Class<?>[] PRIMITIVE_CLASSES = new Class[]{boolean.class, byte.class, char.class,
            double.class, float.class,
            int.class, long.class, short.class, Void.TYPE};

    private static final int PRIMITIVE_CLASS_NAME_MAX_LENGTH = 7;

    /**
     * 获取method方式的接口参数，以逗号分割，拼接clz列表。 如果没有参数，那么void表示
     *
     * @param method
     * @return
     */
    public static String getMethodParamDesc(Method method) {
        if (method.getParameterTypes() == null || method.getParameterTypes().length == 0) {
            return null;
        }

        StringBuilder builder = new StringBuilder();

        Class<?>[] clzs = method.getParameterTypes();

        for (Class<?> clz : clzs) {
            String className = getName(clz);
            builder.append(className).append(PARAM_CLASS_SPLIT);
        }

        return builder.substring(0, builder.length() - 1);
    }

    /**
     * 获取方法的标示 : method_name + "(" + paramDesc + ")"
     *
     * @param method
     * @return
     */
    public static String getMethodDesc(Method method) {
        String methodParamDesc = getMethodParamDesc(method);
        return getMethodDesc(method.getName(), methodParamDesc);
    }

    /**
     * 获取方法的标示 : method_name + "(" + paramDesc + ")"
     *
     * @param
     * @return
     */
    public static String getMethodDesc(String methodName, String paramDesc) {
        if (paramDesc == null) {
            return String.format(METHOD_DESC_VOID, methodName);
        } else {
            return String.format(METHOD_DESC_PARAM, methodName, paramDesc);
        }
    }

    public static Class<?>[] forNames(String classList) throws ClassNotFoundException {
        if (Strings.isNullOrEmpty(classList)) {
            return EMPTY_CLASS_ARRAY;
        }

        String[] classNames = classList.split(PARAM_CLASS_SPLIT);
        Class<?>[] classTypes = new Class<?>[classNames.length];
        for (int i = 0; i < classNames.length; i++) {
            String className = classNames[i];
            classTypes[i] = forName(className);
        }

        return classTypes;
    }

    public static Class<?> forName(String className) throws ClassNotFoundException {
        if (null == className || "".equals(className)) {
            return null;
        }

        Class<?> clz = name2ClassCache.get(className);

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

        clz = forNameWithoutCache(className);

        name2ClassCache.putIfAbsent(className, clz);

        return clz;
    }

    private static Class<?> forNameWithoutCache(String className) throws ClassNotFoundException {
        if (!className.endsWith("[]")) { // not array
            Class<?> clz = getPrimitiveClass(className);

            clz = (clz != null) ? clz : Class.forName(className, true, Thread.currentThread().getContextClassLoader());
            return clz;
        }

        int dimensionSiz = 0;

        while (className.endsWith("[]")) {
            dimensionSiz++;

            className = className.substring(0, className.length() - 2);
        }

        int[] dimensions = new int[dimensionSiz];

        Class<?> clz = getPrimitiveClass(className);

        if (clz == null) {
            clz = Class.forName(className, true, Thread.currentThread().getContextClassLoader());
        }

        return Array.newInstance(clz, dimensions).getClass();
    }

    /**
     * 需要支持一维数组、二维数组等
     *
     * @param
     * @return
     */
    public static String getName(Class<?> clz) {
        if (clz == null) {
            return null;
        }

        String className = class2NameCache.get(clz);

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

        className = getNameWithoutCache(clz);

        // 与name2ClassCache同样道理，如果没有恶心的代码，这块内存大小应该可控
        class2NameCache.putIfAbsent(clz, className);

        return className;
    }

    private static String getNameWithoutCache(Class<?> clz) {
        if (!clz.isArray()) {
            return clz.getName();
        }

        StringBuilder sb = new StringBuilder();
        while (clz.isArray()) {
            sb.append("[]");
            clz = clz.getComponentType();
        }

        return clz.getName() + sb.toString();
    }

    public static Class<?> getPrimitiveClass(String name) {
        // check if is primitive class
        if (name.length() <= PRIMITIVE_CLASS_NAME_MAX_LENGTH) {
            int index = Arrays.binarySearch(PRIMITIVE_NAMES, name);
            if (index >= 0) {
                return PRIMITIVE_CLASSES[index];
            }
        }
        return null;
    }

    /**
     * 获取clz public method
     *

     *

     *      1）不包含构造函数
     *      2）不包含Object.class
     *      3）包含该clz的父类的所有public方法
     *

     *
     * @param clz
     * @return
     */
    public static List<Method> getPublicMethod(Class<?> clz) {
        Method[] methods = clz.getMethods();
        List<Method> ret = new ArrayList<Method>();

        for (Method method : methods) {

            boolean isPublic = Modifier.isPublic(method.getModifiers());
            boolean isNotObjectClass = method.getDeclaringClass() != Object.class;

            if (isPublic && isNotObjectClass) {
                ret.add(method);
            }
        }

        return ret;
    }

    public static Object[] getParameterDefaultVals(Method method) {
        Class[] clzs = method.getParameterTypes();
        if (clzs == null || clzs.length == 0) {
            return new Object[0];
        }
        Object[] vals = new Object[clzs.length];
        for (int i = 0; i < clzs.length; ++i) {
            vals[i] = PrimitiveDefault.getDefaultValue(clzs[i]);
        }
        return vals;

    }

    public static Object getDefaultReturnValue(Class<?> returnType) {
        return PrimitiveDefault.getDefaultValue(returnType);
    }

    private static class PrimitiveDefault {
        private static boolean defaultBoolean;
        private static char defaultChar;
        private static byte defaultByte;
        private static short defaultShort;
        private static int defaultInt;
        private static long defaultLong;
        private static float defaultFloat;
        private static double defaultDouble;

        private static Map<Class<?>, Object> primitiveValues = new HashMap<Class<?>, Object>();

        static {
            primitiveValues.put(boolean.class, defaultBoolean);
            primitiveValues.put(char.class, defaultChar);
            primitiveValues.put(byte.class, defaultByte);
            primitiveValues.put(short.class, defaultShort);
            primitiveValues.put(int.class, defaultInt);
            primitiveValues.put(long.class, defaultLong);
            primitiveValues.put(float.class, defaultFloat);
            primitiveValues.put(double.class, defaultDouble);
        }

        public static Object getDefaultValue(Class<?> valType) {
            return primitiveValues.get(valType);
        }

    }

}


/*
 * Copyright 2017 eagle.jfaster.org.
 *

 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *

 */
package eagle.jfaster.org.util;

import eagle.jfaster.org.exception.EagleFrameException;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.regex.Pattern;

/**
 * Created by fangyanpeng on 2017/12/13.
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class IpUtil {
    /**
     * IP地址的正则表达式.
     */
    public static final String IP_REGEX = "((\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3})";

    private static final Pattern IP_PATTERN = Pattern.compile(IP_REGEX);

    private static volatile String cachedIpAddress;

    /**
     * 获取本机IP地址.
     *
     *

     * 有限获取外网IP地址.
     * 也有可能是链接着路由器的最终IP地址.
     *

     *
     * @return 本机IP地址
     */
    public static String getIp() {
        if (null != cachedIpAddress) {
            return cachedIpAddress;
        }
        Enumeration<NetworkInterface> netInterfaces;
        try {
            netInterfaces = NetworkInterface.getNetworkInterfaces();
        } catch (final SocketException ex) {
            throw new EagleFrameException(ex);
        }
        String localIpAddress = null;
        while (netInterfaces.hasMoreElements()) {
            NetworkInterface netInterface = netInterfaces.nextElement();
            Enumeration<InetAddress> ipAddresses = netInterface.getInetAddresses();
            while (ipAddresses.hasMoreElements()) {
                InetAddress ipAddress = ipAddresses.nextElement();
                if (isPublicIpAddress(ipAddress)) {
                    String publicIpAddress = ipAddress.getHostAddress();
                    cachedIpAddress = publicIpAddress;
                    return publicIpAddress;
                }
                if (isLocalIpAddress(ipAddress)) {
                    localIpAddress = ipAddress.getHostAddress();
                }
            }
        }
        cachedIpAddress = localIpAddress;
        return localIpAddress;
    }

    private static boolean isPublicIpAddress(final InetAddress ipAddress) {
        return !ipAddress.isSiteLocalAddress() && !ipAddress.isLoopbackAddress() && !isV6IpAddress(ipAddress);
    }

    private static boolean isLocalIpAddress(final InetAddress ipAddress) {
        return ipAddress.isSiteLocalAddress() && !ipAddress.isLoopbackAddress() && !isV6IpAddress(ipAddress);
    }

    private static boolean isV6IpAddress(final InetAddress ipAddress) {
        return ipAddress.getHostAddress().contains(":");
    }

    public static boolean isIllegalIp(String ip) {
        return IP_PATTERN.matcher(ip).matches();
    }

    /**
     * 获取本机Host名称.
     *
     * @return 本机Host名称
     */
    public static String getHostName() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (final UnknownHostException ex) {
            throw new EagleFrameException(ex);
        }
    }
}


/*
 * Copyright 2017 eagle.jfaster.org.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * </p>
 */

package eagle.jfaster.org.controller;

import com.google.common.base.Optional;

import eagle.jfaster.org.pojo.RegistryCenterConfiguration;
import eagle.jfaster.org.factory.RegistryCenterFactory;
import eagle.jfaster.org.service.RegistryCenterConfigurationService;
import eagle.jfaster.org.service.impl.RegistryCenterConfigurationServiceImpl;
import eagle.jfaster.org.util.SessionRegistryCenterConfiguration;

import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import java.util.Collection;

/**
 * 注册中心配置的RESTful API.
 *
 * @author fangyanpeng
 */
@RestController
@RequestMapping("/registry-center")
public final class RegistryCenterRestfulApi {

    public static final String REG_CENTER_CONFIG_KEY = "reg_center_config_key";

    private RegistryCenterConfigurationService regCenterService = new RegistryCenterConfigurationServiceImpl();

    /**
     * 判断是否存在已连接的注册中心配置.
     *
     * @param request HTTP请求
     * @return 是否存在已连接的注册中心配置
     */
    @RequestMapping(value = "/activated", method = RequestMethod.GET)
    public boolean activated(final HttpServletRequest request) {
        return regCenterService.loadActivated().isPresent();
    }

    /**
     * 读取注册中心配置集合.
     *
     * @param request HTTP请求
     * @return 注册中心配置集合
     */
    @RequestMapping(value = "/load", method = RequestMethod.GET)
    public Collection<RegistryCenterConfiguration> load(final HttpServletRequest request) {
        Optional<RegistryCenterConfiguration> regCenterConfig = regCenterService.loadActivated();
        if (regCenterConfig.isPresent()) {
            setRegistryCenterNameToSession(regCenterConfig.get(), request.getSession());
        }
        return regCenterService.loadAll().getRegistryCenterConfiguration();
    }

    /**
     * 添加注册中心.
     *
     * @param config 注册中心配置
     * @return 是否添加成功
     */
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public boolean add(final @RequestBody RegistryCenterConfiguration config) {
        return regCenterService.add(config);
    }

    /**
     * 删除注册中心.
     *
     * @param config 注册中心配置
     */
    @RequestMapping(value = "/delete", method = RequestMethod.DELETE)
    public boolean delete(final @RequestBody RegistryCenterConfiguration config) {
        regCenterService.delete(config.getName());
        return true;
    }

    @RequestMapping(value = "/connect", method = RequestMethod.POST)
    public boolean connect(final @RequestBody RegistryCenterConfiguration config, final HttpServletRequest request) {
        boolean isConnected = setRegistryCenterNameToSession(regCenterService.find(config.getName(), regCenterService.loadAll()), request.getSession());
        if (isConnected) {
            regCenterService.load(config.getName());
        }
        return isConnected;
    }

    private boolean setRegistryCenterNameToSession(final RegistryCenterConfiguration regCenterConfig, final HttpSession session) {
        session.setAttribute(REG_CENTER_CONFIG_KEY, regCenterConfig);
        try {
            RegistryCenterFactory.createCoordinatorRegistryCenter(regCenterConfig.getZkAddressList(), regCenterConfig.getNamespace(), Optional.fromNullable(regCenterConfig.getDigest()));
            SessionRegistryCenterConfiguration.setRegistryCenterConfiguration((RegistryCenterConfiguration) session.getAttribute(REG_CENTER_CONFIG_KEY));
        } catch (final Exception ex) {
            return false;
        }
        return true;
    }
}


/*
 * Copyright 2017 eagle.jfaster.org.
 *

 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *

 */

package eagle.jfaster.org;

import org.apache.curator.test.TestingServer;

import java.io.File;
import java.io.IOException;

/**
 * 内存版的内嵌Zookeeper.
 *
 *

 *     仅用于运行例子时无需额外启动Zookeeper. 如有必要, 请使用本地环境可用的Zookeeper代替.
 *

 */
public final class EmbedZookeeperServer {

    private static TestingServer testingServer;

    /**
     * 内存版的内嵌Zookeeper.
     *
     * @param port Zookeeper的通信端口号
     */
    public static void start(final int port) {
        try {
            testingServer = new TestingServer(port, new File(String.format("target/test_zk_data/%s/", System.nanoTime())));
        } catch (final Exception ex) {
            ex.printStackTrace();
        } finally {
            Runtime.getRuntime().addShutdownHook(new Thread() {

                @Override
                public void run() {
                    try {
                        Thread.sleep(1000L);
                        testingServer.close();
                    } catch (final InterruptedException | IOException ex) {
                    }
                }
            });
        }
    }
}


/*
 * Copyright 2017 eagle.jfaster.org.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * </p>
 */
package eagle.jfaster.org.bean.trace;

import com.google.common.base.Strings;

import eagle.jfaster.org.exception.EagleFrameException;
import eagle.jfaster.org.logging.InternalLogger;
import eagle.jfaster.org.logging.InternalLoggerFactory;
import eagle.jfaster.org.rpc.support.OpaqueGenerator;
import eagle.jfaster.org.rpc.support.TraceContext;

import org.aopalliance.aop.Advice;
import org.springframework.aop.framework.*;
import org.springframework.aop.support.AopUtils;
import org.springframework.cglib.core.CodeGenerationException;
import org.springframework.cglib.core.SpringNamingPolicy;
import org.springframework.cglib.transform.impl.UndeclaredThrowableStrategy;
import org.springframework.core.SmartClassLoader;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import org.springframework.aop.Advisor;
import org.springframework.aop.AopInvocationException;
import org.springframework.aop.PointcutAdvisor;
import org.springframework.aop.RawTargetAccess;
import org.springframework.aop.TargetSource;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.CallbackFilter;
import org.springframework.cglib.proxy.Dispatcher;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.cglib.proxy.NoOp;

/**
 * Created by fangyanpeng on 2017/12/16.
 */

public class EagleTraceCglibProxy implements AopProxy, Serializable {

    // Constants for CGLIB callback array indices
    private static final int AOP_PROXY = 0;
    private static final int INVOKE_TARGET = 1;
    private static final int NO_OVERRIDE = 2;
    private static final int DISPATCH_TARGET = 3;
    private static final int DISPATCH_ADVISED = 4;
    private static final int INVOKE_EQUALS = 5;
    private static final int INVOKE_HASHCODE = 6;

    protected final static InternalLogger logger = InternalLoggerFactory.getInstance(EagleTraceCglibProxy.class);

    private static final Map<Class<?>, Boolean> validatedClasses = new WeakHashMap<Class<?>, Boolean>();

    protected final AdvisedSupport advised;

    private Object[] constructorArgs;

    private Class<?>[] constructorArgTypes;

    private final transient EagleTraceCglibProxy.AdvisedDispatcher advisedDispatcher;

    private transient Map<String, Integer> fixedInterceptorMap;

    private transient int fixedInterceptorOffset;

    public EagleTraceCglibProxy(AdvisedSupport config) throws AopConfigException {
        Assert.notNull(config, "AdvisedSupport must not be null");
        if (config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
            throw new AopConfigException("No advisors and no TargetSource specified");
        }
        this.advised = config;
        this.advisedDispatcher = new EagleTraceCglibProxy.AdvisedDispatcher(this.advised);
    }

    public void setConstructorArguments(Object[] constructorArgs, Class<?>[] constructorArgTypes) {
        if (constructorArgs == null || constructorArgTypes == null) {
            throw new IllegalArgumentException("Both 'constructorArgs' and 'constructorArgTypes' need to be specified");
        }
        if (constructorArgs.length != constructorArgTypes.length) {
            throw new IllegalArgumentException("Number of 'constructorArgs' (" + constructorArgs.length +
                    ") must match number of 'constructorArgTypes' (" + constructorArgTypes.length + ")");
        }
        this.constructorArgs = constructorArgs;
        this.constructorArgTypes = constructorArgTypes;
    }

    @Override
    public Object getProxy() {
        return getProxy(null);
    }

    @Override
    public Object getProxy(ClassLoader classLoader) {
        if (logger.isDebugEnabled()) {
            logger.debug("Creating CGLIB proxy: target source is " + this.advised.getTargetSource());
        }

        try {
            Class<?> rootClass = this.advised.getTargetClass();
            Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

            Class<?> proxySuperClass = rootClass;
            if (ClassUtils.isCglibProxyClass(rootClass)) {
                proxySuperClass = rootClass.getSuperclass();
                Class<?>[] additionalInterfaces = rootClass.getInterfaces();
                for (Class<?> additionalInterface : additionalInterfaces) {
                    this.advised.addInterface(additionalInterface);
                }
            }

            // Validate the class, writing log messages as necessary.
            validateClassIfNecessary(proxySuperClass, classLoader);

            // Configure CGLIB Enhancer...
            Enhancer enhancer = createEnhancer();
            if (classLoader != null) {
                enhancer.setClassLoader(classLoader);
                if (classLoader instanceof SmartClassLoader &&
                        ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
                    enhancer.setUseCache(false);
                }
            }
            enhancer.setSuperclass(proxySuperClass);
            enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
            enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
            enhancer.setStrategy(new UndeclaredThrowableStrategy(UndeclaredThrowableException.class));

            Callback[] callbacks = getCallbacks();
            Class<?>[] types = new Class<?>[callbacks.length];
            for (int x = 0; x < types.length; x++) {
                types[x] = callbacks[x].getClass();
            }
            // fixedInterceptorMap only populated at this point, after getCallbacks call above
            enhancer.setCallbackFilter(new EagleTraceCglibProxy.ProxyCallbackFilter(this.advised, this.fixedInterceptorMap, this.fixedInterceptorOffset));
            enhancer.setCallbackTypes(types);

            // Generate the proxy class and create a proxy instance.
            return createProxyClassAndInstance(enhancer, callbacks);
        } catch (CodeGenerationException ex) {
            throw new AopConfigException("Could not generate CGLIB subclass of class [" +
                    this.advised.getTargetClass() + "]: " +
                    "Common causes of this problem include using a final class or a non-visible class",
                    ex);
        } catch (IllegalArgumentException ex) {
            throw new AopConfigException("Could not generate CGLIB subclass of class [" +
                    this.advised.getTargetClass() + "]: " +
                    "Common causes of this problem include using a final class or a non-visible class",
                    ex);
        } catch (Exception ex) {
            // TargetSource.getTarget() failed
            throw new AopConfigException("Unexpected AOP exception", ex);
        }
    }

    protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
        enhancer.setInterceptDuringConstruction(false);
        enhancer.setCallbacks(callbacks);
        return (this.constructorArgs != null ?
                enhancer.create(this.constructorArgTypes, this.constructorArgs) :
                enhancer.create());
    }

    protected Enhancer createEnhancer() {
        return new Enhancer();
    }

    private void validateClassIfNecessary(Class<?> proxySuperClass, ClassLoader proxyClassLoader) {
        if (logger.isInfoEnabled()) {
            synchronized (validatedClasses) {
                if (!validatedClasses.containsKey(proxySuperClass)) {
                    doValidateClass(proxySuperClass, proxyClassLoader);
                    validatedClasses.put(proxySuperClass, Boolean.TRUE);
                }
            }
        }
    }

    private void doValidateClass(Class<?> proxySuperClass, ClassLoader proxyClassLoader) {
        if (!Object.class.equals(proxySuperClass)) {
            Method[] methods = proxySuperClass.getDeclaredMethods();
            for (Method method : methods) {
                int mod = method.getModifiers();
                if (!Modifier.isStatic(mod)) {
                    if (Modifier.isFinal(mod)) {
                        logger.info("Unable to proxy method [" + method + "] because it is final: " +
                                "All calls to this method via a proxy will NOT be routed to the target instance.");
                    } else if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod) && !Modifier.isPrivate(mod) &&
                            proxyClassLoader != null && proxySuperClass.getClassLoader() != proxyClassLoader) {
                        logger.info("Unable to proxy method [" + method + "] because it is package-visible " +
                                "across different ClassLoaders: All calls to this method via a proxy will " +
                                "NOT be routed to the target instance.");
                    }
                }
            }
            doValidateClass(proxySuperClass.getSuperclass(), proxyClassLoader);
        }
    }

    private Callback[] getCallbacks() throws Exception {

        // Choose an "aop" interceptor (used for AOP calls).
        Callback aopInterceptor = new EagleTraceCglibProxy.DynamicAdvisedInterceptor(this.advised);

        // Choose a "straight to target" interceptor. (used for calls that are
        // unadvised but can return this). May be required to expose the proxy.
        Callback targetInterceptor = new EagleTraceCglibProxy.DynamicUnadvisedInterceptor(this.advised.getTargetSource());

        // Choose a "direct to target" dispatcher (used for
        // unadvised calls to static targets that cannot return this).
        Callback targetDispatcher = new EagleTraceCglibProxy.SerializableNoOp();

        Callback[] callbacks = new Callback[]{
                aopInterceptor, // for normal advice
                targetInterceptor, // invoke target without considering advice, if optimized
                new EagleTraceCglibProxy.SerializableNoOp(), // no override for methods mapped to this
                targetDispatcher, this.advisedDispatcher,
                new EagleTraceCglibProxy.EqualsInterceptor(this.advised),
                new EagleTraceCglibProxy.HashCodeInterceptor(this.advised)
        };

        return callbacks;
    }

    /**
     * Process a return value. Wraps a return of {@code this} if necessary to be the
     * {@code proxy} and also verifies that {@code null} is not returned as a primitive.
     */
    private static Object processReturnType(Object proxy, Object target, Method method, Object retVal) {
        // Massage return value if necessary
        if (retVal != null && retVal == target && !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
            // Special case: it returned "this". Note that we can't help
            // if the target sets a reference to itself in another returned object.
            retVal = proxy;
        }
        Class<?> returnType = method.getReturnType();
        if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
            throw new AopInvocationException(
                    "Null return value from advice does not match primitive return type for: " + method);
        }
        return retVal;
    }

    @Override
    public boolean equals(Object other) {
        return (this == other || (other instanceof EagleTraceCglibProxy &&
                AopProxyUtils.equalsInProxy(this.advised, ((EagleTraceCglibProxy) other).advised)));
    }

    @Override
    public int hashCode() {
        return EagleTraceCglibProxy.class.hashCode() * 13 + this.advised.getTargetSource().hashCode();
    }

    /**
     * Serializable replacement for CGLIB's NoOp interface.
     * Public to allow use elsewhere in the framework.
     */
    public static class SerializableNoOp implements NoOp, Serializable {
    }

    /**
     * Interceptor used to invoke a dynamic target without creating a method
     * invocation or evaluating an advice chain. (We know there was no advice
     * for this method.)
     */
    private static class DynamicUnadvisedInterceptor implements MethodInterceptor, Serializable {

        private final TargetSource targetSource;

        public DynamicUnadvisedInterceptor(TargetSource targetSource) {
            this.targetSource = targetSource;
        }

        @Override
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            Object target = this.targetSource.getTarget();
            try {
                Object retVal = methodProxy.invoke(target, args);
                return processReturnType(proxy, target, method, retVal);
            } finally {
                this.targetSource.releaseTarget(target);
            }
        }
    }

    /**
     * Dispatcher for any methods declared on the Advised class.
     */
    private static class AdvisedDispatcher implements Dispatcher, Serializable {

        private final AdvisedSupport advised;

        public AdvisedDispatcher(AdvisedSupport advised) {
            this.advised = advised;
        }

        @Override
        public Object loadObject() throws Exception {
            return this.advised;
        }
    }

    /**
     * Dispatcher for the {@code equals} method.
     * Ensures that the method call is always handled by this class.
     */
    private static class EqualsInterceptor implements MethodInterceptor, Serializable {

        private final AdvisedSupport advised;

        public EqualsInterceptor(AdvisedSupport advised) {
            this.advised = advised;
        }

        @Override
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) {
            Object other = args[0];
            if (proxy == other) {
                return true;
            }
            if (other instanceof Factory) {
                Callback callback = ((Factory) other).getCallback(INVOKE_EQUALS);
                if (!(callback instanceof EagleTraceCglibProxy.EqualsInterceptor)) {
                    return false;
                }
                AdvisedSupport otherAdvised = ((EagleTraceCglibProxy.EqualsInterceptor) callback).advised;
                return AopProxyUtils.equalsInProxy(this.advised, otherAdvised);
            } else {
                return false;
            }
        }
    }

    /**
     * Dispatcher for the {@code hashCode} method.
     * Ensures that the method call is always handled by this class.
     */
    private static class HashCodeInterceptor implements MethodInterceptor, Serializable {

        private final AdvisedSupport advised;

        public HashCodeInterceptor(AdvisedSupport advised) {
            this.advised = advised;
        }

        @Override
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) {
            return EagleTraceCglibProxy.class.hashCode() * 13 + this.advised.getTargetSource().hashCode();
        }
    }

    /**
     * General purpose AOP callback. Used when the target is dynamic or when the
     * proxy is not frozen.
     */
    private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {

        private final AdvisedSupport advised;

        public DynamicAdvisedInterceptor(AdvisedSupport advised) {
            this.advised = advised;
        }

        @Override
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            Class<?> targetClass = null;
            Object target = null;
            try {
                // May be null. Get as late as possible to minimize the time we
                // "own" the target, in case it comes from a pool...
                target = getTarget();
                if (target != null) {
                    targetClass = target.getClass();
                }
                Object retVal;
                if (EagleTraceMethodRecods.needTrace(method, targetClass)) {
                    boolean clear = Strings.isNullOrEmpty(TraceContext.getTraceId());
                    try {
                        if (clear) {
                            TraceContext.setTraceId(OpaqueGenerator.getDistributeOpaque());
                        }
                        retVal = methodProxy.invoke(target, args);
                    } catch (Throwable e) {
                        logger.error("Eagle trace error: ", e);
                        throw new EagleFrameException(e);
                    } finally {
                        if (clear) {
                            TraceContext.clear();
                        }
                    }
                } else {
                    retVal = methodProxy.invoke(target, args);
                }
                retVal = processReturnType(proxy, target, method, retVal);
                return retVal;
            } finally {
                if (target != null) {
                    releaseTarget(target);
                }
            }
        }

        @Override
        public boolean equals(Object other) {
            return (this == other ||
                    (other instanceof EagleTraceCglibProxy.DynamicAdvisedInterceptor &&
                            this.advised.equals(((EagleTraceCglibProxy.DynamicAdvisedInterceptor) other).advised)));
        }

        /**
         * CGLIB uses this to drive proxy creation.
         */
        @Override
        public int hashCode() {
            return this.advised.hashCode();
        }

        protected Object getTarget() throws Exception {
            return this.advised.getTargetSource().getTarget();
        }

        protected void releaseTarget(Object target) throws Exception {
            this.advised.getTargetSource().releaseTarget(target);
        }
    }

    private static class ProxyCallbackFilter implements CallbackFilter {

        private final AdvisedSupport advised;

        private final Map<String, Integer> fixedInterceptorMap;

        private final int fixedInterceptorOffset;

        public ProxyCallbackFilter(AdvisedSupport advised, Map<String, Integer> fixedInterceptorMap, int fixedInterceptorOffset) {
            this.advised = advised;
            this.fixedInterceptorMap = fixedInterceptorMap;
            this.fixedInterceptorOffset = fixedInterceptorOffset;
        }

        @Override
        public int accept(Method method) {
            if (AopUtils.isFinalizeMethod(method)) {
                logger.debug("Found finalize() method - using NO_OVERRIDE");
                return NO_OVERRIDE;
            }
            if (!this.advised.isOpaque() && method.getDeclaringClass().isInterface() &&
                    method.getDeclaringClass().isAssignableFrom(Advised.class)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Method is declared on Advised interface: " + method);
                }
                return DISPATCH_ADVISED;
            }
            // We must always proxy equals, to direct calls to this.
            if (AopUtils.isEqualsMethod(method)) {
                logger.debug("Found 'equals' method: " + method);
                return INVOKE_EQUALS;
            }
            // We must always calculate hashCode based on the proxy.
            if (AopUtils.isHashCodeMethod(method)) {
                logger.debug("Found 'hashCode' method: " + method);
                return INVOKE_HASHCODE;
            }
            Class<?> targetClass = this.advised.getTargetClass();
            // Proxy is not yet available, but that shouldn't matter.
            List<?> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
            boolean haveAdvice = !chain.isEmpty();
            boolean exposeProxy = this.advised.isExposeProxy();
            boolean isStatic = this.advised.getTargetSource().isStatic();
            boolean isFrozen = this.advised.isFrozen();
            if (haveAdvice || !isFrozen) {
                // If exposing the proxy, then AOP_PROXY must be used.
                if (exposeProxy) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Must expose proxy on advised method: " + method);
                    }
                    return AOP_PROXY;
                }
                String key = method.toString();
                // Check to see if we have fixed interceptor to serve this method.
                // Else use the AOP_PROXY.
                if (isStatic && isFrozen && this.fixedInterceptorMap.containsKey(key)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Method has advice and optimisations are enabled: " + method);
                    }
                    // We know that we are optimising so we can use the
                    // FixedStaticChainInterceptors.
                    int index = this.fixedInterceptorMap.get(key);
                    return (index + this.fixedInterceptorOffset);
                } else {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Unable to apply any optimisations to advised method: " + method);
                    }
                    return AOP_PROXY;
                }
            } else {
                // See if the return type of the method is outside the class hierarchy
                // of the target type. If so we know it never needs to have return type
                // massage and can use a dispatcher.
                // If the proxy is being exposed, then must use the interceptor the
                // correct one is already configured. If the target is not static, then
                // cannot use a dispatcher because the target cannot be released.
                if (exposeProxy || !isStatic) {
                    return INVOKE_TARGET;
                }
                Class<?> returnType = method.getReturnType();
                if (targetClass == returnType) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Method " + method +
                                "has return type same as target type (may return this) - using INVOKE_TARGET");
                    }
                    return INVOKE_TARGET;
                } else if (returnType.isPrimitive() || !returnType.isAssignableFrom(targetClass)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Method " + method +
                                " has return type that ensures this cannot be returned- using DISPATCH_TARGET");
                    }
                    return DISPATCH_TARGET;
                } else {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Method " + method +
                                "has return type that is assignable from the target type (may return this) - " +
                                "using INVOKE_TARGET");
                    }
                    return INVOKE_TARGET;
                }
            }
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            if (!(other instanceof EagleTraceCglibProxy.ProxyCallbackFilter)) {
                return false;
            }
            EagleTraceCglibProxy.ProxyCallbackFilter otherCallbackFilter = (EagleTraceCglibProxy.ProxyCallbackFilter) other;
            AdvisedSupport otherAdvised = otherCallbackFilter.advised;
            if (this.advised == null || otherAdvised == null) {
                return false;
            }
            if (this.advised.isFrozen() != otherAdvised.isFrozen()) {
                return false;
            }
            if (this.advised.isExposeProxy() != otherAdvised.isExposeProxy()) {
                return false;
            }
            if (this.advised.getTargetSource().isStatic() != otherAdvised.getTargetSource().isStatic()) {
                return false;
            }
            if (!AopProxyUtils.equalsProxiedInterfaces(this.advised, otherAdvised)) {
                return false;
            }
            // Advice instance identity is unimportant to the proxy class:
            // All that matters is type and ordering.
            Advisor[] thisAdvisors = this.advised.getAdvisors();
            Advisor[] thatAdvisors = otherAdvised.getAdvisors();
            if (thisAdvisors.length != thatAdvisors.length) {
                return false;
            }
            for (int i = 0; i < thisAdvisors.length; i++) {
                Advisor thisAdvisor = thisAdvisors[i];
                Advisor thatAdvisor = thatAdvisors[i];
                if (!equalsAdviceClasses(thisAdvisor, thatAdvisor)) {
                    return false;
                }
                if (!equalsPointcuts(thisAdvisor, thatAdvisor)) {
                    return false;
                }
            }
            return true;
        }

        private boolean equalsAdviceClasses(Advisor a, Advisor b) {
            Advice aa = a.getAdvice();
            Advice ba = b.getAdvice();
            if (aa == null || ba == null) {
                return (aa == ba);
            }
            return aa.getClass().equals(ba.getClass());
        }

        private boolean equalsPointcuts(Advisor a, Advisor b) {
            // If only one of the advisor (but not both) is PointcutAdvisor, then it is a mismatch.
            // Takes care of the situations where an IntroductionAdvisor is used (see SPR-3959).
            return (!(a instanceof PointcutAdvisor) ||
                    (b instanceof PointcutAdvisor &&
                            ObjectUtils.nullSafeEquals(((PointcutAdvisor) a).getPointcut(), ((PointcutAdvisor) b).getPointcut())));
        }

        @Override
        public int hashCode() {
            int hashCode = 0;
            Advisor[] advisors = this.advised.getAdvisors();
            for (Advisor advisor : advisors) {
                Advice advice = advisor.getAdvice();
                if (advice != null) {
                    hashCode = 13 * hashCode + advice.getClass().hashCode();
                }
            }
            hashCode = 13 * hashCode + (this.advised.isFrozen() ? 1 : 0);
            hashCode = 13 * hashCode + (this.advised.isExposeProxy() ? 1 : 0);
            hashCode = 13 * hashCode + (this.advised.isOptimize() ? 1 : 0);
            hashCode = 13 * hashCode + (this.advised.isOpaque() ? 1 : 0);
            return hashCode;
        }
    }

}


/*
 * Copyright 2017 eagle.jfaster.org.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * </p>
 */

package eagle.jfaster.org.client;

import static eagle.jfaster.org.constant.EagleConstants.ASYNC_TIMEOUT_TIMER_PERIOD;
import static eagle.jfaster.org.constant.EagleConstants.GRUOUP_WORKER_THREAD;
import static eagle.jfaster.org.constant.EagleConstants.NETTY_TIMEOUT_TIMER_PERIOD;
import static eagle.jfaster.org.constant.EagleConstants.SOCKET_RCVBUF_SIZE;
import static eagle.jfaster.org.constant.EagleConstants.SOCKET_SNDBUF_SIZE;
import static eagle.jfaster.org.util.InterceptorUtil.onAfter;
import static eagle.jfaster.org.util.InterceptorUtil.onBefore;
import static eagle.jfaster.org.util.InterceptorUtil.onError;

import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

import eagle.jfaster.org.client.channel.AbstractNettyChannel;
import eagle.jfaster.org.client.channel.AsyncNettyChannel;
import eagle.jfaster.org.client.channel.SyncNettyChannel;
import eagle.jfaster.org.client.handler.AsyncMessageHandler;
import eagle.jfaster.org.client.handler.SyncMessageHandler;
import eagle.jfaster.org.client.pool.NettySharedConnPool;
import eagle.jfaster.org.codec.Codec;
import eagle.jfaster.org.codec.Serialization;
import eagle.jfaster.org.coder.NettyDecorder;
import eagle.jfaster.org.coder.NettyEncoder;
import eagle.jfaster.org.config.ConfigEnum;
import eagle.jfaster.org.config.common.MergeConfig;
import eagle.jfaster.org.exception.EagleFrameException;
import eagle.jfaster.org.interceptor.ExecutionInterceptor;
import eagle.jfaster.org.interceptor.context.CurrentExecutionContext;
import eagle.jfaster.org.logging.InternalLogger;
import eagle.jfaster.org.logging.InternalLoggerFactory;
import eagle.jfaster.org.rpc.MethodInvokeCallBack;
import eagle.jfaster.org.rpc.Request;
import eagle.jfaster.org.rpc.ResponseFuture;
import eagle.jfaster.org.spi.SpiClassLoader;
import eagle.jfaster.org.statistic.EagleStatsManager;
import eagle.jfaster.org.statistic.StatisticCallback;
import eagle.jfaster.org.task.AsyncCallbackMonitor;
import eagle.jfaster.org.task.AsyncCallbackTask;
import eagle.jfaster.org.task.HeartBeatTask;
import eagle.jfaster.org.task.TimeoutMonitorTask;
import eagle.jfaster.org.thread.AsyncCallbackExecutor;
import eagle.jfaster.org.transport.Client;
import eagle.jfaster.org.util.ExceptionUtil;
import eagle.jfaster.org.util.RemotingUtil;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.DefaultThreadFactory;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;

/**
 * netty 客户端
 *
 * Created by fangyanpeng1 on 2017/8/1.
 */
@RequiredArgsConstructor
public class NettyClient implements Client, StatisticCallback {

    private final static InternalLogger logger = InternalLoggerFactory.getInstance(NettyClient.class);

    private final MergeConfig config;

    @Getter
    private final MethodInvokeCallBack callBack;

    private final List<ExecutionInterceptor> interceptors;

    private EventLoopGroup workerGroup;

    @Getter
    private Bootstrap bootstrap;

    @Getter
    private AtomicBoolean stat = new AtomicBoolean(false);

    @Setter
    private volatile boolean suspend = false;

    private AtomicBoolean init = new AtomicBoolean(false);

    // 连续失败次数
    private AtomicLong errorCount = new AtomicLong(0);

    @Getter
    private Map<Integer, NettyResponseFuture> callbackMap = new ConcurrentHashMap(256);

    private ScheduledFuture<?> asyncCallbackMonitorFuture = null;

    private ScheduledFuture<?> heartBeatTaskFuture = null;

    private ScheduledFuture<?> callbackCancelFuture = null;

    // 回收过期任务
    private static ScheduledExecutorService callbackCancelExecutor = Executors.newScheduledThreadPool(4);

    private static ScheduledExecutorService commonExecutor = Executors.newScheduledThreadPool(1);

    // 回调执行线程池
    private ThreadPoolExecutor callbackExecutor;

    @Getter
    private BlockingQueue<Runnable> callbackQueue;

    //最大连续失败次数
    private int maxInvokeError = 0;

    private InetSocketAddress remoteAddress;

    private NettySharedConnPool connPool;

    @Override
    public void start() {
        try {
            if (init.compareAndSet(false, true)) {
                remoteAddress = new InetSocketAddress(config.getHost(), config.getPort());
                maxInvokeError = config.getExtInt(ConfigEnum.maxInvokeError.getName(), ConfigEnum.maxInvokeError.getIntValue());
                if (callBack != null) {
                    int callbackWorkerThread = config.getExtInt(ConfigEnum.callbackThread.getName(), ConfigEnum.callbackThread.getIntValue());
                    int callbackQueueSize = config.getExtInt(ConfigEnum.callbackQueueSize.getName(), ConfigEnum.callbackQueueSize.getIntValue());
                    callbackQueue = new LinkedBlockingQueue<>(callbackQueueSize);
                    callbackExecutor = new AsyncCallbackExecutor(callbackWorkerThread, callbackWorkerThread, 10 * 60 * 1000, TimeUnit.MILLISECONDS, callbackQueue, new DefaultThreadFactory("Method callback exec-" + config.getInterfaceName() + "-"));
                    callbackExecutor.allowCoreThreadTimeOut(true);
                    asyncCallbackMonitorFuture = commonExecutor.scheduleWithFixedDelay(new AsyncCallbackMonitor(this), ASYNC_TIMEOUT_TIMER_PERIOD, ASYNC_TIMEOUT_TIMER_PERIOD, TimeUnit.MILLISECONDS);
                }
                boolean useNative = RemotingUtil.isLinuxPlatform() && config.getExtBoolean(ConfigEnum.useNative.getName(), ConfigEnum.useNative.isBooleanValue());
                if (useNative) {
                    workerGroup = new EpollEventLoopGroup(GRUOUP_WORKER_THREAD, new DefaultThreadFactory("Method invoke exec-" + config.getInterfaceName() + "-"));
                } else {
                    workerGroup = new NioEventLoopGroup(GRUOUP_WORKER_THREAD, new DefaultThreadFactory("Method invoke exec-" + config.getInterfaceName() + "-"));
                }
                bootstrap = new Bootstrap();
                final int maxContentLen = config.getExtInt(ConfigEnum.maxContentLength.getName(), ConfigEnum.maxContentLength.getIntValue());
                final Codec codec = SpiClassLoader.getClassLoader(Codec.class).getExtension(config.getExt(ConfigEnum.codec.getName(), ConfigEnum.codec.getValue()));
                final Serialization serialization = SpiClassLoader.getClassLoader(Serialization.class).getExtension(config.getExt(ConfigEnum.serialization.getName(), ConfigEnum.serialization.getValue()));
                final int heartBeatInterval = config.getExtInt(ConfigEnum.heartbeat.getName(), ConfigEnum.heartbeat.getIntValue());
                bootstrap.group(workerGroup).channel(useNative ? EpollSocketChannel.class : NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .option(ChannelOption.SO_KEEPALIVE, false)
                        .option(ChannelOption.SO_SNDBUF, SOCKET_SNDBUF_SIZE)
                        .option(ChannelOption.SO_RCVBUF, SOCKET_RCVBUF_SIZE)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel sc) throws Exception {
                                sc.pipeline().addLast(new NettyEncoder(codec, serialization))
                                        .addLast(new NettyDecorder(maxContentLen, codec, serialization))
                                        .addLast(new IdleStateHandler(0, 0, heartBeatInterval))
                                        .addLast(new NettyConnectionManager(config, connPool, NettyClient.this))
                                        .addLast(NettyClient.this.callBack == null ? new SyncMessageHandler(NettyClient.this) : new AsyncMessageHandler(NettyClient.this));
                            }
                        });
                //定时扫描超时的请求
                callbackCancelFuture = callbackCancelExecutor.scheduleWithFixedDelay(new TimeoutMonitorTask(this), NETTY_TIMEOUT_TIMER_PERIOD, NETTY_TIMEOUT_TIMER_PERIOD, TimeUnit.MICROSECONDS);
                connPool = new NettySharedConnPool(config, this);
                heartBeatTaskFuture = commonExecutor.scheduleWithFixedDelay(new HeartBeatTask(config, connPool, this), heartBeatInterval, heartBeatInterval, TimeUnit.SECONDS);
                EagleStatsManager.registerStatsCallback(this);
                stat.set(true);

            }
        } catch (Throwable e) {
            logger.error("Error start netty client ", e);
            throw new EagleFrameException(e.getMessage());
        }
    }

    //只有单线程执行该方法，不用加同步
    public AbstractNettyChannel newChannel() throws InterruptedException {
        ChannelFuture channelFuture = bootstrap.connect(remoteAddress).sync();
        Channel channel = channelFuture.channel();
        return this.callBack == null ? new SyncNettyChannel(this, channel) : new AsyncNettyChannel(this, channel);
    }

    public NettyResponseFuture removeCallBack(Integer opaque) {
        return callbackMap.remove(opaque);
    }

    public void addCallBack(Integer opaque, NettyResponseFuture future) {
        callbackMap.put(opaque, future);
    }

    public void executeInvokeCallback(final ResponseFuture responseFuture) {
        boolean runInThisThread = false;
        if (callbackExecutor != null) {
            try {
                callbackExecutor.submit(new AsyncCallbackTask(responseFuture, interceptors));
            } catch (Throwable e) {
                runInThisThread = true;
                logger.info("execute callback in executor exception, maybe executor busy", e);
            }
        } else {
            runInThisThread = true;
        }
        if (runInThisThread) {
            try {
                responseFuture.executeCallback(interceptors);
            } catch (Throwable e) {
                logger.info("executeInvokeCallback Exception", e);
            }
        }
    }

    @Override
    public MergeConfig getConfig() {
        return config;
    }

    @Override
    public boolean isAlive() {
        return stat.get() && !suspend;
    }

    @Override
    public void shutdown(boolean shutdown) {
        try {
            if (init.compareAndSet(true, false)) {
                workerGroup.shutdownGracefully();
                //不能关闭callBackCancleExecutor，因为是多个client公用的。
                callbackCancelFuture.cancel(true);
                if (shutdown) {
                    callbackCancelExecutor.shutdownNow();
                }

                if (callbackExecutor != null) {
                    callbackExecutor.shutdownNow();
                }
                if (asyncCallbackMonitorFuture != null) {
                    asyncCallbackMonitorFuture.cancel(true);
                }
                heartBeatTaskFuture.cancel(true);
                if (shutdown) {
                    commonExecutor.shutdownNow();
                }
                callbackMap.clear();
                connPool.shutdown();
                logger.info("Netty client normal shutdown");
            }
        } catch (Throwable e) {
            logger.error("Netty client shutdown ", e);
        }
    }

    @Override
    public Object request(Request request) {
        AbstractNettyChannel channel = null;
        Throwable ex = null;
        try {
            onBefore(request, interceptors);
            channel = connPool.getConnection();
            return channel.request(request, connPool);
        } catch (Throwable e) {
            ex = e;
            logger.error("NettyClient request error, interface: '" + config.getInterfaceName() + "', host: '" + config.identity() + "'", e);
            if (channel != null) {
                connPool.invalidateConnection(channel);
            }
            throw ExceptionUtil.handleException(e);
        } finally {
            if (ex != null) {
                onError(request, interceptors, ex);
            } else if (callBack == null) { //同步调用直接执行onAfter
                onAfter(request, interceptors);
            }
            CurrentExecutionContext.clean();
        }
    }

    /**
     * 联系调用失败超过配置的次数，将该client设置为无效
     */
    public void incrErrorCount() {
        long count = errorCount.incrementAndGet();
        // 如果节点是可用状态，同时当前连续失败的次数超过限制maxClientConnection次，那么把该节点标示为不可用
        if (count >= maxInvokeError && stat.get()) {
            if (stat.compareAndSet(true, false)) {
                logger.error("NettyClient unavailable Error: config='" + config.getInterfaceName() + "' '" + config.identity() + "'");
            }
        }
    }

    /**
     * 通过心跳机制检测如果client恢复，则重新启用client
     */
    public void resetErrorCount() {
        errorCount.set(0);
        if (!stat.get() && init.get() && errorCount.intValue() < maxInvokeError && stat.compareAndSet(false, true)) {
            logger.info("NettyClient recover available: interfaceName='" + config.getInterfaceName() + "' '" + config.identity() + "'");
        }
    }

    @Override
    public String statistic() {
        if (callbackQueue == null) {
            if (callbackMap.size() < 100) {
                return null;
            }
        } else {
            if (callbackMap.size() < 100 && callbackQueue.size() < 100) {
                return null;
            }
        }
        return String.format("[%s] callbackMapSize: '%d' asyncCallbackQueueSize: '%d'", config.identity(), callbackMap.size(), callbackQueue == null ? 0 : callbackQueue.size());
    }

}


/*
 * Copyright 2017 eagle.jfaster.org.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * </p>
 */

package eagle.jfaster.org.factory;

import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;

import eagle.jfaster.org.CoordinatorRegistryCenter;
import eagle.jfaster.org.ZookeeperRegistryCenter;
import eagle.jfaster.org.config.ConfigEnum;
import eagle.jfaster.org.config.common.MergeConfig;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.util.concurrent.ConcurrentHashMap;

/**
 * 注册中心工厂.
 *
 * @author fangyanpeng
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class RegistryCenterFactory {

    private static final ConcurrentHashMap<HashCode, CoordinatorRegistryCenter> REG_CENTER_REGISTRY = new ConcurrentHashMap<>();

    /**
     * 创建注册中心.
     *
     * @param connectString 注册中心连接字符串
     * @param namespace 注册中心命名空间
     * @param digest 注册中心凭证
     * @return 注册中心对象
     */
    public static CoordinatorRegistryCenter createCoordinatorRegistryCenter(final String connectString, final String namespace, final Optional<String> digest) {
        Hasher hasher = Hashing.md5().newHasher().putString(connectString, Charsets.UTF_8).putString(namespace, Charsets.UTF_8);
        if (digest.isPresent()) {
            hasher.putString(digest.get(), Charsets.UTF_8);
        }
        HashCode hashCode = hasher.hash();
        CoordinatorRegistryCenter result = REG_CENTER_REGISTRY.get(hashCode);
        if (null != result) {
            return result;
        }
        MergeConfig zkConfig = new MergeConfig();
        zkConfig.addExt(ConfigEnum.address.getName(), connectString);
        zkConfig.addExt(ConfigEnum.namespace.getName(), namespace);
        if (digest.isPresent()) {
            zkConfig.addExt(ConfigEnum.digest.getName(), digest.get());
        }
        result = new ZookeeperRegistryCenter(zkConfig);
        result.init();
        REG_CENTER_REGISTRY.put(hashCode, result);
        return result;
    }
}


/*
 * Copyright 2017 eagle.jfaster.org.
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * </p>
 */

package eagle.jfaster.org.factory;

import com.google.common.base.Optional;

import eagle.jfaster.org.service.InterfaceApiService;
import eagle.jfaster.org.service.impl.InterfaceApiServiceImpl;

/**
 * @author fangyanpeng
 */
public class ServiceApiFactory {

    /**
     * 创建服务管理API对象.
     *
     * @param connectString 注册中心连接字符串
     * @param namespace 注册中心命名空间
     * @param digest 注册中心凭证
     * @return 服务管理API对象
     */
    public static InterfaceApiService createServiceAPI(final String connectString, final String namespace, final Optional<String> digest) {
        return new InterfaceApiServiceImpl(RegistryCenterFactory.createCoordinatorRegistryCenter(connectString, namespace, digest));
    }
}


/*
 * Copyright (C) 2009 Jayway AB
 * Copyright (C) 2007-2008 JVending Masa
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.simpligility.maven.plugins.android.standalonemojos;

import com.android.ddmlib.IDevice;
import com.android.ddmlib.testrunner.ITestRunListener;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
import com.simpligility.maven.plugins.android.AndroidTestRunListener;
import com.simpligility.maven.plugins.android.CommandExecutor;
import com.simpligility.maven.plugins.android.DeviceCallback;
import com.simpligility.maven.plugins.android.ExecutionException;
import com.simpligility.maven.plugins.android.config.ConfigHandler;
import com.simpligility.maven.plugins.android.config.ConfigPojo;
import com.simpligility.maven.plugins.android.config.PullParameter;
import com.simpligility.maven.plugins.android.configuration.MonkeyRunner;
import com.simpligility.maven.plugins.android.configuration.Program;

import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.interpolation.os.Os;
import org.codehaus.plexus.util.cli.shell.BourneShell;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Can execute monkey runner programs.

 * Implements parsing parameters from pom or command line arguments and sets useful defaults as well. This goal will
 * invoke monkey runner scripts. If the application crashes during the exercise, this goal can fail the build.

 * A typical usage of this goal can be found at Quality tools for Android project.
 *
 * @see Monkey docs by Google
 * @see Stack Over Flow thread for parsing monkey output.
 * @author Stéphane Nicolas - snicolas@octo.com
 */
@SuppressWarnings( "unused" )
@Mojo( name = "monkeyrunner" )
public class MonkeyRunnerMojo extends AbstractAndroidMojo
{
    /**
     * -Dmaven.test.skip is commonly used with Maven to skip tests. We honor it.
     */
    @Parameter( property = "maven.test.skip", defaultValue = "false", readonly = true )
    private boolean mavenTestSkip;

    /**
     * -DskipTests is commonly used with Maven to skip tests. We honor it too.
     */
    @Parameter( property = "skipTests", defaultValue = "false", readonly = true )
    private boolean mavenSkipTests;
    /**
     * -Dmaven.test.failure.ignore is commonly used with Maven to prevent failure of build when (some) tests fail. We
     * honor it too.
     */
    @Parameter( property = "maven.test.failure.ignore", defaultValue = "false", readonly = true )
    private boolean mavenTestFailureIgnore;

    /**
     * -Dmaven.test.failure.ignore is commonly used with Maven to prevent failure of build when (some) tests fail. We
     * honor it too.
     */
    @Parameter( property = "testFailureIgnore", defaultValue = "false", readonly = true )
    private boolean mavenIgnoreTestFailure;

    /**
     * The configuration for the monkey runner goal.
     *
     *

     * <monkeyrunner>
     *   <skip>false</skip>
     * </monkeyrunner>
     *

     *
     * Full configuration can use these parameters.
     *
     *

     *  <monkeyrunner>
     *    <skip>false</skip>
     *    <createReport>true</createReport>
     *  </monkeyrunner>
     *

     *
     * Alternatively to the plugin configuration values can also be configured as properties on the command line as
     * android.lint.* or in pom or settings file as properties like lint*.
     */
    @Parameter
    @ConfigPojo
    private MonkeyRunner monkeyrunner;

    /**
     * Enables or disables monkey runner test goal. If true it will be skipped; if false, it
     * will be run. Defaults to true.
     */
    @Parameter( property = "android.monkeyrunner.skip" )
    private Boolean monkeyRunnerSkip;

    @PullParameter( defaultValue = "true" )
    private Boolean parsedSkip;

    /**
     * (Optional) Specifies a .jar file containing a plugin for monkeyrunner. To learn more about monkeyrunner plugins,
     * see Extending monkeyrunner
     * with plugins. You can add as many plugins as you want.
     *
     * Defaults to no plugins.
     */
    @Parameter( property = "android.monkeyrunner.plugins" )
    private String[] monkeyPlugins;

    @PullParameter( defaultValueGetterMethod = "getPlugins" )
    private String[] parsedPlugins;

    /**
     * Runs the contents of the file as a Python program.
     *
     *

     * <programs>
     *   <program>
     *     <filename>foo.py</filename>
     *     <options>bar</options>
     *   <program>
     *   <program>
     *     <filename>foo2.py</filename>
     *   <program>
     *   [..]
     * </programs>
     *

     */
    @Parameter
    @PullParameter( required = false, defaultValueGetterMethod = "getPrograms" )
    private List< Program > parsedPrograms;

    /**
     * Create a junit xml format compatible output file containing the test results for each device the instrumentation
     * tests run on.

     *

     * The files are stored in target/surefire-reports and named TEST-deviceid.xml. The deviceid for an emulator is
     * deviceSerialNumber_avdName_manufacturer_model. The serial number is commonly emulator-5554 for the first emulator
     * started with numbers increasing. avdName is as defined in the SDK tool. The manufacturer is typically "unknown"
     * and the model is typically "sdk".

     * The deviceid for an actual devices is deviceSerialNumber_manufacturer_model.

     *

     * The file contains system properties from the system running the Android Maven Plugin (JVM) and device properties
     * from the device/emulator the tests are running on.

     *

     * The file contains a single TestSuite for all tests and a TestCase for each test method. Errors and failures are
     * logged in the file and the system log with full stack traces and other details available.
     *
     * Defaults to false.
     *
     */
    @Parameter( property = "android.monkeyrunner.createReport" )
    private Boolean monkeyCreateReport;

    @PullParameter( defaultValue = "false" )
    private Boolean parsedCreateReport;

    /**
     * Decides whether or not to inject device serial number as a parameter to each monkey runner script. The parameter
     * will be the first parameter passed to the script. This parameter allows to support monkey runner tests on
     * multiple devices. In that case, monkey runner scripts have to be modified to take the new parameter into account.
     * Follow that thread on stack over flow to learn more about
     * it.
     */
    @Parameter( property = "android.monkeyrunner.injectDeviceSerialNumberIntoScript" )
    private Boolean monkeyInjectDeviceSerialNumberIntoScript;

    @PullParameter( defaultValue = "false" )
    private Boolean parsedInjectDeviceSerialNumberIntoScript;

    private long elapsedTime;

    private ITestRunListener[] mTestListeners;

    private Map< String, String > runMetrics;

    private String mRunName;

    private int eventCount;

    private TestIdentifier mCurrentTestIndentifier;

    private MonkeyRunnerErrorListener errorListener;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException
    {
        ConfigHandler configHandler = new ConfigHandler( this, this.session, this.execution );
        configHandler.parseConfiguration();

        doWithDevices( new DeviceCallback()
        {
            @Override
            public void doWithDevice( IDevice device ) throws MojoExecutionException, MojoFailureException
            {
                AndroidTestRunListener testRunListener = new AndroidTestRunListener( device, getLog(),
                        parsedCreateReport, false, "", "", targetDirectory );
                if ( isEnableIntegrationTest() )
                {
                    run( device, testRunListener );
                }
            }
        } );
    }

    /**
     * Whether or not tests are enabled.
     *
     * @return a boolean indicating whether or not tests are enabled.
     */
    protected boolean isEnableIntegrationTest()
    {
        return !parsedSkip && !mavenTestSkip && !mavenSkipTests;
    }

    /**
     * Whether or not test failures should be ignored.
     *
     * @return a boolean indicating whether or not test failures should be ignored.
     */
    protected boolean isIgnoreTestFailures()
    {
        return mavenIgnoreTestFailure || mavenTestFailureIgnore;
    }

    /**
     * Actually plays tests.
     *
     * @param device
     *            the device on which tests are going to be executed.
     * @param iTestRunListeners
     *            test run listeners.
     * @throws MojoExecutionException
     *             if exercising app threw an exception and isIgnoreTestFailures is false..
     * @throws MojoFailureException
     *             if exercising app failed and isIgnoreTestFailures is false.
     */
    protected void run( IDevice device, ITestRunListener... iTestRunListeners ) throws MojoExecutionException,
            MojoFailureException
    {

        this.mTestListeners = iTestRunListeners;

        getLog().debug( "Parsed values for Android Monkey Runner invocation: " );

        CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
        if ( !Os.isFamily( Os.FAMILY_WINDOWS ) )
        {
            executor.setCustomShell( new CustomBourneShell() );
        }
        executor.setLogger( this.getLog() );

        String command = getAndroidSdk().getMonkeyRunnerPath();

        List< String > pluginParameters = new ArrayList< String >();

        if ( parsedPlugins != null && parsedPlugins.length != 0 )
        {
            for ( String plugin : parsedPlugins )
            {
                String pluginFilePath = new File( project.getBasedir(), plugin ).getAbsolutePath();
                pluginParameters.add( "-plugin " + pluginFilePath );
            }
        }

        if ( parsedPrograms != null && !parsedPrograms.isEmpty() )
        {
            handleTestRunStarted();
            errorListener = new MonkeyRunnerErrorListener();
            executor.setErrorListener( errorListener );

            for ( Program program : parsedPrograms )
            {
                List< String > parameters = new ArrayList< String >( pluginParameters );

                String programFileName = new File( project.getBasedir(), program.getFilename() ).getAbsolutePath();
                parameters.add( programFileName );
                String testName = programFileName;
                if ( testName.contains( "/" ) )
                {
                    testName.substring( testName.indexOf( '/' ) + 1 );
                }
                mCurrentTestIndentifier = new TestIdentifier( "MonkeyTest ", testName );

                String programOptions = program.getOptions();
                if ( parsedInjectDeviceSerialNumberIntoScript != null && parsedInjectDeviceSerialNumberIntoScript )
                {
                    parameters.add( device.getSerialNumber() );
                }
                if ( programOptions != null && !StringUtils.isEmpty( programOptions ) )
                {
                    parameters.add( programOptions );
                }

                try
                {
                    getLog().info( "Running command: " + command );
                    getLog().info( "with parameters: " + parameters );
                    handleTestStarted();
                    executor.setCaptureStdOut( true );
                    executor.executeCommand( command, parameters, true );
                    handleTestEnded();
                }
                catch ( ExecutionException e )
                {
                    getLog().info( "Monkey runner produced errors" );
                    handleTestRunFailed( e.getMessage() );

                    if ( !isIgnoreTestFailures() )
                    {
                        getLog().info( "Project is configured to fail on error." );
                        getLog().info(
                                "Inspect monkey runner reports or re-run with -X to see monkey runner errors in log" );
                        getLog().info( "Failing build as configured. Ignore following error message." );
                        if ( errorListener.hasError )
                        {
                            getLog().info( "Stack trace is:" );
                            getLog().info( errorListener.getStackTrace() );
                        }
                        throw new MojoExecutionException( "", e );
                    }
                }

                if ( errorListener.hasError() )
                {
                    handleCrash();
                }
            }
            handleTestRunEnded();
        }

        getLog().info( "Monkey runner test runs completed successfully." );
    }

    private void handleTestRunStarted()
    {
        runMetrics = new HashMap< String, String >();
        elapsedTime = System.currentTimeMillis();
        for ( ITestRunListener listener : mTestListeners )
        {
            listener.testRunStarted( mRunName, eventCount );
        }
    }

    private void handleTestRunFailed( String error )
    {
        for ( ITestRunListener listener : mTestListeners )
        {
            listener.testRunFailed( error );
        }
    }

    private void handleTestRunEnded()
    {
        elapsedTime = System.currentTimeMillis() - elapsedTime;

        for ( ITestRunListener listener : mTestListeners )
        {
            listener.testRunEnded( elapsedTime, runMetrics );
        }
    }

    private void handleTestStarted()
    {
        System.out.println( "TEST START " + mTestListeners.length );
        for ( ITestRunListener listener : mTestListeners )
        {
            listener.testStarted( mCurrentTestIndentifier );
        }
    }

    private void handleTestEnded()
    {
        if ( mCurrentTestIndentifier != null )
        {
            for ( ITestRunListener listener : mTestListeners )
            {
                listener.testEnded( mCurrentTestIndentifier, new HashMap< String, String >() );
            }
            mCurrentTestIndentifier = null;
        }
    }

    private void handleCrash()
    {

        String trace = errorListener.getStackTrace();

        for ( ITestRunListener listener : mTestListeners )
        {
            listener.testFailed( mCurrentTestIndentifier, trace );
        }
        mCurrentTestIndentifier = null;

    }

    private final class MonkeyRunnerErrorListener implements CommandExecutor.ErrorListener
    {
        private StringBuilder stackTraceBuilder = new StringBuilder();
        private boolean hasError = false;

        @Override
        public boolean isError( String error )
        {

            // Unconditionally ignore *All* build warning if configured to
            if ( isIgnoreTestFailures() )
            {
                return false;
            }

            if ( hasError )
            {
                stackTraceBuilder.append( error ).append( '\n' );
            }

            final Pattern pattern = Pattern.compile( ".*error.*|.*exception.*", Pattern.CASE_INSENSITIVE );
            final Matcher matcher = pattern.matcher( error );

            // If the the reg.exp actually matches, we can safely say this is not an error
            // since in theory the user told us so
            if ( matcher.matches() )
            {
                hasError = true;
                stackTraceBuilder.append( error ).append( '\n' );
                return true;
            }

            // Otherwise, it is just another error
            return false;
        }

        public String getStackTrace()
        {
            if ( hasError )
            {
                return stackTraceBuilder.toString();
            }
            else
            {
                return null;
            }
        }

        public boolean hasError()
        {
            return hasError;
        }
    }

    /**
     * @return default plugins.
     */
    public String[] getPlugins()
    {
        return parsedPlugins;
    }

    private static final class CustomBourneShell extends BourneShell
    {
        @Override
        public List< String > getShellArgsList()
        {
            List< String > shellArgs = new ArrayList< String >();
            List< String > existingShellArgs = super.getShellArgsList();

            if ( existingShellArgs != null && !existingShellArgs.isEmpty() )
            {
                shellArgs.addAll( existingShellArgs );
            }

            return shellArgs;
        }

        @Override
        public String[] getShellArgs()
        {
            String[] shellArgs = super.getShellArgs();
            if ( shellArgs == null )
            {
                shellArgs = new String[ 0 ];
            }

            return shellArgs;
        }

    }

    public List< Program > getPrograms()
    {
        // return null if not set
        return parsedPrograms;
    }
}