/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat 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 io.fabric8.maven.core.util.kubernetes;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.validation.constraints.NotNull;

import io.fabric8.kubernetes.api.model.ObjectMeta;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Helper methods to compare the user configuration on entities
 */
public class UserConfigurationCompare {
    private static final transient Logger LOG = LoggerFactory.getLogger(UserConfigurationCompare.class);

    protected static final Set<String> ignoredProperties = new HashSet<>(Collections.singletonList("status"));

    /**
     * This method detects if the user has changed the configuration of an entity.
     *

     * It compares the user configuration of 2 object trees ignoring any
     * runtime status or timestamp information.
     *
     * @return true if the configurations are equal.
     */
    public static boolean configEqual(Object entity1, Object entity2) {
        if (entity1 == entity2) {
            return true;
        } else if (entity1 == null || entity2 == null) {
            return false;
        } else if (entity1 instanceof Map) {
            return configEqualMap((Map) entity1, castTo(Map.class, entity2));
        } else if (entity2 instanceof Map) {
            return configEqualMap((Map) entity1, castTo(Map.class, entity2));
        } else if (entity2 instanceof ObjectMeta) {
            return configEqualObjectMeta((ObjectMeta) entity1, castTo(ObjectMeta.class, entity2));
        } else if (entity1 instanceof Collection && entity2 instanceof Collection) {
            return collectionsEqual((Collection) entity1, (Collection) entity2);
        } else {
            Class<?> aClass = getCommonDenominator(entity1.getClass(), entity2.getClass());
            if (aClass != null) {
                Object castEntity2 = castTo(aClass, entity2);
                if (castEntity2 == null) {
                    return false;
                } else if (aClass.getPackage().getName().startsWith("io.fabric8")) {
                    return configEqualKubernetesDTO(entity1, entity2, aClass);
                }
            }
            return java.util.Objects.equals(entity1, entity2);
        }
    }

    static <L,R> boolean collectionsEqual(Collection<L> left, Collection<R> right) {
        return subCollection(left, right) && subCollection(right, left);
    }

    static <L, R> boolean itemExists(L item, Collection<R> collection) {
        for (R candidate : collection) {
            if (configEqual(item, candidate)) {
                return true;
            }
        }
        return false;
    }

    static <L, R> boolean subCollection(Collection<L> left, Collection<R> right) {
        for (L candidate : left) {
            if (!itemExists(candidate, right)) {
                return false;
            }
        }
        return true;
    }

    static Class getCommonDenominator(Class left, Class right) {
        if (left.equals(right)) {
            return left;
        } else if (left.isAssignableFrom(right)) {
            return getCommonDenominator(left, right.getSuperclass());
        } else if (right.isAssignableFrom(left)) {
            return getCommonDenominator(left.getSuperclass(), right);
        } else {
            //no match
            return null;
        }
    }

    /**
     * Compares 2 instances of the given Kubernetes DTO class to see if the user has changed their configuration.
     *

     * This method will ignore properties {@link #ignoredProperties} such as status or timestamp properties
     */
    protected static boolean configEqualKubernetesDTO(@NotNull Object entity1, @NotNull Object entity2, @NotNull Class<?> clazz) {
        // lets iterate through the objects making sure we've not
        BeanInfo beanInfo = null;
        try {
            beanInfo = Introspector.getBeanInfo(clazz);
        } catch (IntrospectionException e) {
            LOG.warn("Failed to get beanInfo for " + clazz.getName() + ". " + e, e);
            return false;
        }
        try {
            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                String name = propertyDescriptor.getName();
                if (ignoredProperties.contains(name)) {
                    continue;
                }
                Method readMethod = propertyDescriptor.getReadMethod();
                if (readMethod != null) {
                    Object value1 = invokeMethod(entity1, readMethod);
                    Object value2 = invokeMethod(entity2, readMethod);
                    if (value1 != null && value2 != null && !configEqual(value1, value2)) {
                        return false;
                    }
                }
            }
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    protected static Object invokeMethod(@NotNull Object entity, Method readMethod) throws InvocationTargetException, IllegalAccessException {
        try {
            return readMethod.invoke(entity);
        } catch (Exception e) {
            LOG.warn("Failed to invoke method " + readMethod + " on " + entity + ". " + e, e);
            throw e;
        }
    }

    protected static boolean configEqualObjectMeta(ObjectMeta entity1, ObjectMeta entity2) {
        if (entity1 == entity2) {
            return true;
        } else if (entity1 == null || entity2 == null) {
            return false;
        }
        // TODO should we ignore annotations?
        return java.util.Objects.equals(entity1.getName(), entity2.getName()) &&
                java.util.Objects.equals(entity1.getNamespace(), entity2.getNamespace()) &&
                configEqualMap(entity1.getLabels(), entity2.getLabels()) &&
                configEqualMap(entity1.getAnnotations(), entity2.getAnnotations());
    }

    protected static <T> T castTo(Class<T> clazz, Object entity) {
        if (clazz.isInstance(entity)) {
            return clazz.cast(entity);
        } else {
            if (entity != null) {
                LOG.warn("Invalid class " + entity.getClass().getName() + " when expecting " + clazz.getName() + " for instance: " + entity);
            }
            return null;
        }
    }

    protected static boolean configEqualMap(Map entity1, Map entity2) {
        if (entity1 == entity2) {
            return true;
        } else if (entity1 == null || entity2 == null) {
            return false;
        }
        int size1 = size(entity1);
        int size2 = size(entity2);
        if (size1 != size2) {
            return false;
        }
        Set<Map.Entry> entries = entity1.entrySet();
        for (Map.Entry entry : entries) {
            Object key = entry.getKey();
            Object value = entry.getValue();
            Object value2 = entity2.get(key);
            if (!configEqual(value, value2)) {
                return false;
            }
        }
        return true;
    }

    protected static boolean configEqualList(List v1, List v2) {
        int size1 = size(v1);
        int size2 = size(v2);
        if (size1 != size2) {
            return false;
        }
        int idx = 0;
        for (Object value : v1) {
            Object value2 = v2.get(idx++);
            if (!configEqual(value, value2)) {
                return false;
            }
        }
        return true;
    }

    protected static int size(Map map) {
        return (map == null) ? 0 : map.size();
    }

    protected static int size(Collection coll) {
        return (coll == null) ? 0 : coll.size();
    }

}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat 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 io.fabric8.maven.core.util.kubernetes;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.KubernetesList;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.maven.core.util.ResourceUtil;
import io.fabric8.openshift.api.model.Parameter;
import io.fabric8.openshift.api.model.Template;
import io.fabric8.openshift.client.OpenShiftClient;
import io.fabric8.openshift.client.OpenShiftNotAvailableException;
import org.apache.commons.lang3.StringUtils;

/**
 * @author roland
 * @since 23.05.17
 */
public class OpenshiftHelper {

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    public static final String DEFAULT_API_VERSION = "v1";

    public static OpenShiftClient asOpenShiftClient(KubernetesClient client) {
        if (client instanceof OpenShiftClient) {
            return (OpenShiftClient) client;
        }
        try {
            return client.adapt(OpenShiftClient.class);
        } catch (KubernetesClientException | OpenShiftNotAvailableException e) {
            return null;
        }
    }

    public static boolean isOpenShift(KubernetesClient client) {
        return client.isAdaptable(OpenShiftClient.class);
    }

    public static KubernetesList processTemplatesLocally(Template entity, boolean failOnMissingParameterValue) throws IOException {
        List<HasMetadata> objects = null;
        if (entity != null) {
            objects = entity.getObjects();
            if (objects == null || objects.isEmpty()) {
                return null;
            }
        }
        List<Parameter> parameters = entity != null ? entity.getParameters() : null;
        if (parameters != null && !parameters.isEmpty()) {
            String json = "{\"kind\": \"List\", \"apiVersion\": \"" + DEFAULT_API_VERSION + "\",\n" +
                          "  \"items\": " + ResourceUtil.toJson(objects) + " }";

            // lets make a few passes in case there's expressions in values
            for (int i = 0; i < 5; i++) {
                for (Parameter parameter : parameters) {
                    String name = parameter.getName();
                    String from = "${" + name + "}";
                    String value = parameter.getValue();

                    // TODO generate random strings for passwords etc!
                    if (StringUtils.isBlank(value)) {
                        if (failOnMissingParameterValue) {
                            throw new IllegalArgumentException("No value available for parameter name: " + name);
                        } else {
                            value = "";
                        }
                    }
                    json = json.replace(from, value);
                }
            }
            return  OBJECT_MAPPER.readerFor(KubernetesList.class).readValue(json);
        } else {
            KubernetesList answer = new KubernetesList();
            answer.setItems(objects);
            return answer;
        }
    }

    public static boolean isCancelled(String status) {
        return "Cancelled".equals(status);
    }

    public static boolean isFailed(String status) {
        return status != null && (status.startsWith("Fail") || status.startsWith("Error"));
    }

    public static boolean isCompleted(String status) {
        return "Complete".equals(status);
    }

    public static boolean isFinished(String status) {
        return isCompleted(status) || isFailed(status) || isCancelled(status);
    }

