/*
 * Copyright (c) 2008-2016, Hazelcast, Inc. All Rights Reserved.
 *
 * 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.hazelcast.simulator.common;

import com.hazelcast.simulator.utils.CommandLineExitException;
import com.hazelcast.simulator.utils.CommonUtils;
import org.apache.log4j.Logger;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import static com.hazelcast.simulator.utils.CloudProviderUtils.isTrueCloud;
import static com.hazelcast.simulator.utils.CommonUtils.closeQuietly;
import static com.hazelcast.simulator.utils.CommonUtils.rethrow;
import static com.hazelcast.simulator.utils.FileUtils.fileAsText;
import static com.hazelcast.simulator.utils.FileUtils.getSimulatorHome;
import static com.hazelcast.simulator.utils.FileUtils.getUserDir;
import static com.hazelcast.simulator.utils.FileUtils.newFile;
import static java.lang.Integer.parseInt;
import static java.lang.String.format;

/**
 * Loads the Hazelcast Simulator properties file.
 * <p>
 * This class will always load the properties in the <tt>${SIMULATOR_HOME}/conf/simulator.properties</tt> as defaults. If an
 * explicit {$value #PROPERTIES_FILE_NAME} file is configured or {$value #PROPERTIES_FILE_NAME} is available in the working dir,
 * it will override the properties from the default.
 */
@SuppressWarnings("checkstyle:methodcount")
public class SimulatorProperties {

    public static final String PROPERTIES_FILE_NAME = "simulator.properties";

    public static final int DEFAULT_AGENT_PORT = 9000;
    public static final String CLOUD_PROVIDER = "CLOUD_PROVIDER";
    public static final String CLOUD_IDENTITY = "CLOUD_IDENTITY";
    public static final String CLOUD_CREDENTIAL = "CLOUD_CREDENTIAL";

    private static final int WORKER_TIMEOUT_FACTOR = 3;

    private static final Logger LOGGER = Logger.getLogger(SimulatorProperties.class);

    private final Map<String, Value> properties = new HashMap<String, Value>();

    public SimulatorProperties() {
        File defaultPropFile = newFile(getSimulatorHome(), "conf", PROPERTIES_FILE_NAME);
        properties.put("SIMULATOR_VERSION", new Value(true, CommonUtils.getSimulatorVersion()));

        LOGGER.info(format("Loading default %s: %s", PROPERTIES_FILE_NAME, defaultPropFile.getAbsolutePath()));
        check(defaultPropFile);
        load(defaultPropFile, true);
    }

    public Map<String, String> asMap() {
        Map<String, String> map = asPublicMap();

        if (isTrueCloud(getCloudProvider())) {
            map.put(CLOUD_IDENTITY, getCloudIdentity());
            map.put(CLOUD_CREDENTIAL, getCloudCredential());
        }

        return map;
    }

    public Map<String, String> asPublicMap() {
        Map<String, String> map = new HashMap<String, String>();
        for (Map.Entry entry : properties.entrySet()) {
            String key = (String) entry.getKey();
            map.put(key, get(key));
        }

        map.remove(CLOUD_IDENTITY);
        map.remove(CLOUD_CREDENTIAL);
        return map;
    }

    /**
     * Initializes the SimulatorProperties with additional properties.
     *
     * @param file the file to load the properties from. If {@code null}, the {@value #PROPERTIES_FILE_NAME} file in the working
     *             directory is tried.
     * @return this
     * @throws CommandLineExitException if the given file does not exist. If file is {@code null} no exception will be thrown,
     *                                  even if the {@value #PROPERTIES_FILE_NAME} file in the working directory cannot be found.
     */
    public SimulatorProperties init(File file) {
        if (file == null) {
            // if no file is explicitly given, we look in the working directory
            file = new File(getUserDir(), PROPERTIES_FILE_NAME);
            if (!file.exists()) {
                LOGGER.info(format("Found no %s in working directory, relying on default properties", PROPERTIES_FILE_NAME));
                return null;
            }
        }

        LOGGER.info(format("Loading additional %s: %s", PROPERTIES_FILE_NAME, file.getAbsolutePath()));
        check(file);
        load(file, false);
        return this;
    }

    private void check(File file) {
        if (!file.exists()) {
            throw new CommandLineExitException(format("Could not find %s: %s", PROPERTIES_FILE_NAME, file.getAbsolutePath()));
        }
    }

    void load(File file) {
        load(file, false);
    }

    void load(File file, boolean isDefault) {
        FileInputStream in = null;
        try {
            in = new FileInputStream(file);

            Properties p = new Properties();
            p.load(in);

            for (Map.Entry entry : p.entrySet()) {
                String key = (String) entry.getKey();

                String value = (String) entry.getValue();
                properties.put(key, new Value(isDefault, value));
            }

            if (containsKey("HAZELCAST_VERSION_SPEC")) {
                throw new IOException("'HAZELCAST_VERSION_SPEC' property is no longer supported, use 'VERSION_SPEC' instead.");
            }

            if (containsKey("MACHINE_SPEC")) {
                throw new IOException("'MACHINE_SPEC' property is no longer supported, use 'INSTANCE_TYPE', instead.");
            }

            if (containsKey("USER")) {
                throw new IOException("'USER' property is no longer supported, Use 'SIMULATOR_USER' instead.");
            }
        } catch (IOException e) {
            throw rethrow(e);
        } finally {
            closeQuietly(in);
        }
    }

    public boolean containsKey(String name) {
        return properties.containsKey(name);
    }

    public int getInt(String property) {
        return Integer.parseInt(get(property));
    }

    public String getSshOptions() {
        return get("SSH_OPTIONS", "");
    }

    public String getUser() {
        return get("SIMULATOR_USER", "simulator");
    }

    public String getJdkFlavor() {
        return get("JDK_FLAVOR", "outofthebox");
    }

    public String getJdkVersion() {
        return get("JDK_VERSION", "7");
    }

    public String getVersionSpec() {
        return get("VERSION_SPEC", "outofthebox");
    }

    public int getWorkerPingIntervalSeconds() {
        return parseInt(get("WORKER_PING_INTERVAL_SECONDS", "60"));
    }

    public int getWorkerLastSeenTimeoutSeconds() {
        return getWorkerPingIntervalSeconds() * WORKER_TIMEOUT_FACTOR;
    }

    public int getMemberWorkerShutdownDelaySeconds() {
        return parseInt(get("MEMBER_WORKER_SHUTDOWN_DELAY_SECONDS", "5"));
    }

    public int getWaitForWorkerShutdownTimeoutSeconds() {
        return parseInt(get("WAIT_FOR_WORKER_SHUTDOWN_TIMEOUT_SECONDS", "120"));
    }

    public int getTestCompletionTimeoutSeconds() {
        return parseInt(get("TEST_COMPLETION_TIMEOUT_SECONDS", "300"));
    }

    public int getCoordinatorPort() {
        return parseInt(get("COORDINATOR_PORT", "0"));
    }

    public int getAgentThreadPoolSize() {
        return parseInt(get("AGENT_THREAD_POOL_SIZE", "0"));
    }

    public int getAgentPort() {
        return parseInt(get("AGENT_PORT", "9000"));
    }

    public String getCloudProvider() {
        return get(CLOUD_PROVIDER);
    }

    public void setCloudProvider(String value) {
        set(CLOUD_PROVIDER, value);
    }

    public String getCloudIdentity() {
        return loadDirectOrFile(CLOUD_IDENTITY);
    }

    public String getCloudCredential() {
        return loadDirectOrFile(CLOUD_CREDENTIAL);
    }

    private String loadDirectOrFile(String property) {
        String value = get(property);
        if (value == null) {
            throw new IllegalArgumentException(format("Simulator property '%s' is not found", property));
        }

        File file = newFile(value);
        if (file.exists()) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(format("Loading simulator property value for %s from file: %s", property, file.getAbsolutePath()));
            }

            return fileAsText(file).trim();
        } else if (value.endsWith(".identity") || value.endsWith(".credential") || value.endsWith(".txt")) {
            throw new CommandLineExitException("file [" + value + "] is not found");
        } else {
            return value.trim();
        }
    }

    public String get(String name) {
        return get(name, null);
    }

    public String get(String name, String defaultValue) {
        Value value = properties.get(name);

        String result = null;

        // if a non default value has been set; that is being returned because highest priority
        if (value != null && !value.isDefault) {
            result = value.text;
        }

        // then we check system properties
        if (result == null) {
            result = System.getProperty(name);
        }

        // then we check environment
        // checking environment is useful for external configuration of simulator properties using exported variables
        if (result == null) {
            result = System.getenv(name);
        }

        // then we check the default value from the simulator.properties from the distribution
        if (result == null && value != null) {
            result = value.text;
        }

        // and eventually we default to the default value.
        if (result == null) {
            result = defaultValue;
        }

        return fixValue(name, result);
    }

    public SimulatorProperties set(String name, String value) {
        properties.put(name, new Value(false, value));
        return this;
    }

    public SimulatorProperties setIfNotNull(String name, String value) {
        if (value != null) {
            set(name, value);
        }
        return this;
    }

    public String getDefaultsAsString() {
        return fileAsText(newFile(getSimulatorHome(), "conf", PROPERTIES_FILE_NAME));
    }

    private String fixValue(String name, String value) {
        if (value == null) {
            return null;
        }

        if ("GROUP_NAME".equals(name)) {
            String username = System.getProperty("user.name").toLowerCase();

            StringBuilder fixedUserName = new StringBuilder();
            for (char character : username.toCharArray()) {
                if (Character.isLetter(character) || Character.isDigit(character)) {
                    fixedUserName.append(character);
                }
            }

            return value.replace("${username}", fixedUserName.toString());
        }

        value = value.trim();
        return value;
    }

    private static final class Value {
        final boolean isDefault;
        final String text;

        private Value(boolean isDefault, String text) {
            this.isDefault = isDefault;
            this.text = text;
        }
    }
}


