/**
 * 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 io.fabric8.maven.core.config.PlatformMode;
import io.fabric8.maven.core.model.GroupArtifactVersion;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.ContainerPort;
import io.fabric8.kubernetes.api.model.ContainerPortBuilder;
import io.fabric8.kubernetes.api.model.ContainerStatus;
import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.KubernetesListBuilder;
import io.fabric8.kubernetes.api.model.KubernetesResource;
import io.fabric8.kubernetes.api.model.LabelSelector;
import io.fabric8.kubernetes.api.model.LabelSelectorBuilder;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodSpec;
import io.fabric8.kubernetes.api.model.PodSpecBuilder;
import io.fabric8.kubernetes.api.model.PodStatus;
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
import io.fabric8.kubernetes.api.model.ReplicationController;
import io.fabric8.kubernetes.api.model.ReplicationControllerSpec;
import io.fabric8.kubernetes.api.model.apps.*;
import io.fabric8.kubernetes.api.model.batch.Job;
import io.fabric8.kubernetes.api.model.batch.JobSpec;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.internal.HasMetadataComparator;
import io.fabric8.maven.core.util.MapUtil;
import io.fabric8.maven.core.util.ResourceUtil;
import io.fabric8.maven.core.util.ResourceVersioning;
import io.fabric8.maven.docker.config.ImageConfiguration;
import io.fabric8.maven.docker.util.ImageName;
import io.fabric8.maven.docker.util.Logger;
import io.fabric8.openshift.api.model.Build;
import io.fabric8.openshift.api.model.DeploymentConfig;
import io.fabric8.openshift.api.model.DeploymentConfigSpec;
import io.fabric8.openshift.api.model.Template;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.slf4j.LoggerFactory;

import static io.fabric8.maven.core.util.Constants.RESOURCE_APP_CATALOG_ANNOTATION;
import static io.fabric8.maven.core.util.Constants.RESOURCE_SOURCE_URL_ANNOTATION;

/**
 * Utility class for handling Kubernetes resource descriptors
 *
 * @author roland
 * @since 02/05/16
 */
public class KubernetesResourceUtil {

    private static final transient org.slf4j.Logger LOG = LoggerFactory.getLogger(KubernetesResourceUtil.class);

    public static final String API_VERSION = "v1";
    public static final String API_EXTENSIONS_VERSION = "extensions/v1beta1";
    public static final String API_APPS_VERSION = "apps/v1";
    public static final String JOB_VERSION = "batch/v1";
    public static final String OPENSHIFT_V1_VERSION = "apps.openshift.io/v1";
    public static final ResourceVersioning DEFAULT_RESOURCE_VERSIONING = new ResourceVersioning()
            .withCoreVersion(API_VERSION)
            .withExtensionsVersion(API_EXTENSIONS_VERSION)
            .withAppsVersion(API_APPS_VERSION)
            .withOpenshiftV1Version(OPENSHIFT_V1_VERSION)
            .withJobVersion(JOB_VERSION);

    public static final HashSet<Class<?>> SIMPLE_FIELD_TYPES = new HashSet<>();

    public static final String CONTAINER_NAME_REGEX = "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$";

    protected static final String DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssX";

    /**
     * Read all Kubernetes resource fragments from a directory and create a {@link KubernetesListBuilder} which
     * can be adapted later.
     *
     * @param apiVersions the api versions to use
     * @param defaultName the default name to use when none is given
     * @param resourceFiles files to add.
     * @return the list builder
     * @throws IOException
     */
    public static KubernetesListBuilder readResourceFragmentsFrom(PlatformMode platformMode, ResourceVersioning apiVersions,
                                                                  String defaultName,
                                                                  File[] resourceFiles) throws IOException {
        KubernetesListBuilder builder = new KubernetesListBuilder();
        if (resourceFiles != null) {
            for (File file : resourceFiles) {
                HasMetadata resource = getResource(platformMode, apiVersions, file, defaultName);
                builder.addToItems(resource);
            }
        }
        return builder;
    }