    public static Template combineTemplates(Template firstTemplate, Template template) {
        List<HasMetadata> objects = template.getObjects();
        if (objects != null) {
            for (HasMetadata object : objects) {
                addTemplateObject(firstTemplate, object);
            }
        }
        List<Parameter> parameters = firstTemplate.getParameters();
        if (parameters == null) {
            parameters = new ArrayList<>();
            firstTemplate.setParameters(parameters);
        }
        combineParameters(parameters, template.getParameters());
        String name = KubernetesHelper.getName(template);
        if (StringUtils.isNotBlank(name)) {
            // lets merge all the fabric8 annotations using the template id qualifier as a postfix
            Map<String, String> annotations = KubernetesHelper.getOrCreateAnnotations(firstTemplate);
            Map<String, String> otherAnnotations = KubernetesHelper.getOrCreateAnnotations(template);
            Set<Map.Entry<String, String>> entries = otherAnnotations.entrySet();
            for (Map.Entry<String, String> entry : entries) {
                String key = entry.getKey();
                String value = entry.getValue();
                if (!annotations.containsKey(key)) {
                    annotations.put(key, value);
                }
            }
        }
        return firstTemplate;
    }

    // =============================================================================================

    private static void combineParameters(List<Parameter> parameters, List<Parameter> otherParameters) {
        if (otherParameters != null && otherParameters.size() > 0) {
            Map<String, Parameter> map = new HashMap<>();
            for (Parameter parameter : parameters) {
                map.put(parameter.getName(), parameter);
            }
            for (Parameter otherParameter : otherParameters) {
                String name = otherParameter.getName();
                Parameter original = map.get(name);
                if (original == null) {
                    parameters.add(otherParameter);
                } else {
                    if (StringUtils.isNotBlank(original.getValue())) {
                        original.setValue(otherParameter.getValue());
                    }
                }
            }
        }
    }

    private static void addTemplateObject(Template template, HasMetadata object) {
        List<HasMetadata> objects = template.getObjects();
        objects.add(object);
        template.setObjects(objects);
    }

    public static boolean isOpenShiftClient(KubernetesClient kubernetes) {
        return asOpenShiftClient(kubernetes) != null;
    }
}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat 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 io.fabric8.maven.generator.api;

import java.util.Properties;

import io.fabric8.maven.core.config.ProcessorConfig;
import io.fabric8.maven.core.util.Configs;

/**
 */
public class GeneratorConfig {

    private static final String GENERATOR_PROP_PREFIX = "fabric8.generator";

    private final String name;
    private final ProcessorConfig config;
    private final Properties properties;

    public GeneratorConfig(Properties properties, String name, ProcessorConfig config) {
        this.config =  config;
        this.name = name;
        this.properties = properties;
    }

    /**
     * Get a configuration value
     *
     * @param key key to lookup. If it implements also {@link DefaultValueProvider} then use this for a default value
     * @return the defa
     */
    public String get(Configs.Key key) {
        return get(key, key.def());
    }

    /**
     * Get a config value with a default
     * @param key key part to lookup. The whole key is build up from prefix + "." + key. If key is null,
     *            then only the prefix is used for the lookup (this is suitable for enrichers having only one config option)
     * @param defaultVal the default value to use when the no config is set
     * @return the value looked up or the default value.
     */
    public String get(Configs.Key key, String defaultVal) {
        String keyVal = key != null ? key.name() : "";
        String val = config != null ? config.getConfig(name, key.name()) : null;
        if (val == null) {
            String fullKey = GENERATOR_PROP_PREFIX + "." + name + "." + key;
            val = Configs.getSystemPropertyWithMavenPropertyAsFallback(properties, fullKey);
        }
        return val != null ? val : defaultVal;
    }

}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat 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 io.fabric8.maven.watcher.api;

import java.util.Properties;

import io.fabric8.maven.core.config.ProcessorConfig;
import io.fabric8.maven.core.util.Configs;

/**
 * @author nicola
 * @since 09/02/17
 */
public class WatcherConfig {

    private static final String WATCHER_PROP_PREFIX = "fabric8.watcher";

    private final String name;
    private final ProcessorConfig config;
    private final Properties projectProperties;

    public WatcherConfig(Properties projectProperties, String name, ProcessorConfig config) {
        this.config = config;
        this.name = name;
        this.projectProperties = projectProperties;
    }

    /**
     * Get a configuration value
     *
     * @param key key to lookup
     * @return the value
     */
    public String get(Configs.Key key) {
        return get(key, key.def());
    }

    /**
     * Get a config value with a default. If no value is given, as a last resort, project properties are looked up.
     *
     * @param key key part to lookup. The whole key is build up from prefix + "." + key. If key is null,
     *            then only the prefix is used for the lookup (this is suitable for enrichers having only one config option)
     * @param defaultVal the default value to use when the no config is set
     * @return the value looked up or the default value.
     */
    public String get(Configs.Key key, String defaultVal) {
        String val = config != null ? config.getConfig(name, key.name()) : null;

        if (val == null) {
            String fullKey = WATCHER_PROP_PREFIX + "." + name + "." + key;
            val = Configs.getSystemPropertyWithMavenPropertyAsFallback(projectProperties, fullKey);
        }
        return val != null ? val : defaultVal;
    }

}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat 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 io.fabric8.maven.core.config;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugins.annotations.Parameter;

/**
 * Configuration for enrichers and generators
 *
 * @author roland
 * @since 24/07/16
 */
public class ProcessorConfig {

    public static final ProcessorConfig EMPTY = new ProcessorConfig();
    /**
     * Modules to includes, should holde <include> elements
     */
    @Parameter
    @JsonProperty(value = "includes")
    List<String> includes = new ArrayList<>();

    /**
     * Modules to excludes, should hold <exclude> elements
     */
    @Parameter
    @JsonProperty(value = "excludes")
    Set<String> excludes = new HashSet<>();

    /**
     * Configuration for enricher / generators
     */
    // See http://stackoverflow.com/questions/38628399/using-map-of-maps-as-maven-plugin-parameters/38642613 why
    // a "TreeMap" is used as parameter and not "Map<String, String>"
    @Parameter
    @JsonProperty(value = "config")
    Map<String, TreeMap> config = new HashMap<>();

    public ProcessorConfig() { }

    public ProcessorConfig(List<String> includes, Set<String> excludes, Map<String, TreeMap> config) {
        this.includes = includes != null ? includes : Collections.<String>emptyList();
        this.excludes = excludes != null ? excludes : Collections.<String>emptySet();
        if (config != null) {
            this.config = config;
        }
    }

    public String getConfig(String name, String key) {
        TreeMap processorMap =  config.get(name);
        return processorMap != null ? (String) processorMap.get(key) : null;
    }

    /**
     * Return full configuration as raw string-string values
     *
     * @param name name of the enricher / generator
     * @return unmodifiable map of the original config
     */
    public Map<String, String> getConfigMap(String name) {
        return config.containsKey(name) ?
            Collections.unmodifiableMap(config.get(name)) :
            Collections.<String, String>emptyMap();
    }

    /**
     * Order elements according to the order provided by the include statements.
     * If no includes has been configured, return the given list unaltered.
     * Otherwise arrange the elements from the list in to the include order and return a new
     * list.
     *
     * If an include specifies an element which does not exist, an exception is thrown.
     *
     * @param namedList the list to order
     * @param type a description used in an error message (like 'generator' or 'enricher')
     * @param <T> the concrete type
     * @return the ordered list according to the algorithm described above
     * @throws IllegalArgumentException if the includes reference an non existing element
     */
    public <T extends Named> List<T> prepareProcessors(List<T> namedList, String type) {
        List<T> ret = new ArrayList<>();
        Map<String, T> lookup = new HashMap<>();
        for (T named : namedList) {
            lookup.put(named.getName(), named);
        }
        for (String inc : includes) {
            if (use(inc)) {
                T named = lookup.get(inc);
                if (named == null) {
                    List<String> keys = new ArrayList<>(lookup.keySet());
                    Collections.sort(keys);
                    throw new IllegalArgumentException(
                        "No " + type + " with name '" + inc +
                        "' found to include. " +
                        "Please check spelling in your profile / config and your project dependencies. Included " + type + "s: " +
                        StringUtils.join(keys,", "));
                }
                ret.add(named);
            }
        }
        return ret;
    }

    public boolean use(String inc) {
        return !excludes.contains(inc) && includes.contains(inc);
    }

    /**
     * Clone a processorConfig, resulting in a copy of the given config
     *
     * @param processorConfig config to clone
     * @return the cloned config
     */
    public static ProcessorConfig cloneProcessorConfig(ProcessorConfig processorConfig) {
        return mergeProcessorConfigs(processorConfig);
    }

    /**
     * Merge in another processor configuration, with a lower priority. I.e. the latter a config is
     * in the argument list, the less priority it has. This means:
     *
     *

     *
A configuration earlier in the list overrides configuration later

     *
Includes and exclude earlier in the list take precedence of the includes/excludes later in the list

     *

     *
     * @param processorConfigs configs to merge into the current config
     * @return a merged configuration for convenience of chaining and returning. This is a new object and can e.g.
     */
    public static ProcessorConfig mergeProcessorConfigs(ProcessorConfig ... processorConfigs) {
        // Merge the configuration
        Map<String, TreeMap> configs = mergeConfig(processorConfigs);

        // Get all includes
        Set<String> excludes = mergeExcludes(processorConfigs);

        // Find the set of includes, which are the ones from the profile + the ones configured
        List<String> includes = mergeIncludes(processorConfigs);

        return new ProcessorConfig(includes, excludes, configs);
    }

    private static Set<String> mergeExcludes(ProcessorConfig ... configs) {
        Set<String> ret = new HashSet<>();
        for (ProcessorConfig config : configs) {
            if (config != null) {
                Set<String> excludes = config.excludes;
                if (excludes != null) {
                    ret.addAll(excludes);
                }
            }
        }
        return ret;
    }