/*
 * Copyright (c) 2008-2016, Hazelcast, Inc. All Rights Reserved.
 *
 * 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.hazelcast.simulator.utils;

import com.hazelcast.simulator.common.TestCase;
import com.hazelcast.simulator.probes.Probe;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static com.hazelcast.simulator.utils.ReflectionUtils.getFieldValue0;
import static java.lang.String.format;
import static java.lang.reflect.Modifier.isFinal;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;

/**
 * Contains support functionality for binding properties.
 */
public final class PropertyBindingSupport {

    private static final String NULL_LITERAL = "null";

    private PropertyBindingSupport() {
    }

    /**
     * Binds a single property contained in the {@link TestCase} instance onto the object instance.
     *
     * There will be no warning if the property is not defined in the {@link TestCase}.
     * There will be no exception if the property will not be found in the object instance, just a warning.
     *
     * @param instance     Instance were the property should be injected
     * @param testCase     TestCase which contains
     * @param propertyName Name of the property which should be injected
     * @return true if a matching property was found in the testCase, false otherwise.
     * @throws RuntimeException if there was an error binding
     */
    public static boolean bind(Object instance, TestCase testCase, String propertyName) {
        String value = getValue(testCase, propertyName);
        if (value == null) {
            return false;
        }

        return bind0(instance, propertyName, value);
    }

    public static Set<String> bindAll(Object instance, TestCase testCase) {
        Set<String> usedProperties = new HashSet<String>();

        for (Map.Entry<String, String> entry : testCase.getProperties().entrySet()) {
            String fullPropertyPath = entry.getKey().trim();
            String value = entry.getValue().trim();

            if (bind0(instance, fullPropertyPath, value)) {
                usedProperties.add(fullPropertyPath);
            }
        }

        return usedProperties;
    }

    /**
     * Returns a single property contained in the {@link TestCase} instance.
     *
     * There will be no warning if the property is not defined in the {@link TestCase}.
     *
     * @param testCase     TestCase which contains
     * @param propertyName Name of the property which should be injected
     * @return Value of the property if found, {@code null} otherwise
     */
    private static String getValue(TestCase testCase, String propertyName) {
        if (testCase == null) {
            return null;
        }

        String propertyValue = testCase.getProperty(propertyName);
        if (propertyValue == null || propertyValue.isEmpty()) {
            return null;
        }
        return propertyValue;
    }

    static boolean bind0(Object object, String property, String value) {
        value = value.trim();

        String[] path = property.split("\\.");

        object = findTargetObject(object, property, path);
        if (object == null) {
            return false;
        }

        Field field = findField(object.getClass(), path[path.length - 1]);
        if (field == null || isProbeField(field)) {
            return false;
        }

        try {
            setField(field, object, value);
            return true;
        } catch (Exception e) {
            throw new BindException(format("Failed to bind value [%s] to property [%s.%s] of type [%s]",
                    value, object.getClass().getName(), property, field.getType()), e);
        }
    }

    private static Object findTargetObject(Object parent, String property, String[] path) {
        for (int i = 0; i < path.length - 1; i++) {
            Class<?> clazz = parent.getClass();
            String fieldName = path[i];
            Field field = findField(clazz, fieldName);
            if (field == null) {
                if (i == 0) {
                    // we have no match at all
                    return null;
                } else {
                    // we found at least one item in the path
                    throw new BindException(
                            format("Failed to find field [%s.%s] in property [%s]", clazz.getName(), fieldName, property));
                }
            }

            Object child = getFieldValue0(parent, field, clazz.getName(), property);
            if (child == null) {
                try {
                    child = field.getType().newInstance();
                    field.set(parent, child);
                } catch (InstantiationException e) {
                    throw new BindException(format("Failed to initialize null field '%s'", field), e);
                } catch (IllegalAccessException e) {
                    throw new BindException(format("Failed to initialize null field '%s'", field), e);
                }
            }

            parent = child;
        }
        return parent;
    }

    private static Field findField(Class clazz, String fieldName) {
        try {
            Field field = clazz.getDeclaredField(fieldName);
            if (!isPublic(field.getModifiers())) {
                throw new BindException(format("Property [%s.%s] is not public", clazz.getName(), fieldName));
            }
            if (isStatic(field.getModifiers())) {
                throw new BindException(format("Property [%s.%s] is static", clazz.getName(), fieldName));
            }
            if (isFinal(field.getModifiers())) {
                throw new BindException(format("Property [%s.%s] is final", clazz.getName(), fieldName));
            }
            return field;
        } catch (NoSuchFieldException e) {
            Class superClass = clazz.getSuperclass();
            if (superClass == null) {
                return null;
            }
            return findField(superClass, fieldName);
        }
    }

    private static boolean isProbeField(Field field) {
        return Probe.class.equals(field.getType());
    }

    private static void setField(Field field, Object object, String value) throws IllegalAccessException {
        if (setIntegralField(field, object, value)
                || setFloatingPointField(field, object, value)
                || setNonNumericField(field, object, value)) {
            return;
        }

        throw new BindException(format("Property [%s.%s] has unsupported type '%s'",
                object.getClass().getName(), field.getName(), field.getType()));
    }

    private static boolean setIntegralField(Field field, Object object, String value) throws IllegalAccessException {
        value = removeUnderscores(value);

        if (Byte.TYPE.equals(field.getType())) {
            // primitive byte
            field.set(object, Byte.parseByte(value));
        } else if (Byte.class.equals(field.getType())) {
            setByteField(object, value, field);
        } else if (Short.TYPE.equals(field.getType())) {
            // primitive short
            field.set(object, Short.parseShort(value));
        } else if (Short.class.equals(field.getType())) {
            setShortField(object, value, field);
        } else if (Integer.TYPE.equals(field.getType())) {
            // primitive integer
            field.set(object, Integer.parseInt(value));
        } else if (Integer.class.equals(field.getType())) {
            setIntegerField(object, value, field);
        } else if (Long.TYPE.equals(field.getType())) {
            // primitive long
            field.set(object, Long.parseLong(value));
        } else if (Long.class.equals(field.getType())) {
            setLongField(object, value, field);
        } else {
            return false;
        }
        return true;
    }

    public static String removeUnderscores(String s) {
        return s.replaceAll("_", "");
    }

    private static boolean setFloatingPointField(Field field, Object object, String value) throws IllegalAccessException {
        value = removeUnderscores(value);

        if (Float.TYPE.equals(field.getType())) {
            // primitive float
            field.set(object, Float.parseFloat(value));
        } else if (Float.class.equals(field.getType())) {
            setFloatField(object, value, field);
        } else if (Double.TYPE.equals(field.getType())) {
            // primitive double
            field.set(object, Double.parseDouble(value));
        } else if (Double.class.equals(field.getType())) {
            setDoubleField(object, value, field);
        } else {
            return false;
        }
        return true;
    }

    private static boolean setNonNumericField(Field field, Object object, String value) throws IllegalAccessException {
        if (Boolean.TYPE.equals(field.getType())) {
            setPrimitiveBooleanField(object, value, field);
        } else if (Boolean.class.equals(field.getType())) {
            setBooleanField(object, value, field);
        } else if (field.getType().isAssignableFrom(Class.class)) {
            setClassField(object, value, field);
        } else if (String.class.equals(field.getType())) {
            setStringField(object, value, field);
        } else if (field.getType().isEnum()) {
            setEnumField(object, value, field);
        } else {
            return false;
        }
        return true;
    }

    private static void setByteField(Object object, String value, Field field) throws IllegalAccessException {
        if (NULL_LITERAL.equals(value)) {
            field.set(object, null);
        } else {
            field.set(object, Byte.parseByte(value));
        }
    }

    private static void setShortField(Object object, String value, Field field) throws IllegalAccessException {
        if (NULL_LITERAL.equals(value)) {
            field.set(object, null);
        } else {
            field.set(object, Short.parseShort(value));
        }
    }

    private static void setIntegerField(Object object, String value, Field field) throws IllegalAccessException {
        if (NULL_LITERAL.equals(value)) {
            field.set(object, null);
        } else {
            field.set(object, Integer.parseInt(value));
        }
    }

    private static void setLongField(Object object, String value, Field field) throws IllegalAccessException {
        if (NULL_LITERAL.equals(value)) {
            field.set(object, null);
        } else {
            field.set(object, Long.parseLong(value));
        }
    }

    private static void setFloatField(Object object, String value, Field field) throws IllegalAccessException {
        if (NULL_LITERAL.equals(value)) {
            field.set(object, null);
        } else {
            field.set(object, Float.parseFloat(value));
        }
    }

    private static void setDoubleField(Object object, String value, Field field) throws IllegalAccessException {
        if (NULL_LITERAL.equals(value)) {
            field.set(object, null);
        } else {
            field.set(object, Double.parseDouble(value));
        }
    }

    private static void setPrimitiveBooleanField(Object object, String value, Field field) throws IllegalAccessException {
        if ("true".equals(value)) {
            field.set(object, true);
        } else if ("false".equals(value)) {
            field.set(object, false);
        } else {
            throw new NumberFormatException(format("Unrecognized boolean value: %s", value));
        }
    }