    /**
     * Read a Kubernetes resource fragment and add meta information extracted from the filename
     * to the resource descriptor. I.e. the following elements are added if not provided in the fragment:
     *
     *

     *
name - Name of the resource added to metadata

     *
kind - Resource's kind

     *
apiVersion - API version (given as parameter to this method)

     *

     *
     *
     * @param apiVersions the API versions to add if not given.
     * @param file file to read, whose name must match {@link #FILENAME_PATTERN}.  @return map holding the fragment
     * @param appName resource name specifying resources belonging to this application
     */
    public static HasMetadata getResource(PlatformMode platformMode, ResourceVersioning apiVersions,
                                          File file, String appName) throws IOException {
        Map<String,Object> fragment = readAndEnrichFragment(platformMode, apiVersions, file, appName);
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.convertValue(fragment, HasMetadata.class);
        } catch (ClassCastException exp) {
            throw new IllegalArgumentException(String.format("Resource fragment %s has an invalid syntax (%s)", file.getPath(), exp.getMessage()));
        }
    }

    public static File[] listResourceFragments(File resourceDir) {
        final Pattern filenamePattern = Pattern.compile(FILENAME_PATTERN);
        final Pattern exludePattern = Pattern.compile(PROFILES_PATTERN);
        return resourceDir.listFiles((File dir, String name) -> filenamePattern.matcher(name).matches() && !exludePattern.matcher(name).matches());
    }

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

    protected final static Map<String,String> FILENAME_TO_KIND_MAPPER = new HashMap<>();
    protected final static Map<String,String> KIND_TO_FILENAME_MAPPER = new HashMap<>();

    static {
        initializeKindFilenameMapper();
    }

    protected final static void initializeKindFilenameMapper() {
        final Map<String, List<String>> mappings = KindFilenameMapperUtil.loadMappings();
        updateKindFilenameMapper(mappings);
    }

    protected final static void remove(String kind, String filename) {
        FILENAME_TO_KIND_MAPPER.remove(filename);
        KIND_TO_FILENAME_MAPPER.remove(kind);
    }

    public final static void updateKindFilenameMapper(final Map<String, List<String>> mappings) {

        final Set<Map.Entry<String, List<String>>> entries = mappings.entrySet();

        for (Map.Entry<String, List<String>> entry : entries) {

            final List<String> filenameTypes = entry.getValue();
            final String kind = entry.getKey();
            for (String filenameType : filenameTypes) {
                FILENAME_TO_KIND_MAPPER.put(filenameType, kind);
            }

            // In previous version, last one wins, so we do the same.
            KIND_TO_FILENAME_MAPPER.put(kind, filenameTypes.get(filenameTypes.size() - 1));

        }

    }

    private static final String FILENAME_PATTERN = "^(?<name>.*?)(-(?<type>[^-]+))?\\.(?<ext>yaml|yml|json)$";
    private static final String PROFILES_PATTERN = "^profiles?\\.ya?ml$";

    // Read fragment and add default values
    private static Map<String, Object> readAndEnrichFragment(PlatformMode platformMode, ResourceVersioning apiVersions,
                                                             File file, String appName) throws IOException {
        Pattern pattern = Pattern.compile(FILENAME_PATTERN, Pattern.CASE_INSENSITIVE);
        Matcher matcher = pattern.matcher(file.getName());
        if (!matcher.matches()) {
            throw new IllegalArgumentException(
                String.format("Resource file name '%s' does not match pattern <name>-<type>.(yaml|yml|json)", file.getName()));
        }
        String name = matcher.group("name");
        String type = matcher.group("type");
        String ext = matcher.group("ext").toLowerCase();
        String kind;

        Map<String,Object> fragment = readFragment(file, ext);

        if (type != null) {
            kind = getAndValidateKindFromType(file, type);
        } else {
            // Try name as type
            kind = FILENAME_TO_KIND_MAPPER.get(name.toLowerCase());
            if (kind != null) {
                // Name is in fact the type, so lets erase the name.
                name = null;
            }
        }

        addKind(fragment, kind, file.getName());

        String apiVersion = apiVersions.getCoreVersion();
        if (Objects.equals(kind, "Ingress")) {
            apiVersion = apiVersions.getExtensionsVersion();
        } else if (Objects.equals(kind, "StatefulSet") || Objects.equals(kind, "Deployment")) {
            apiVersion = apiVersions.getAppsVersion();
        } else if (Objects.equals(kind, "Job")) {
            apiVersion = apiVersions.getJobVersion();
        } else if (Objects.equals(kind, "DeploymentConfig") && platformMode == PlatformMode.openshift) {
            apiVersion = apiVersions.getOpenshiftV1version();
        }
        addIfNotExistent(fragment, "apiVersion", apiVersion);

        Map<String, Object> metaMap = getMetadata(fragment);
        // No name means: generated app name should be taken as resource name
        addIfNotExistent(metaMap, "name", StringUtils.isNotBlank(name) ? name : appName);

        return fragment;
    }

    private static String getAndValidateKindFromType(File file, String type) {
        String kind;
        kind = FILENAME_TO_KIND_MAPPER.get(type.toLowerCase());
        if (kind == null) {
            throw new IllegalArgumentException(
                String.format("Unknown type '%s' for file %s. Must be one of : %s",
                              type, file.getName(), StringUtils.join(FILENAME_TO_KIND_MAPPER.keySet().iterator(), ", ")));
        }
        return kind;
    }

    private static void addKind(Map<String, Object> fragment, String kind, String fileName) {
        if (kind == null && !fragment.containsKey("kind")) {
            throw new IllegalArgumentException(
                "No type given as part of the file name (e.g. 'app-rc.yml') " +
                "and no 'Kind' defined in resource descriptor " + fileName);
        }
        addIfNotExistent(fragment, "kind", kind);
    }

    public static void removeItemFromKubernetesBuilder(KubernetesListBuilder builder, HasMetadata item) {
        List<HasMetadata> items = builder.buildItems();
        List<HasMetadata> newListItems = new ArrayList<>();
        for(HasMetadata listItem : items) {
            if(!listItem.equals(item)) {
                newListItems.add(listItem);
            }
        }
        builder.withItems(newListItems);
    }
    // ===============================================================================================

    private static Map<String, Object> getMetadata(Map<String, Object> fragment) {
        Object mo = fragment.get("metadata");
        Map<String, Object> meta;
        if (mo == null) {
            meta = new HashMap<>();
            fragment.put("metadata", meta);
            return meta;
        } else if (mo instanceof Map) {
            return (Map<String, Object>) mo;
        } else {
            throw new IllegalArgumentException("Metadata is expected to be a Map, not a " + mo.getClass());
        }
    }

    private static void addIfNotExistent(Map<String, Object> fragment, String key, String value) {
        if (!fragment.containsKey(key)) {
            fragment.put(key, value);
        }
    }

    private static Map<String,Object> readFragment(File file, String ext) throws IOException {
        ObjectMapper mapper = new ObjectMapper("json".equals(ext) ? new JsonFactory() : new YAMLFactory());
        TypeReference<HashMap<String,Object>> typeRef = new TypeReference<HashMap<String,Object>>() {};
        try {
            Map<String, Object> ret = mapper.readValue(file, typeRef);
            return ret != null ? ret : new HashMap<String, Object>();
        } catch (JsonProcessingException e) {
            throw new JsonMappingException(String.format("[%s] %s", file, e.getMessage()), e.getLocation(), e);
        }
    }

    public static String getNameWithSuffix(String name, String kind) {
        String suffix =  KIND_TO_FILENAME_MAPPER.get(kind);
        return suffix != null ? name +  "-" + suffix : name;
    }

    public static String extractContainerName(GroupArtifactVersion groupArtifactVersion, ImageConfiguration imageConfig) {
        String alias = imageConfig.getAlias();
        return alias != null ? alias : extractImageUser(imageConfig.getName(), groupArtifactVersion.getGroupId()) + "-" + groupArtifactVersion.getArtifactId();
    }

    private static String extractImageUser(String image, String groupId) {
        ImageName name = new ImageName(image);
        String imageUser = name.getUser();
        String projectGroupId = groupId;
        if(imageUser != null) {
            return imageUser;
        } else {
            if(projectGroupId == null || projectGroupId.matches(CONTAINER_NAME_REGEX)) {
                return projectGroupId;
            }
            else {
                return projectGroupId.replaceAll("[^a-zA-Z0-9-]", "").replaceFirst("^-*(.*?)-*$","$1");
            }
        }
    }

    public static Map<String, String> removeVersionSelector(Map<String, String> selector) {
        Map<String, String> answer = new HashMap<>(selector);
        answer.remove("version");
        return answer;
    }

    public static boolean checkForKind(KubernetesListBuilder builder, String... kinds) {
        Set<String> kindSet = new HashSet<>(Arrays.asList(kinds));
        for (HasMetadata item : builder.getItems()) {
            if (kindSet.contains(item.getKind())) {
                return true;
            }
        }
        return false;
    }

    public static boolean addPort(List<ContainerPort> ports, String portNumberText, String portName, Logger log) {
        if (StringUtils.isBlank(portNumberText)) {
            return false;
        }
        int portValue;
        try {
            portValue = Integer.parseInt(portNumberText);
        } catch (NumberFormatException e) {
            log.warn("Could not parse remote debugging port %s as an integer: %s", portNumberText, e);
            return false;
        }
        for (ContainerPort port : ports) {
            String name = port.getName();
            Integer containerPort = port.getContainerPort();
            if (containerPort != null && containerPort.intValue() == portValue) {
                return false;
            }
        }
        ports.add(new ContainerPortBuilder().withName(portName).withContainerPort(portValue).build());
        return true;
    }

    public static boolean setEnvVar(List<EnvVar> envVarList, String name, String value) {
        for (EnvVar envVar : envVarList) {
            String envVarName = envVar.getName();
            if (Objects.equals(name, envVarName)) {
                String oldValue = envVar.getValue();
                if (Objects.equals(value, oldValue)) {
                    return false;
                } else {
                    envVar.setValue(value);
                    return true;
                }
            }
        }
        EnvVar env = new EnvVarBuilder().withName(name).withValue(value).build();
        envVarList.add(env);
        return true;
    }

    public static String getEnvVar(List<EnvVar> envVarList, String name, String defaultValue) {
        String answer = defaultValue;
        if (envVarList != null) {
            for (EnvVar envVar : envVarList) {
                String envVarName = envVar.getName();
                if (Objects.equals(name, envVarName)) {
                    String value = envVar.getValue();
                    if (StringUtils.isNotBlank(value)) {
                        return value;
                    }
                }
            }
        }
        return answer;
    }

    public static boolean removeEnvVar(List<EnvVar> envVarList, String name) {
        boolean removed = false;
        for (Iterator<EnvVar> it = envVarList.iterator(); it.hasNext(); ) {
            EnvVar envVar = it.next();
            String envVarName = envVar.getName();
            if (name.equals(envVarName)) {
                it.remove();
                removed = true;
            }
        }
        return removed;
    }

    /**
     * Convert a map of env vars to a list of K8s EnvVar objects.
     * @param envVars the name-value map containing env vars which must not be null
     * @return list of converted env vars
     */
    public static List<EnvVar> convertToEnvVarList(Map<String, String> envVars) {
        List<EnvVar> envList = new LinkedList<>();
        for (Map.Entry<String, String> entry : envVars.entrySet()) {
            String name = entry.getKey();
            String value = entry.getValue();

            if (name != null) {
                EnvVar env = new EnvVarBuilder().withName(name).withValue(value).build();
                envList.add(env);
            }
        }
        return envList;
    }

    public static void validateKubernetesMasterUrl(URL masterUrl) throws MojoExecutionException {
        if (masterUrl == null || StringUtils.isBlank(masterUrl.toString())) {
            throw new MojoExecutionException("Cannot find Kubernetes master URL. Have you started a cluster via `mvn fabric8:cluster-start` or connected to a remote cluster via `kubectl`?");
        }
    }

    public static void handleKubernetesClientException(KubernetesClientException e, Logger logger) throws MojoExecutionException {
        Throwable cause = e.getCause();
        if (cause instanceof UnknownHostException) {
            logger.error("Could not connect to kubernetes cluster!");
            logger.error("Have you started a local cluster via `mvn fabric8:cluster-start` or connected to a remote cluster via `kubectl`?");
            logger.info("For more help see: http://fabric8.io/guide/getStarted/");
            logger.error("Connection error: %s", cause);

            String message = "Could not connect to kubernetes cluster. Have you started a cluster via `mvn fabric8:cluster-start` or connected to a remote cluster via `kubectl`? Error: " + cause;
            throw new MojoExecutionException(message, e);
        } else {
            throw new MojoExecutionException(e.getMessage(), e);
        }
    }

    public static String getBuildStatusPhase(Build build) {
        if (build != null && build.getStatus() != null) {
            return build.getStatus().getPhase();
        }
        return null;
    }

    public static String getBuildStatusReason(Build build) {
        if (build != null && build.getStatus() != null) {
            String reason = build.getStatus().getReason();
            String phase = build.getStatus().getPhase();
            if (StringUtils.isNotBlank(phase)) {
                if (StringUtils.isNotBlank(reason)) {
                    return phase + ": " + reason;
                } else {
                    return phase;
                }
            } else {
                return StringUtils.defaultIfEmpty(reason, "");
            }
        }
        return "";
    }

    public static Pod getNewestPod(Collection<Pod> pods) {
        if (pods == null || pods.isEmpty()) {
            return null;
        }
        List<Pod> sortedPods = new ArrayList<>(pods);
        Collections.sort(sortedPods, (p1, p2) -> {
            Date t1 = getCreationTimestamp(p1);
            Date t2 = getCreationTimestamp(p2);
            if (t1 != null) {
                if (t2 == null) {
                    return 1;
                } else {
                    return t1.compareTo(t2);
                }
            } else if (t2 == null) {
                return 0;
            }
            return -1;
        });
        return sortedPods.get(sortedPods.size() - 1);
    }

    public static Date getCreationTimestamp(HasMetadata hasMetadata) {
        ObjectMeta metadata = hasMetadata.getMetadata();
        if (metadata != null) {
            return parseTimestamp(metadata.getCreationTimestamp());
        }
        return null;
    }

    private static Date parseTimestamp(String text) {
        if (StringUtils.isBlank(text)) {
            return null;
        }
        return parseDate(text);
    }

    public static Date parseDate(String text) {
        try {
            return new SimpleDateFormat(DATE_TIME_FORMAT).parse(text);
        } catch (ParseException e) {
            LOG.warn("Unable to parse date: " + text, e);
            return null;
        }
    }

    public static boolean podHasContainerImage(Pod pod, String imageName) {
        if (pod != null) {
            PodSpec spec = pod.getSpec();
            if (spec != null) {
                List<Container> containers = spec.getContainers();
                if (containers != null) {
                    for (Container container : containers) {
                        if (Objects.equals(imageName, container.getImage())) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    public static String getDockerContainerID(Pod pod) {
        PodStatus status = pod.getStatus();
        if (status != null) {
            List<ContainerStatus> containerStatuses = status.getContainerStatuses();
            if (containerStatuses != null) {
                for (ContainerStatus containerStatus : containerStatuses) {
                    String containerID = containerStatus.getContainerID();
                    if (StringUtils.isNotBlank(containerID)) {
                        String prefix = "://";
                        int idx = containerID.indexOf(prefix);
                        if (idx > 0) {
                            return containerID.substring(idx + prefix.length());
                        }
                        return containerID;
                    }
                }
            }
        }
        return null;
    }

    public static boolean isNewerResource(HasMetadata newer, HasMetadata older) {
        Date t1 = getCreationTimestamp(newer);
        Date t2 = getCreationTimestamp(older);
        return t1 != null && (t2 == null || t1.compareTo(t2) > 0);
    }

    /**
     * Uses reflection to copy over default values from the defaultValues object to the targetValues
     * object similar to the following:
     *
     *
\    * if( values.get${FIELD}() == null ) {
     *   values.(with|set){FIELD}(defaultValues.get${FIELD});
     * }
     *
     *
     * Only fields that which use primitives, boxed primitives, or String object are copied.
     *
     * @param targetValues
     * @param defaultValues
     */
    public static void mergeSimpleFields(Object targetValues, Object defaultValues) {
        Class<?> tc = targetValues.getClass();
        Class<?> sc = defaultValues.getClass();
        for (Method targetGetMethod : tc.getMethods()) {
            if (!targetGetMethod.getName().startsWith("get")) {
                continue;
            }

            Class<?> fieldType = targetGetMethod.getReturnType();
            if (!SIMPLE_FIELD_TYPES.contains(fieldType)) {
                continue;
            }

            String fieldName = targetGetMethod.getName().substring(3);
            Method withMethod = null;
            try {
                withMethod = tc.getMethod("with" + fieldName, fieldType);
            } catch (NoSuchMethodException e) {
                try {
                    withMethod = tc.getMethod("set" + fieldName, fieldType);
                } catch (NoSuchMethodException e2) {
                    continue;
                }
            }

            Method sourceGetMethod = null;
            try {
                sourceGetMethod = sc.getMethod("get" + fieldName);
            } catch (NoSuchMethodException e) {
                continue;
            }

            try {
                if (targetGetMethod.invoke(targetValues) == null) {
                    withMethod.invoke(targetValues, sourceGetMethod.invoke(defaultValues));
                }
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e.getCause());
            }
        }
    }

    public static String mergePodSpec(PodSpecBuilder builder, PodSpec defaultPodSpec, String defaultName) {
        String defaultApplicationContainerName = null;
        List<Container> containers = builder.buildContainers();
        List<Container> defaultContainers = defaultPodSpec.getContainers();
        int size = defaultContainers.size();
        if (size > 0) {
            if (containers == null || containers.isEmpty()) {
                builder.addToContainers(defaultContainers.toArray(new Container[size]));
            } else {
                for (Container defaultContainer : defaultContainers) {
                    Container container = null;
                    for (Container fragmentContainer : containers) {
                        if (fragmentContainer.getName() == null || fragmentContainer.getName().equals(defaultContainer.getName())) {
                            container = fragmentContainer;
                            defaultApplicationContainerName = defaultContainer.getName();
                            break;
                        }
                    }

                    if (container == null) {
                        container = new Container();
                        containers.add(container);
                    }
                    mergeSimpleFields(container, defaultContainer);
                    List<EnvVar> defaultEnv = defaultContainer.getEnv();
                    if (defaultEnv != null) {
                        for (EnvVar envVar : defaultEnv) {
                            ensureHasEnv(container, envVar);
                        }
                    }
                    List<ContainerPort> defaultPorts = defaultContainer.getPorts();
                    if (defaultPorts != null) {
                        for (ContainerPort port : defaultPorts) {
                            ensureHasPort(container, port);
                        }
                    }
                    if (container.getReadinessProbe() == null) {
                        container.setReadinessProbe(defaultContainer.getReadinessProbe());
                    }
                    if (container.getLivenessProbe() == null) {
                        container.setLivenessProbe(defaultContainer.getLivenessProbe());
                    }
                    if (container.getSecurityContext() == null) {
                        container.setSecurityContext(defaultContainer.getSecurityContext());
                    }
                }
                builder.withContainers(containers);
            }
        } else if (!containers.isEmpty()) {
            // lets default the container name if there's none specified in the custom yaml file
                for (Container container : containers) {
                if (StringUtils.isBlank(container.getName())) {
                    container.setName(defaultName);
                    break; // do it for one container only, but not necessarily the first one
                }
            }
            builder.withContainers(containers);
        }
        return defaultApplicationContainerName; // Return the main application container's name.
    }

    private static void ensureHasEnv(Container container, EnvVar envVar) {
        List<EnvVar> envVars = container.getEnv();
        if (envVars == null) {
            envVars = new ArrayList<>();
            container.setEnv(envVars);
        }
        for (EnvVar var : envVars) {
            if (Objects.equals(var.getName(), envVar.getName())) {
                // lets replace the object so that we can update the value or valueFrom
                envVars.remove(var);
                envVars.add(envVar);
                return;
            }
        }
        envVars.add(envVar);
    }

    private static void ensureHasPort(Container container, ContainerPort port) {
        List<ContainerPort> ports = container.getPorts();
        if (ports == null) {
            ports = new ArrayList<>();
            container.setPorts(ports);
        }
        for (ContainerPort cp : ports) {
            String n1 = cp.getName();
            String n2 = port.getName();
            if (n1 != null && n2 != null && n1.equals(n2)) {
                return;
            }
            Integer p1 = cp.getContainerPort();
            Integer p2 = port.getContainerPort();
            if (p1 != null && p2 != null && p1.intValue() == p2.intValue()) {
                return;
            }
        }
        ports.add(port);
    }

    public static String getSourceUrlAnnotation(HasMetadata item) {
        return KubernetesHelper.getOrCreateAnnotations(item).get(RESOURCE_SOURCE_URL_ANNOTATION);
    }

    public static void setSourceUrlAnnotationIfNotSet(HasMetadata item, String sourceUrl) {
        Map<String, String> annotations = KubernetesHelper.getOrCreateAnnotations(item);
        if (!annotations.containsKey(RESOURCE_SOURCE_URL_ANNOTATION)) {
            annotations.put(RESOURCE_SOURCE_URL_ANNOTATION, sourceUrl);
            item.getMetadata().setAnnotations(annotations);
        }
    }

    public static boolean isAppCatalogResource(HasMetadata templateOrConfigMap) {
        String catalogAnnotation = KubernetesHelper.getOrCreateAnnotations(templateOrConfigMap).get(RESOURCE_APP_CATALOG_ANNOTATION);
        return "true".equals(catalogAnnotation);
    }

    public static Set<HasMetadata> loadResources(File manifest) throws IOException {
        Object dto = ResourceUtil.load(manifest, KubernetesResource.class);
        if (dto == null) {
            throw new IllegalStateException("Cannot load kubernetes manifest " + manifest);
        }

        if (dto instanceof Template) {
            Template template = (Template) dto;
            dto = OpenshiftHelper.processTemplatesLocally(template, false);
        }

        Set<HasMetadata> entities = new TreeSet<>(new HasMetadataComparator());
        entities.addAll(KubernetesHelper.toItemList(dto));
        return entities;
    }

    public static LabelSelector getPodLabelSelector(Set<HasMetadata> entities) {
        LabelSelector chosenSelector = null;
        for (HasMetadata entity : entities) {
            LabelSelector selector = getPodLabelSelector(entity);
            if (selector != null) {
                if (chosenSelector != null && !chosenSelector.equals(selector)) {
                    throw new IllegalArgumentException("Multiple selectors found for the given entities: " + chosenSelector + " - " + selector);
                }
                chosenSelector = selector;
            }
        }
        return chosenSelector;
    }

    public static LabelSelector getPodLabelSelector(HasMetadata entity) {
        LabelSelector selector = null;
        if (entity instanceof Deployment) {
            Deployment resource = (Deployment) entity;
            DeploymentSpec spec = resource.getSpec();
            if (spec != null) {
                selector = spec.getSelector();
            }
        } else if (entity instanceof ReplicaSet) {
            ReplicaSet resource = (ReplicaSet) entity;
            ReplicaSetSpec spec = resource.getSpec();
            if (spec != null) {
                selector = spec.getSelector();
            }
        } else if (entity instanceof DeploymentConfig) {
            DeploymentConfig resource = (DeploymentConfig) entity;
            DeploymentConfigSpec spec = resource.getSpec();
            if (spec != null) {
                selector = toLabelSelector(spec.getSelector());
            }
        } else if (entity instanceof ReplicationController) {
            ReplicationController resource = (ReplicationController) entity;
            ReplicationControllerSpec spec = resource.getSpec();
            if (spec != null) {
                selector = toLabelSelector(spec.getSelector());
            }
        } else if (entity instanceof DaemonSet) {
            DaemonSet resource = (DaemonSet) entity;
            DaemonSetSpec spec = resource.getSpec();
            if (spec != null) {
                selector = spec.getSelector();
            }
        } else if (entity instanceof StatefulSet) {
            StatefulSet resource = (StatefulSet) entity;
            StatefulSetSpec spec = resource.getSpec();
            if (spec != null) {
                selector = spec.getSelector();
            }
        } else if (entity instanceof Job) {
            Job resource = (Job) entity;
            JobSpec spec = resource.getSpec();
            if (spec != null) {
                selector = spec.getSelector();
            }
        }
        return selector;
    }

    private static LabelSelector toLabelSelector(Map<String, String> matchLabels) {
        if (matchLabels != null && !matchLabels.isEmpty()) {
            return new LabelSelectorBuilder().withMatchLabels(matchLabels).build();
        }
        return null;
    }

    /**
     * Merges the given resources together into a single resource.
     *
     * If switchOnLocalCustomisation is false then the overrides from item2 are merged into item1
     *
     * @return the newly merged resources
     */
    public static HasMetadata mergeResources(HasMetadata item1, HasMetadata item2, Logger log, boolean switchOnLocalCustomisation) {
        if (item1 instanceof Deployment && item2 instanceof Deployment) {
            return mergeDeployments((Deployment) item1, (Deployment) item2, log, switchOnLocalCustomisation);
        }
        if (item1 instanceof ConfigMap && item2 instanceof ConfigMap) {
            ConfigMap cm1 = (ConfigMap) item1;
            ConfigMap cm2 = (ConfigMap) item2;
            return mergeConfigMaps(cm1, cm2, log, switchOnLocalCustomisation);
        }
        mergeMetadata(item1, item2);
        return item1;
    }

    protected static HasMetadata mergeConfigMaps(ConfigMap cm1, ConfigMap cm2, Logger log, boolean switchOnLocalCustomisation) {
        ConfigMap cm1OrCopy = cm1;
        if (!switchOnLocalCustomisation) {
            // lets copy the original to avoid modifying it
            cm1OrCopy = new ConfigMapBuilder(cm1OrCopy).build();
        }

        log.info("Merging 2 resources for " + KubernetesHelper.getKind(cm1OrCopy) + " " + KubernetesHelper.getName(cm1OrCopy) + " from " + getSourceUrlAnnotation(cm1OrCopy) + " and " + getSourceUrlAnnotation(cm2) +
" and removing " + getSourceUrlAnnotation(cm1OrCopy));
        cm1OrCopy.setData(mergeMapsAndRemoveEmptyStrings(cm2.getData(), cm1OrCopy.getData()));
        mergeMetadata(cm1OrCopy, cm2);
        return cm1OrCopy;
    }

    protected static HasMetadata mergeDeployments(Deployment resource1, Deployment resource2, Logger log, boolean switchOnLocalCustomisation) {
        Deployment resource1OrCopy = resource1;
        if (!switchOnLocalCustomisation) {
            // lets copy the original to avoid modifying it
            resource1OrCopy = new DeploymentBuilder(resource1OrCopy).build();
        }
        HasMetadata answer = resource1OrCopy;
        DeploymentSpec spec1 = resource1OrCopy.getSpec();
        DeploymentSpec spec2 = resource2.getSpec();
        if (spec1 == null) {
            resource1OrCopy.setSpec(spec2);
        } else {
            PodTemplateSpec template1 = spec1.getTemplate();
            PodTemplateSpec template2 = null;
            if (spec2 != null) {
                template2 = spec2.getTemplate();
            }
            if (template1 != null && template2 != null) {
                mergeMetadata(template1, template2);
            }
            if (template1 == null) {
                spec1.setTemplate(template2);
            } else {
                PodSpec podSpec1 = template1.getSpec();
                PodSpec podSpec2 = null;
                if (template2 != null) {
                    podSpec2 = template2.getSpec();
                }
                if (podSpec1 == null) {
                    template1.setSpec(podSpec2);
                } else {
                    String defaultName = null;
                    PodTemplateSpec updateTemplate = template1;
                    if (switchOnLocalCustomisation) {
                        HasMetadata override = resource2;
                        if (isLocalCustomisation(podSpec1)) {
                            updateTemplate = template2;
                            PodSpec tmp = podSpec1;
                            podSpec1 = podSpec2;
                            podSpec2 = tmp;
                        } else {
                            answer = resource2;
                            override = resource1OrCopy;
                        }
                        mergeMetadata(answer, override);
                    } else {
                        mergeMetadata(resource1OrCopy, resource2);
                    }
                    if (updateTemplate != null) {
                        if (podSpec2 == null) {
                            updateTemplate.setSpec(podSpec1);
                        } else {
                            PodSpecBuilder podSpecBuilder = new PodSpecBuilder(podSpec1);
                            mergePodSpec(podSpecBuilder, podSpec2, defaultName);
                            updateTemplate.setSpec(podSpecBuilder.build());
                        }
                    }
                    return answer;
                }
            }
        }
        log.info("Merging 2 resources for " + KubernetesHelper.getKind(resource1OrCopy) + " " + KubernetesHelper.getName(resource1OrCopy) + " from " + getSourceUrlAnnotation(resource1OrCopy) + " and " + getSourceUrlAnnotation(resource2) + " and removing " + getSourceUrlAnnotation(resource1OrCopy));
        return resource1OrCopy;
    }

    private static void mergeMetadata(PodTemplateSpec item1, PodTemplateSpec item2) {
        if (item1 != null && item2 != null) {
            ObjectMeta metadata1 = item1.getMetadata();
            ObjectMeta metadata2 = item2.getMetadata();
            if (metadata1 == null) {
                item1.setMetadata(metadata2);
            } else if (metadata2 != null) {
                metadata1.setAnnotations(mergeMapsAndRemoveEmptyStrings(metadata2.getAnnotations(), metadata1.getAnnotations()));
                metadata1.setLabels(mergeMapsAndRemoveEmptyStrings(metadata2.getLabels(), metadata1.getLabels()));
            }
        }
    }

    protected static void mergeMetadata(HasMetadata item1, HasMetadata item2) {
        if (item1 != null && item2 != null) {
            ObjectMeta metadata1 = item1.getMetadata();
            ObjectMeta metadata2 = item2.getMetadata();
            if (metadata1 == null) {
                item1.setMetadata(metadata2);
            } else if (metadata2 != null) {
                metadata1.setAnnotations(mergeMapsAndRemoveEmptyStrings(metadata2.getAnnotations(), metadata1.getAnnotations()));
                metadata1.setLabels(mergeMapsAndRemoveEmptyStrings(metadata2.getLabels(), metadata1.getLabels()));
            }
        }
    }

    /**
     * Returns a merge of the given maps and then removes any resulting empty string values (which is the way to remove, say, a label or annotation
     * when overriding
     */
    private static Map<String, String> mergeMapsAndRemoveEmptyStrings(Map<String, String> overrideMap, Map<String, String> originalMap) {
        Map<String, String> answer = MapUtil.mergeMaps(overrideMap, originalMap);
        Set<Map.Entry<String, String>> entries = overrideMap.entrySet();
        for (Map.Entry<String, String> entry : entries) {
            String value = entry.getValue();
            if (value == null || value.isEmpty()) {
                String key = entry.getKey();
                answer.remove(key);
            }
        }
        return answer;
    }

    // lets use presence of an image name as a clue that we are just enriching things a little
    // rather than a full complete manifest
    // we could also use an annotation?
    private static boolean isLocalCustomisation(PodSpec podSpec) {
        List<Container> containers = podSpec.getContainers() != null ? podSpec.getContainers() : Collections.<Container>emptyList();
        for (Container container : containers) {
            if (StringUtils.isNotBlank(container.getImage())) {
                return false;
            }
        }
        return true;
    }
}


/**
 * 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.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.fabric8.maven.core.util.Configs;
import io.fabric8.maven.core.util.PrefixedLogger;
import io.fabric8.maven.generator.api.PortsExtractor;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.project.MavenProject;

public abstract class AbstractPortsExtractor implements PortsExtractor {

    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
    private static final ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory());

    private static final String DOT = ".";
    private static final String JSON_EXTENSION = ".json";
    private static final String YAML_EXTENSION = ".yaml";
    private static final String YML_EXTENSION = ".yml";
    private static final String PROPERTIES_EXTENSION = ".properties";

    private static final String NUMBER_REGEX = "\\d+";

    private static final String PORT_REGEX = "([a-zA-Z0-9_]+)(([\\.-_]+p)|([P]))ort";
    private static final Pattern PORT_PATTERN = Pattern.compile(PORT_REGEX);

    private final PrefixedLogger log;

    public AbstractPortsExtractor(PrefixedLogger log) {
        this.log = log;
    }

    /**
     * @return The name of the system property that points to the path of the configuration file.
     */
    public abstract String getConfigPathPropertyName();

    /**
     * Finds the name of the configuration file from the {@link MavenProject}.
     * @param project   The {@link MavenProject} to use.
     * @return          The path to the configuration file or null if none has been found
     */
    public abstract String getConfigPathFromProject(MavenProject project);

    public File getConfigLocation(MavenProject project) {
        String propertyName = getConfigPathPropertyName();
        if (StringUtils.isBlank(propertyName)) {
            return null;
        }
        // The system property / Maven property has priority over what is specified in the pom.
        String configPath = Configs.getSystemPropertyWithMavenPropertyAsFallback(project.getProperties(), getConfigPathPropertyName());
        if (configPath == null) {
            configPath = getConfigPathFromProject(project);
        }
        if (StringUtils.isBlank(configPath)) {
            return null;
        }
        return Paths.get(configPath).toFile();
    }

    @Override
    public Map<String, Integer> extract(MavenProject project) {
        Map<String, Integer> answer = new HashMap<>();
        File configFile = getConfigLocation(project);
        if (configFile == null) {
            // No config file configured
            return answer;
        }
        if (!configFile.exists()) {
            log.warn("Could not find config: %s. Ignoring.", configFile.getAbsolutePath());
            return answer;
        }

        try {
            Map<String, String> configMap = readConfig(configFile);
            for (Map.Entry<String, String> entry : configMap.entrySet()) {
                String key = entry.getKey();
                if (isValidPortPropertyKey(key)) {
                    addPortIfValid(answer, key, entry.getValue());
                }
            }
            return answer;
        } catch (IOException e) {
            log.warn("Error reading config: [%s], due to: [%s]. Ignoring.", configFile.getAbsolutePath(), e.getMessage());
            return answer;
        }
    }

    /**
     * Reads the configuration from the file.
     * @param f
     * @return
     * @throws IOException
     */
    private Map<String, String> readConfig(File f) throws IOException {
        Map<String, String> map;
        if (f.getName().endsWith(JSON_EXTENSION)) {
            map = flatten(JSON_MAPPER.readValue(f, Map.class));
        } else if (f.getName().endsWith(YAML_EXTENSION) || f.getName().endsWith(YML_EXTENSION)) {
            map = flatten(YAML_MAPPER.readValue(f, Map.class));
        } else if (f.getName().endsWith(PROPERTIES_EXTENSION)) {
            Properties properties = new Properties();
            properties.load(new FileInputStream(f));
            map = propertiesToMap(properties);
        } else {
            throw new IllegalArgumentException("Can't read configuration from: [" + f.getName() + "]. Unknown file extension.");
        }
        return map;
    }

    /**
     * Flattens a nested map into a Map<String, String>.
     * @param map   The target map.
     * @return      The flattened map.
     */
    private Map<String, String> flatten(Map map) {
        Map<String, String> flat = new HashMap<>();
        for (Object key : map.keySet()) {
            String stringKey = String.valueOf(key);
            Object value = map.get(key);
            if (value instanceof String) {
                flat.put(stringKey, (String) value);
            } else if (value instanceof Map) {
                for (Map.Entry<String, String> entry : flatten((Map) value).entrySet()) {
                    flat.put(
                            new StringBuilder(stringKey).append(DOT).append(entry.getKey()).toString(),
                            entry.getValue());
                }
            } else {
                flat.put(stringKey, String.valueOf(value));
            }
        }
        return flat;
    }

    /**
     * Converts a {@link Properties} object to a {@link Map}.
     * @param properties    The properties object.
     * @return              The map.
     */
    private Map<String, String> propertiesToMap(Properties properties) {
        Map<String, String> map = new HashMap<>();
        for(Map.Entry<Object, Object> entry : properties.entrySet()) {
            map.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
        }
        return map;
    }

    /**
     * Checks if the given string matches the port property key convention.
     * The regex for the convention is ([a-zA-Z0-9_]+)(([\.-_]+p{1})|([P]{1}))ort.
     * @param candidate The string to check
     * @return
     */
    private boolean isValidPortPropertyKey(String candidate) {
        return PORT_PATTERN.matcher(candidate).matches();
    }

    /**
     * Adds a port to the list.
     * @param map   The list.
     * @param key   The key.
     * @param port  The candidate port.
     */
    private void addPortIfValid(Map<String, Integer> map, String key, String port) {
        if (StringUtils.isNotBlank(port)) {
            String t = port.trim();
            if (t.matches(NUMBER_REGEX)) {
                map.put(key, Integer.parseInt(t));
            }
        }
    }
}


/**
 * 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.handler;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;

import io.fabric8.kubernetes.api.model.ExecAction;
import io.fabric8.kubernetes.api.model.HTTPGetAction;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.Probe;
import io.fabric8.kubernetes.api.model.TCPSocketAction;
import io.fabric8.maven.core.config.ProbeConfig;
import io.fabric8.maven.core.util.Commandline;

import static org.apache.commons.lang3.StringUtils.isNotBlank;

/**
 * @author roland
 * @since 07/04/16
 */
public class ProbeHandler {

    public Probe getProbe(ProbeConfig probeConfig)  {
        if (probeConfig == null) {
            return null;
        }

        Probe probe = new Probe();
        Integer initialDelaySeconds = probeConfig.getInitialDelaySeconds();
        if (initialDelaySeconds != null) {
            probe.setInitialDelaySeconds(initialDelaySeconds);
        }
        Integer timeoutSeconds = probeConfig.getTimeoutSeconds();
        if (timeoutSeconds != null) {
            probe.setTimeoutSeconds(timeoutSeconds);
        }
        Integer failureThreshold = probeConfig.getFailureThreshold();
        if(failureThreshold != null) {
            probe.setFailureThreshold(failureThreshold);
        }
        Integer successThreshold = probeConfig.getSuccessThreshold();
        if(successThreshold != null) {
            probe.setSuccessThreshold(successThreshold);
        }
        HTTPGetAction getAction = getHTTPGetAction(probeConfig.getGetUrl());
        if (getAction != null) {
            probe.setHttpGet(getAction);
            return probe;
        }
        ExecAction execAction = getExecAction(probeConfig.getExec());
        if (execAction != null) {
            probe.setExec(execAction);
            return probe;
        }
        TCPSocketAction tcpSocketAction = getTCPSocketAction(probeConfig.getGetUrl(), probeConfig.getTcpPort());
        if (tcpSocketAction != null) {
            probe.setTcpSocket(tcpSocketAction);
            return probe;
        }

        return null;
    }

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

    private HTTPGetAction getHTTPGetAction(String getUrl) {
        if (getUrl == null || !getUrl.subSequence(0,4).toString().equalsIgnoreCase("http")) {
            return null;
        }
        try {
            URL url = new URL(getUrl);
            return new HTTPGetAction(url.getHost(),
                    null /* headers */,
                    url.getPath(),
                    new IntOrString(url.getPort()),
                    url.getProtocol().toUpperCase());
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException("Invalid URL " + getUrl + " given for HTTP GET readiness check");
        }
    }

    private TCPSocketAction getTCPSocketAction(String getUrl, String port) {
        if (port != null) {
            IntOrString portObj = new IntOrString(port);
            try {
                Integer portInt = Integer.parseInt(port);
                portObj.setIntVal(portInt);
            } catch (NumberFormatException e) {
                portObj.setStrVal(port);
            }
            if(getUrl==null)
                return new TCPSocketAction(getUrl, portObj);
            String validurl = getUrl.replaceFirst("(([a-zA-Z])+)://","http://");
            try{
                URL url = new URL(validurl);
                return new TCPSocketAction(url.getHost(), portObj);
            }
            catch (MalformedURLException e){
                throw new IllegalArgumentException("Invalid URL " + getUrl + " given for TCP readiness check");
            }
        }
        return null;
    }

    private ExecAction getExecAction(String execCmd) {
        if (isNotBlank(execCmd)) {
            List<String> splitCommandLine = Commandline.translateCommandline(execCmd);
            if (!splitCommandLine.isEmpty()) {
                return new ExecAction(splitCommandLine);
            }
        }
        return 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.enricher.api;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;

import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import io.fabric8.maven.core.config.ProcessorConfig;
import io.fabric8.maven.core.config.ResourceConfig;
import io.fabric8.maven.core.model.Configuration;
import io.fabric8.maven.core.model.Dependency;
import io.fabric8.maven.core.model.GroupArtifactVersion;
import io.fabric8.maven.core.util.MavenUtil;
import io.fabric8.maven.docker.config.ImageConfiguration;
import io.fabric8.maven.docker.util.Logger;
import io.fabric8.maven.enricher.api.util.MavenConfigurationExtractor;
import io.fabric8.maven.enricher.api.util.ProjectClassLoaders;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Plugin;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.codehaus.plexus.util.xml.Xpp3Dom;

/**
 * The context given to each enricher from where it can extract build specific information.
 *
 * @author roland
 * @since 01/04/16
 */
public class MavenEnricherContext implements EnricherContext {

    // overall configuration for the build
    private Configuration configuration;

    private Settings settings;

    private Map<String, String> processingInstruction;

    private MavenProject project;
    private Logger log;

    private MavenSession session;

    private Properties properties;

    private MavenEnricherContext() {}

    @Override
    public Configuration getConfiguration() {
        return configuration;
    }

    public Settings getSettings() {
        return settings;
    }

    public Map<String, String> getProcessingInstructions() {
        return processingInstruction;
    }

    public void setProcessingInstructions(Map<String, String> instruction) {
        this.processingInstruction = instruction;
    }

    @Override
    public Logger getLog() {
        return log;
    }

    @Override
    public GroupArtifactVersion getGav() {

        return new GroupArtifactVersion(project.getGroupId(),
                                        project.getArtifactId(),
                                        project.getVersion());
    }

    @Override
    public File getProjectDirectory() {
        return getProject().getBasedir();
    }

    @Override
    public List<Dependency> getDependencies(boolean transitive) {
        final Set<Artifact> artifacts = transitive ?
            getProject().getArtifacts() : getProject().getDependencyArtifacts();

        final List<Dependency> dependencies = new ArrayList<>();

        for (Artifact artifact : artifacts) {
            dependencies.add(
                new Dependency(new GroupArtifactVersion(artifact.getGroupId(),
                                                        artifact.getArtifactId(),
                                                        artifact.getVersion()),
                               artifact.getType(),
                               artifact.getScope(),
                               artifact.getFile()));
        }

        return dependencies;
    }

    @Override
    public boolean hasPlugin(String groupId, String artifactId) {
        if (groupId != null) {
            return MavenUtil.hasPlugin(getProject(), groupId, artifactId);
        } else {
            return MavenUtil.hasPluginOfAnyGroupId(getProject(), artifactId);
        }
    }

    @Override
    public ProjectClassLoaders getProjectClassLoaders() {
        return new ProjectClassLoaders(MavenUtil.getCompileClassLoader(getProject())
        );
    }

    @Override
    public Object getProperty(String key) {
        return properties != null ? properties.getProperty(key) : null;
    }

    // ========================================================================
    // Maven specific methods, only available after casting
    public MavenProject getProject() {
        return project;
    }

    //Method used in MOJO
    public String getDockerJsonConfigString(final Settings settings, final String serverId) {
        Server server = getServer(settings, serverId);
        if (server == null) {
            return "";
        }

        JsonObject auth = new JsonObject();
        auth.add("username", new JsonPrimitive(server.getUsername()));
        auth.add("password", new JsonPrimitive(server.getPassword()));

        String mail = getConfigurationValue(server, "email");
        if (!StringUtils.isBlank(mail)) {
            auth.add("email", new JsonPrimitive(mail));
        }

        JsonObject json = new JsonObject();
        json.add(serverId, auth);
        return json.toString();
    }

    public Server getServer(final Settings settings, final String serverId) {
        if (settings == null || StringUtils.isBlank(serverId)) {
            return null;
        }
        return settings.getServer(serverId);
    }

    private String getConfigurationValue(final Server server, final String key) {

        final Xpp3Dom configuration = (Xpp3Dom) server.getConfiguration();
        if (configuration == null) {
            return null;
        }

        final Xpp3Dom node = configuration.getChild(key);
        if (node == null) {
            return null;
        }

        return node.getValue();
    }
    // =======================================================================================================
    public static class Builder {

        private MavenEnricherContext ctx = new MavenEnricherContext();

        private ResourceConfig resources;
        private List<ImageConfiguration> images;
        private ProcessorConfig processorConfig;

        public Builder session(MavenSession session) {
            ctx.session = session;
            return this;
        }

        public Builder log(Logger log) {
            ctx.log = log;
            return this;
        }

        public Builder project(MavenProject project) {
            ctx.project = project;
            return this;
        }

        public Builder config(ProcessorConfig config) {
            this.processorConfig = config;
            return this;
        }

        public Builder resources(ResourceConfig resources) {
            this.resources = resources;
            return this;
        }

        public Builder images(List<ImageConfiguration> images) {
            this.images = images;
            return this;
        }

        public Builder settings(Settings settings) {
            ctx.settings = settings;
            return this;
        }

        public Builder properties(Properties properties) {
            ctx.properties = properties;
            return this;
        }

        public Builder processingInstructions(Map<String, String> pi) {
            ctx.processingInstruction = pi;
            return this;
        }

        public MavenEnricherContext build() {
            ctx.configuration =
                new Configuration.Builder()
                    .properties(ctx.project.getProperties())
                    .images(images)
                    .resource(resources)
                    .processorConfig(processorConfig)
                    .pluginConfigLookup(
                        (system, id) -> {
                            if (!"maven".equals(system)) {
                                return Optional.empty();
                            }
                            final Plugin plugin = ctx.project.getPlugin(id);
                            if (plugin == null) {
                                return Optional.empty();
                            }
                            return Optional.of(MavenConfigurationExtractor.extract((Xpp3Dom) plugin.getConfiguration()));
                        })
                    .secretConfigLookup(
                        id -> {
                            Settings settings = ctx.session.getSettings();
                            if (settings == null || StringUtils.isBlank(id)) {
                                return Optional.empty();
                            }
                            Server server = settings.getServer(id);
                            if (server == null) {
                                return Optional.empty();
                            }
                            Map<String, Object> config = MavenConfigurationExtractor.extract((Xpp3Dom) server.getConfiguration());
                            config.put("id", server.getId());
                            config.put("username", server.getUsername());
                            config.put("password", server.getPassword());
                            return Optional.of(config);
                        })
                    .build();
            return ctx;
        }

    }
}


/**
 * 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.model;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.function.BiFunction;
import java.util.function.Function;

import io.fabric8.maven.core.config.ProcessorConfig;
import io.fabric8.maven.core.config.ResourceConfig;
import io.fabric8.maven.docker.config.ImageConfiguration;

/**
 * Configuration class which holds various configuration
 * related coponents
 *
 * @author roland
 * @since 12.10.18
 */
public class Configuration {

    // Project properties
    private Properties properties = new Properties();

    // List of image configuration used when building
    private List<ImageConfiguration> images;

    // Configuration influencing the resource generation
    private ResourceConfig resource;

    // Lookup plugin project configuration
    private BiFunction<String, String, Optional<Map<String,Object>>> pluginConfigLookup;

    // Lookup secret configuration
    private Function<String, Optional<Map<String,Object>>> secretConfigLookup;

    // Processor config which holds all the configuration for processors / enrichers
    private ProcessorConfig processorConfig;

    private Configuration() {
    }

    public Properties getProperties() {
        return properties;
    }

    public Optional<List<ImageConfiguration>> getImages() {
        return Optional.ofNullable(images);
    }

    public Optional<ResourceConfig> getResource() {
        return Optional.ofNullable(resource);
    }

    public Optional<ProcessorConfig> getProcessorConfig() {
        return Optional.ofNullable(processorConfig);
    }

    /**
     * Gets plugin configuration values. Since there can be inner values,
     * it returns a Map of Objects where an Object can be a
     * simple type, List or another Map.
     *
     * @param system the underlying build platform (e.g. "maven")
     * @param id which plugin configuration to pick
     * @return configuration map specific to this id
     */
    public Optional<Map<String, Object>> getPluginConfiguration(String system, String id) {
        return pluginConfigLookup.apply(system, id);
    }

    /**
     * Gets configuration values. Since there can be inner values,
     * it returns a Map of Objects where an Object can be a
     * simple type, List or another Map.
     *
     * @param id id specific to the secret store
     * @return configuration map specific to this id
     */
    public Optional<Map<String, Object>> getSecretConfiguration(String id) {
        return secretConfigLookup.apply(id);
    }

    public String getProperty(String name) {
        return properties.getProperty(name);
    }

    public String getPropertyWithSystemOverride(String name) {
        String ret = System.getProperty(name);
        if (ret != null) {
            return ret;
        }
        return getProperty(name);
    }

    public static class Builder {
        private Configuration cfg = new Configuration();

        public Builder properties(Properties props) {
            cfg.properties = props;
            return this;
        }

        public Builder images(List<ImageConfiguration> images) {
            cfg.images = images;
            return this;
        }

        public Builder resource(ResourceConfig resource) {
            cfg.resource = resource;
            return this;
        }

        public Builder processorConfig(ProcessorConfig config) {
            cfg.processorConfig = config;
            return this;
        }

        public Builder pluginConfigLookup(BiFunction<String, String, Optional<Map<String,Object>>> pluginConfigLookup) {
            cfg.pluginConfigLookup = pluginConfigLookup;
            return this;
        }

        public Builder secretConfigLookup(Function<String, Optional<Map<String,Object>>> secretConfigLookup) {
            cfg.secretConfigLookup = secretConfigLookup;
            return this;
        }

        public Configuration build() {
            return cfg;
        }
    }
}


/**
 * 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 java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.Socket;
import java.net.URL;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import io.fabric8.maven.docker.util.Logger;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.maven.plugin.MojoExecutionException;

/**
 *
 * Utilities for download and more
 * @author roland
 * @since 14/10/16
 */
public class IoUtil {

    /**
     * Download with showing the progress a given URL and store it in a file
     * @param log logger used to track progress
     * @param downloadUrl url to download
     * @param target target file where to store the downloaded data
     * @throws MojoExecutionException
     */
    public static void download(Logger log, URL downloadUrl, File target) throws MojoExecutionException {
        log.progressStart();
        try {
            OkHttpClient client =
                new OkHttpClient.Builder()
                    .readTimeout(30, TimeUnit.MINUTES).build();
            Request request = new Request.Builder()
                .url(downloadUrl)
                .build();
            Response response = client.newCall(request).execute();

            try (OutputStream out = new FileOutputStream(target);
                 InputStream im = response.body().byteStream()) {

                long length = response.body().contentLength();
                InputStream in = response.body().byteStream();
                byte[] buffer = new byte[8192];

                long readBytes = 0;
                while (true) {
                    int len = in.read(buffer);
                    readBytes += len;
                    log.progressUpdate(target.getName(), "Downloading", getProgressBar(readBytes, length));
                    if (len <= 0) {
                        out.flush();
                        break;
                    }
                    out.write(buffer, 0, len);
                }
            }
        } catch (IOException e) {
            throw new MojoExecutionException("Failed to download URL " + downloadUrl + " to  " + target + ": " + e, e);
        } finally {
            log.progressFinished();
        }

    }

    /**
     * Find a free (on localhost) random port in the range [49152, 65535] after 100 attempts.
     *
     * @return a random port where a server socket can be bound to
     */
    public static int getFreeRandomPort() {
        // 100 attempts should be enough
        return getFreeRandomPort(49152, 65535, 100);
    }

    /**
     * Find a free (on localhost) random port in the specified range after the given number of attempts.
     */
    public static int getFreeRandomPort(int min, int max, int attempts) {
        Random random = new Random();
        for (int i=0; i < attempts; i++) {
            int port = min + random.nextInt(max - min + 1);
            try (Socket socket = new Socket("localhost", port)) {
            } catch (ConnectException e) {
                return port;
            } catch (IOException e) {
                throw new IllegalStateException("Error while trying to check open ports", e);
            }
        }
        throw new IllegalStateException("Cannot find a free random port in the range [" + min + ", " + max + "] after " + attempts + " attempts");
    }

    /**
     * Returns an identifier from the given string that can be used as file name.
     */
    public static String sanitizeFileName(String name) {
        if (name != null) {
            return name.replaceAll("[^A-Za-z0-9]+", "-");
        }

        return null;
    }

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

    private static int PROGRESS_LENGTH = 50;

    private static String getProgressBar(long bytesRead, long length) {
        StringBuffer ret = new StringBuffer("[");
        if (length > - 1) {
            int bucketSize = (int) (length / PROGRESS_LENGTH + 0.5);
            int index = (int) (bytesRead / bucketSize + 0.5);
            for (int i = 0; i < PROGRESS_LENGTH; i++) {
                ret.append(i < index ? "=" : (i == index ? ">" : " "));
            }
            ret.append(String.format("] %.2f MB/%.2f MB",
                                 ((float) bytesRead / (1024 * 1024)),
                                 ((float) length / (1024 * 1024))));
        } else {
            int bucketSize = 200 * 1024; // 200k
            int index = (int) (bytesRead / bucketSize + 0.5) % PROGRESS_LENGTH;
            for (int i = 0; i < PROGRESS_LENGTH; i++) {
                ret.append(i == index ? "*" : " ");
            }
            ret.append("]");
        }

        return ret.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.core.util;

import org.apache.commons.lang3.StringUtils;

/**
 */
public class VersionUtil {
    /**
     * Compares two version strings such that "1.10.1" > "1.4" etc
     */
    public static int compareVersions(String v1, String v2) {
        String[] components1 = split(v1);
        String[] components2 = split(v2);
        int diff;
        int length = Math.min(components1.length, components2.length);
        for (int i = 0; i < length; i++) {
            String s1 = components1[i];
            String s2 = components2[i];
            Integer i1 = tryParseInteger(s1);
            Integer i2 = tryParseInteger(s2);
            if (i1 != null && i2 != null) {
                diff = i1.compareTo(i2);
            } else {
                // lets assume strings instead
                diff = s1.compareTo(s2);
            }
            if (diff != 0) {
                return diff;
            }
        }
        diff = Integer.compare(components1.length, components2.length);
        if (diff == 0) {
            if (v1 == v2) {
                return 0;
            }
            /* if v1 == null then v2 can't be null here (see 'if' above).
               So for v1 == null its always smaller than v2 */;
            return v1 != null ? v1.compareTo(v2) : -1;
        }
        return diff;
    }

    private static String[] split(String text) {
        if (text != null) {
            return text.split("[\\.-]");
        } else {
            return new String[0];
        }
    }

    private static Integer tryParseInteger(String text) {
        if (StringUtils.isNotBlank(text)) {
            try {
                return Integer.parseInt(text);
            } catch (NumberFormatException e) {
                // ignore
            }
        }
        return 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.core.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.fabric8.maven.core.config.ProcessorConfig;
import io.fabric8.maven.core.config.Profile;
import org.apache.maven.shared.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Helper class for dealing with profiles.
 *
 * @author roland
 * @since 25/07/16
 */
public class ProfileUtil {

    private ProfileUtil() {}

    private static final Logger log = LoggerFactory.getLogger(ProfileUtil.class);

    // Allowed profile names
    public static final String[] PROFILE_FILENAMES = {"profiles%s.yml", "profiles%s.yaml", "profiles%s"};

    // Mapper for handling YAML formats
    private static final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());

    // Default profile which will be always there
    public static final String DEFAULT_PROFILE = "default";

    /**
     * Find a profile. Profiles are looked up at various locations:
     *
     *

     *
A given directory with the name profiles.yml (and variations, {@link #findProfile(String, File)}

     *

     * @param profileArg the profile's name
     * @param resourceDir a directory to check for profiles.
     * @return the profile found or the default profile if none of this name is given
     * @throws IOException
     */
    public static Profile findProfile(String profileArg, File resourceDir) throws IOException {
        try {
            String profile = profileArg == null ? DEFAULT_PROFILE : profileArg;
            Profile profileFound = lookup(profile, resourceDir);
            if (profileFound != null) {
                if(profileFound.getParentProfile() != null) {
                    profileFound = inheritFromParentProfile(profileFound, resourceDir);
                    log.info(profileFound + " inheriting resources from " + profileFound.getParentProfile());
                }
                return profileFound;
            } else {
                throw new IllegalArgumentException("No profile '" + profile + "' defined");
            }
        } catch (IOException e) {
            throw new IOException("Error while looking up profile " + profileArg + ": " + e.getMessage(),e);
        }
    }

    private static Profile inheritFromParentProfile(Profile aProfile, File resourceDir) throws IOException {
        Profile aParentProfile = lookup(aProfile.getParentProfile(), resourceDir);
        if(aParentProfile != null) {
            aProfile.setEnricherConfig(ProcessorConfig.mergeProcessorConfigs(aProfile.getEnricherConfig(), aParentProfile.getEnricherConfig()));
            aProfile.setGeneratorConfig(ProcessorConfig.mergeProcessorConfigs(aProfile.getGeneratorConfig(), aParentProfile.getGeneratorConfig()));
            aProfile.setWatcherConfig(ProcessorConfig.mergeProcessorConfigs(aProfile.getWatcherConfig(), aParentProfile.getWatcherConfig()));
        } else {
            throw new IllegalArgumentException("No parent profile '" + aProfile.getParentProfile() + "' defined");
        }
        return aProfile;
    }

    /**
     * Find an enricher or generator config, possibly via a profile and merge it with a given configuration.
     *
     * @param configExtractor how to extract the config from a profile when found
     * @param profile the profile name (can be null, then no profile is used)
     * @param resourceDir resource directory where to lookup the profile (in addition to a classpath lookup)
     * @return the merged configuration which can be empty if no profile is given
     * @param config the provided configuration
     * @throws IOException
     */
    public static ProcessorConfig blendProfileWithConfiguration(ProcessorConfigurationExtractor configExtractor,
                                                                String profile,
                                                                File resourceDir,
                                                                ProcessorConfig config) throws IOException {
        // Get specified profile or the default profile
        ProcessorConfig profileConfig = extractProcesssorConfiguration(configExtractor, profile, resourceDir);

        return ProcessorConfig.mergeProcessorConfigs(config, profileConfig);
    }

    /**
     * Lookup profiles from a given directory and merge it with a profile of the
     * same name found in the classpath
     *
     * @param name name of the profile to lookup
     * @param directory directory to lookup
     * @return Profile found or null
     * @throws IOException if somethings fails during lookup
     */
    public static Profile lookup(String name, File directory) throws IOException {
        // First check from the classpath, these profiles are used as a basis
        List<Profile> profiles = readProfileFromClasspath(name);

        File profileFile = findProfileYaml(directory);
        if (profileFile != null) {
            List<Profile> fileProfiles = fromYaml(new FileInputStream(profileFile));
            for (Profile profile : fileProfiles) {
                if (profile.getName().equals(name)) {
                    profiles.add(profile);
                    break;
                }
            }
        }
        // "larger" orders are "earlier" in the list
        Collections.sort(profiles, Collections.<Profile>reverseOrder());
        return mergeProfiles(profiles);
    }

    private static ProcessorConfig extractProcesssorConfiguration(ProcessorConfigurationExtractor extractor,
                                                                 String profile,
                                                                 File resourceDir) throws IOException {
        Profile profileFound = findProfile(profile, resourceDir);
        return extractor.extract(profileFound);
    }

    private static Profile mergeProfiles(List<Profile> profiles) {
        Profile ret = null;
        for (Profile profile : profiles) {
            if (profile != null) {
                if (ret == null) {
                    ret = new Profile(profile);
                } else {
                    ret = new Profile(ret, profile);
                }
            }
        }
        return ret;
    }

    // Read all default profiles first, then merge in custom profiles found on the classpath
    private static List<Profile> readProfileFromClasspath(String name) throws IOException {
        List<Profile> ret = new ArrayList<>();
        ret.addAll(readAllFromClasspath(name, "default"));
        ret.addAll(readAllFromClasspath(name, ""));
        return ret;
    }

    /**
     * Read all profiles found in the classpath.
     *
     * @param name name of the profile to lookup
     * @param ext to use (e.g. 'default' for checking 'profile-default.yml'. Can also be null or empty.
     * @return all profiles with this name stored in files with this extension
     *
     * @throws IOException if reading of a profile fails
     */
    public static List<Profile> readAllFromClasspath(String name, String ext) throws IOException {
        List<Profile > ret = new ArrayList<>();
        for (String location : getMetaInfProfilePaths(ext)) {
            for (String url : ClassUtil.getResources(location)) {
                for (Profile profile : fromYaml(new URL(url).openStream())) {
                    if (name.equals(profile.getName())) {
                        ret.add(profile);
                    }
                }
            }
        }
        return ret;
    }

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

    // check for various variations of profile files
    private static File findProfileYaml(File directory) {
        for (String profileFile : PROFILE_FILENAMES) {
            File ret = new File(directory, String.format(profileFile, ""));
            if (ret.exists()) {
                return ret;
            }
        }
        return null;
    }

    // prepend meta-inf location
    private static List<String> getMetaInfProfilePaths(String ext) {
        List<String> ret = new ArrayList<>(PROFILE_FILENAMES.length);
        for (String p : PROFILE_FILENAMES) {
            ret.add("META-INF/fabric8/" + getProfileFileName(p,ext));
        }
        return ret;
    }

    private static String getProfileFileName(String fileName, String ext) {
        return String.format(fileName, StringUtils.isNotBlank(ext) ? "-" + ext : "");
    }

    /**
     * Load a profile from an input stream. This must be in YAML format
     *
     * @param is inputstream to read the profile from
     * @return the de-serialized profile
     * @throws IOException if deserialization fails
     */
    public static List<Profile> fromYaml(InputStream is) throws IOException {
        TypeReference<List<Profile>> typeRef = new TypeReference<List<Profile>>() {};
        return mapper.readValue(is, typeRef);
    }

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

    // Use to select either a generator or enricher config
    public interface ProcessorConfigurationExtractor {
        ProcessorConfig extract(Profile profile);
    }

    /**
     * Get the generator configuration
     */
    public final static ProcessorConfigurationExtractor GENERATOR_CONFIG = profile -> profile.getGeneratorConfig();

    /**
     * Get the enricher configuration
     */
    public final static ProcessorConfigurationExtractor ENRICHER_CONFIG = profile -> profile.getEnricherConfig();

    /**
     * Get the watcher configuration
     */
    public final static ProcessorConfigurationExtractor WATCHER_CONFIG = profile -> profile.getWatcherConfig();

}


/**
 * 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 java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import io.fabric8.maven.docker.util.Logger;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.maven.project.MavenProject;

/**
 * @author roland
 * @since 24/07/16
 */
public class ClassUtil {

    public static Set<String> getResources(String resource) throws IOException {
        return getResources(resource, null);
    }

    public static Set<String> getResources(String resource, List<ClassLoader> additionalClassLoaders) throws IOException {

        ClassLoader[] classLoaders = mergeClassLoaders(additionalClassLoaders);

        Set<String> ret = new HashSet<>();
        for (ClassLoader cl : classLoaders) {
            Enumeration<URL> urlEnum = cl.getResources(resource);
            ret.addAll(extractUrlAsStringsFromEnumeration(urlEnum));
        }
        return ret;
    }

    private static ClassLoader[] mergeClassLoaders(List<ClassLoader> additionalClassLoaders) {
        ClassLoader[] classLoaders;

        if (additionalClassLoaders != null && !additionalClassLoaders.isEmpty()) {
            classLoaders = ArrayUtils.addAll(getClassLoaders(), additionalClassLoaders.toArray(new ClassLoader[additionalClassLoaders.size()]));
        }
        else {
            classLoaders = getClassLoaders();
        }
        return classLoaders;
    }

    private static ClassLoader[] getClassLoaders() {
        return new ClassLoader[] {
            Thread.currentThread().getContextClassLoader(),
            PluginServiceFactory.class.getClassLoader()
        };
    }

    private static Set<String> extractUrlAsStringsFromEnumeration(Enumeration<URL> urlEnum) {
        Set<String> ret = new HashSet<String>();
        while (urlEnum.hasMoreElements()) {
            ret.add(urlEnum.nextElement().toExternalForm());
        }
        return ret;
    }

    public static <T> Class<T> classForName(String className, List<ClassLoader> additionalClassLoaders) {
        ClassLoader[] classLoaders = mergeClassLoaders(additionalClassLoaders);
        Set<ClassLoader> tried = new HashSet<>();
        for (ClassLoader loader : classLoaders) {
            // Go up the classloader stack to eventually find the server class. Sometimes the WebAppClassLoader
            // hide the server classes loaded by the parent class loader.
            while (loader != null) {
                try {
                    if (!tried.contains(loader)) {
                        return (Class<T>) Class.forName(className, true, loader);
                    }
                } catch (ClassNotFoundException ignored) {}
                tried.add(loader);
                loader = loader.getParent();
            }
        }
        return null;
    }

    /**
     * Find all classes below a certain directory which contain
     * main() classes
     *
     * @param rootDir the directory to start from
     * @return List of classes with "public void static main(String[] args)" methods. Can be empty, but not null.
     * @exception IOException if something goes wrong
     */
    public static List<String> findMainClasses(File rootDir) throws IOException {
        List<String> ret = new ArrayList<>();
        if (!rootDir.exists()) {
            return ret;
        }
        if (!rootDir.isDirectory()) {
            throw new IllegalArgumentException(String.format("Path %s is not a directory",rootDir.getPath()));
        }
        findClasses(ret, rootDir, rootDir.getAbsolutePath() + "/");
        return ret;
    }

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

    private static final FileFilter DIR_FILTER = pathname -> pathname.isDirectory() && !pathname.getName().startsWith(".");

    private static final FileFilter CLASS_FILE_FILTER = file -> (file.isFile() && file.getName().endsWith(".class"));

    private static void findClasses(List<String> classes, File dir, String prefix) throws IOException {
        for (File subDir : dir.listFiles(DIR_FILTER)) {
            findClasses(classes, subDir, prefix);
        }

        for (File classFile : dir.listFiles(CLASS_FILE_FILTER)) {
            try (InputStream is = new FileInputStream(classFile)) {
                if (hasMainMethod(is)) {
                    classes.add(convertToClass(classFile.getAbsolutePath(), prefix));
                }
            }
        }
    }

    private static boolean hasMainMethod(InputStream is) throws IOException {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.makeClass(is);
            CtClass stringClass = pool.get("java.lang.String[]");
            CtMethod mainMethod = ctClass.getDeclaredMethod("main", new CtClass[] { stringClass });
            return mainMethod.getReturnType() == CtClass.voidType &&
                   Modifier.isStatic(mainMethod.getModifiers()) &&
                   Modifier.isPublic(mainMethod.getModifiers());
        } catch (NotFoundException e) {
            return false;
        }
    }

    private static String convertToClass(String name, String prefix) {
        String ret = name.replaceAll("[/\\\\]", ".");
        ret = ret.substring(0, name.length() - ".class".length());
        return ret.substring(prefix.length());
    }

    public static URLClassLoader createProjectClassLoader(List<String> elements, Logger log) {

        try {

            List<URL> compileJars = new ArrayList<>();

            for (String element : elements) {
                compileJars.add(new File(element).toURI().toURL());
            }

            return new URLClassLoader(compileJars.toArray(new URL[compileJars.size()]),
                    PluginServiceFactory.class.getClassLoader());

        } catch (Exception e) {
            log.warn("Instructed to use project classpath, but cannot. Continuing build if we can: ", e);
        }

        // return an empty CL .. don't want to have to deal with NULL later
        // if somehow we incorrectly call this method
        return new URLClassLoader(new URL[]{});
    }
}