    private static List<String> mergeIncludes(ProcessorConfig ... configs) {
        List<String> ret = new ArrayList<>();
        for (ProcessorConfig config : configs) {
            if (config != null) {
                List<String> includes = config.includes;
                if (includes != null) {
                    ret.addAll(includes);
                }
            }
        }
        return removeDups(ret);
    }

    // Remove duplicates such that the earlier element remains and the latter is removed
    // Only good for small list (that's what we expect for enrichers and generators)
    private static List<String> removeDups(List<String> list) {
        List<String> ret = new ArrayList<>();
        for (String el : list) {
            if (!ret.contains(el)) {
                ret.add(el);
            }
        }
        return ret;
    }

    private static Map<String, TreeMap> mergeConfig(ProcessorConfig ... processorConfigs) {
        Map<String, TreeMap> ret = new HashMap<>();
        if (processorConfigs.length > 0) {
            // Reverse iteration order so that earlier entries have a higher precedence
            for (int i = processorConfigs.length - 1; i >= 0; i--) {
                ProcessorConfig processorConfig = processorConfigs[i];
                if (processorConfig != null) {
                    Map<String, TreeMap> config = processorConfig.config;
                    if (config != null) {
                        for (Map.Entry<String, TreeMap> entry : config.entrySet()) {
                            TreeMap newValues = entry.getValue();
                            if (newValues != null) {
                                TreeMap existing = ret.get(entry.getKey());
                                if (existing == null) {
                                    ret.put(entry.getKey(), new TreeMap(newValues));
                                } else {
                                    for (Map.Entry newValue : (Set<Map.Entry>) newValues.entrySet()) {
                                        existing.put(newValue.getKey(), newValue.getValue());
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return ret.size() > 0 ? ret : null;
    }

}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat 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 io.fabric8.maven.generator.api.support;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import io.fabric8.maven.core.config.OpenShiftBuildStrategy;
import io.fabric8.maven.core.config.RuntimeMode;
import io.fabric8.maven.core.util.Configs;
import io.fabric8.maven.core.util.PrefixedLogger;
import io.fabric8.maven.docker.config.BuildImageConfiguration;
import io.fabric8.maven.docker.config.ImageConfiguration;
import io.fabric8.maven.docker.util.ImageName;
import io.fabric8.maven.generator.api.FromSelector;
import io.fabric8.maven.generator.api.Generator;
import io.fabric8.maven.generator.api.GeneratorConfig;
import io.fabric8.maven.generator.api.GeneratorContext;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.utils.StringUtils;

/**
 * @author roland
 * @since 15/05/16
 */
abstract public class BaseGenerator implements Generator {

    private final GeneratorContext context;
    private final String name;
    private final GeneratorConfig config;
    protected final PrefixedLogger log;
    private final FromSelector fromSelector;

    private enum Config implements Configs.Key {
        // The image name
        name,

        // The alias to use (default to the generator name)
        alias,

        // whether the generator should always add to already existing image configurationws
        add {{d = "false"; }},

        // Base image
        from,

        // Base image mode (only relevant for OpenShift)
        fromMode,

        // Optional registry
        registry;

        public String def() { return d; } protected String d;
    }
    public BaseGenerator(GeneratorContext context, String name) {
        this(context, name, null);
    }

    public BaseGenerator(GeneratorContext context, String name, FromSelector fromSelector) {
        this.context = context;
        this.name = name;
        this.fromSelector = fromSelector;
        this.config = new GeneratorConfig(context.getProject().getProperties(), getName(), context.getConfig());
        this.log = new PrefixedLogger(name, context.getLogger());
    }

    protected MavenProject getProject() {
        return context.getProject();
    }

    public String getName() {
        return name;
    }

    public GeneratorContext getContext() {
        return context;
    }

    protected String getConfig(Configs.Key key) {
        return config.get(key);
    }

    protected String getConfig(Configs.Key key, String defaultVal) {
        return config.get(key, defaultVal);
    }

    // Get 'from' as configured without any default and image stream tag handling
    protected String getFromAsConfigured() {
        return getConfigWithFallback(Config.from, "fabric8.generator.from", null);
    }

    /**
     * Add the base image either from configuration or from a given selector
     *
     * @param builder for the build image configuration to add the from to.
     */
    protected void addFrom(BuildImageConfiguration.Builder builder) {
        String fromMode = getConfigWithFallback(Config.fromMode, "fabric8.generator.fromMode", getFromModeDefault(context.getRuntimeMode()));
        String from = getConfigWithFallback(Config.from, "fabric8.generator.from", null);
        if ("docker".equalsIgnoreCase(fromMode)) {
            String fromImage = from;
            if (fromImage == null) {
                fromImage = fromSelector != null ? fromSelector.getFrom() : null;
            }
            builder.from(fromImage);
            log.info("Using Docker image %s as base / builder", fromImage);
        } else if ("istag".equalsIgnoreCase(fromMode)) {
            Map<String, String> fromExt = new HashMap<>();
            if (from != null) {
                ImageName iName = new ImageName(from);
                // user/project is considered to be the namespace
                String tag = iName.getTag();
                if (StringUtils.isBlank(tag)) {
                    tag = "latest";
                }
                fromExt.put(OpenShiftBuildStrategy.SourceStrategy.name.key(), iName.getSimpleName() + ":" + tag);
                if (iName.getUser() != null) {
                    fromExt.put(OpenShiftBuildStrategy.SourceStrategy.namespace.key(), iName.getUser());
                }
                fromExt.put(OpenShiftBuildStrategy.SourceStrategy.kind.key(), "ImageStreamTag");
            } else {
                fromExt = fromSelector != null ? fromSelector.getImageStreamTagFromExt() : null;
            }
            if (fromExt != null) {
                String namespace = fromExt.get(OpenShiftBuildStrategy.SourceStrategy.namespace.key());
                if (namespace != null) {
                    log.info("Using ImageStreamTag '%s' from namespace '%s' as builder image",
                             fromExt.get(OpenShiftBuildStrategy.SourceStrategy.name.key()), namespace);
                } else {
                    log.info("Using ImageStreamTag '%s' as builder image",
                             fromExt.get(OpenShiftBuildStrategy.SourceStrategy.name.key()));
                }
                builder.fromExt(fromExt);
            }
        } else {
            throw new IllegalArgumentException(String.format("Invalid 'fromMode' in generator configuration for '%s'", getName()));
        }
    }

    // Use "istag" as default for "redhat" versions of this plugin
    private String getFromModeDefault(RuntimeMode mode) {
        if (mode == RuntimeMode.openshift && fromSelector != null && fromSelector.isRedHat()) {
            return "istag";
        } else {
            return "docker";
        }
    }

    /**
     * Get Image name with a standard default
     *
     * @return Docker image name which is never null
     */
    protected String getImageName() {
        if (RuntimeMode.isOpenShiftMode(getProject().getProperties())) {
            return getConfigWithFallback(Config.name, "fabric8.generator.name", "%a:%l");
        } else {
            return getConfigWithFallback(Config.name, "fabric8.generator.name", "%g/%a:%l");
        }
    }

    /**
     * Get the docker registry where the image should be located.
     * It returns null in Openshift mode.
     *
     * @return The docker registry if configured
     */
    protected String getRegistry() {
        if (!RuntimeMode.isOpenShiftMode(getProject().getProperties())) {
            return getConfigWithFallback(Config.registry, "fabric8.generator.registry", null);
        }

        return null;
    }

    /**
     * Get alias name with the generator name as default
     * @return an alias which is never null;
     */
    protected String getAlias() {
        return getConfigWithFallback(Config.alias, "fabric8.generator.alias", getName());
    }

    protected boolean shouldAddImageConfiguration(List<ImageConfiguration> configs) {
        return !containsBuildConfiguration(configs) || Configs.asBoolean(getConfig(Config.add));
    }

    protected String getConfigWithFallback(Config name, String key, String defaultVal) {
        String value = getConfig(name);
        if (value == null) {
            value = Configs.getSystemPropertyWithMavenPropertyAsFallback(getProject().getProperties(), key);
        }
        return value != null ? value : defaultVal;
    }

    protected void addLatestTagIfSnapshot(BuildImageConfiguration.Builder buildBuilder) {
        MavenProject project = getProject();
        if (project.getVersion().endsWith("-SNAPSHOT")) {
            buildBuilder.tags(Collections.singletonList("latest"));
        }
    }

    private boolean containsBuildConfiguration(List<ImageConfiguration> configs) {
        for (ImageConfiguration config : configs) {
            if (config.getBuildConfiguration() != null) {
                return true;
            }
        }
        return false;
    }

}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat 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 io.fabric8.maven.core.util;

import org.apache.maven.plugin.MojoExecutionException;
import io.fabric8.maven.docker.util.Logger;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

/**
 * File related methods which cannot be found elsewhere
 * @author roland
 * @since 23.05.17
 */
public class FileUtil {

    public static File createTempDirectory() {
        try {
            return Files.createTempDirectory("fabric8").toFile();
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static File getRelativePath(File baseDir, File file) {
        Path baseDirPath = Paths.get(baseDir.getAbsolutePath());
        Path filePath = Paths.get(file.getAbsolutePath());
        return baseDirPath.relativize(filePath).toFile();
    }

    public static String stripPrefix(String text, String prefix) {
        if (text.startsWith(prefix)) {
            return text.substring(prefix.length());
        }
        return text;
    }

    public static String stripPostfix(String text, String postfix) {
        if (text.endsWith(postfix)) {
            return text.substring(text.length() - postfix.length());
        }
        return text;
    }

   /**
     * Returns the absolute path to a file with name fileName
     * @param fileName the name of a file
     * @return absolute path to the file
     */
    public static String getAbsolutePath(String fileName) {
        return Paths.get(fileName).toAbsolutePath().toString();
    }

    /**
     * Returns the absolute path to a resource addressed by the given url
     * @param url resource URL
     * @return absolute path to the resource
     */
    public static String getAbsolutePath(URL url) {
        try {
            return url != null ? Paths.get(url.toURI()).toAbsolutePath().toString() : null;
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

    public static void downloadRemotes(final File outputDirectory, List<String> remotes, Logger log) {

        if (!outputDirectory.exists()) {
            try {
                Files.createDirectories(outputDirectory.toPath());
            } catch (IOException e) {
                throw new IllegalArgumentException(e);
            }
        }

        remotes.stream()
                .map(remote -> {
                    try {
                        return new URL(remote);
                    } catch (MalformedURLException e) {
                        throw new IllegalArgumentException(e);
                    }
                })
                .forEach(url -> {
                    try {
                        IoUtil.download(log, url, new File(outputDirectory, getOutputName(url)));
                    } catch (MojoExecutionException e) {
                        throw new IllegalArgumentException(e);
                    }
                });
    }

    private static String getOutputName(URL url) {
        final String path = url.getPath();

        final int slashIndex = path.lastIndexOf('/');
        if (slashIndex >= 0) {
            return path.substring(slashIndex + 1);
        } else {
            throw new IllegalArgumentException(String.format("URL %s should contain a name file to be downloaded.", url.toString()));
        }

    }
}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat 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 io.fabric8.maven.enricher.deprecated;

import java.net.ConnectException;
import java.util.Stack;

import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.maven.core.access.ClusterAccess;
import io.fabric8.maven.core.util.Configs;
import io.fabric8.maven.core.util.kubernetes.KubernetesHelper;
import io.fabric8.maven.core.util.kubernetes.ServiceUrlUtil;
import io.fabric8.maven.enricher.api.BaseEnricher;
import io.fabric8.maven.enricher.api.MavenEnricherContext;
import org.apache.commons.lang3.StringUtils;

public abstract class AbstractLiveEnricher extends BaseEnricher {

    private KubernetesClient kubernetesClient;

    private enum Config implements Configs.Key {
        online,
        namespace;

        public String def() { return d; } protected String d;
    }

    AbstractLiveEnricher(MavenEnricherContext buildContext, String name) {
        super(buildContext, name);
    }

    /**
     * Returns true if in offline mode, "false" if not speciied.
     * Can be overriden by
     */
    boolean isOnline() {
        String isOnline = getConfig(Config.online);
        if (isOnline != null) {
            return Configs.asBoolean(isOnline);
        }
        Boolean ret = asBooleanFromGlobalProp("fabric8.online");
        return ret != null ? ret : getDefaultOnline();
    }

    /**
     * Return the value to return if no online mode is explicitely specified.
     * Can be overridden, by default it returns false.
     *
     * @return the default value.
     */
    protected boolean getDefaultOnline() {
        return false;
    }

    /**
     * Returns the external access to the given service name
     *
     * @param serviceName name of the service
     * @param protocol URL protocol such as http
     */
    protected String getExternalServiceURL(String serviceName, String protocol) {
        if (!isOnline()) {
            getLog().info("Not looking for service " + serviceName + " as we are in offline mode");
            return null;
        } else {
            try {
                KubernetesClient kubernetes = getKubernetes();
                String ns = kubernetes.getNamespace();
                if (StringUtils.isBlank(ns)) {
                    ns = getNamespace();
                }
                Service service = kubernetes.services().inNamespace(ns).withName(serviceName).get();
                return service != null ?
                    ServiceUrlUtil.getServiceURL(kubernetes, serviceName, ns, protocol, true) :
                    null;
            } catch (Throwable e) {
                Throwable cause = e;

                boolean notFound = false;
                boolean connectError = false;
                Stack<Throwable> stack = unfoldExceptions(e);
                while (!stack.isEmpty()) {
                    Throwable t = stack.pop();
                    if (t instanceof ConnectException || "No route to host".equals(t.getMessage())) {
                        getLog().warn("Cannot connect to Kubernetes to find URL for service %s : %s",
                                      serviceName, cause.getMessage());
                        return null;
                    } else if (t instanceof IllegalArgumentException ||
                               t.getMessage() != null && t.getMessage().matches("^No.*found.*$")) {
                        getLog().warn("%s", cause.getMessage());
                        return null;
                    };
                }
                getLog().warn("Cannot find URL for service %s : %s", serviceName, cause.getMessage());
                return null;
            }
        }
    }

    /**
     * Creates an Iterable to walk the exception from the bottom up
     * (the last caused by going upwards to the root exception).
     *
     * @param exception the exception
     * @return the Iterable
     * @see java.lang.Iterable
     */
    protected Stack<Throwable> unfoldExceptions(Throwable exception) {
        Stack<Throwable> throwables = new Stack<>();

        Throwable current = exception;
        // spool to the bottom of the caused by tree
        while (current != null) {
            throwables.push(current);
            current = current.getCause();
        }
        return throwables;
    }

    // ====================================================================================

    // Check a global prop from the project or system props
    protected Boolean asBooleanFromGlobalProp(String prop) {
        String value = getContext().getConfiguration().getProperty(prop);
        if (value == null) {
            value = System.getProperty(prop);
        }
        return value != null ? Boolean.valueOf(value) : null;
    }

    private KubernetesClient getKubernetes() {
        if (kubernetesClient == null) {
            String namespace = getNamespace();
            kubernetesClient = new ClusterAccess(namespace).createDefaultClient(log);
        }
        return kubernetesClient;
    }

    // Get names space in the order:
    // - plugin configuration
    // - default name space from the kubernetes helper
    // - "default"
    private String getNamespace() {
        String namespace = getConfig(Config.namespace);
        if (StringUtils.isNotBlank(namespace)){
            return namespace;
        }

        namespace = getContext().getConfiguration().getProperty("fabric8.namespace");
        if (StringUtils.isNotBlank(namespace)){
            return namespace;
        }

        namespace = System.getProperty("fabric8.namespace");
        if (StringUtils.isNotBlank(namespace)){
            return namespace;
        }

        return KubernetesHelper.getDefaultNamespace();
    }
}


package com.github.vanroy.springboot.autoconfigure.data.jest;

import java.io.Closeable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import io.searchbox.client.JestClient;
import io.searchbox.client.JestClientFactory;
import io.searchbox.client.config.HttpClientConfig;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpHost;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.node.InternalSettingsPreparer;
import org.elasticsearch.node.Node;
import org.elasticsearch.node.NodeValidationException;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.transport.Netty4Plugin;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchProperties;
import org.springframework.boot.autoconfigure.elasticsearch.jest.HttpClientConfigBuilderCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.SocketUtils;
import org.springframework.util.StringUtils;

/**
 * Jest Elasticsearch configuration.
 * @author Julien Roy
 */
@Configuration
@EnableConfigurationProperties({ElasticsearchJestProperties.class, ElasticsearchProperties.class})
public class ElasticsearchJestAutoConfiguration implements DisposableBean {

	private static final Log logger = LogFactory.getLog(ElasticsearchJestAutoConfiguration.class);

	private static final String INTERNAL_TEST_CLUSTER_NAME = "internal-test-cluster-name";

	@Autowired
	private ElasticsearchJestProperties properties;

	@Autowired(required = false)
	private ElasticsearchProperties esNodeproperties;

	@Autowired(required = false)
	private JestClientFactory jestClientFactory;

	@Autowired(required = false)
	private ObjectProvider<List<HttpClientConfigBuilderCustomizer>> builderCustomizers;

	private Closeable node;

	@Bean
	@ConditionalOnMissingBean(JestClient.class)
	@ConditionalOnMissingClass("org.elasticsearch.node.Node")
	public JestClient client() {
		return createJestClient(properties.getUri());
	}

	@Bean
	@ConditionalOnMissingBean(JestClient.class)
	@ConditionalOnClass(Node.class)
	public JestClient testClient() throws NodeValidationException {

		if (StringUtils.isEmpty(properties.getUri())) {
			int httpPort = createInternalNode();
			return createJestClient("http://localhost:" + httpPort);
		} else {
			return createJestClient(properties.getUri());
		}
	}

	@Override
	public void destroy() throws Exception {
		if (this.node != null) {
			try {
				if (logger.isInfoEnabled()) {
					logger.info("Closing Elasticsearch client");
				}
				this.node.close();
			} catch (Exception ex) {
				if (logger.isErrorEnabled()) {
					logger.error("Error closing Elasticsearch client: ", ex);
				}
			}
		}
	}

	/**
	 * Create Jest client with URI
	 * @param uri URI of Elasticsearch
	 * @return JestClient
	 */
	private JestClient createJestClient(String uri) {

		HttpClientConfig.Builder builder = new HttpClientConfig.Builder(uri)
			.maxTotalConnection(properties.getMaxTotalConnection())
			.defaultMaxTotalConnectionPerRoute(properties.getDefaultMaxTotalConnectionPerRoute())
			.maxConnectionIdleTime(properties.getMaxConnectionIdleTime(), TimeUnit.MILLISECONDS)
			.readTimeout(properties.getReadTimeout())
			.multiThreaded(properties.getMultiThreaded());

		if (StringUtils.hasText(this.properties.getUsername())) {
			builder.defaultCredentials(this.properties.getUsername(), this.properties.getPassword());
		}

		String proxyHost = this.properties.getProxy().getHost();
		if (StringUtils.hasText(proxyHost)) {
			Integer proxyPort = this.properties.getProxy().getPort();
			Assert.notNull(proxyPort, "Proxy port must not be null");
			builder.proxy(new HttpHost(proxyHost, proxyPort));
		}

		List<HttpClientConfigBuilderCustomizer> configBuilderCustomizers = builderCustomizers != null ? builderCustomizers.getIfAvailable() : new ArrayList<>();
		if (!CollectionUtils.isEmpty(configBuilderCustomizers)) {
			logger.info("Custom HttpClientConfigBuilderCustomizers detected. Applying these to the HttpClientConfig builder.");
			configBuilderCustomizers.stream().forEach(customizer -> customizer.customize(builder));
			logger.info("Custom HttpClientConfigBuilderCustomizers applied.");
		}

		JestClientFactory factory = jestClientFactory != null ? jestClientFactory : new JestClientFactory();
		factory.setHttpClientConfig(builder.build());
		return factory.getObject();
	}

	/**
	 * Create internal Elasticsearch node.
	 * @return HTTP port of node
	 */
	private int createInternalNode() throws NodeValidationException {

		if (logger.isInfoEnabled()) {
			logger.info("Create test ES node");
		}

		int port = SocketUtils.findAvailableTcpPort();
		String clusterName = INTERNAL_TEST_CLUSTER_NAME + UUID.randomUUID();

		Settings.Builder settingsBuilder = Settings.builder()
				.put("cluster.name", clusterName)
				.put("http.type", "netty4")
				.put("http.port", String.valueOf(port));

		if (this.esNodeproperties != null) {
			this.esNodeproperties.getProperties().forEach(settingsBuilder::put);
		}

		Collection<Class<? extends Plugin>> plugins = scanPlugins();
		plugins.add(Netty4Plugin.class);

		this.node = new InternalNode(settingsBuilder.build(), plugins).start();

		return Integer.parseInt(settingsBuilder.get("http.port"));
	}

	/**
	 * List all official ES plugins available on ClassPath.
	 * @return List of plugins class
	 */
	@SuppressWarnings("unchecked")
	private static Collection<Class<? extends Plugin>> scanPlugins() {
		ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(false);
		componentProvider.addIncludeFilter(new AssignableTypeFilter(Plugin.class));

		return componentProvider.findCandidateComponents("org.elasticsearch.plugin").stream()
				.map(BeanDefinition::getBeanClassName)
				.map(name -> {
					try {
						return (Class<? extends Plugin>) Class.forName(name);
					} catch (ClassNotFoundException e) {
						logger.warn("Cannot load class on plugin detection", e);
						return null;
					}
				})
				.collect(Collectors.toSet());
	}

	/**
	 * Specific InternalNode class used to specify plugins.
	 */
	private static class InternalNode extends Node {

		InternalNode(Settings settings, Collection<Class<? extends Plugin>> classpathPlugins) {
			super(InternalSettingsPreparer.prepareEnvironment(settings, null), classpathPlugins);
		}
	}
}


/*
 * Copyright 2013-2017 the original author or authors.
 *
 * 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.github.vanroy.springdata.jest;

import static org.springframework.data.elasticsearch.core.query.Criteria.*;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import org.elasticsearch.common.geo.GeoDistance;
import org.elasticsearch.index.query.*;
import org.springframework.data.elasticsearch.core.geo.GeoBox;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.util.Assert;

/**
 * CriteriaFilterProcessor
 *
 * @author Franck Marchand
 * @author Mohsin Husen
 * @author Artur Konczak
 *
 */
class CriteriaFilterProcessor {

	QueryBuilder createFilterFromCriteria(Criteria criteria) {
		List<QueryBuilder> fbList = new LinkedList<>();
		QueryBuilder filter = null;

		ListIterator<Criteria> chainIterator = criteria.getCriteriaChain().listIterator();

		while (chainIterator.hasNext()) {
			QueryBuilder fb = null;
			Criteria chainedCriteria = chainIterator.next();
			if (chainedCriteria.isOr()) {
				fb = QueryBuilders.boolQuery();
				for(QueryBuilder f: createFilterFragmentForCriteria(chainedCriteria)){
					((BoolQueryBuilder)fb).should(f);
				}
				fbList.add(fb);
			} else if (chainedCriteria.isNegating()) {
				List<QueryBuilder> negationFilters = buildNegationFilter(criteria.getField().getName(), criteria.getFilterCriteriaEntries().iterator());

				if (!negationFilters.isEmpty()) {
					fbList.addAll(negationFilters);
				}
			} else {
				fbList.addAll(createFilterFragmentForCriteria(chainedCriteria));
			}
		}

		if (!fbList.isEmpty()) {
			if (fbList.size() == 1) {
				filter = fbList.get(0);
			} else {
				filter = QueryBuilders.boolQuery();
				for(QueryBuilder f: fbList) {
					((BoolQueryBuilder)filter).must(f);
				}
			}
		}
		return filter;
	}

	private List<QueryBuilder> createFilterFragmentForCriteria(Criteria chainedCriteria) {
		Iterator<Criteria.CriteriaEntry> it = chainedCriteria.getFilterCriteriaEntries().iterator();
		List<QueryBuilder> filterList = new LinkedList<>();

		String fieldName = chainedCriteria.getField().getName();
		Assert.notNull(fieldName, "Unknown field");
		QueryBuilder filter = null;

		while (it.hasNext()) {
			Criteria.CriteriaEntry entry = it.next();
			filter = processCriteriaEntry(entry.getKey(), entry.getValue(), fieldName);
			filterList.add(filter);
		}

		return filterList;
	}

	private QueryBuilder processCriteriaEntry(OperationKey key, Object value, String fieldName) {
		if (value == null) {
			return null;
		}
		QueryBuilder filter = null;

		switch (key) {
			case WITHIN: {
				GeoDistanceQueryBuilder geoDistanceQueryBuilder = QueryBuilders.geoDistanceQuery(fieldName);

				Assert.isTrue(value instanceof Object[], "Value of a geo distance filter should be an array of two values.");
				Object[] valArray = (Object[]) value;
				Assert.noNullElements(valArray, "Geo distance filter takes 2 not null elements array as parameter.");
				Assert.isTrue(valArray.length == 2, "Geo distance filter takes a 2-elements array as parameter.");
				Assert.isTrue(valArray[0] instanceof GeoPoint || valArray[0] instanceof String || valArray[0] instanceof Point, "First element of a geo distance filter must be a GeoPoint, a Point or a text");
				Assert.isTrue(valArray[1] instanceof String || valArray[1] instanceof Distance, "Second element of a geo distance filter must be a text or a Distance");

				StringBuilder dist = new StringBuilder();

				if (valArray[1] instanceof Distance) {
					extractDistanceString((Distance) valArray[1], dist);
				} else {
					dist.append((String) valArray[1]);
				}

				if (valArray[0] instanceof GeoPoint) {
					GeoPoint loc = (GeoPoint) valArray[0];
					geoDistanceQueryBuilder.point(loc.getLat(),loc.getLon()).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
				} else if (valArray[0] instanceof Point) {
					GeoPoint loc = GeoPoint.fromPoint((Point) valArray[0]);
					geoDistanceQueryBuilder.point(loc.getLat(), loc.getLon()).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
				} else {
					String loc = (String) valArray[0];
					if (loc.contains(",")) {
						String c[] = loc.split(",");
						geoDistanceQueryBuilder.point(Double.parseDouble(c[0]), Double.parseDouble(c[1])).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
					} else {
						geoDistanceQueryBuilder.geohash(loc).distance(dist.toString()).geoDistance(GeoDistance.PLANE);
					}
				}
				filter = geoDistanceQueryBuilder;

				break;
			}

			case BBOX: {
				filter = QueryBuilders.geoBoundingBoxQuery(fieldName);

				Assert.isTrue(value instanceof Object[], "Value of a boundedBy filter should be an array of one or two values.");
				Object[] valArray = (Object[]) value;
				Assert.noNullElements(valArray, "Geo boundedBy filter takes a not null element array as parameter.");

				if (valArray.length == 1) {
					//GeoEnvelop
					oneParameterBBox((GeoBoundingBoxQueryBuilder) filter, valArray[0]);
				} else if (valArray.length == 2) {
					//2x GeoPoint
					//2x text
					twoParameterBBox((GeoBoundingBoxQueryBuilder) filter, valArray);
				} else {
					//error
					Assert.isTrue(false, "Geo distance filter takes a 1-elements array(GeoBox) or 2-elements array(GeoPoints or Strings(format lat,lon or geohash)).");
				}
				break;
			}
		}

		return filter;
	}

	/**
	 * extract the distance string from a {@link org.springframework.data.geo.Distance} object.
	 *
	 * @param distance distance object to extract string from
	 * @param sb StringBuilder to build the distance string
	 */
	private void extractDistanceString(Distance distance, StringBuilder sb) {
		// handle Distance object
		sb.append((int) distance.getValue());

		Metrics metric = (Metrics) distance.getMetric();

		switch (metric) {
			case KILOMETERS:
				sb.append("km");
				break;
			case MILES:
				sb.append("mi");
				break;
		}
	}

	private void oneParameterBBox(GeoBoundingBoxQueryBuilder filter, Object value) {
		Assert.isTrue(value instanceof GeoBox || value instanceof Box, "single-element of boundedBy filter must be type of GeoBox or Box");

		GeoBox geoBBox;
		if (value instanceof Box) {
			Box sdbox = (Box) value;
			geoBBox = GeoBox.fromBox(sdbox);
		} else {
			geoBBox = (GeoBox) value;
		}

		filter.setCorners(geoBBox.getTopLeft().getLat(), geoBBox.getTopLeft().getLon(), geoBBox.getBottomRight().getLat(), geoBBox.getBottomRight().getLon());
	}

	private static boolean isType(Object[] array, Class clazz) {
		for (Object o : array) {
			if (!clazz.isInstance(o)) {
				return false;
			}
		}
		return true;
	}

	private void twoParameterBBox(GeoBoundingBoxQueryBuilder filter, Object[] values) {
		Assert.isTrue(isType(values, GeoPoint.class) || isType(values, String.class), " both elements of boundedBy filter must be type of GeoPoint or text(format lat,lon or geohash)");
		if (values[0] instanceof GeoPoint) {
			GeoPoint topLeft = (GeoPoint) values[0];
			GeoPoint bottomRight = (GeoPoint) values[1];
			filter.setCorners(topLeft.getLat(), topLeft.getLon(), bottomRight.getLat(), bottomRight.getLon());
		} else {
			String topLeft = (String) values[0];
			String bottomRight = (String) values[1];
			filter.setCorners(topLeft, bottomRight);
		}
	}

	private List<QueryBuilder> buildNegationFilter(String fieldName, Iterator<Criteria.CriteriaEntry> it) {
		List<QueryBuilder> notFilterList = new LinkedList<>();

		while (it.hasNext()) {
			Criteria.CriteriaEntry criteriaEntry = it.next();
			QueryBuilder notFilter = QueryBuilders.boolQuery().mustNot(processCriteriaEntry(criteriaEntry.getKey(), criteriaEntry.getValue(), fieldName));
			notFilterList.add(notFilter);
		}

		return notFilterList;
	}
}


/*
 * Copyright 2014-2017 the original author or authors.
 *
 * 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.github.vanroy.springdata.jest;

import static org.elasticsearch.common.xcontent.XContentFactory.*;
import static org.springframework.util.StringUtils.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.annotation.Transient;
import org.springframework.data.elasticsearch.annotations.*;
import org.springframework.data.elasticsearch.core.completion.Completion;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;

/**
 * @author Rizwan Idrees
 * @author Mohsin Husen
 * @author Artur Konczak
 * @author Kevin Leturc
 * @author Alexander Volz
 * @author Dennis Maaß
 * @author Pavel Luhin
 * @author Mark Paluch
 * @author Sascha Woo
 * @author Nordine Bittich
 */
class MappingBuilder {

	public static final String FIELD_DATA = "fielddata";
	public static final String FIELD_STORE = "store";
	public static final String FIELD_TYPE = "type";
	public static final String FIELD_INDEX = "index";
	public static final String FIELD_FORMAT = "format";
	public static final String FIELD_SEARCH_ANALYZER = "search_analyzer";
	public static final String FIELD_INDEX_ANALYZER = "analyzer";
	public static final String FIELD_PROPERTIES = "properties";
	public static final String FIELD_PARENT = "_parent";

	public static final String COMPLETION_PRESERVE_SEPARATORS = "preserve_separators";
	public static final String COMPLETION_PRESERVE_POSITION_INCREMENTS = "preserve_position_increments";
	public static final String COMPLETION_MAX_INPUT_LENGTH = "max_input_length";

	public static final String TYPE_VALUE_KEYWORD = "keyword";
	public static final String TYPE_VALUE_GEO_POINT = "geo_point";
	public static final String TYPE_VALUE_COMPLETION = "completion";

	private static SimpleTypeHolder SIMPLE_TYPE_HOLDER = SimpleTypeHolder.DEFAULT;

	static XContentBuilder buildMapping(Class clazz, String indexType, String idFieldName, String parentType) throws IOException {

		XContentBuilder mapping = jsonBuilder().startObject().startObject(indexType);
		// Parent
		if (hasText(parentType)) {
			mapping.startObject(FIELD_PARENT).field(FIELD_TYPE, parentType).endObject();
		}

		// Properties
		XContentBuilder xContentBuilder = mapping.startObject(FIELD_PROPERTIES);

		mapEntity(xContentBuilder, clazz, true, idFieldName, "", false, FieldType.Auto, null);

		return xContentBuilder.endObject().endObject().endObject();
	}

	private static void mapEntity(XContentBuilder xContentBuilder, Class clazz, boolean isRootObject, String idFieldName,
								  String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType, Field fieldAnnotation) throws IOException {

		java.lang.reflect.Field[] fields = retrieveFields(clazz);

		if (!isRootObject && (isAnyPropertyAnnotatedAsField(fields) || nestedOrObjectField)) {
			String type = FieldType.Object.toString().toLowerCase();
			if (nestedOrObjectField) {
				type = fieldType.toString().toLowerCase();
			}
			XContentBuilder t = xContentBuilder.startObject(nestedObjectFieldName).field(FIELD_TYPE, type);

			if (nestedOrObjectField && FieldType.Nested == fieldType && fieldAnnotation.includeInParent()) {
				t.field("include_in_parent", fieldAnnotation.includeInParent());
			}
			t.startObject(FIELD_PROPERTIES);
		}

		for (java.lang.reflect.Field field : fields) {

			if (field.isAnnotationPresent(Transient.class) || isInIgnoreFields(field, fieldAnnotation)) {
				continue;
			}

			if (field.isAnnotationPresent(Mapping.class)) {
				String mappingPath = field.getAnnotation(Mapping.class).mappingPath();
				if (hasText(mappingPath)) {
					ClassPathResource mappings = new ClassPathResource(mappingPath);
					if (mappings.exists()) {
						xContentBuilder.rawField(field.getName(), mappings.getInputStream(), XContentType.JSON);
						continue;
					}
				}
			}

			boolean isGeoPointField = isGeoPointField(field);
			boolean isCompletionField = isCompletionField(field);

			Field singleField = field.getAnnotation(Field.class);
			if (!isGeoPointField && !isCompletionField && isEntity(field) && isAnnotated(field)) {
				if (singleField == null) {
					continue;
				}
				boolean nestedOrObject = isNestedOrObjectField(field);
				mapEntity(xContentBuilder, getFieldType(field), false, "", field.getName(), nestedOrObject, singleField.type(), field.getAnnotation(Field.class));
				if (nestedOrObject) {
					continue;
				}
			}

			MultiField multiField = field.getAnnotation(MultiField.class);

			if (isGeoPointField) {
				applyGeoPointFieldMapping(xContentBuilder, field);
			}

			if (isCompletionField) {
				CompletionField completionField = field.getAnnotation(CompletionField.class);
				applyCompletionFieldMapping(xContentBuilder, field, completionField);
			}

			if (isRootObject && singleField != null && isIdField(field, idFieldName)) {
				applyDefaultIdFieldMapping(xContentBuilder, field);
			} else if (multiField != null) {
				addMultiFieldMapping(xContentBuilder, field, multiField, isNestedOrObjectField(field));
			} else if (singleField != null) {
				addSingleFieldMapping(xContentBuilder, field, singleField, isNestedOrObjectField(field));
			}
		}

		if (!isRootObject && isAnyPropertyAnnotatedAsField(fields) || nestedOrObjectField) {
			xContentBuilder.endObject().endObject();
		}
	}

	private static java.lang.reflect.Field[] retrieveFields(Class clazz) {
		// Create list of fields.
		List<java.lang.reflect.Field> fields = new ArrayList<>();

		// Keep backing up the inheritance hierarchy.
		Class targetClass = clazz;
		do {
			fields.addAll(Arrays.asList(targetClass.getDeclaredFields()));
			targetClass = targetClass.getSuperclass();
		}
		while (targetClass != null && targetClass != Object.class);

		return fields.toArray(new java.lang.reflect.Field[fields.size()]);
	}

	private static boolean isAnnotated(java.lang.reflect.Field field) {
		return field.getAnnotation(Field.class) != null ||
				field.getAnnotation(MultiField.class) != null ||
				field.getAnnotation(GeoPointField.class) != null ||
				field.getAnnotation(CompletionField.class) != null;
	}

	private static void applyGeoPointFieldMapping(XContentBuilder xContentBuilder, java.lang.reflect.Field field) throws IOException {
		xContentBuilder.startObject(field.getName());
		xContentBuilder.field(FIELD_TYPE, TYPE_VALUE_GEO_POINT);
		xContentBuilder.endObject();
	}

	private static void applyCompletionFieldMapping(XContentBuilder xContentBuilder, java.lang.reflect.Field field, CompletionField annotation) throws IOException {
		xContentBuilder.startObject(field.getName());
		xContentBuilder.field(FIELD_TYPE, TYPE_VALUE_COMPLETION);
		if (annotation != null) {
			xContentBuilder.field(COMPLETION_MAX_INPUT_LENGTH, annotation.maxInputLength());
			xContentBuilder.field(COMPLETION_PRESERVE_POSITION_INCREMENTS, annotation.preservePositionIncrements());
			xContentBuilder.field(COMPLETION_PRESERVE_SEPARATORS, annotation.preserveSeparators());
			if (hasText(annotation.searchAnalyzer())) {
				xContentBuilder.field(FIELD_SEARCH_ANALYZER, annotation.searchAnalyzer());
			}
			if (hasText(annotation.analyzer())) {
				xContentBuilder.field(FIELD_INDEX_ANALYZER, annotation.analyzer());
			}
		}
		xContentBuilder.endObject();
	}

	private static void applyDefaultIdFieldMapping(XContentBuilder xContentBuilder, java.lang.reflect.Field field)
			throws IOException {
		xContentBuilder.startObject(field.getName())
				.field(FIELD_TYPE, TYPE_VALUE_KEYWORD)
				.field(FIELD_INDEX, true);
		xContentBuilder.endObject();
	}

	/**
	 * Add mapping for @Field annotation
	 *
	 * @throws IOException
	 */
	private static void addSingleFieldMapping(XContentBuilder builder, java.lang.reflect.Field field, Field annotation, boolean nestedOrObjectField) throws IOException {
		builder.startObject(field.getName());
		addFieldMappingParameters(builder, annotation, nestedOrObjectField);
		builder.endObject();
	}

	/**
	 * Add mapping for @MultiField annotation
	 *
	 * @throws IOException
	 */
	private static void addMultiFieldMapping(
			XContentBuilder builder,
			java.lang.reflect.Field field,
			MultiField annotation,
			boolean nestedOrObjectField) throws IOException {

		// main field
		builder.startObject(field.getName());
		addFieldMappingParameters(builder, annotation.mainField(), nestedOrObjectField);

		// inner fields
		builder.startObject("fields");
		for (InnerField innerField : annotation.otherFields()) {
			builder.startObject(innerField.suffix());
			addFieldMappingParameters(builder, innerField, false);
			builder.endObject();
		}
		builder.endObject();

		builder.endObject();
	}

	private static void addFieldMappingParameters(XContentBuilder builder, Object annotation, boolean nestedOrObjectField) throws IOException {
		boolean index = true;
		boolean store = false;
		boolean fielddata = false;
		FieldType type = null;
		DateFormat dateFormat = null;
		String datePattern = null;
		String analyzer = null;
		String searchAnalyzer = null;

		if (annotation instanceof Field) {
			// @Field
			Field fieldAnnotation = (Field) annotation;
			index = fieldAnnotation.index();
			store = fieldAnnotation.store();
			fielddata = fieldAnnotation.fielddata();
			type = fieldAnnotation.type();
			dateFormat = fieldAnnotation.format();
			datePattern = fieldAnnotation.pattern();
			analyzer = fieldAnnotation.analyzer();
			searchAnalyzer = fieldAnnotation.searchAnalyzer();
		} else if (annotation instanceof InnerField) {
			// @InnerField
			InnerField fieldAnnotation = (InnerField) annotation;
			index = fieldAnnotation.index();
			store = fieldAnnotation.store();
			fielddata = fieldAnnotation.fielddata();
			type = fieldAnnotation.type();
			dateFormat = fieldAnnotation.format();
			datePattern = fieldAnnotation.pattern();
			analyzer = fieldAnnotation.analyzer();
			searchAnalyzer = fieldAnnotation.searchAnalyzer();
		} else {
			throw new IllegalArgumentException("annotation must be an instance of @Field or @InnerField");
		}

		if (!nestedOrObjectField) {
			builder.field(FIELD_STORE, store);
		}
		if (fielddata) {
			builder.field(FIELD_DATA, fielddata);
		}
		if (type != FieldType.Auto) {
			builder.field(FIELD_TYPE, type.name().toLowerCase());

			if (type == FieldType.Date && dateFormat != DateFormat.none) {
				builder.field(FIELD_FORMAT, dateFormat == DateFormat.custom ? datePattern : dateFormat.toString());
			}
		}
		if (!index) {
			builder.field(FIELD_INDEX, index);
		}
		if (hasText(analyzer)) {
			builder.field(FIELD_INDEX_ANALYZER, analyzer);
		}
		if (hasText(searchAnalyzer)) {
			builder.field(FIELD_SEARCH_ANALYZER, searchAnalyzer);
		}
	}

	protected static boolean isEntity(java.lang.reflect.Field field) {
		TypeInformation typeInformation = ClassTypeInformation.from(field.getType());
		Class<?> clazz = getFieldType(field);
		boolean isComplexType = !SIMPLE_TYPE_HOLDER.isSimpleType(clazz);
		return isComplexType && !Map.class.isAssignableFrom(typeInformation.getType());
	}

	protected static Class<?> getFieldType(java.lang.reflect.Field field) {

		ResolvableType resolvableType = ResolvableType.forField(field);

		if (resolvableType.isArray()) {
			return resolvableType.getComponentType().getRawClass();
		}

		ResolvableType componentType = resolvableType.getGeneric(0);
		if (Iterable.class.isAssignableFrom(field.getType())
				&& componentType != ResolvableType.NONE) {
			return componentType.getRawClass();
		}

		return resolvableType.getRawClass();
	}

	private static boolean isAnyPropertyAnnotatedAsField(java.lang.reflect.Field[] fields) {
		if (fields != null) {
			for (java.lang.reflect.Field field : fields) {
				if (field.isAnnotationPresent(Field.class)) {
					return true;
				}
			}
		}
		return false;
	}

	private static boolean isIdField(java.lang.reflect.Field field, String idFieldName) {
		return idFieldName.equals(field.getName());
	}

	private static boolean isInIgnoreFields(java.lang.reflect.Field field, Field parentFieldAnnotation) {
		if (null != parentFieldAnnotation) {
			String[] ignoreFields = parentFieldAnnotation.ignoreFields();
			return Arrays.asList(ignoreFields).contains(field.getName());
		}
		return false;
	}

	private static boolean isNestedOrObjectField(java.lang.reflect.Field field) {
		Field fieldAnnotation = field.getAnnotation(Field.class);
		return fieldAnnotation != null && (FieldType.Nested == fieldAnnotation.type() || FieldType.Object == fieldAnnotation.type());
	}

	private static boolean isGeoPointField(java.lang.reflect.Field field) {
		return field.getType() == GeoPoint.class || field.getAnnotation(GeoPointField.class) != null;
	}

	private static boolean isCompletionField(java.lang.reflect.Field field) {
		return field.getType() == Completion.class;
	}
}


package com.braintreepayments.api.models;

import android.text.TextUtils;

import com.braintreepayments.api.Json;
import com.braintreepayments.api.internal.GraphQLConstants.Keys;

import org.json.JSONArray;
import org.json.JSONObject;

import java.util.HashSet;
import java.util.Set;

/**
 * Contains the remote GraphQL configuration for the Braintree SDK.
 */
public class GraphQLConfiguration {

    private String mUrl;
    private Set<String> mFeatures;

    /**
     * Parse a {@link GraphQLConfiguration} from json.
     *
     * @param json The {@link JSONObject} to parse.
     * @return a {@link GraphQLConfiguration} instance with the data that was able to be parsed from the {@link
     * JSONObject}.
     */
    public static GraphQLConfiguration fromJson(JSONObject json) {
        if (json == null) {
            json = new JSONObject();
        }

        GraphQLConfiguration graphQLConfiguration = new GraphQLConfiguration();
        graphQLConfiguration.mUrl = Json.optString(json, Keys.URL, "");
        graphQLConfiguration.mFeatures = parseJsonFeatures(json.optJSONArray(Keys.FEATURES));

        return graphQLConfiguration;
    }

    /**
     * @return {@code true} if GraphQL is enabled, {@code false} otherwise.
     */
    public boolean isEnabled() {
        return !TextUtils.isEmpty(mUrl);
    }

    /**
     * @return the GraphQL url.
     */
    public String getUrl() {
        return mUrl;
    }

    /**
     * Check if a specific feature is enabled in the GraphQL API.
     *
     * @param feature The feature to check.
     * @return {@code true} if GraphQL is enabled and the feature is enabled, {@code false} otherwise.
     */
    public boolean isFeatureEnabled(String feature) {
        return isEnabled() && mFeatures.contains(feature);
    }

    private static Set<String> parseJsonFeatures(JSONArray jsonArray) {
        Set<String> features = new HashSet<>();
        if (jsonArray != null) {
            for (int i = 0; i < jsonArray.length(); i++) {
                features.add(jsonArray.optString(i, ""));
            }
        }

        return features;
    }
}


package com.paypal.android.sdk.data.collector;

import android.content.Context;
import androidx.annotation.MainThread;

import lib.android.paypal.com.magnessdk.Environment;
import lib.android.paypal.com.magnessdk.MagnesResult;
import lib.android.paypal.com.magnessdk.MagnesSDK;
import lib.android.paypal.com.magnessdk.MagnesSettings;
import lib.android.paypal.com.magnessdk.MagnesSource;

public class PayPalDataCollector {
    /**
     * Gets a Client Metadata ID at the time of payment activity. Once a user initiates a PayPal payment
     * from their device, PayPal uses the Client Metadata ID to verify that the payment is
     * originating from a valid, user-consented device and application. This helps reduce fraud and
     * decrease declines. This method MUST be called prior to initiating a pre-consented payment (a
     * "future payment") from a mobile device. Pass the result to your server, to include in the
     * payment request sent to PayPal. Do not otherwise cache or store this value.
     *
     * @param context
     * @return clientMetadataId Your server will send this to PayPal
     */
    @MainThread
    public static String getClientMetadataId(Context context) {
        PayPalDataCollectorRequest request = new PayPalDataCollectorRequest()
                .setApplicationGuid(InstallationIdentifier.getInstallationGUID(context));

        return getClientMetadataId(context, request);
    }

    /**
     * Gets a Client Metadata ID at the time of payment activity. Once a user initiates a PayPal payment
     * from their device, PayPal uses the Client Metadata ID to verify that the payment is
     * originating from a valid, user-consented device and application. This helps reduce fraud and
     * decrease declines. This method MUST be called prior to initiating a pre-consented payment (a
     * "future payment") from a mobile device. Pass the result to your server, to include in the
     * payment request sent to PayPal. Do not otherwise cache or store this value.
     *
     * @param context
     * @param clientMetadataId The desired client metadata ID
     * @return clientMetadataId Your server will send this to PayPal
     */
    @MainThread
    public static String getClientMetadataId(Context context, String clientMetadataId) {
        PayPalDataCollectorRequest request = new PayPalDataCollectorRequest()
                .setApplicationGuid(InstallationIdentifier.getInstallationGUID(context))
                .setClientMetadataId(clientMetadataId);

        return getClientMetadataId(context, request);
    }

    /**
     * Gets a Client Metadata ID at the time of payment activity. Once a user initiates a PayPal payment
     * from their device, PayPal uses the Client Metadata ID to verify that the payment is
     * originating from a valid, user-consented device and application. This helps reduce fraud and
     * decrease declines. This method MUST be called prior to initiating a pre-consented payment (a
     * "future payment") from a mobile device. Pass the result to your server, to include in the
     * payment request sent to PayPal. Do not otherwise cache or store this value.
     *
     * @param context An Android context.
     * @param request configures what data to collect.
     * @return
     */
    @MainThread
    public static String getClientMetadataId(Context context, PayPalDataCollectorRequest request) {
        if (context == null) {
            return "";
        }

        MagnesSDK magnesInstance = MagnesSDK.getInstance();
        MagnesSettings.Builder magnesSettingsBuilder = new MagnesSettings.Builder(context)
                .setMagnesSource(MagnesSource.BRAINTREE)
                .disableBeacon(request.isDisableBeacon())
                .setMagnesEnvironment(Environment.LIVE)
                .setAppGuid(request.getApplicationGuid());

        magnesInstance.setUp(magnesSettingsBuilder.build());

        MagnesResult result = magnesInstance.collectAndSubmit(context, request.getClientMetadataId(), request.getAdditionalData());

        return result.getPaypalClientMetaDataId();
    }
}


package com.braintreepayments.api.models;

import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;

import com.braintreepayments.api.Json;

import org.json.JSONException;
import org.json.JSONObject;

/**
 * Class to parse and contain American Express rewards balance
 */
public class AmericanExpressRewardsBalance implements Parcelable {

    private static final String ERROR_KEY = "error";
    private static final String ERROR_CODE_KEY = "code";
    private static final String ERROR_MESSAGE_KEY = "message";
    private static final String CONVERSION_RATE_KEY = "conversionRate";
    private static final String CURRENCY_AMOUNT_KEY = "currencyAmount";
    private static final String CURRENCY_ISO_CODE_KEY = "currencyIsoCode";
    private static final String REQUEST_ID_KEY = "requestId";
    private static final String REWARDS_AMOUNT_KEY = "rewardsAmount";
    private static final String REWARDS_UNIT_KEY = "rewardsUnit";

    private String mErrorCode;
    private String mErrorMessage;
    private String mConversionRate;
    private String mCurrencyAmount;
    private String mCurrencyIsoCode;
    private String mRequestId;
    private String mRewardsAmount;
    private String mRewardsUnit;

    /**
     * Used to parse a response from the Braintree Gateway to be used for American Express rewards balance.
     *
     * @param jsonString The json response from the Braintree Gateway American Express rewards balance route.
     * @return The {@link com.braintreepayments.api.models.AmericanExpressRewardsBalance} with rewards balance data.
     * @throws JSONException when parsing fails.
     */
    public static AmericanExpressRewardsBalance fromJson(String jsonString) throws JSONException {
        JSONObject json = new JSONObject(jsonString);

        AmericanExpressRewardsBalance rewardsBalance = new AmericanExpressRewardsBalance();

        if (json.has(ERROR_KEY)) {
            JSONObject errorJson = json.getJSONObject(ERROR_KEY);
            rewardsBalance.mErrorMessage = errorJson.getString(ERROR_MESSAGE_KEY);
            rewardsBalance.mErrorCode = errorJson.getString(ERROR_CODE_KEY);
        }

        rewardsBalance.mConversionRate = Json.optString(json, CONVERSION_RATE_KEY, null);
        rewardsBalance.mCurrencyAmount = Json.optString(json, CURRENCY_AMOUNT_KEY, null);
        rewardsBalance.mCurrencyIsoCode = Json.optString(json, CURRENCY_ISO_CODE_KEY, null);
        rewardsBalance.mRequestId = Json.optString(json, REQUEST_ID_KEY, null);
        rewardsBalance.mRewardsAmount = Json.optString(json, REWARDS_AMOUNT_KEY, null);
        rewardsBalance.mRewardsUnit = Json.optString(json, REWARDS_UNIT_KEY, null);

        return rewardsBalance;
    }

    /**
     * @return An error code when there was an issue fetching the rewards balance
     */
    @Nullable
    public String getErrorCode() {
        return mErrorCode;
    }

    /**
     * @return An error message when there was an issue fetching the rewards balance
     */
    @Nullable
    public String getErrorMessage() {
        return mErrorMessage;
    }

    /**
     * @return The conversion rate associated with the rewards balance
     */
    @Nullable
    public String getConversionRate() {
        return mConversionRate;
    }

    /**
     * @return The currency amount associated with the rewards balance
     */
    @Nullable
    public String getCurrencyAmount() {
        return mCurrencyAmount;
    }

    /**
     * @return The currency ISO code associated with the rewards balance
     */
    @Nullable
    public String getCurrencyIsoCode() {
        return mCurrencyIsoCode;
    }

    /**
     * @return The request ID used when fetching the rewards balance
     */
    @Nullable
    public String getRequestId() {
        return mRequestId;
    }

    /**
     * @return The rewards amount associated with the rewards balance
     */
    @Nullable
    public String getRewardsAmount() {
        return mRewardsAmount;
    }

    /**
     * @return The rewards unit associated with the rewards balance
     */
    @Nullable
    public String getRewardsUnit() {
        return mRewardsUnit;
    }

    public AmericanExpressRewardsBalance() {}

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mErrorCode);
        dest.writeString(mErrorMessage);
        dest.writeString(mConversionRate);
        dest.writeString(mCurrencyAmount);
        dest.writeString(mCurrencyIsoCode);
        dest.writeString(mRequestId);
        dest.writeString(mRewardsAmount);
        dest.writeString(mRewardsUnit);
    }

    private AmericanExpressRewardsBalance(Parcel in) {
        mErrorCode = in.readString();
        mErrorMessage = in.readString();
        mConversionRate = in.readString();
        mCurrencyAmount = in.readString();
        mCurrencyIsoCode = in.readString();
        mRequestId = in.readString();
        mRewardsAmount = in.readString();
        mRewardsUnit = in.readString();
    }

    public static final Creator<AmericanExpressRewardsBalance> CREATOR = new Creator<AmericanExpressRewardsBalance>() {
        public AmericanExpressRewardsBalance createFromParcel(Parcel source) {
            return new AmericanExpressRewardsBalance(source);
        }

        public AmericanExpressRewardsBalance[] newArray(int size) {
            return new AmericanExpressRewardsBalance[size];
        }
    };
}


package com.braintreepayments.api.models;

import android.os.Parcel;
import android.os.Parcelable;

import org.json.JSONException;
import org.json.JSONObject;

/**
 * {@link PaymentMethodNonce} representing a {@link VenmoAccountNonce}
 * @see PaymentMethodNonce
 */
public class VenmoAccountNonce extends PaymentMethodNonce implements Parcelable {

    protected static final String TYPE = "VenmoAccount";
    protected static final String API_RESOURCE_KEY = "venmoAccounts";
    private static final String VENMO_DETAILS_KEY = "details";
    private static final String VENMO_USERNAME_KEY = "username";

    private String mUsername;

    public VenmoAccountNonce(String nonce, String description, String username) {
        mNonce = nonce;
        mDescription = description;
        mUsername = username;
    }

    /**
     * Convert an API response to an {@link VenmoAccountNonce}.
     *
     * @param json Raw JSON response from Braintree of a {@link VenmoAccountNonce}.
     * @return {@link VenmoAccountNonce}.
     * @throws JSONException when parsing the response fails.
     */
    public static VenmoAccountNonce fromJson(String json) throws JSONException {
        VenmoAccountNonce venmoAccountNonce = new VenmoAccountNonce();
        venmoAccountNonce.fromJson(VenmoAccountNonce.getJsonObjectForType(API_RESOURCE_KEY, new JSONObject(json)));

        return venmoAccountNonce;
    }

    protected void fromJson(JSONObject json) throws JSONException {
        super.fromJson(json);

        JSONObject details = json.getJSONObject(VENMO_DETAILS_KEY);
        mUsername = details.getString(VENMO_USERNAME_KEY);
        mDescription = mUsername;
    }

    /**
     * @return the Venmo username
     */
    public String getUsername() {
        return mUsername;
    }

    @Override
    public String getTypeLabel() {
        return "Venmo";
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        super.writeToParcel(dest, flags);
        dest.writeString(mUsername);
    }

    public VenmoAccountNonce() {}

    protected VenmoAccountNonce(Parcel in) {
        super(in);
        mUsername = in.readString();
    }

    public static final Creator<VenmoAccountNonce> CREATOR = new Creator<VenmoAccountNonce>() {
        @Override
        public VenmoAccountNonce createFromParcel(Parcel in) {
            return new VenmoAccountNonce(in);
        }

        @Override
        public VenmoAccountNonce[] newArray(int size) {
            return new VenmoAccountNonce[size];
        }
    };
}