    private static void setBooleanField(Object object, String value, Field field) throws IllegalAccessException {
        if (NULL_LITERAL.equals(value)) {
            field.set(object, null);
        } else if ("true".equals(value)) {
            field.set(object, true);
        } else if ("false".equals(value)) {
            field.set(object, false);
        } else {
            throw new NumberFormatException(format("Unrecognized Boolean value: %s", value));
        }
    }

    private static void setClassField(Object object, String value, Field field) throws IllegalAccessException {
        if (NULL_LITERAL.equals(value)) {
            field.set(object, null);
        } else {
            try {
                field.set(object, PropertyBindingSupport.class.getClassLoader().loadClass(value));
            } catch (Exception e) {
                throw new BindException(format("Exception while binding class to field %s", field), e);
            }
        }
    }

    private static void setStringField(Object object, String value, Field field) throws IllegalAccessException {
        if (NULL_LITERAL.equals(value)) {
            field.set(object, null);
        } else {
            field.set(object, value);
        }
    }

    private static void setEnumField(Object object, String value, Field field) throws IllegalAccessException {
        if (NULL_LITERAL.equals(value)) {
            field.set(object, null);
        } else {
            try {
                Object enumValue = getEnumValue(value, field);
                field.set(object, enumValue);
            } catch (Exception e) {
                throw new BindException(format("Exception while binding Enum to field %s", field), e);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private static Enum getEnumValue(String value, Field field) throws Exception {
        Class<? extends Enum> type = (Class<? extends Enum>) field.getType();
        Method method = type.getMethod("values");
        Enum[] values = (Enum[]) method.invoke(null);

        for (Enum enumValue : values) {
            if (enumValue.name().equalsIgnoreCase(value)) {
                return enumValue;
            }
        }

        throw new BindException(format("Could not find enum value %s.%s", type.getSimpleName(), value));
    }
}


/*
 * Copyright (c) 2008-2016, Hazelcast, Inc. All Rights Reserved.
 *
 * 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.hazelcast.simulator.utils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import static com.hazelcast.simulator.utils.Preconditions.checkNotNull;
import static java.lang.String.format;

public final class ReflectionUtils {

    private ReflectionUtils() {
    }

    public static List<Field> getFields(Class classType, Class<? extends Annotation> annotationClass) {
        List<Field> fields = new ArrayList<Field>();
        do {
            for (Field field : classType.getDeclaredFields()) {
                if (field.isAnnotationPresent(annotationClass)) {
                    fields.add(field);
                }
            }
            classType = classType.getSuperclass();
        } while (classType != null);
        return fields;
    }

    public static Field getField(Class classType, String fieldName, Class fieldType) {
        Field field;
        do {
            field = findField(classType, fieldName, fieldType);
            if (field != null) {
                return field;
            }
            classType = classType.getSuperclass();
        } while (classType != null);

        return null;
    }

    public static void setFieldValue(Object instance, Field field, Object value) {
        field.setAccessible(true);
        setFieldValue0(instance, field, value);
    }

    public static <E> E getFieldValue(Object instance, String fieldName) {
        checkNotNull(instance, "Object to retrieve field from can't be null");

        Field field;
        Class<?> clazz = instance.getClass();
        try {
            field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
        } catch (Exception e) {
            throw new ReflectionException(e);
        }
        return getFieldValue0(instance, field, clazz.getName(), fieldName);
    }

    /**
     * Gets the value for a static field.
     *
     * @param clazz     class which contains the field
     * @param fieldName name of the field
     * @param fieldType type of the field
     * @param <E>       type of the field
     * @return the value of the static field
     */
    public static <E> E getStaticFieldValue(Class clazz, String fieldName, Class fieldType) {
        Field field = getField(clazz, fieldName, fieldType);
        if (field == null) {
            throw new ReflectionException(format("Field %s.%s is not found", clazz.getName(), fieldName));
        }

        field.setAccessible(true);
        return getFieldValue0(null, field, clazz.getName(), fieldName);
    }

    /**
     * Searches a method by name.
     *
     * @param clazz      Class to scan
     * @param methodName Name of the method
     * @return the found method or <tt>null</tt> if no method was found
     */
    public static Method getMethodByName(Class clazz, String methodName) {
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.getName().equals(methodName)) {
                return method;
            }
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    public static <E> E invokeMethod(Object classInstance, Method method, Object... args) throws Exception {
        if (method == null) {
            return null;
        }

        try {
            return (E) method.invoke(classInstance, args);
        } catch (InvocationTargetException e) {
            if (e.getCause() instanceof Error) {
                throw (Error) e.getCause();
            }
            throw (Exception) e.getCause();
        }
    }

    public static <E> void invokePrivateConstructor(Class<E> classType) throws Exception {
        Constructor<E> constructor = classType.getDeclaredConstructor();
        constructor.setAccessible(true);
        constructor.newInstance();
    }

    static void setFieldValue0(Object instance, Field field, Object value) {
        try {
            field.set(instance, value);
        } catch (IllegalAccessException e) {
            throw new ReflectionException(e);
        }
    }

    @SuppressWarnings("unchecked")
    static <E> E getFieldValue0(Object instance, Field field, String className, String fieldName) {
        try {
            return (E) field.get(instance);
        } catch (IllegalAccessException e) {
            throw new ReflectionException(format("Failed to access %s.%s", className, fieldName), e);
        }
    }

    private static Field findField(Class classType, String fieldName, Class fieldType) {
        for (Field field : classType.getDeclaredFields()) {
            if (field.getName().equals(fieldName)) {
                boolean isAssignableToType = (fieldType != null && field.getType().isAssignableFrom(fieldType));
                boolean isPrimitiveType = (fieldType == null && field.getType().isPrimitive());
                if (isAssignableToType || isPrimitiveType) {
                    return field;
                }
            }
        }
        return null;
    }
}


/*
 * Copyright (c) 2008-2016, Hazelcast, Inc. All Rights Reserved.
 *
 * 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.hazelcast.simulator.agent.workerprocess;

import com.hazelcast.simulator.agent.operations.CreateWorkerOperation;
import com.hazelcast.simulator.coordinator.operations.FailureOperation;
import com.hazelcast.simulator.protocol.Promise;
import com.hazelcast.simulator.protocol.Server;
import com.hazelcast.simulator.protocol.core.SimulatorAddress;
import com.hazelcast.simulator.protocol.operation.LogOperation;
import com.hazelcast.simulator.utils.ThreadSpawner;
import org.apache.log4j.Logger;

import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

import static com.hazelcast.simulator.common.FailureType.WORKER_CREATE_ERROR;
import static com.hazelcast.simulator.protocol.core.SimulatorAddress.workerAddress;
import static com.hazelcast.simulator.utils.NativeUtils.getPID;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.log4j.Level.DEBUG;

public class WorkerProcessManager {

    private static final Logger LOGGER = Logger.getLogger(WorkerProcessManager.class);

    // we need a high number of threads so we can do things in parallel like starting up members.
    private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(200);

    private final ConcurrentMap<SimulatorAddress, WorkerProcess> workerProcesses
            = new ConcurrentHashMap<SimulatorAddress, WorkerProcess>();
    private final Server server;
    private final SimulatorAddress agentAddress;
    private final String publicAddress;

    public WorkerProcessManager(Server server, SimulatorAddress agentAddress, String publicAddress) {
        this.server = server;
        this.agentAddress = agentAddress;
        this.publicAddress = publicAddress;
    }

    public String getPublicAddress() {
        return publicAddress;
    }

    public SimulatorAddress getAgentAddress() {
        return agentAddress;
    }

    // launching is done asynchronous so we don't block the calling thread (messaging thread)
    public void launch(CreateWorkerOperation op, Promise promise) {
        WorkerParameters workerParameters = op.getWorkerParameters();

        // we add the pid to the worker-parameters so the worker can check if the agent is still alive.
        workerParameters.set("agent.pid", getPID());

        WorkerProcessLauncher launcher = new WorkerProcessLauncher(WorkerProcessManager.this, workerParameters);
        LaunchSingleWorkerTask task = new LaunchSingleWorkerTask(launcher, workerParameters, promise);
        executorService.schedule(task, op.getDelayMs(), MILLISECONDS);
    }

    public void add(SimulatorAddress workerAddress, WorkerProcess workerProcess) {
        workerProcesses.put(workerAddress, workerProcess);
    }

    public void remove(WorkerProcess process) {
        workerProcesses.remove(process.getAddress());
    }

    Collection<WorkerProcess> getWorkerProcesses() {
        return workerProcesses.values();
    }

    public void updateLastSeenTimestamp(SimulatorAddress workerAddress) {
        WorkerProcess workerProcess = workerProcesses.get(workerAddress);
        if (workerProcess == null) {
            LOGGER.warn("update LastSeenTimestamp for unknown WorkerJVM: " + workerAddress);
            return;
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Updated LastSeenTimestamp for: " + workerAddress);
        }
        workerProcess.updateLastSeen();
    }

    public void shutdown() {
        executorService.shutdown();
        ThreadSpawner spawner = new ThreadSpawner("workerJvmManagerShutdown", true);
        for (final WorkerProcess workerProcess : new ArrayList<WorkerProcess>(workerProcesses.values())) {
            spawner.spawn(new Runnable() {
                @Override
                public void run() {
                    shutdown(workerProcess);
                }
            });
        }
        spawner.awaitCompletion();
    }

    void shutdown(WorkerProcess workerProcess) {
        workerProcesses.remove(workerProcess.getAddress());
        try {
            // this sends SIGTERM on *nix
            Process process = workerProcess.getProcess();
            if (process == null) {
                LOGGER.error("TODO: Embedded worker not SHUTDOWN!!!");
            } else {
                process.destroy();
                process.waitFor();
            }
        } catch (Exception e) {
            LOGGER.error("Failed to destroy Worker process: " + workerProcess, e);
        }
    }

    final class LaunchSingleWorkerTask implements Runnable {

        private final WorkerProcessLauncher launcher;
        private final WorkerParameters parameters;
        private final Promise promise;

        private LaunchSingleWorkerTask(WorkerProcessLauncher launcher,
                                       WorkerParameters parameters,
                                       Promise promise) {
            this.launcher = launcher;
            this.parameters = parameters;
            this.promise = promise;
        }

        @Override
        public void run() {
            try {
                launch();

                // it was the last worker needing to be created; so lets answer the promise.
                promise.answer("SUCCESS");
            } catch (Exception e) {
                LOGGER.error("Failed to start Worker:" + workerProcesses, e);

                SimulatorAddress workerAddress
                        = workerAddress(agentAddress.getAddressIndex(), parameters.intGet("WORKER_INDEX"));

                server.sendCoordinator(new FailureOperation("Failed to start worker [" + workerAddress + "]",
                        WORKER_CREATE_ERROR, workerAddress, agentAddress.toString(), e));

                promise.answer(e.getMessage());
            } catch (Throwable t) {
                // just try to log; anything else than exception no point in dealing with promises
                LOGGER.error("Failed to start Worker", t);
            }
        }

        private void launch() throws Exception {
            launcher.launch();

            int workerIndex = parameters.intGet("WORKER_INDEX");

            String workerType = parameters.getWorkerType();
            SimulatorAddress workerAddress = workerAddress(agentAddress.getAgentIndex(), workerIndex);

            LogOperation logOperation = new LogOperation(
                    format("Created %s Worker %s", workerType, workerAddress), DEBUG);

            server.sendCoordinator(logOperation);
        }
    }
}


/*
 * Copyright (c) 2008-2016, Hazelcast, Inc. All Rights Reserved.
 *
 * 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.hazelcast.simulator.utils;

import org.apache.log4j.Logger;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.util.List;

import static com.hazelcast.simulator.utils.FileUtils.deleteQuiet;
import static com.hazelcast.simulator.utils.FileUtils.writeText;

public final class NativeUtils {

    private static final Logger LOGGER = Logger.getLogger(NativeUtils.class);

    private NativeUtils() {
    }

    public static String execute(String command) {
        return new BashCommand(command).execute();
    }

    public static String execute(String command, boolean throwException) {
        return new BashCommand(command)
                .setThrowsException(throwException)
                .execute();
    }

    /**
     * http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-process-id
     *
     * @return the PID of this JVM or <tt>-1</tt> if not PID was retrieved
     */
    public static int getPID() {
        Integer pid = getPidFromManagementBean();
        if (pid != null) {
            return pid;
        }
        pid = getPidViaReflection();
        if (pid != null) {
            return pid;
        }
        return -1;
    }

    public static String getInputArgs() {
        RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean();
        List<String> arguments = runtimeMxBean.getInputArguments();

        StringBuilder sb = new StringBuilder();
        for (String argument : arguments) {
            sb.append(argument);
            sb.append(' ');
        }
        return sb.toString();
    }

    static Integer getPidFromManagementBean() {
        String name = ManagementFactory.getRuntimeMXBean().getName();
        return getPidFromBeanString(name);
    }

    static Integer getPidFromBeanString(String name) {
        int indexOf = name.indexOf('@');
        if (indexOf == -1) {
            return null;
        }
        String pidString = name.substring(0, indexOf);
        try {
            return Integer.parseInt(pidString);
        } catch (NumberFormatException e) {
            LOGGER.warn(e);
            return null;
        }
    }

    static Integer getPidViaReflection() {
        try {
            RuntimeMXBean runtime = java.lang.management.ManagementFactory.getRuntimeMXBean();
            java.lang.reflect.Field jvm = runtime.getClass().getDeclaredField("jvm");
            jvm.setAccessible(true);
            Object management = jvm.get(runtime);
            java.lang.reflect.Method pidMethod = management.getClass().getDeclaredMethod("getProcessId");
            pidMethod.setAccessible(true);

            return (Integer) pidMethod.invoke(management);
        } catch (Exception e) {
            LOGGER.warn(e);
            return null;
        }
    }

    public static File writePid(File pidFile) {
        deleteQuiet(pidFile);
        writeText("" + getPID(), pidFile);
        return pidFile;
    }
}


/*
 * Copyright (c) 2008-2016, Hazelcast, Inc. All Rights Reserved.
 *
 * 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.hazelcast.simulator.utils;

import org.apache.log4j.Logger;

import javax.jms.Connection;
import java.io.Closeable;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collection;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import java.util.jar.JarFile;

import static com.hazelcast.simulator.utils.EmptyStatement.ignore;
import static com.hazelcast.simulator.utils.FormatUtils.NEW_LINE;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

public final class CommonUtils {

    private CommonUtils() {
    }

    public static String getSimulatorVersion() {
        String implementationVersion = CommonUtils.class.getPackage().getImplementationVersion();
        return (implementationVersion != null) ? implementationVersion : "SNAPSHOT";
    }

    public static RuntimeException rethrow(Throwable throwable) {
        if (throwable instanceof RuntimeException) {
            throw (RuntimeException) throwable;
        } else {
            throw new CommandLineExitException(throwable);
        }
    }

    public static String throwableToString(Throwable throwable) {
        StringWriter stringWriter = new StringWriter();
        throwable.printStackTrace(new PrintWriter(stringWriter));
        return stringWriter.toString();
    }

    public static void closeQuietly(Closeable... closeables) {
        for (Closeable c : closeables) {
            closeQuietly(c);
        }
    }

    public static void closeQuietly(Connection connection) {
        if (connection == null) {
            return;
        }

        try {
            connection.close();
        } catch (Exception ignore) {
            ignore(ignore);
        }
    }

    public static void closeQuietly(Collection<? extends Closeable> closeables) {
        for (Closeable c : closeables) {
            closeQuietly(c);
        }
    }

    public static void closeQuietly(Closeable closeable) {
        if (closeable == null) {
            return;
        }
        try {
            closeable.close();
        } catch (IOException ignore) {
            ignore(ignore);
        }
    }

    public static void closeQuietly(JarFile jarFile) {
        if (jarFile == null) {
            return;
        }
        try {
            jarFile.close();
        } catch (IOException ignore) {
            ignore(ignore);
        }
    }

    public static void joinThread(Thread thread) {
        if (thread == null) {
            return;
        }
        try {
            thread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void joinThread(Thread thread, long timeoutMillis) {
        if (thread == null) {
            return;
        }
        try {
            thread.join(timeoutMillis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void await(CountDownLatch latch) {
        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static boolean await(CountDownLatch latch, long timeout, TimeUnit timeUnit) {
        try {
            return latch.await(timeout, timeUnit);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return false;
    }

    public static void awaitTermination(ExecutorService executorService, long timeout, TimeUnit timeUnit) {
        try {
            executorService.awaitTermination(timeout, timeUnit);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static long currentTimeSeconds() {
        return MILLISECONDS.toSeconds(System.currentTimeMillis());
    }

    public static void sleepSeconds(long seconds) {
        try {
            SECONDS.sleep(seconds);
        } catch (InterruptedException ignore) {
            ignore(ignore);
        }
    }

    public static void sleepUntilMs(long deadlineMs) {
        long now = System.currentTimeMillis();
        long duration = deadlineMs - now;
        if (duration > 0) {
            sleepMillis(duration);
        }
    }

    public static void sleepMillis(long millis) {
        try {
            MILLISECONDS.sleep(millis);
        } catch (InterruptedException ignore) {
            ignore(ignore);
        }
    }

    public static void sleepNanos(long nanos) {
        if (nanos <= 0) {
            return;
        }
        LockSupport.parkNanos(nanos);
    }

    public static void sleepTimeUnit(TimeUnit timeUnit, long timeout) {
        try {
            timeUnit.sleep(timeout);
        } catch (InterruptedException e) {
            ignore(e);
        }
    }

    public static void sleepSecondsThrowException(int seconds) {
        try {
            SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            throw rethrow(e);
        }
    }

    public static void sleepMillisThrowException(int millis) {
        try {
            MILLISECONDS.sleep(millis);
        } catch (InterruptedException e) {
            throw rethrow(e);
        }
    }

    /**
     * Sleeps a random amount of time.
     * <p>
     * The call is ignored if maxDelayNanos equals or smaller than zero.
     *
     * @param random        the Random used to randomize
     * @param maxDelayNanos the maximum sleeping period in nanoseconds
     */
    public static void sleepRandomNanos(Random random, long maxDelayNanos) {
        if (maxDelayNanos <= 0) {
            return;
        }
        long randomValue = Math.abs(random.nextLong() + 1);
        sleepNanos(randomValue % maxDelayNanos);
    }

    public static long getElapsedSeconds(long started) {
        return NANOSECONDS.toSeconds(System.nanoTime() - started);
    }

    public static void exitWithError() {
        exit(1);
    }

    public static void exitWithError(Logger logger, String msg, Throwable throwable) {
        if (throwable instanceof CommandLineExitException) {
            String logMessage = throwable.getMessage();
            if (throwable.getCause() != null) {
                String throwableString = throwableToString(throwable.getCause());
                logMessage += NEW_LINE + throwableString;
            }
            logger.fatal(logMessage);
        } else {
            String throwableString = throwableToString(throwable);
            logger.fatal(msg + NEW_LINE + throwableString);
        }
        exit(1);
    }

    public static void exit(int code) {
        System.exit(code);
    }
}


/*
 * Copyright (c) 2008-2016, Hazelcast, Inc. All Rights Reserved.
 *
 * 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.hazelcast.simulator.utils;

import java.util.Iterator;
import java.util.Locale;

import static java.lang.String.format;
import static java.util.Arrays.fill;

public final class FormatUtils {

    public static final String NEW_LINE = System.getProperty("line.separator");
    public static final String HORIZONTAL_RULER = "=========================================================================";

    private static final double ONE_HUNDRED = 100;
    private static final int IP_ADDRESS_LENGTH = 15;
    private static final int PERCENTAGE_FORMAT_LENGTH = 6;
    private static final String DEFAULT_DELIMITER = ", ";

    private static final int SECONDS_PER_MINUTE = 60;
    private static final int MINUTES_PER_HOUR = 60;
    private static final int HOURS_PER_DAY = 24;

    private static final int SI_BYTES_FACTOR = 1000;
    private static final int IEC_BYTES_FACTOR = 1024;

    private FormatUtils() {
    }

    /**
     * Formats an IP address by adding padding to the left.
     *
     * @param ipAddress the IP address
     * @return the formatted IP address
     */
    public static String formatIpAddress(String ipAddress) {
        return padLeft(ipAddress, IP_ADDRESS_LENGTH);
    }

    /**
     * Formats a percentage of two numbers and adds padding to the left.
     *
     * @param value     the value of the percentage
     * @param baseValue the base value of the percentage
     * @return the formatted percentage
     */
    public static String formatPercentage(long value, long baseValue) {
        double percentage = (baseValue > 0 ? (ONE_HUNDRED * value) / baseValue : 0);
        return formatDouble(percentage, PERCENTAGE_FORMAT_LENGTH);
    }

    /**
     * Formats a double number and adds padding to the left.
     *
     * Very inefficient implementation, but a lot easier than to deal with the formatting API.
     *
     * @param number number to format
     * @param length width of padding
     * @return formatted number
     */
    public static String formatDouble(double number, int length) {
        return padLeft(format(Locale.US, "%,.2f", number), length);
    }

    /**
     * Formats a long number and adds padding to the left.
     *
     * Very inefficient implementation, but a lot easier than to deal with the formatting API.
     *
     * @param number number to format
     * @param length width of padding
     * @return formatted number
     */
    public static String formatLong(long number, int length) {
        return padLeft(format(Locale.US, "%,d", number), length);
    }

    public static String padRight(String argument, int length) {
        if (length <= 0) {
            return argument;
        }
        return format("%-" + length + "s", argument);
    }

    public static String padLeft(String argument, int length) {
        if (length <= 0) {
            return argument;
        }
        return format("%" + length + "s", argument);
    }

    public static String fillString(int length, char charToFill) {
        if (length == 0) {
            return "";
        }
        char[] array = new char[length];
        fill(array, charToFill);
        return new String(array);
    }

    public static String secondsToHuman(long seconds) {
        long time = seconds;

        long moduloSeconds = time % SECONDS_PER_MINUTE;
        time /= SECONDS_PER_MINUTE;

        long minutes = time % MINUTES_PER_HOUR;
        time /= MINUTES_PER_HOUR;

        long hours = time % HOURS_PER_DAY;
        time /= HOURS_PER_DAY;

        long days = time;

        return format("%02dd %02dh %02dm %02ds", days, hours, minutes, moduloSeconds);
    }

    public static String humanReadableByteCount(long bytes, boolean si) {
        int unit = si ? SI_BYTES_FACTOR : IEC_BYTES_FACTOR;
        if (bytes < unit) {
            return bytes + " B";
        }
        int exp = (int) (Math.log(bytes) / Math.log(unit));
        String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
        return format(Locale.US, "%.1f %sB", bytes / Math.pow(unit, exp), pre);
    }

    public static String join(Iterable<?> collection) {
        return join(collection, DEFAULT_DELIMITER);
    }

    public static String join(Iterable<?> collection, String delimiter) {
        StringBuilder builder = new StringBuilder();
        Iterator<?> iterator = collection.iterator();
        while (iterator.hasNext()) {
            Object entry = iterator.next();
            builder.append(entry);
            if (iterator.hasNext()) {
                builder.append(delimiter);
            }
        }
        return builder.toString();
    }
}


/*
 * Copyright (C) 2016 Square, Inc.
 *
 * 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.squareup.whorlwind;

import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.Log;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.functions.Action;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.spec.KeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.crypto.Cipher;
import okio.ByteString;

import static android.Manifest.permission.USE_BIOMETRIC;
import static android.Manifest.permission.USE_FINGERPRINT;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;

@RequiresApi(Build.VERSION_CODES.M)
final class RealWhorlwind extends Whorlwind {
  private final Context context;
  private final FingerprintManager fingerprintManager;
  @SuppressWarnings("WeakerAccess") // Used in nested class. Removing synthetic accessor.
  final Storage storage;
  private final String keyAlias;
  private final KeyStore keyStore;
  private final KeyPairGenerator keyGenerator;
  private final KeyFactory keyFactory;
  private final AtomicBoolean readerScanning;
  @SuppressWarnings("WeakerAccess") // Used in nested class. Removing synthetic accessor.
  final Object dataLock = new Object();

  RealWhorlwind(Context context, FingerprintManager fingerprintManager, Storage storage,
      String keyAlias, KeyStore keyStore, KeyPairGenerator keyGenerator, KeyFactory keyFactory) {
    this.context = context;
    this.fingerprintManager = fingerprintManager;
    this.storage = storage;
    this.keyAlias = keyAlias;
    this.keyStore = keyStore;
    this.keyGenerator = keyGenerator;
    this.keyFactory = keyFactory;

    readerScanning = new AtomicBoolean();
  }

  @Override public boolean canStoreSecurely() {
    return hasPermission()
        && isHardwareDetected(fingerprintManager)
        && hasEnrolledFingerprints(fingerprintManager);
  }

  private boolean hasPermission() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
      return checkSelfPermission(USE_FINGERPRINT) == PERMISSION_GRANTED;
    } else {
      return checkSelfPermission(USE_BIOMETRIC) == PERMISSION_GRANTED;
    }
  }

  private int checkSelfPermission(String permission) {
    return context.checkPermission(permission, android.os.Process.myPid(),
        android.os.Process.myUid());
  }

  void checkCanStoreSecurely() {
    if (!canStoreSecurely()) {
      throw new IllegalStateException(
          "Can't store securely. Check canStoreSecurely() before attempting to read/write.");
    }
  }

  @Override public Completable write(@NonNull final String name, @Nullable final ByteString value) {
    return Completable.fromAction(new Action() {
      @Override public void run() throws Exception {
        checkCanStoreSecurely();

        synchronized (dataLock) {
          if (value == null) {
            storage.remove(name);
            return;
          }

          prepareKeyStore();

          Cipher cipher = createCipher();
          cipher.init(Cipher.ENCRYPT_MODE, getPublicKey());

          storage.put(name, ByteString.of(cipher.doFinal(value.toByteArray())));
        }
      }
    });
  }

  @Override public Observable<ReadResult> read(@NonNull String name) {
    return Observable.create(new FingerprintAuthOnSubscribe(fingerprintManager, storage, name, //
        readerScanning, dataLock, this));
  }

  /**
   * Prepares the key store and our keys for encrypting/decrypting. Keys will be generated if we
   * haven't done so yet, and keys will be re-generated if the old ones have been invalidated. In
   * both cases, our K/V store will be cleared before continuing.
   */
  void prepareKeyStore() {
    try {
      Key key = keyStore.getKey(keyAlias, null);
      Certificate certificate = keyStore.getCertificate(keyAlias);
      if (key != null && certificate != null) {
        try {
          createCipher().init(Cipher.DECRYPT_MODE, key);

          // We have a keys in the store and they're still valid.
          return;
        } catch (KeyPermanentlyInvalidatedException e) {
          Log.d(TAG, "Key invalidated.");
        }
      }

      storage.clear();

      keyGenerator.initialize(new KeyGenParameterSpec.Builder(keyAlias,
          KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) //
          .setBlockModes(KeyProperties.BLOCK_MODE_ECB) //
          .setUserAuthenticationRequired(true) //
          .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) //
          .build());

      keyGenerator.generateKeyPair();
    } catch (GeneralSecurityException e) {
      throw new RuntimeException(e);
    }
  }

  static Cipher createCipher() throws GeneralSecurityException {
    return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_RSA
        + "/"
        + KeyProperties.BLOCK_MODE_ECB
        + "/"
        + KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1);
  }

  @SuppressWarnings("WeakerAccess") PublicKey getPublicKey() throws GeneralSecurityException {
    PublicKey publicKey = keyStore.getCertificate(keyAlias).getPublicKey();

    // In contradiction to the documentation, the public key returned from the key store is only
    // unlocked after the user has authenticated with their fingerprint. This is unnecessary
    // (and broken) for encryption using asynchronous keys, so we work around this by re-creating
    // our own copy of the key. See known issues at
    // http://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.html
    KeySpec spec = new X509EncodedKeySpec(publicKey.getEncoded());
    return keyFactory.generatePublic(spec);
  }

  PrivateKey getPrivateKey() throws GeneralSecurityException {
    return (PrivateKey) keyStore.getKey(keyAlias, null);
  }

  void removeKey() {
    try {
      keyStore.deleteEntry(keyAlias);
    } catch (Exception e) {
      Log.d(TAG, "Remove key failed", e);
    }
  }
}


/*
 * Thrifty
 *
 * Copyright (c) Microsoft Corporation
 *
 * All rights reserved.
 *
 * 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
 *
 * THIS CODE IS PROVIDED ON AN  *AS IS* BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
 * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE,
 * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
 *
 * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
 */

/*
 * This file is derived from the file TCompactProtocol.java, in the Apache
 * Thrift implementation.  The original license header is reproduced
 * below:
 */

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.microsoft.thrifty.protocol;

import com.microsoft.thrifty.TType;
import com.microsoft.thrifty.transport.Transport;
import okio.Buffer;
import okio.ByteString;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.ProtocolException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;

/**
 * Json protocol implementation for thrift.
 * <p>
 * This is a full-featured protocol supporting write and read.
 */
public class JsonProtocol extends Protocol {

    private static final byte[] COMMA = new byte[]{','};
    private static final byte[] COLON = new byte[]{':'};
    private static final byte[] LBRACE = new byte[]{'{'};
    private static final byte[] RBRACE = new byte[]{'}'};
    private static final byte[] LBRACKET = new byte[]{'['};
    private static final byte[] RBRACKET = new byte[]{']'};
    private static final byte[] QUOTE = new byte[]{'"'};
    private static final byte[] BACKSLASH = new byte[]{'\\'};

    private static final byte[] ESCSEQ = new byte[]{'\\', 'u', '0', '0'};

    private static final long VERSION = 1;

    private static final byte[] JSON_CHAR_TABLE = {
            /*  0 1 2 3 4 5 6 7 8 9 A B C D E F */
            0, 0, 0, 0, 0, 0, 0, 0, 'b', 't', 'n', 0, 'f', 'r', 0, 0, // 0
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1
            1, 1, '"', 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2
    };

    private static final String ESCAPE_CHARS = "\"\\/bfnrt";

    private static final byte[] ESCAPE_CHAR_VALS = {
            '"', '\\', '/', '\b', '\f', '\n', '\r', '\t',
    };

    // Stack of nested contexts that we may be in
    private ArrayDeque<JsonBaseContext> contextStack = new ArrayDeque<>();

    // Current context that we are in
    private JsonBaseContext context = new JsonBaseContext();

    // Reader that manages a 1-byte buffer
    private LookaheadReader reader = new LookaheadReader();

    // Write out the TField names as a string instead of the default integer value
    private boolean fieldNamesAsString = false;

    // Push a new Json context onto the stack.
    private void pushContext(JsonBaseContext c) {
        contextStack.push(context);
        context = c;
    }

    // Pop the last Json context off the stack
    private void popContext() {
        context = contextStack.pop();
    }

    // Reset the context stack to its initial state
    private void resetContext() {
        while (!contextStack.isEmpty()) {
            popContext();
        }
    }

    public JsonProtocol(Transport transport) {
        super(transport);
    }

    public JsonProtocol(Transport transport, boolean fieldNamesAsString) {
        super(transport);
        this.fieldNamesAsString = fieldNamesAsString;
    }

    @Override
    public void reset() {
        contextStack.clear();
        context = new JsonBaseContext();
        reader = new LookaheadReader();
    }

    // Temporary buffer used by several methods
    private byte[] tmpbuf = new byte[4];

    // Read a byte that must match b[0]; otherwise an exception is thrown.
    // Marked protected to avoid synthetic accessor in JsonListContext.read
    // and JsonPairContext.read
    protected void readJsonSyntaxChar(byte[] b) throws IOException {
        byte ch = reader.read();
        if (ch != b[0]) {
            throw new ProtocolException("Unexpected character:" + (char) ch);
        }
    }

    // Convert a byte containing a hex char ('0'-'9' or 'a'-'f') into its
    // corresponding hex value
    private static byte hexVal(byte ch) throws IOException {
        if ((ch >= '0') && (ch <= '9')) {
            return (byte) ((char) ch - '0');
        } else if ((ch >= 'a') && (ch <= 'f')) {
            return (byte) ((char) ch - 'a' + 10);
        } else {
            throw new ProtocolException("Expected hex character");
        }
    }

    // Convert a byte containing a hex value to its corresponding hex character
    private static byte hexChar(byte val) {
        val = (byte) (val & 0x0F);
        if (val < 10) {
            return (byte) ((char) val + '0');
        } else {
            return (byte) ((char) (val - 10) + 'a');
        }
    }

    // Write the bytes in array buf as a Json characters, escaping as needed
    private void writeJsonString(byte[] b) throws IOException {
        context.write();
        transport.write(QUOTE);
        int len = b.length;
        for (int i = 0; i < len; i++) {
            if ((b[i] & 0x00FF) >= 0x30) {
                if (b[i] == BACKSLASH[0]) {
                    transport.write(BACKSLASH);
                    transport.write(BACKSLASH);
                } else {
                    transport.write(b, i, 1);
                }
            } else {
                tmpbuf[0] = JSON_CHAR_TABLE[b[i]];
                if (tmpbuf[0] == 1) {
                    transport.write(b, i, 1);
                } else if (tmpbuf[0] > 1) {
                    transport.write(BACKSLASH);
                    transport.write(tmpbuf, 0, 1);
                } else {
                    transport.write(ESCSEQ);
                    tmpbuf[0] = hexChar((byte) (b[i] >> 4));
                    tmpbuf[1] = hexChar(b[i]);
                    transport.write(tmpbuf, 0, 2);
                }
            }
        }
        transport.write(QUOTE);
    }

    // Write out number as a Json value. If the context dictates so, it will be
    // wrapped in quotes to output as a Json string.
    private void writeJsonInteger(long num) throws IOException {
        context.write();
        String str = Long.toString(num);
        boolean escapeNum = context.escapeNum();
        if (escapeNum) {
            transport.write(QUOTE);
        }
        try {
            byte[] buf = str.getBytes("UTF-8");
            transport.write(buf);
        } catch (UnsupportedEncodingException e) {
            throw new AssertionError(e);
        }
        if (escapeNum) {
            transport.write(QUOTE);
        }
    }

    // Write out a double as a Json value. If it is NaN or infinity or if the
    // context dictates escaping, write out as Json string.
    private void writeJsonDouble(double num) throws IOException {
        context.write();
        String str = Double.toString(num);
        boolean special = false;
        switch (str.charAt(0)) {
            case 'N': // NaN
            case 'I': // Infinity
                special = true;
                break;
            case '-':
                if (str.charAt(1) == 'I') { // -Infinity
                    special = true;
                }
                break;
            default:
                break;
        }

        boolean escapeNum = special || context.escapeNum();
        if (escapeNum) {
            transport.write(QUOTE);
        }
        try {
            byte[] b = str.getBytes("UTF-8");
            transport.write(b, 0, b.length);
        } catch (UnsupportedEncodingException e) {
            throw new AssertionError(e);
        }
        if (escapeNum) {
            transport.write(QUOTE);
        }
    }

    private void writeJsonObjectStart() throws IOException {
        context.write();
        transport.write(LBRACE);
        pushContext(new JsonPairContext());
    }

    private void writeJsonObjectEnd() throws IOException {
        popContext();
        transport.write(RBRACE);
    }

    private void writeJsonArrayStart() throws IOException {
        context.write();
        transport.write(LBRACKET);
        pushContext(new JsonListContext());
    }

    private void writeJsonArrayEnd() throws IOException {
        popContext();
        transport.write(RBRACKET);
    }

    @Override
    public void writeMessageBegin(String name, byte typeId, int seqId) throws IOException {
        resetContext(); // THRIFT-3743
        writeJsonArrayStart();
        writeJsonInteger(VERSION);
        try {
            byte[] b = name.getBytes("UTF-8");
            writeJsonString(b);
        } catch (UnsupportedEncodingException e) {
            throw new AssertionError(e);
        }
        writeJsonInteger(typeId);
        writeJsonInteger(seqId);
    }

    @Override
    public void writeMessageEnd() throws IOException {
        writeJsonArrayEnd();
    }

    @Override
    public void writeStructBegin(String structName) throws IOException {
        writeJsonObjectStart();
    }

    @Override
    public void writeStructEnd() throws IOException {
        writeJsonObjectEnd();
    }

    @Override
    public void writeFieldBegin(String fieldName, int fieldId, byte typeId) throws IOException {
        if (fieldNamesAsString) {
            writeString(fieldName);
        } else {
            writeJsonInteger(fieldId);
        }
        writeJsonObjectStart();
        writeJsonString(JsonTypes.ttypeToJson(typeId));
    }

    @Override
    public void writeFieldEnd() throws IOException {
        writeJsonObjectEnd();
    }

    @Override
    public void writeFieldStop() {
    }

    @Override
    public void writeMapBegin(byte keyTypeId, byte valueTypeId, int mapSize) throws IOException {
        writeJsonArrayStart();
        writeJsonString(JsonTypes.ttypeToJson(keyTypeId));
        writeJsonString(JsonTypes.ttypeToJson(valueTypeId));
        writeJsonInteger(mapSize);
        writeJsonObjectStart();
    }

    @Override
    public void writeMapEnd() throws IOException {
        writeJsonObjectEnd();
        writeJsonArrayEnd();
    }

    @Override
    public void writeListBegin(byte elementTypeId, int listSize) throws IOException {
        writeJsonArrayStart();
        writeJsonString(JsonTypes.ttypeToJson(elementTypeId));
        writeJsonInteger(listSize);
    }

    @Override
    public void writeListEnd() throws IOException {
        writeJsonArrayEnd();
    }

    @Override
    public void writeSetBegin(byte elementTypeId, int setSize) throws IOException {
        writeJsonArrayStart();
        writeJsonString(JsonTypes.ttypeToJson(elementTypeId));
        writeJsonInteger(setSize);
    }

    @Override
    public void writeSetEnd() throws IOException {
        writeJsonArrayEnd();
    }

    @Override
    public void writeBool(boolean b) throws IOException {
        writeJsonInteger(b ? (long) 1 : (long) 0);
    }

    @Override
    public void writeByte(byte b) throws IOException {
        writeJsonInteger((long) b);
    }

    @Override
    public void writeI16(short i16) throws IOException {
        writeJsonInteger((long) i16);
    }

    @Override
    public void writeI32(int i32) throws IOException {
        writeJsonInteger((long) i32);
    }

    @Override
    public void writeI64(long i64) throws IOException {
        writeJsonInteger(i64);
    }

    @Override
    public void writeDouble(double dub) throws IOException {
        writeJsonDouble(dub);
    }

    @Override
    public void writeString(String str) throws IOException {
        try {
            byte[] b = str.getBytes("UTF-8");
            writeJsonString(b);
        } catch (UnsupportedEncodingException e) {
            throw new AssertionError(e);
        }
    }

    @Override
    public void writeBinary(ByteString buf) throws IOException {
        writeString(buf.base64());
    }

    /**
     * Reading methods.
     */

    // Read in a Json string, unescaping as appropriate.. Skip reading from the
    // context if skipContext is true.
    private ByteString readJsonString(boolean skipContext)
            throws IOException {
        Buffer buffer = new Buffer();
        ArrayList<Character> codeunits = new ArrayList<>();
        if (!skipContext) {
            context.read();
        }
        readJsonSyntaxChar(QUOTE);
        while (true) {
            byte ch = reader.read();
            if (ch == QUOTE[0]) {
                break;
            }
            if (ch == ESCSEQ[0]) {
                ch = reader.read();
                if (ch == ESCSEQ[1]) {
                    transport.read(tmpbuf, 0, 4);
                    short cu = (short) (
                            ((short) hexVal(tmpbuf[0]) << 12)
                                    + ((short) hexVal(tmpbuf[1]) << 8)
                                    + ((short) hexVal(tmpbuf[2]) << 4)
                                    + (short) hexVal(tmpbuf[3]));
                    try {
                        if (Character.isHighSurrogate((char) cu)) {
                            if (codeunits.size() > 0) {
                                throw new ProtocolException("Expected low surrogate char");
                            }
                            codeunits.add((char) cu);
                        } else if (Character.isLowSurrogate((char) cu)) {
                            if (codeunits.size() == 0) {
                                throw new ProtocolException("Expected high surrogate char");
                            }

                            codeunits.add((char) cu);
                            buffer.write(new String(new int[]{codeunits.get(0), codeunits.get(1)}, 0, 2)
                                    .getBytes("UTF-8"));
                            codeunits.clear();
                        } else {
                            buffer.write(new String(new int[]{cu}, 0, 1).getBytes("UTF-8"));
                        }
                        continue;
                    } catch (UnsupportedEncodingException e) {
                        throw new AssertionError(e);
                    } catch (IOException ex) {
                        throw new ProtocolException("Invalid unicode sequence");
                    }
                } else {
                    int off = ESCAPE_CHARS.indexOf(ch);
                    if (off == -1) {
                        throw new ProtocolException("Expected control char");
                    }
                    ch = ESCAPE_CHAR_VALS[off];
                }
            }
            buffer.write(new byte[]{ch});
        }
        return buffer.readByteString();
    }

    // Return true if the given byte could be a valid part of a Json number.
    private boolean isJsonNumeric(byte b) {
        switch (b) {
            case '+':
            case '-':
            case '.':
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case 'E':
            case 'e':
                return true;
        }
        return false;
    }

    // Read in a sequence of characters that are all valid in Json numbers. Does
    // not do a complete regex check to validate that this is actually a number.
    private String readJsonNumericChars() throws IOException {
        StringBuilder strbld = new StringBuilder();
        while (true) {
            byte ch = reader.peek();
            if (!isJsonNumeric(ch)) {
                break;
            }
            strbld.append((char) reader.read());
        }
        return strbld.toString();
    }

    // Read in a Json number. If the context dictates, read in enclosing quotes.
    private long readJsonInteger() throws IOException {
        context.read();
        if (context.escapeNum()) {
            readJsonSyntaxChar(QUOTE);
        }
        String str = readJsonNumericChars();
        if (context.escapeNum()) {
            readJsonSyntaxChar(QUOTE);
        }
        try {
            return Long.valueOf(str);
        } catch (NumberFormatException ex) {
            throw new ProtocolException("Bad data encountered in numeric data");
        }
    }

    // Read in a Json double value. Throw if the value is not wrapped in quotes
    // when expected or if wrapped in quotes when not expected.
    private double readJsonDouble() throws IOException {
        context.read();
        if (reader.peek() == QUOTE[0]) {
            ByteString str = readJsonString(true);
            double dub = Double.valueOf(str.utf8());
            if (!context.escapeNum() && !Double.isNaN(dub)
                    && !Double.isInfinite(dub)) {
                // Throw exception -- we should not be in a string in this case
                throw new ProtocolException("Numeric data unexpectedly quoted");
            }
            return dub;
        } else {
            if (context.escapeNum()) {
                // This will throw - we should have had a quote if escapeNum == true
                readJsonSyntaxChar(QUOTE);
            }
            try {
                return Double.valueOf(readJsonNumericChars());
            } catch (NumberFormatException ex) {
                throw new ProtocolException("Bad data encountered in numeric data");
            }
        }
    }

    // Read in a Json string containing base-64 encoded data and decode it.
    private ByteString readJsonBase64() throws IOException {
        ByteString str = readJsonString(false);
        return ByteString.decodeBase64(str.utf8());
    }

    private void readJsonObjectStart() throws IOException {
        context.read();
        readJsonSyntaxChar(LBRACE);
        pushContext(new JsonPairContext());
    }

    private void readJsonObjectEnd() throws IOException {
        readJsonSyntaxChar(RBRACE);
        popContext();
    }

    private void readJsonArrayStart() throws IOException {
        context.read();
        readJsonSyntaxChar(LBRACKET);
        pushContext(new JsonListContext());
    }

    private void readJsonArrayEnd() throws IOException {
        readJsonSyntaxChar(RBRACKET);
        popContext();
    }

    @Override
    public MessageMetadata readMessageBegin() throws IOException {
        resetContext(); // THRIFT-3743
        readJsonArrayStart();
        if (readJsonInteger() != VERSION) {
            throw new ProtocolException("Message contained bad version.");
        }
        String name;
        try {
            name = readJsonString(false).utf8();
        } catch (UnsupportedEncodingException ex) {
            throw new AssertionError(ex);
        }
        byte type = (byte) readJsonInteger();
        int seqid = (int) readJsonInteger();
        return new MessageMetadata(name, type, seqid);
    }

    @Override
    public void readMessageEnd() throws IOException {
        readJsonArrayEnd();
    }

    @Override
    public StructMetadata readStructBegin() throws IOException {
        readJsonObjectStart();
        return new StructMetadata("");
    }

    @Override
    public void readStructEnd() throws IOException {
        readJsonObjectEnd();
    }

    @Override
    public FieldMetadata readFieldBegin() throws IOException {
        byte ch = reader.peek();
        byte type;
        short id = 0;
        if (ch == RBRACE[0]) {
            type = TType.STOP;
        } else {
            id = (short) readJsonInteger();
            readJsonObjectStart();
            type = JsonTypes.jsonToTtype(readJsonString(false).toByteArray());
        }
        return new FieldMetadata("", type, id);
    }

    @Override
    public void readFieldEnd() throws IOException {
        readJsonObjectEnd();
    }

    @Override
    public MapMetadata readMapBegin() throws IOException {
        readJsonArrayStart();
        byte keyType = JsonTypes.jsonToTtype(readJsonString(false).toByteArray());
        byte valueType = JsonTypes.jsonToTtype(readJsonString(false).toByteArray());
        int size = (int) readJsonInteger();
        readJsonObjectStart();
        return new MapMetadata(keyType, valueType, size);
    }

    @Override
    public void readMapEnd() throws IOException {
        readJsonObjectEnd();
        readJsonArrayEnd();
    }

    @Override
    public ListMetadata readListBegin() throws IOException {
        readJsonArrayStart();
        byte elemType = JsonTypes.jsonToTtype(readJsonString(false).toByteArray());
        int size = (int) readJsonInteger();
        return new ListMetadata(elemType, size);
    }

    @Override
    public void readListEnd() throws IOException {
        readJsonArrayEnd();
    }

    @Override
    public SetMetadata readSetBegin() throws IOException {
        readJsonArrayStart();
        byte elemType = JsonTypes.jsonToTtype(readJsonString(false).toByteArray());
        int size = (int) readJsonInteger();
        return new SetMetadata(elemType, size);
    }

    @Override
    public void readSetEnd() throws IOException {
        readJsonArrayEnd();
    }

    @Override
    public boolean readBool() throws IOException {
        return (readJsonInteger() == 0 ? false : true);
    }

    @Override
    public byte readByte() throws IOException {
        return (byte) readJsonInteger();
    }

    @Override
    public short readI16() throws IOException {
        return (short) readJsonInteger();
    }

    @Override
    public int readI32() throws IOException {
        return (int) readJsonInteger();
    }

    @Override
    public long readI64() throws IOException {
        return readJsonInteger();
    }

    @Override
    public double readDouble() throws IOException {
        return readJsonDouble();
    }

    @Override
    public String readString() throws IOException {
        return readJsonString(false).utf8();
    }

    @Override
    public ByteString readBinary() throws IOException {
        return readJsonBase64();
    }

    // Holds up to one byte from the transport
    protected class LookaheadReader {

        private boolean hasData;
        private byte[] data = new byte[1];

        // Return and consume the next byte to be read, either taking it from the
        // data buffer if present or getting it from the transport otherwise.
        protected byte read() throws IOException {
            if (hasData) {
                hasData = false;
            } else {
                transport.read(data, 0, 1);
            }
            return data[0];
        }

        // Return the next byte to be read without consuming, filling the data
        // buffer if it has not been filled already.
        protected byte peek() throws IOException {
            if (!hasData) {
                transport.read(data, 0, 1);
            }
            hasData = true;
            return data[0];
        }
    }

    private static final class JsonTypes {
        static final byte[] BOOLEAN = new byte[]{'t', 'f'};
        static final byte[] BYTE = new byte[]{'i', '8'};
        static final byte[] I16 = new byte[]{'i', '1', '6'};
        static final byte[] I32 = new byte[]{'i', '3', '2'};
        static final byte[] I64 = new byte[]{'i', '6', '4'};
        static final byte[] DOUBLE = new byte[]{'d', 'b', 'l'};
        static final byte[] STRUCT = new byte[]{'r', 'e', 'c'};
        static final byte[] STRING = new byte[]{'s', 't', 'r'};
        static final byte[] MAP = new byte[]{'m', 'a', 'p'};
        static final byte[] LIST = new byte[]{'l', 's', 't'};
        static final byte[] SET = new byte[]{'s', 'e', 't'};

        static byte[] ttypeToJson(byte typeId) {
            switch (typeId) {
                case TType.STOP:
                    throw new IllegalArgumentException("Unexpected STOP type");
                case TType.VOID:
                    throw new IllegalArgumentException("Unexpected VOID type");
                case TType.BOOL:
                    return BOOLEAN;
                case TType.BYTE:
                    return BYTE;
                case TType.DOUBLE:
                    return DOUBLE;
                case TType.I16:
                    return I16;
                case TType.I32:
                    return I32;
                case TType.I64:
                    return I64;
                case TType.STRING:
                    return STRING;
                case TType.STRUCT:
                    return STRUCT;
                case TType.MAP:
                    return MAP;
                case TType.SET:
                    return SET;
                case TType.LIST:
                    return LIST;
                default:
                    throw new IllegalArgumentException(
                            "Unknown TType ID: " + typeId);
            }
        }

        static byte jsonToTtype(byte[] jsonId) {
            byte result = TType.STOP;
            if (jsonId.length > 1) {
                switch (jsonId[0]) {
                    case 'd':
                        result = TType.DOUBLE;
                        break;
                    case 'i':
                        switch (jsonId[1]) {
                            case '8':
                                result = TType.BYTE;
                                break;
                            case '1':
                                result = TType.I16;
                                break;
                            case '3':
                                result = TType.I32;
                                break;
                            case '6':
                                result = TType.I64;
                                break;
                        }
                        break;
                    case 'l':
                        result = TType.LIST;
                        break;
                    case 'm':
                        result = TType.MAP;
                        break;
                    case 'r':
                        result = TType.STRUCT;
                        break;
                    case 's':
                        if (jsonId[1] == 't') {
                            result = TType.STRING;
                        } else {
                            result = TType.SET;
                        }
                        break;
                    case 't':
                        result = TType.BOOL;
                        break;
                }
            }
            if (result == TType.STOP) {
                throw new IllegalArgumentException(
                        "Unknown json type ID: " + Arrays.toString(jsonId));
            }
            return result;
        }

        private JsonTypes() {
            throw new AssertionError("no instances");
        }
    }

    // Base class for tracking Json contexts that may require inserting/reading
    // additional Json syntax characters
    // This base context does nothing.
    protected static class JsonBaseContext {
        protected void write() throws IOException {
        }

        protected void read() throws IOException {
        }

        protected boolean escapeNum() {
            return false;
        }
    }

    // Context for Json lists. Will insert/read commas before each item except
    // for the first one
    protected class JsonListContext extends JsonBaseContext {
        private boolean first = true;

        @Override
        protected void write() throws IOException {
            if (first) {
                first = false;
            } else {
                transport.write(COMMA);
            }
        }

        @Override
        protected void read() throws IOException {
            if (first) {
                first = false;
            } else {
                readJsonSyntaxChar(COMMA);
            }
        }
    }

    // Context for Json records. Will insert/read colons before the value portion
    // of each record pair, and commas before each key except the first. In
    // addition, will indicate that numbers in the key position need to be
    // escaped in quotes (since Json keys must be strings).
    protected class JsonPairContext extends JsonBaseContext {
        private boolean first = true;
        private boolean colon = true;

        @Override
        protected void write() throws IOException {
            if (first) {
                first = false;
                colon = true;
            } else {
                transport.write(colon ? COLON : COMMA);
                colon = !colon;
            }
        }

        @Override
        protected void read() throws IOException {
            if (first) {
                first = false;
                colon = true;
            } else {
                readJsonSyntaxChar(colon ? COLON : COMMA);
                colon = !colon;
            }
        }

        @Override
        protected boolean escapeNum() {
            return colon;
        }
    }
}


/*
 * Thrifty
 *
 * Copyright (c) Microsoft Corporation
 *
 * All rights reserved.
 *
 * 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
 *
 * THIS CODE IS PROVIDED ON AN  *AS IS* BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
 * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE,
 * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
 *
 * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
 */
package com.microsoft.thrifty.service;

import com.microsoft.thrifty.Struct;
import com.microsoft.thrifty.ThriftException;
import com.microsoft.thrifty.protocol.MessageMetadata;
import com.microsoft.thrifty.protocol.Protocol;

import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Implements a basic service client that executes methods synchronously.
 *
 * <p>Unlike the Apache implementation, there is no presumption made here about framed encoding
 * at the transport level.  If your backend requires framing, be sure to
 * configure your {@link Protocol} and {@link com.microsoft.thrifty.transport.Transport}
 * objects appropriately.
 */
public class ClientBase implements Closeable {

    /**
     * A sequence ID generator; contains the most-recently-used
     * sequence ID (or zero, if no calls have been made).
     */
    private final AtomicInteger seqId = new AtomicInteger(0);

    /**
     * A flag indicating whether the client is active and connected.
     */
    final AtomicBoolean running = new AtomicBoolean(true);

    private final Protocol protocol;

    protected ClientBase(Protocol protocol) {
        this.protocol = protocol;
    }

    /**
     * When invoked by a derived instance, sends the given call to the server.
     *
     * @param methodCall the remote method call to be invoked
     * @return the result of the method call
     */
    protected final Object execute(MethodCall<?> methodCall) throws Exception {
        if (!running.get()) {
            throw new IllegalStateException("Cannot write to a closed service client");
        }

        try {
            return invokeRequest(methodCall);
        } catch (ServerException e) {
            throw e.thriftException;
        }
    }

    /**
     * Closes this service client and the underlying protocol.
     *
     * Subclasses that override this method need to set {@link #running} to false and call {@link #closeProtocol()}.
     */
    @Override
    public void close() throws IOException {
        if (!running.compareAndSet(true, false)) {
            return;
        }
        closeProtocol();
    }

    void closeProtocol() {
        try {
            protocol.close();
        } catch (IOException ignored) {
            // nope
        }
    }

    /**
     * Send the given call to the server.
     *
     * @param call the remote method call to be invoked
     * @return the result of the method call
     * @throws ServerException wrapper around {@link ThriftException}. Callers should catch and unwrap this.
     * @throws IOException from the protocol
     * @throws Exception exception received from server implements {@link com.microsoft.thrifty.Struct}
     */
    final Object invokeRequest(MethodCall<?> call) throws Exception {
        boolean isOneWay = call.callTypeId == TMessageType.ONEWAY;
        int sid = seqId.incrementAndGet();

        protocol.writeMessageBegin(call.name, call.callTypeId, sid);
        call.send(protocol);
        protocol.writeMessageEnd();
        protocol.flush();

        if (isOneWay) {
            // No response will be received
            return null;
        }

        MessageMetadata metadata = protocol.readMessageBegin();

        if (metadata.seqId != sid) {
            throw new ThriftException(
                    ThriftException.Kind.BAD_SEQUENCE_ID,
                    "Unrecognized sequence ID");
        }

        if (metadata.type == TMessageType.EXCEPTION) {
            ThriftException e = ThriftException.read(protocol);
            protocol.readMessageEnd();
            throw new ServerException(e);
        } else if (metadata.type != TMessageType.REPLY) {
            throw new ThriftException(
                    ThriftException.Kind.INVALID_MESSAGE_TYPE,
                    "Invalid message type: " + metadata.type);
        }

        if (metadata.seqId != seqId.get()) {
            throw new ThriftException(
                    ThriftException.Kind.BAD_SEQUENCE_ID,
                    "Out-of-order response");
        }

        if (!metadata.name.equals(call.name)) {
            throw new ThriftException(
                    ThriftException.Kind.WRONG_METHOD_NAME,
                    "Unexpected method name in reply; expected " + call.name
                            + " but received " + metadata.name);
        }

        try {
            Object result = call.receive(protocol, metadata);
            protocol.readMessageEnd();
            return result;
        } catch (Exception e) {
            if (e instanceof Struct) {
                // Business as usual
                protocol.readMessageEnd();
            }
            throw e;
        }
    }

    static class ServerException extends Exception {
        final ThriftException thriftException;

        ServerException(ThriftException thriftException) {
            this.thriftException = thriftException;
        }
    }
}