/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jboss.pnc.model;

import org.hibernate.validator.constraints.Email;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.HashSet;
import java.util.Set;

/**
 * The Class User maps the user that triggered the builds, and are linked to the BuildRecord
 *
 * @author avibelli
 */
@Entity
@Table(name = "UserTable", uniqueConstraints = { @UniqueConstraint(name = "uk_user_email", columnNames = { "email" }),
        @UniqueConstraint(name = "uk_user_username", columnNames = { "username" }) })
public class User implements GenericEntity<Integer> {

    private static final long serialVersionUID = 8437525005838384722L;

    public static final String DEFAULT_SORTING_FIELD = "username";
    public static final String SEQUENCE_NAME = "user_id_seq";

    @Id
    @SequenceGenerator(name = SEQUENCE_NAME, sequenceName = SEQUENCE_NAME, initialValue = 100, allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE_NAME)
    private Integer id;

    @Column(unique = true)
    @NotNull
    @Email
    @Size(max=255)
    private String email;

    @Size(max=50)
    private String firstName;

    @Size(max=50)
    private String lastName;

    /**
     * OAUTH token, used to pass around. Property is set once user is authenticated,
     * note that having a token doesn't necessary mean user is logged-in a token needs to be validated.
     */
    @Transient
    private String loginToken;

    @Column(unique = true)
    @NotNull
    @Size(max=50)
    private String username;

    @OneToMany(mappedBy = "user")
    private Set<BuildRecord> buildRecords;

    /**
     * Instantiates a new user.
     */
    public User() {
    }

    /**
     * Gets the id.
     *
     * @return the id
     */
    @Override
    public Integer getId() {
        return this.id;
    }

    /**
     * Sets the id.
     *
     * @param id the new id
     */
    @Override
    public void setId(Integer id) {
        this.id = id;
    }

    /**
     * Gets the email.
     *
     * @return the email
     */
    public String getEmail() {
        return this.email;
    }

    /**
     * Sets the email.
     *
     * @param email the new email
     */
    public void setEmail(String email) {
        this.email = email;
    }

    /**
     * Gets the first name.
     *
     * @return the first name
     */
    public String getFirstName() {
        return this.firstName;
    }

    /**
     * Sets the first name.
     *
     * @param firstName the new first name
     */
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    /**
     * Gets the last name.
     *
     * @return the last name
     */
    public String getLastName() {
        return this.lastName;
    }

    /**
     * Sets the last name.
     *
     * @param lastName the new last name
     */
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    /**
     * Gets the username.
     *
     * @return the username
     */
    public String getUsername() {
        return this.username;
    }

    /**
     * Sets the username.
     *
     * @param username the new username
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * Gets the project build record.
     *
     * @return the project build record
     */
    public Set<BuildRecord> getBuildRecords() {
        return buildRecords;
    }

    /**
     * Sets the project build record.
     *
     * @param buildRecords the new project build record
     */
    public void setBuildRecords(Set<BuildRecord> buildRecords) {
        this.buildRecords = buildRecords;
    }

    /**
     * Adds the project build record.
     *
     * @param buildRecord the project build record
     * @return the project build record
     */
    public BuildRecord addBuildRecord(BuildRecord buildRecord) {

        getBuildRecords().add(buildRecord);
        buildRecord.setUser(this);

        return buildRecord;
    }

    /**
     * Removes the project build record.
     *
     * @param buildRecord the project build record
     * @return the project build record
     */
    public BuildRecord removeBuildRecord(BuildRecord buildRecord) {
        getBuildRecords().remove(buildRecord);
        buildRecord.setUser(null);
        return buildRecord;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((username == null) ? 0 : username.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        User other = (User) obj;
        if (username == null) {
            if (other.username != null) {
                return false;
            }
        } else if (!username.equals(other.username)) {
            return false;
        }
        return true;
    }

    public String getLoginToken() {
        return loginToken;
    }

    public void setLoginToken(String loginToken) {
        this.loginToken = loginToken;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", email='" + email + '\'' +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", loginToken='" + loginToken + '\'' +
                ", username='" + username + '\'' +
                '}';
    }

    public static class Builder {

        private Integer id;

        private String email;

        private String firstName;

        private String lastName;

        private String username;

        private Set<BuildRecord> buildRecords;

        private Builder() {
            buildRecords = new HashSet<>();
        }

        public static Builder newBuilder() {
            return new Builder();
        }

        public User build() {

            User user = new User();
            user.setId(id);
            user.setEmail(email);
            user.setFirstName(firstName);
            user.setLastName(lastName);
            user.setUsername(username);

            // Set the bi-directional mapping
            for (BuildRecord buildRecord : buildRecords) {
                buildRecord.setUser(user);
            }
            user.setBuildRecords(buildRecords);

            return user;
        }

        public Builder id(Integer id) {
            this.id = id;
            return this;
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        public Builder firstName(String firstName) {
            this.firstName = firstName;
            return this;
        }

        public Builder lastName(String lastName) {
            this.lastName = lastName;
            return this;
        }

        public Builder username(String username) {
            this.username = username;
            return this;
        }

        public Builder buildRecord(BuildRecord buildRecord) {
            this.buildRecords.add(buildRecord);
            return this;
        }

        public Builder buildRecords(Set<BuildRecord> buildRecords) {
            this.buildRecords = buildRecords;
            return this;
        }

    }

}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jboss.pnc.coordinator.maintenance;

import org.jboss.pnc.common.concurrent.NamedThreadFactory;
import org.jboss.pnc.model.BuildConfigSetRecord;
import org.jboss.pnc.model.BuildRecord;
import org.jboss.pnc.spi.datastore.repositories.BuildConfigSetRecordRepository;
import org.jboss.pnc.spi.datastore.repositories.BuildRecordRepository;
import org.jboss.pnc.spi.exception.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;

/**
 * @author Matej Lazar
 */
@Dependent
public class TemporaryBuildsCleanerAsyncInvoker {

    private Logger logger = LoggerFactory.getLogger(TemporaryBuildsCleanerAsyncInvoker.class);

    private TemporaryBuildsCleaner temporaryBuildsCleaner;
    private BuildRecordRepository buildRecordRepository;

    private ExecutorService executorService;

    private BuildConfigSetRecordRepository buildConfigSetRecordRepository;

    @Deprecated //CDI workaround
    public TemporaryBuildsCleanerAsyncInvoker() {
    }

    @Inject
    public TemporaryBuildsCleanerAsyncInvoker(
            TemporaryBuildsCleaner temporaryBuildsCleaner,
            BuildRecordRepository buildRecordRepository,
            BuildConfigSetRecordRepository buildConfigSetRecordRepository) {
        this.temporaryBuildsCleaner = temporaryBuildsCleaner;
        this.buildRecordRepository = buildRecordRepository;
        this.buildConfigSetRecordRepository = buildConfigSetRecordRepository;

        executorService = Executors.newSingleThreadExecutor(new NamedThreadFactory("build-coordinator.TemporaryBuildsCleanerAsyncInvoker"));
    }

    /**
     * Deletes a single temporary build.
     *
     * @param buildRecordId ID of the build to be deleted
     * @param authToken Bearer token
     * @param onComplete Operation to be executed after deletion operation
     * @return True if the build exists and deletion started otherwise, false is build doesn't exist
     * @throws ValidationException Thrown when build cannot be deleted
     */
    public boolean deleteTemporaryBuild(Integer buildRecordId, String authToken, Consumer<Result> onComplete) throws ValidationException {
        BuildRecord buildRecord = buildRecordRepository.findByIdFetchAllProperties(buildRecordId);
        if (buildRecord == null) {
            return false;
        }

        if (!buildRecord.isTemporaryBuild()) {
            throw new ValidationException("Only deletion of the temporary builds is allowed");
        }

        executorService.submit(() -> {
            try {
                Result result = temporaryBuildsCleaner.deleteTemporaryBuild(buildRecordId, authToken);
                onComplete.accept(result);
            } catch (ValidationException e) {
                logger.error("Failed to delete temporary buildRecord.id: " + buildRecordId + ".", e);
                onComplete.accept(new Result(buildRecordId.toString(), Result.Status.FAILED, "Failed to delete temporary buildRecord."));
            }
        });

        return true;
    }

    public void deleteTemporaryBuildConfigSetRecord(Integer buildConfigSetRecordId, String authToken, Consumer<Result> onComplete) throws ValidationException {
        BuildConfigSetRecord buildConfigSetRecord = buildConfigSetRecordRepository.queryById(buildConfigSetRecordId);
        if (!buildConfigSetRecord.isTemporaryBuild()) {
            throw new ValidationException("Only deletion of the temporary builds is allowed");
        }
        executorService.submit(() -> {
            try {
                Result result = temporaryBuildsCleaner.deleteTemporaryBuildConfigSetRecord(buildConfigSetRecordId, authToken);
                onComplete.accept(result);
            } catch (ValidationException e) {
                logger.error("Failed to delete temporary buildConfigSetRecord.id: " + buildConfigSetRecordId + ".", e);
                onComplete.accept(new Result(buildConfigSetRecordId.toString(), Result.Status.FAILED, "Failed to delete temporary buildConfigSetRecord."));
            }
        });
    }

}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jboss.pnc.model;

import org.jboss.pnc.enums.BuildType;
import org.hibernate.annotations.ForeignKey;
import org.hibernate.annotations.Index;
import org.hibernate.annotations.Type;
import org.hibernate.envers.Audited;
import org.hibernate.envers.NotAudited;
import org.hibernate.envers.RelationTargetAuditMode;

import javax.persistence.CascadeType;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.MapKeyColumn;
import javax.persistence.PersistenceException;
import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * The Class BuildConfiguration cointains the information needed to execute a build of a project, i.e. the sources,
 * the build script, the build system image needed to run, the project configurations that need to be triggered after
 * a successful build. Note that creationTime and lastModificationTime are handled internally via JPA settings
 * and therefore do not have public setters.
 * <p>
 * (project + name) should be unique
 *
 * @author avibelli
 * @author Jakub Bartecek
 */
@Entity
@Audited
@Table(uniqueConstraints = {@UniqueConstraint(name = "UK_BUILD_CONFIGURATION_NAME", columnNames = {"name", "active"})})
public class BuildConfiguration implements GenericEntity<Integer>, Cloneable {

    private static final long serialVersionUID = -5890729679489304114L;

    public static final String DEFAULT_SORTING_FIELD = "name";
    public static final String SEQUENCE_NAME = "build_configuration_id_seq";

    @Id
    @SequenceGenerator(name = SEQUENCE_NAME, sequenceName = SEQUENCE_NAME, initialValue = 100, allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE_NAME)
    private Integer id;

    @NotNull
    @Size(max=255)
    private String name;

    @Lob
    @Type(type = "org.hibernate.type.TextType")
    private String buildScript;

    @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
    @NotNull
    @Index(name="idx_buildconfiguration_repositoryconfiguration")
    @ManyToOne(optional = false)
    @JoinColumn(updatable = true, nullable = false)
    private RepositoryConfiguration repositoryConfiguration;

    /**
     * Revision to build.
     * This may not be the final revision on which the
     * actual build gets executed, but MUST be the starting point
     * of the build process.
     */
    @Size(max=255)
    private String scmRevision;

    @Lob
    @Type(type = "org.hibernate.type.TextType")
    private String description;

    @NotAudited
    @ManyToOne
    @ForeignKey(name = "fk_build_configuration_product_version")
    @Index(name="idx_build_configuration_product_version")
    private ProductVersion productVersion;

    @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
    @NotNull
    @ManyToOne(cascade = { CascadeType.REFRESH, CascadeType.DETACH })
    @ForeignKey(name = "fk_buildconfiguration_project")
    @Index(name="idx_buildconfiguration_project")
    private Project project;

    @Enumerated(EnumType.STRING)
    private BuildType buildType;

    @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
    @NotNull
    @ManyToOne(cascade = { CascadeType.REFRESH, CascadeType.DETACH })
    @ForeignKey(name = "fk_buildconfiguration_buildenvironment")
    @Index(name="idx_buildconfiguration_buildenvironment")
    private BuildEnvironment buildEnvironment;

    @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED)
    @ManyToMany(mappedBy = "buildConfigurations")
    private Set<BuildConfigurationSet> buildConfigurationSets;

    @NotNull
    @Column(columnDefinition = "timestamp with time zone", updatable=false)
    private Date creationTime;

    @NotNull
    @Column(columnDefinition = "timestamp with time zone")
    private Date lastModificationTime;

    @PreUpdate
    void onUpdate() {
        lastModificationTime = new Date();
    }

    /**
     * Normally set to true.
     * If BuildConfiguration is no longer to be used (is archived) - this is set to **null**
     *
     * Workaround to have a database constraint for active configuration name
     */
    private Boolean active;

    /**
     * The set of build configs upon which this build depends. The build configs contained in dependencies should normally be
     * completed before this build config is executed. Similar to Maven dependencies.
     */
    @NotAudited
    @ManyToMany(cascade = { CascadeType.REFRESH })
    @JoinTable(name = "build_configuration_dep_map", joinColumns = {
            @JoinColumn(name = "dependency_id", referencedColumnName = "id") }, inverseJoinColumns = {
                    @JoinColumn(name = "dependant_id", referencedColumnName = "id") })
    @ForeignKey(name = "fk_build_configuration_dep_map_dependency", inverseName = "fk_build_configuration_dep_map_dependant")
    @Index(name="idx_build_configuration_dep_map_dependency", columnNames={"dependency_id", "dependant_id"} )
    private Set<BuildConfiguration> dependencies;

    /**
     * The set of build configs which depend upon this config. These builds must normally be built after this build is
     * completed. This is the reverse relationship as Maven dependencies.
     */
    @NotAudited
    @ManyToMany(mappedBy = "dependencies")
    private Set<BuildConfiguration> dependants;

    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "build_configuration_parameters")
    @MapKeyColumn(length = 50, name = "key", nullable = false)
    @Column(name = "value", nullable = false, length = 8192)
    private Map<String, String> genericParameters = new HashMap<>();

    /**
     * Instantiates a new project build configuration.
     */
    public BuildConfiguration() {
        dependencies = new HashSet<>();
        dependants = new HashSet<>();
        buildConfigurationSets = new HashSet<>();

        // The lastModificationTime needs to be non-null for certain use cases even though the
        // actual value is managed by JPA.  For example, if saving an entity with a relation
        // to BuildConfiguration, JPA will not allow the related entity to be saved unless
        // the Build Configuration contains both an ID and a non-null lastModificationTime
        lastModificationTime = Date.from(Instant.now());
    }

    /**
     * Set the creation time immediately before the config is persisted
     */
    @PrePersist
    private void initCreationTime() {
        this.creationTime = Date.from(Instant.now());
    }

    @PreRemove
    private void removeConfigurationFromSets() {
        for (BuildConfigurationSet bcs : buildConfigurationSets) {
            bcs.getBuildConfigurations().remove(this);
        }
    }

    @Override
    public Integer getId() {
        return id;
    }

    @Override
    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getBuildScript() {
        return buildScript;
    }

    public void setBuildScript(String buildScript) {
        this.buildScript = buildScript;
    }

    public String getScmRevision() {
        return scmRevision;
    }

    public void setScmRevision(String scmRevision) {
        this.scmRevision = scmRevision;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Project getProject() {
        return project;
    }

    public void setProject(Project project) {
        this.project = project;
    }

    /**
     * @return the productVersions associated with this build config
     */
    public ProductVersion getProductVersion() {
        return productVersion;
    }

    /**
     * @param productVersion the productVersion associated with this build config
     */
    public void setProductVersion(ProductVersion productVersion) {
        this.productVersion = productVersion;
    }

    public BuildEnvironment getBuildEnvironment() {
        return buildEnvironment;
    }

    public void setBuildEnvironment(BuildEnvironment buildEnvironment) {
        this.buildEnvironment = buildEnvironment;
    }

    /**
     * @return the buildConfigurationSets which contain this build configuration
     */
    public Set<BuildConfigurationSet> getBuildConfigurationSets() {
        return buildConfigurationSets;
    }

    /**
     * @param buildConfigurationSets the list of buildConfigurationSets
     */
    public void setBuildConfigurationSets(Set<BuildConfigurationSet> buildConfigurationSets) {
        if (buildConfigurationSets == null) {
            this.buildConfigurationSets.clear();
        } else {
            this.buildConfigurationSets = buildConfigurationSets;
        }
    }

    /**
     * @param buildConfigurationSet add to the list of buildConfigurationSets
     */
    public void addBuildConfigurationSet(BuildConfigurationSet buildConfigurationSet) {
        if (!this.buildConfigurationSets.contains(buildConfigurationSet)) {
            this.buildConfigurationSets.add(buildConfigurationSet);
        }
    }

    /**
     * @param buildConfigurationSet remove from the list of buildConfigurationSets
     */
    public void removeBuildConfigurationSet(BuildConfigurationSet buildConfigurationSet) {
        this.buildConfigurationSets.remove(buildConfigurationSet);
    }

    /**
     * @return the dependency build configs (only direct dependencies)
     */
    public Set<BuildConfiguration> getDependencies() {
        return dependencies;
    }

    /**
     * @param dependencies the set of build configs upon which this build depends
     */
    public void setDependencies(Set<BuildConfiguration> dependencies) {
        this.dependencies = dependencies;
    }

    public boolean addDependency(BuildConfiguration dependency) {
        // Don't allow a build config to depend on itself
        if (dependency.getId().equals(this.getId())) {
            throw new PersistenceException("A build configuration cannot depend on itself");
        }
        // Verify that we are not creating a circular dependency
        if (dependency.getAllDependencies().contains(this)) {
            List<BuildConfiguration> depPath = dependency.dependencyDepthFirstSearch(this);
            String depPathString = depPath.stream().map(dep -> dep.getName()).collect(Collectors.joining(" -> "));
            throw new PersistenceException("Unable to add dependency, would create a circular reference: " + depPathString);
        }

        boolean result = dependencies.add(dependency);
        if (!dependency.getDependants().contains(this)) {
            dependency.addDependant(this);
        }
        return result;
    }

    public boolean removeDependency(BuildConfiguration dependency) {
        boolean result = dependencies.remove(dependency);
        if (dependency.getDependants().contains(this)) {
            dependency.removeDependant(this);
        }
        return result;
    }

    /**
     * Gets the full set of indirect dependencies (dependencies of dependencies). In cases where a particular dependency is both
     * a direct dependency and is an indirect dependency, it will be included in the set.
     *
     * @return The set of indirect dependencies
     */
    public Set<BuildConfiguration> getIndirectDependencies() {
        Set<BuildConfiguration> indirectDependencies = new HashSet<BuildConfiguration>();
        List<BuildConfiguration> configsToCheck = new ArrayList<BuildConfiguration>();
        configsToCheck.addAll(getDependencies());
        while (!configsToCheck.isEmpty()) {
            BuildConfiguration nextConfig = configsToCheck.get(0);
            for (BuildConfiguration nextDep : nextConfig.getDependencies()) {
                if (!indirectDependencies.contains(nextDep)) {
                    indirectDependencies.add(nextDep);
                    configsToCheck.add(nextDep);
                }
            }
            configsToCheck.remove(nextConfig);
        }
        return indirectDependencies;
    }

    /**
     * Perform a depth first search of the dependencies to find a match of the given build config. Returns a list with a single
     * build config (this), if no match is found.
     *
     * @param buildConfig The build config to search for
     * @return A list of the build configurations in the path between this config and the given config.
     */
    public List<BuildConfiguration> dependencyDepthFirstSearch(BuildConfiguration buildConfig) {
        List<BuildConfiguration> path = new ArrayList<>();
        path.add(this);
        return this.dependencyDepthFirstSearch(buildConfig, path);
    }

    private List<BuildConfiguration> dependencyDepthFirstSearch(BuildConfiguration buildConfig, List<BuildConfiguration> path) {
        for (BuildConfiguration dep : getDependencies()) {
            path.add(dep);
            if (dep.equals(buildConfig)) {
                return path;
            } else {
                path = dep.dependencyDepthFirstSearch(buildConfig, path);
                if (path.get(path.size() - 1).equals(buildConfig)) {
                    return path;
                }
            }
            path.remove(path.size() - 1);
        }
        return path;
    }

    /**
     * Get the full set of both the direct and indirect dependencies.
     *
     * @return A set containing both direct and indirect dependencies
     */
    public Set<BuildConfiguration> getAllDependencies() {
        Set<BuildConfiguration> allDependencies = new HashSet<BuildConfiguration>();
        allDependencies.addAll(getDependencies());
        allDependencies.addAll(getIndirectDependencies());
        return allDependencies;
    }

    /**
     * @return the set of build configs which depend on this build
     */
    public Set<BuildConfiguration> getDependants() {
        return dependants;
    }

    public void setDependants(Set<BuildConfiguration> dependants) {
        this.dependants = dependants;
    }

    /**
     * This method is private because a dependant should never be added externally. Instead the dependency relation should be
     * set up using the addDependency method
     *
     * @param dependant
     * @return
     */
    private boolean addDependant(BuildConfiguration dependant) {
        boolean result = dependants.add(dependant);
        if (!dependant.getDependencies().contains(this)) {
            dependant.addDependency(this);
        }
        return result;
    }

    /**
     * This method is private because a dependant should never be removed externally. Instead the dependency relation should be
     * set up using the removeDependency method
     *
     * @param dependant
     * @return
     */
    private boolean removeDependant(BuildConfiguration dependant) {
        boolean result = dependants.remove(dependant);
        if (dependant.getDependencies().contains(this)) {
            dependant.removeDependency(this);
        }
        return result;
    }

    /**
     * @return the creationTime
     */
    public Date getCreationTime() {
        return creationTime;
    }

    /**
     * @param creationTime The time at which this config was created
     */
    public void setCreationTime(Date creationTime) {
        this.creationTime = creationTime;
    }

    /**
     * @return the lastModificationTime
     */
    public Date getLastModificationTime() {
        return lastModificationTime;
    }

    /**
     * @param lastModificationTime the time at which this config was last modified
     */
    public void setLastModificationTime(Date lastModificationTime) {
        if(lastModificationTime != null) {
            this.lastModificationTime = lastModificationTime;
        }
    }

    /**
     * @return true if this build config should no longer be used for builds
     */
    public boolean isArchived() {
        return !Boolean.TRUE.equals(active);
    }

    public void setArchived(boolean archived) {
        this.active = archived ? null : true;
    }

    /**
     * Get the current product milestone (if any) associated with this build config.
     *
     * @return The current product milestone for the product version associated with this build config, or null if there is none
     */
    public ProductMilestone getCurrentProductMilestone() {
        if(getProductVersion() == null) {
            return null;
        }
        return getProductVersion().getCurrentProductMilestone();
    }

    public RepositoryConfiguration getRepositoryConfiguration() {
        return repositoryConfiguration;
    }

    public void setRepositoryConfiguration(RepositoryConfiguration repositoryConfiguration) {
        this.repositoryConfiguration = repositoryConfiguration;
    }

    public Boolean getActive() {
        return active;
    }

    public void setActive(Boolean active) {
        this.active = active;
    }

    public Map<String, String> getGenericParameters() {
        return genericParameters;
    }

    public void setGenericParameters(Map<String, String> genericParameters) {
        this.genericParameters = genericParameters;
    }

    public BuildType getBuildType() {
        return buildType;
    }

    public void setBuildType(BuildType buildType) {
        this.buildType = buildType;
    }

    @Override
    public String toString() {
        return "BuildConfiguration " + getId() + " [project=" + getProject() + ", name=" + getName() + ", active=" + active + "]";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;

        BuildConfiguration that = (BuildConfiguration) o;

        if (id != null ? !id.equals(that.id) : that.id != null)
            return false;

        return true;
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }

    public static final String CLONE_PREFIX_DATE_FORMAT = "yyyyMMddHHmmss";

    /**
     * Creates a shallow based clone and overrides {@link #creationTime}, {@link #name} and erases {@link #id}.
     *
     * @return This objects clone.
     */
    @Override
    public BuildConfiguration clone() {
        //do not use this.clone as it clones the entity proxy object
        BuildConfiguration clone = new BuildConfiguration();
        clone.id = null;
        Date now = Date.from(Instant.now());
        clone.active = true;
        clone.buildConfigurationSets = new HashSet<>(buildConfigurationSets);
        clone.buildEnvironment = buildEnvironment;
        clone.buildScript = buildScript;
        clone.creationTime = now;
        clone.dependants = new HashSet<>(dependants);
        clone.dependencies = new HashSet<>(dependencies);
        clone.description = description;
        clone.genericParameters = new HashMap<>(genericParameters);
        clone.name = retrieveCloneName(name, now);
        clone.productVersion = productVersion;
        clone.project = project;
        clone.buildType = buildType;
        clone.repositoryConfiguration = repositoryConfiguration;
        clone.scmRevision = scmRevision;
        return clone;
    }

    /**
     * Change the BC clone name into date_original-BC-name where date will be for every clone updated and for original BC names
     * will be added.
     *
     * Example: clone1 of pslegr-BC on Wednesday October,21st, 2015: 20151021095415_pslegr-BC
     *
     * clone2 of 20151021095415_pslegr-BC on Thursday October,22nd, 2015: 20151022nnnnnn_pslegr-BC
     *
     * clone3 of pslegr-BC on Friday October,23rd, 2015: 20151023nnnnnn_pslegr-BC
     *
     * @param bcName
     * @param now
     * @return A correct name for the cloned BuildConfiguration
     */
    public static String retrieveCloneName(String bcName, Date now) {

        String bcNameToAppend = "";

        int index = bcName.indexOf("_");

        if (index == -1) {
            // No '_' was found, need to append date prefix to whole bcName
            bcNameToAppend = bcName;

        } else {
            // A '_' char was found, need to analyze if the prefix is a date (to
            // be replaced with new one)
            String prefix = bcName.substring(0, index);

            if (prefix.length() == CLONE_PREFIX_DATE_FORMAT.length()) {
                try {
                    new SimpleDateFormat(CLONE_PREFIX_DATE_FORMAT).parse(prefix);
                    // The prefix was a date, need to append new date to a substring
                    // of original bcName
                    bcNameToAppend = bcName.substring(index + 1);
                } catch (ParseException ex) {
                    // The prefix was not a date, need to append date prefix to
                    // whole bcName
                    bcNameToAppend = bcName;
                }
            } else {
                bcNameToAppend = bcName;
            }
        }

        StringBuilder sb = new StringBuilder();
        sb.append(new SimpleDateFormat(CLONE_PREFIX_DATE_FORMAT).format(now)).append("_").append(bcNameToAppend);
        return sb.toString();

    }

    public boolean dependsOn(BuildConfiguration other) {
        return getAllDependencies().contains(other);
    }

    public boolean dependsOnAny(Collection<BuildConfiguration> otherList) {
        return otherList.stream().anyMatch(this::dependsOn);
    }

    public static class Builder {

        private Integer id;

        private String name;

        private String buildScript;

        private RepositoryConfiguration repositoryConfiguration;

        private String scmRevision;

        private String description;

        private Project project;

        private BuildType buildType;

        private BuildEnvironment buildEnvironment;

        private ProductVersion productVersion;

        private Set<BuildConfiguration> dependencies;

        private Set<BuildConfiguration> dependants;

        private Set<BuildConfigurationSet> buildConfigurationSets;

        private Date creationTime;

        private Date lastModificationTime;

        private boolean archived = false;

        private Map<String, String> genericParameters = new HashMap<>();

        private Builder() {
            dependencies = new HashSet<>();
            dependants = new HashSet<>();
            buildConfigurationSets = new HashSet<>();
        }

        public static Builder newBuilder() {
            return new Builder();
        }

        public BuildConfiguration build() {
            BuildConfiguration buildConfiguration = new BuildConfiguration();
            buildConfiguration.setId(id);
            buildConfiguration.setName(name);
            buildConfiguration.setBuildScript(buildScript);
            buildConfiguration.setRepositoryConfiguration(repositoryConfiguration);
            buildConfiguration.setScmRevision(scmRevision);
            buildConfiguration.setDescription(description);

            // Set the bi-directional mapping
            if (project != null) {
                project.addBuildConfiguration(buildConfiguration);
            }
            buildConfiguration.setProject(project);
            buildConfiguration.setBuildType(buildType);
            buildConfiguration.setBuildEnvironment(buildEnvironment);
            buildConfiguration.setCreationTime(creationTime);
            buildConfiguration.setLastModificationTime(lastModificationTime);
            buildConfiguration.setArchived(archived);
            buildConfiguration.setGenericParameters(genericParameters);
            buildConfiguration.setBuildConfigurationSets(buildConfigurationSets);
            buildConfiguration.setProductVersion(productVersion);

            for (BuildConfigurationSet buildConfigurationSet : buildConfigurationSets) {
                buildConfigurationSet.addBuildConfiguration(buildConfiguration);
            }

            // Set the bi-directional mapping
            for (BuildConfiguration dependency : dependencies) {
                if (!dependency.getDependants().contains(buildConfiguration)) {
                    dependency.addDependant(buildConfiguration);
                }
            }
            buildConfiguration.setDependencies(dependencies);
            for (BuildConfiguration dependant : dependants) {
                if (!dependant.getDependencies().contains(buildConfiguration)) {
                    dependant.addDependant(buildConfiguration);
                }
            }
            buildConfiguration.setDependants(dependants);

            return buildConfiguration;
        }

        public Builder id(Integer id) {
            this.id = id;
            return this;
        }

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder buildScript(String buildScript) {
            this.buildScript = buildScript;
            return this;
        }

        public Builder repositoryConfiguration(RepositoryConfiguration repositoryConfiguration) {
            this.repositoryConfiguration = repositoryConfiguration;
            return this;
        }

        public Builder scmRevision(String scmRevision) {
            this.scmRevision = scmRevision;
            return this;
        }

        public Builder description(String description) {
            this.description = description;
            return this;
        }

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

        public Builder buildType(BuildType buildType) {
            this.buildType = buildType;
            return this;
        }

        public Builder buildEnvironment(BuildEnvironment buildEnvironment) {
            this.buildEnvironment = buildEnvironment;
            return this;
        }

        public Builder productVersion(ProductVersion productVersion) {
            this.productVersion = productVersion;
            return this;
        }

        public Builder dependency(BuildConfiguration dependency) {
            this.dependencies.add(dependency);
            return this;
        }

        public Builder dependencies(Set<BuildConfiguration> dependencies) {
            this.dependencies = dependencies;
            return this;
        }

        public Builder buildConfigurationSet(BuildConfigurationSet buildConfigurationSet) {
            this.buildConfigurationSets.add(buildConfigurationSet);
            return this;
        }

        public Builder buildConfigurationSets(Set<BuildConfigurationSet> buildConfigurationSets) {
            this.buildConfigurationSets = buildConfigurationSets;
            return this;
        }

        public Builder creationTime(Date creationTime) {
            this.creationTime = creationTime;
            return this;
        }

        public Builder lastModificationTime(Date lastModificationTime) {
            this.lastModificationTime = lastModificationTime;
            return this;
        }

        public Builder archived(boolean archived) {
            this.archived = archived;
            return this;
        }

        public Builder genericParameters(Map<String, String> genericParameters) {
            this.genericParameters = genericParameters;
            return this;
        }

    }

}


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

package org.jboss.pnc.mock.executor;

import org.jboss.pnc.common.concurrent.MDCExecutors;
import org.jboss.pnc.common.concurrent.NamedThreadFactory;
import org.jboss.pnc.mock.builddriver.BuildDriverResultMock;
import org.jboss.pnc.mock.model.builders.TestProjectConfigurationBuilder;
import org.jboss.pnc.mock.repositorymanager.RepositoryManagerResultMock;
import org.jboss.pnc.enums.BuildStatus;
import org.jboss.pnc.enums.BuildExecutionStatus;
import org.jboss.pnc.spi.builddriver.BuildDriverResult;
import org.jboss.pnc.spi.events.BuildExecutionStatusChangedEvent;
import org.jboss.pnc.spi.executor.BuildExecutionConfiguration;
import org.jboss.pnc.spi.executor.BuildExecutionSession;
import org.jboss.pnc.spi.executor.BuildExecutor;
import org.jboss.pnc.spi.executor.exceptions.ExecutorException;
import org.jboss.pnc.spi.repositorymanager.RepositoryManagerResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;

/**
 * @author Matej Lazar
 */
@ApplicationScoped
public class BuildExecutorMock implements BuildExecutor {

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

    private final Map<Integer, BuildExecutionSession> runningExecutions = new HashMap<>();

    private final ExecutorService executor = MDCExecutors.newFixedThreadPool(4, new NamedThreadFactory("build-executor-mock"));

    private final Map<Integer, CompletableFuture<Integer>> runningFutures = new HashMap<>();
//    @Deprecated //CDI workaround
//    public BuildExecutorMock() {
//    }
//
//    @Inject
//    public BuildExecutorMock(RepositoryManagerFactory repositoryManagerFactory, BuildDriverFactory buildDriverFactory, EnvironmentDriverFactory environmentDriverFactory, Configuration configuration) {
//
//    }

    @Override
    public BuildExecutionSession startBuilding(
            BuildExecutionConfiguration buildExecutionConfiguration,
            Consumer<BuildExecutionStatusChangedEvent> onBuildExecutionStatusChangedEvent,
            String accessToken)
            throws ExecutorException {

        log.info("Starting mock build execution for buildExecutionConfiguration.id {}", buildExecutionConfiguration.getId());

        BuildExecutionSession buildExecutionSession = new BuildExecutionSessionMock(buildExecutionConfiguration, onBuildExecutionStatusChangedEvent);
        buildExecutionSession.setStatus(BuildExecutionStatus.NEW);

        runningExecutions.put(buildExecutionConfiguration.getId(), buildExecutionSession);
        Consumer<BuildExecutionStatus> onCompleteInternal = (buildStatus) -> {
            log.debug("Removing buildExecutionConfiguration.id [" + buildExecutionConfiguration.getId() + "] form list of running tasks.");
            runningExecutions.remove(buildExecutionConfiguration.getId());
            buildExecutionSession.setStatus(buildStatus);
        };

        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> mockBuild(buildExecutionSession), executor)
                                .handleAsync((buildPassed, e) -> complete(buildPassed, e, onCompleteInternal), executor);
        runningFutures.put(buildExecutionConfiguration.getId(), future);
        return buildExecutionSession;
    }

    private Integer complete(Boolean buildPassed, Throwable e, Consumer<BuildExecutionStatus> onCompleteInternal) {
        if (e != null) {
            log.error("Error in mock build.", e);
        }

        log.debug("Completing mock build.");
        if (buildPassed) {
            onCompleteInternal.accept(BuildExecutionStatus.DONE);
        } else {
            onCompleteInternal.accept(BuildExecutionStatus.DONE_WITH_ERRORS);
        }
        return -1;
    }

    private Boolean mockBuild(BuildExecutionSession buildExecutionSession) {
        log.debug("Building {}.", buildExecutionSession.getId());
        BuildDriverResult driverResult;
        Boolean buildPassed;
        if (TestProjectConfigurationBuilder.FAIL.equals(buildExecutionSession.getBuildExecutionConfiguration().getBuildScript())) {
            log.debug("Marking build {} as Failed.", buildExecutionSession.getId());
            driverResult = BuildDriverResultMock.mockResult(BuildStatus.FAILED);
            buildExecutionSession.setStatus(BuildExecutionStatus.BUILD_COMPLETED_WITH_ERROR);
            buildPassed = false;
        } else if (TestProjectConfigurationBuilder.FAIL_WITH_DELAY.equals(buildExecutionSession.getBuildExecutionConfiguration().getBuildScript())) {
            log.debug("Waiting for a while for a build {}.", buildExecutionSession.getId());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                log.warn("Build mock has been interrupted.", e);
            }
            log.debug("Marking build {} as Failed.", buildExecutionSession.getId());
            driverResult = BuildDriverResultMock.mockResult(BuildStatus.FAILED);
            buildPassed = false;
        } else if (TestProjectConfigurationBuilder.CANCEL.equals(buildExecutionSession.getBuildExecutionConfiguration().getBuildScript())) {
            log.debug("Waiting for a while for a build {} to be canceled.", buildExecutionSession.getId());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                log.warn("Build mock has been interrupted.", e);
            }
            driverResult = BuildDriverResultMock.mockResult(BuildStatus.SUCCESS);
            buildPassed = false;
        } else {
            log.debug("Marking build {} as Success.", buildExecutionSession.getId());
            driverResult = BuildDriverResultMock.mockResult(BuildStatus.SUCCESS);
            RepositoryManagerResult repositoryManagerResult = RepositoryManagerResultMock.mockResult();
            buildExecutionSession.setRepositoryManagerResult(repositoryManagerResult);
            buildPassed = true;
        }

        buildExecutionSession.setBuildDriverResult(driverResult);
        return buildPassed;
    }

    @Override
    public BuildExecutionSession getRunningExecution(int buildExecutionTaskId) {
        return runningExecutions.get(buildExecutionTaskId);
    }

    @Override
    public void shutdown() {

    }

    @Override
    public void cancel(Integer executionConfigurationId) throws ExecutorException {
        BuildExecutionSession buildExecutionSession = runningExecutions.get(executionConfigurationId);
        if (buildExecutionSession == null) {
            log.error("Unable to cancel build {}. The build is not running.", executionConfigurationId);
            return;
        }
        log.info("Cancelling build {}.", executionConfigurationId);
        runningFutures.get(executionConfigurationId).cancel(true);
        BuildDriverResult driverResult = BuildDriverResultMock.mockResult(BuildStatus.CANCELLED);
        buildExecutionSession.setBuildDriverResult(driverResult);
    }
}


/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2019 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jboss.pnc.common;

import org.jboss.pnc.common.json.AbstractModuleConfig;
import org.jboss.pnc.common.json.ConfigurationJSONParser;
import org.jboss.pnc.common.json.ConfigurationParseException;
import org.jboss.pnc.common.json.GlobalModuleGroup;
import org.jboss.pnc.common.json.moduleprovider.ConfigProvider;
import org.jboss.pnc.common.util.IoUtils;
import org.jboss.pnc.common.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.enterprise.context.ApplicationScoped;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by Matej Lazar on 2014-12-02.
 *
 * @author Jakub Bartecek <jbartece@redhat.com>
 */
@ApplicationScoped
public class Configuration {

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

    public static final String CONFIG_SYSPROP = "pnc-config-file";

    private final String CONFIG_STRING;

    private Map<Class<?>, AbstractModuleConfig> configCache = new HashMap<>();

    private GlobalModuleGroup globalConfig;

    private ConfigurationJSONParser configurationJsonParser = new ConfigurationJSONParser();

    /**
     *
     * @throws ConfigurationParseException Thrown if configuration file couldn't be loaded or parsed
     */
    public Configuration() throws ConfigurationParseException {

        try (InputStream configStream = this.getConfigStream()) {
            log.debug("Initializing configuration.");
            this.CONFIG_STRING = StringUtils.replaceEnv(IoUtils.readStreamAsString(configStream));
            log.debug("Config string with replaced environment variables: {}", this.CONFIG_STRING);
        } catch (IOException e) {
            throw new ConfigurationParseException("Config could not be parsed.", e);
        }
    }

    /**
     * Reads configuration for module
     *
     * @param provider configuration provider of given module config type
     * @param <T> module config
     * @return Loaded configuration
     * @throws ConfigurationParseException Thrown if configuration file couldn't be loaded or parsed
     */
    @SuppressWarnings("unchecked")
    public <T extends AbstractModuleConfig> T getModuleConfig(ConfigProvider<T> provider) throws ConfigurationParseException {
        Class<T> moduleClass = provider.getType();
        if(configCache.containsKey(moduleClass))
            return (T) configCache.get(moduleClass);

        synchronized(this) {
            if(configCache.containsKey(moduleClass)) {
                return (T) configCache.get(moduleClass);
            }

            T config = configurationJsonParser.parseJSONPNCConfig(CONFIG_STRING,  provider);
            configCache.put(moduleClass, config);
            return config;
        }
    }

    public GlobalModuleGroup getGlobalConfig() throws ConfigurationParseException{
        if(globalConfig != null)
            return globalConfig;

        synchronized(this){
            if(globalConfig != null) {
                return globalConfig;
            }

            globalConfig = configurationJsonParser.parseJSONGlobalConfig(CONFIG_STRING);
            return globalConfig;
        }
    }

    private InputStream getConfigStream() throws IOException {
        String configFileName = System.getProperty(CONFIG_SYSPROP);
        if (configFileName == null)
            configFileName = "pnc-config.json";
        log.info("Loading configuration from file: " + configFileName);

        //Try to open stream from full path
        File file = new File(configFileName);
        if (file.exists())
            return new FileInputStream(file);

        //Try to open stream using classloader
        final InputStream inStream = getClass().getClassLoader().getResourceAsStream(configFileName);
        if (inStream != null)
            return inStream;

        throw new FileNotFoundException("Missing project config file.");
    }
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.tomcat.junit4;

import com.github.mjeanroy.junit.servers.junit4.JunitServerRunner;
import com.github.mjeanroy.junit.servers.servers.AbstractConfiguration;
import com.github.mjeanroy.junit.servers.tomcat.EmbeddedTomcat;
import com.github.mjeanroy.junit.servers.tomcat.EmbeddedTomcatConfiguration;
import com.github.mjeanroy.junit.servers.tomcat.exceptions.IllegalTomcatConfigurationException;
import org.junit.runners.model.InitializationError;

import static com.github.mjeanroy.junit.servers.engine.Servers.findConfiguration;

/**
 * Rule that can be used to start and stop embedded tomcat server.
 */
public class TomcatServerJunit4Runner extends JunitServerRunner {

	/**
	 * Create runner.
	 *
	 * @param klass Running class.
	 * @throws InitializationError If an error occurred while starting embedded server.
	 */
	public TomcatServerJunit4Runner(Class<?> klass) throws InitializationError {
		super(klass, instantiate(klass));
	}

	/**
	 * Instantiate embedded tomcat to be used in tests.
	 *
	 * @param klass The tested class.
	 * @return The embedded tomcat.
	 */
	private static EmbeddedTomcat instantiate(Class<?> klass) {
		EmbeddedTomcatConfiguration configuration = extractConfiguration(klass);
		return configuration == null ? new EmbeddedTomcat() : new EmbeddedTomcat(configuration);
	}

	/**
	 * Try to extract {@link EmbeddedTomcat} configuration from the test class: search for a static
	 * field/method annotated with {@link com.github.mjeanroy.junit.servers.annotations.TestServerConfiguration}).
	 *
	 * @param klass The test class.
	 * @return The {@link EmbeddedTomcat} configuration.
	 * @throws IllegalTomcatConfigurationException If extracted annotation is not an instance of {@link EmbeddedTomcatConfiguration}.
	 */
	private static EmbeddedTomcatConfiguration extractConfiguration(Class<?> klass) {
		AbstractConfiguration configuration = findConfiguration(klass);

		if (configuration == null) {
			return null;
		}

		if (!(configuration instanceof EmbeddedTomcatConfiguration)) {
			throw new IllegalTomcatConfigurationException();
		}

		return (EmbeddedTomcatConfiguration) configuration;
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.jupiter;

import com.github.mjeanroy.junit.servers.annotations.TestHttpClient;
import com.github.mjeanroy.junit.servers.annotations.TestServer;
import com.github.mjeanroy.junit.servers.annotations.TestServerConfiguration;
import com.github.mjeanroy.junit.servers.client.HttpClient;
import com.github.mjeanroy.junit.servers.engine.AnnotationsHandlerRunner;
import com.github.mjeanroy.junit.servers.engine.EmbeddedServerRunner;
import com.github.mjeanroy.junit.servers.engine.Servers;
import com.github.mjeanroy.junit.servers.servers.AbstractConfiguration;
import com.github.mjeanroy.junit.servers.servers.EmbeddedServer;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;

import static com.github.mjeanroy.junit.servers.commons.Preconditions.notNull;

/**
 * Extension for Junit Jupiter.
 *
 * This jupiter will:
 *
 *

 *
Inject an embedded server before all tests and stop it after all tests.

 *
Read server configuration annotated with {@link TestServerConfiguration}

 *

 *     Resolve parameters of type (or any parameters inheriting from):
 *

 *
{@link EmbeddedServer}

 *
{@link AbstractConfiguration}

 *
{@link HttpClient}

 *

 *

 *

 *     Inject class field annotated with:
 *

 *
{@link TestServer}

 *
{@link TestServerConfiguration}

 *
{@link TestHttpClient}

 *

 *

 *

 *
 * Note that it is strongly recommended to use parameter instead instead of class injection.
 *
 * For example:
 *
 *

 *  @ExtendWith(JunitServerExtension.class)
 *  public class MyTest {
 *
 *    @Test
 *    void testGET(HttpClient client) {
 *      HttpResponse rsp = client.prepareGet("/path")
 *        .acceptJson()
 *        .execute();
 *
 *      Assertions.assertTrue(rsp.status() == 200);
 *    }
 *  }
 *

 *
 * The extension may also be used with {@link RegisterExtension}, in this case you can use it in two ways:
 *
 *

 *

 *     If the extension is declared as {@code static}, the server will be started before all tests
 *     and stopped after all tests (the recommended way).
 *

 *

 *     If the extension is not declared as {@code static}, the server will be started before each test
 *     and stopped after each test.
 *

 *

 *
 * For example:
 *
 *

 * public class MyTest {
 *
 *   // The `static` here means that the server will be started before all tests
 *   // and stopped after all tests.
 *   // Remove the `static` keyword to start/stop server before/after each test (not recommended).
 *   @RegisterExtension
 *   static JunitServerExtension extension = new JunitServerExtension();
 *
 *   @Test
 *   void testGET(HttpClient client) {
 *     HttpResponse rsp = client.prepareGet("/path")
 *       .acceptJson()
 *       .execute();
 *
 *     Assertions.assertTrue(rsp.status() == 200);
 *   }
 * }
 *

 */
public class JunitServerExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver {
	/**
	 * The namespace where each jupiter variable will be stored.
	 */
	private static final Namespace NAMESPACE = Namespace.create(JunitServerExtension.class.getName());

	/**
	 * The name of the {@link EmbeddedServerRunner} instance in the internal store.
	 */
	private static final String SERVER_RUNNER_KEY = "serverAdapter";

	/**
	 * The name of the {@link EmbeddedServerRunner} start mode flag in the internal store.
	 */
	private static final String SERVER_RUNNER_STATIC_MODE = "serverAdapterMode";

	/**
	 * The name of the {@link AnnotationsHandlerRunner} instance in the internal store.
	 */
	private static final String ANNOTATIONS_RUNNER_KEY = "annotationsAdapter";

	/**
	 * The list of parameter resolvers.
	 */
	private static final Map<Class<?>, ParameterResolverFunction> RESOLVERS = new HashMap<Class<?>, ParameterResolverFunction>() {{
		put(EmbeddedServer.class, EmbeddedServerParameterResolverFunction.getInstance());
		put(AbstractConfiguration.class, ConfigurationResolverFunction.getInstance());
		put(HttpClient.class, HttpClientParameterResolverFunction.getInstance());
	}};

	/**
	 * The embedded server to use.
	 */
	private final EmbeddedServer<?> server;

	/**
	 * The embedded server configuration when instantiating it.
	 */
	private final AbstractConfiguration configuration;

	/**
	 * Create the jupiter with default server that will be automatically detected using the Service Provider
	 * API.
	 */
	public JunitServerExtension() {
		this.server = null;
		this.configuration = null;
	}

	/**
	 * Create the jupiter with given server to start/stop before/after tests.
	 *
	 * @param server The embedded server to use.
	 * @throws NullPointerException If {@code server} is {@code null}.
	 */
	public JunitServerExtension(EmbeddedServer<?> server) {
		this.server = notNull(server, "server");
		this.configuration = null;
	}

	/**
	 * Create the jupiter with given server configuration.
	 *
	 * @param configuration The embedded server configuration to use.
	 * @throws NullPointerException If {@code configuration} is {@code null}.
	 */
	public JunitServerExtension(AbstractConfiguration configuration) {
		this.server = null;
		this.configuration = configuration;
	}

	@Override
	public void beforeAll(ExtensionContext context) {
		registerEmbeddedServer(context, true);
	}

	@Override
	public void afterAll(ExtensionContext context) {
		unregisterEmbeddedServer(context, true);
	}

	@Override
	public void beforeEach(ExtensionContext context) {
		EmbeddedServerRunner serverAdapter = findEmbeddedServerAdapterInStore(context);

		// The extension was not declared as a static extension.
		if (serverAdapter == null) {
			serverAdapter = registerEmbeddedServer(context, false);
		}

		EmbeddedServer<?> server = serverAdapter.getServer();
		AbstractConfiguration configuration = server.getConfiguration();
		AnnotationsHandlerRunner annotationsAdapter = new AnnotationsHandlerRunner(server, configuration);
		annotationsAdapter.beforeEach(context.getRequiredTestInstance());

		putAnnotationsHandlerAdapterInStore(context, annotationsAdapter);
	}

	@Override
	public void afterEach(ExtensionContext context) {
		try {
			Object target = context.getRequiredTestInstance();
			AnnotationsHandlerRunner annotationsAdapter = findAnnotationsHandlerAdapterInStore(context);
			annotationsAdapter.afterEach(target);
		}
		finally {
			unregisterEmbeddedServer(context, false);
			removeAnnotationsHandlerAdapterFromStore(context);
		}
	}

	@Override
	public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
		final Parameter parameter = parameterContext.getParameter();
		final Class<?> parameterClass = parameter.getType();

		// Fast case: a perfect match.
		if (RESOLVERS.containsKey(parameterClass)) {
			return true;
		}

		for (Class<?> klass : RESOLVERS.keySet()) {
			if (klass.isAssignableFrom(parameterClass)) {
				return true;
			}
		}

		return false;
	}

	@Override
	public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
		final Parameter parameter = parameterContext.getParameter();
		final Class<?> parameterClass = parameter.getType();
		final EmbeddedServerRunner serverAdapter = findEmbeddedServerAdapterInStore(extensionContext);

		// Fast case: a perfect match.
		if (RESOLVERS.containsKey(parameterClass)) {
			return RESOLVERS.get(parameterClass).resolve(parameterContext, serverAdapter);
		}

		for (Class<?> klass : RESOLVERS.keySet()) {
			if (klass.isAssignableFrom(parameterClass)) {
				return RESOLVERS.get(klass).resolve(parameterContext, serverAdapter);
			}
		}

		// Should not happen since Junit framework will call this method if, and only if, the
		// method `supportsParameter` has previously returned `true`.
		return null;
	}

	/**
	 * Instantiate server (implementation to use is automatically detected using the Service Provider
	 * API).
	 *
	 * @param testClass The test class instance.
	 * @param configuration The embedded server configuration to use.
	 * @return The embedded server.
	 */
	protected EmbeddedServer<?> instantiateServer(Class<?> testClass, AbstractConfiguration configuration) {
		return configuration == null ? Servers.instantiate(testClass) : Servers.instantiate(configuration);
	}

	/**
	 * Start and register the embedded server.
	 *
	 * The embedded server may be used in two modes:
	 *
	 *

	 *
Started before all tests, and stopped after all tests, this is the static mode (the extension has been used as a static extension).

	 *
Started before each tests, and stopped after eacg tests, this is the non static mode (the extension has not been used as a static extension).

	 *

	 *
	 * @param context The test context.
	 * @param staticMode {@code true} if the extension has been registered as a static extension, {@code false} otherwise.
	 * @return The registered adapter.
	 */
	private EmbeddedServerRunner registerEmbeddedServer(ExtensionContext context, boolean staticMode) {
		Class<?> testClass = context.getRequiredTestClass();
		EmbeddedServer<?> server = this.server == null ? instantiateServer(testClass, configuration) : this.server;

		EmbeddedServerRunner serverAdapter = new EmbeddedServerRunner(server);
		serverAdapter.beforeAll();

		putEmbeddedServerAdapterInStore(context, serverAdapter, staticMode);

		return serverAdapter;
	}

	/**
	 * Stop and remove from the store the started embedded server.
	 *
	 * @param context The test context.
	 * @param staticMode {@code true} if the extension has been registered as a static extension, {@code false} otherwise.
	 * @see #registerEmbeddedServer(ExtensionContext, boolean)
	 */
	private void unregisterEmbeddedServer(ExtensionContext context, boolean staticMode) {
		boolean registeredAsStatic = findInStore(context, SERVER_RUNNER_STATIC_MODE);
		if (registeredAsStatic == staticMode) {
			try {
				EmbeddedServerRunner serverAdapter = findEmbeddedServerAdapterInStore(context);
				serverAdapter.afterAll();
			}
			finally {
				removeEmbeddedServerAdapterFromStore(context);
			}
		}
	}

	/**
	 * Find {@link EmbeddedServerRunner} instance in the test context store.
	 *
	 * @param context The Junit-Jupiter test context.
	 * @return The current stored adapter.
	 */
	private static EmbeddedServerRunner findEmbeddedServerAdapterInStore(ExtensionContext context) {
		return (EmbeddedServerRunner) findInStore(context, SERVER_RUNNER_KEY);
	}

	/**
	 * Put {@link EmbeddedServerRunner} instance in the test context store.
	 *
	 * @param context The Junit-Jupiter test context.
	 * @param serverAdapter The instance to store.
	 */
	private static void putEmbeddedServerAdapterInStore(ExtensionContext context, EmbeddedServerRunner serverAdapter, boolean staticMode) {
		putInStore(context, SERVER_RUNNER_KEY, serverAdapter);
		putInStore(context, SERVER_RUNNER_STATIC_MODE, staticMode);
	}

	/**
	 * Remove {@link EmbeddedServerRunner} instance from the test context store.
	 *
	 * @param context The Junit-Jupiter test context.
	 */
	private static void removeEmbeddedServerAdapterFromStore(ExtensionContext context) {
		removeFromStore(context, SERVER_RUNNER_KEY);
		removeFromStore(context, SERVER_RUNNER_STATIC_MODE);
	}

	/**
	 * Find {@link AnnotationsHandlerRunner} instance in the test context store.
	 *
	 * @param context The Junit-Jupiter test context.
	 * @return The current stored adapter.
	 */
	private static AnnotationsHandlerRunner findAnnotationsHandlerAdapterInStore(ExtensionContext context) {
		return findInStore(context, ANNOTATIONS_RUNNER_KEY);
	}

	/**
	 * Put {@link AnnotationsHandlerRunner} instance in the test context store.
	 *
	 * @param context The Junit-Jupiter test context.
	 * @param annotationsHandlerAdapter The instance to store.
	 */
	private static void putAnnotationsHandlerAdapterInStore(ExtensionContext context, AnnotationsHandlerRunner annotationsHandlerAdapter) {
		putInStore(context, ANNOTATIONS_RUNNER_KEY, annotationsHandlerAdapter);
	}

	/**
	 * Remove {@link AnnotationsHandlerRunner} instance from the test context store.
	 *
	 * @param context The Junit-Jupiter test context.
	 */
	private static void removeAnnotationsHandlerAdapterFromStore(ExtensionContext context) {
		removeFromStore(context, ANNOTATIONS_RUNNER_KEY);
	}

	/**
	 * Put value in the internal store.
	 *
	 * @param context The Junit-Jupiter test context.
	 * @param name The name of the value to look for.
	 * @param value The value to store.
	 * @param <T> The type of the value to look for.
	 */
	@SuppressWarnings("unchecked")
	private static <T> void putInStore(ExtensionContext context, String name, T value) {
		getStore(context).put(name, value);
	}

	/**
	 * Find value in the internal store.
	 *
	 * @param context The Junit-Jupiter test context.
	 * @param name The name of the value to look for.
	 * @param <T> The type of the value to look for.
	 * @return The value currently stored.
	 */
	@SuppressWarnings("unchecked")
	private static <T> T findInStore(ExtensionContext context, String name) {
		return (T) getStore(context).get(name);
	}

	/**
	 * Remove given entry from the internal store.
	 *
	 * @param context The Junit-Jupiter test context.
	 * @param name The entry name to remove.
	 */
	private static void removeFromStore(ExtensionContext context, String name) {
		getStore(context).remove(name);
	}

	/**
	 * Get the internal store from the Junit-Jupiter test context.
	 *
	 * @param extensionContext The test context.
	 * @return The store.
	 */
	private static Store getStore(ExtensionContext extensionContext) {
		return extensionContext.getStore(NAMESPACE);
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.commons;

import com.github.mjeanroy.junit.servers.exceptions.UrlException;

import java.net.URI;
import java.net.URISyntaxException;

/**
 * Static URL utilities.
 *
 *

 *
 * Internal API: these methods are part of the internal API and may be removed, have their signature change,
 * or have their access level decreased from public to protected, package, or private in future versions without notice.
 */
public final class UrlUtils {

	private static final String[] HTTP_SCHEMES = new String[] { "http://", "https://" };

	/**
	 * The separator used in URL paths.
	 */
	private static final char PATH_SEPARATOR = '/';

	// Ensure non instantiation
	private UrlUtils() {
	}

	/**
	 * Create URI object and wrap {@link URISyntaxException} to {@link UrlException}.
	 *
	 * @param scheme URL scheme.
	 * @param host URL host.
	 * @param port URL port.
	 * @param path URL path.
	 * @return The final URI.
	 * @throws UrlException If URI cannot be built because of an invalid parameter.
	 */
	public static URI createUri(String scheme, String host, int port, String path) {
		try {
			return new URI(scheme, null, host, port, path, null, null);
		}
		catch (URISyntaxException ex) {
			throw new UrlException(scheme, host, port, path, ex);
		}
	}

	/**
	 * Ensure that given path is an absolute path (i.e starts with {@code '/'}.
	 *
	 * @param path The path.
	 * @return Absolute path.
	 */
	public static String ensureAbsolutePath(String path) {
		if (path == null || path.isEmpty()) {
			return String.valueOf(PATH_SEPARATOR);
		}

		return path.charAt(0) == PATH_SEPARATOR ? path : PATH_SEPARATOR + path;
	}

	/**
	 * Concatenate two path value.
	 *
	 * @param path Path prefix.
	 * @param endpoint Path suffix.
	 * @return Final path.
	 */
	public static String concatenatePath(String path, String endpoint) {
		String firstSegment = ensureAbsolutePath(path);
		if (endpoint == null || endpoint.isEmpty()) {
			return firstSegment;
		}

		StringBuilder sb = new StringBuilder(firstSegment);
		if (path.charAt(path.length() - 1) != PATH_SEPARATOR) {
			sb.append(PATH_SEPARATOR);
		}

		if (endpoint.charAt(0) == PATH_SEPARATOR) {
			sb.append(endpoint.substring(1));
		}
		else {
			sb.append(endpoint);
		}

		return sb.toString();
	}

	/**
	 * Check if given {@code url} starts with HTTP scheme (i.e {@code "http"} or {@code "https"}).
	 *
	 * @param url The URL to check.
	 * @return {@code true} if {@code url} starts with HTTP scheme, {@code false} otherwise.
	 */
	public static boolean startsWithHttpScheme(String url) {
		if (url == null || url.isEmpty()) {
			return false;
		}

		for (String scheme : HTTP_SCHEMES) {
			int prefixLength = scheme.length();
			if (url.length() > prefixLength) {
				String prefix = url.substring(0, prefixLength);
				if (prefix.equalsIgnoreCase(scheme)) {
					return true;
				}
			}
		}

		return false;
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.tomcat;

import com.github.mjeanroy.junit.servers.commons.CompositeClassLoader;
import com.github.mjeanroy.junit.servers.exceptions.ServerInitializationException;
import com.github.mjeanroy.junit.servers.exceptions.ServerStartException;
import com.github.mjeanroy.junit.servers.exceptions.ServerStopException;
import com.github.mjeanroy.junit.servers.servers.AbstractEmbeddedServer;
import org.apache.catalina.Context;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.tomcat.util.scan.StandardJarScanner;

import javax.servlet.ServletContext;
import java.io.File;

import static com.github.mjeanroy.junit.servers.commons.Strings.isNotBlank;
import static com.github.mjeanroy.junit.servers.tomcat.EmbeddedTomcatConfiguration.defaultConfiguration;

/**
 * Embedded server using tomcat as implementation.
 */
public class EmbeddedTomcat extends AbstractEmbeddedServer<Tomcat, EmbeddedTomcatConfiguration> {

	/**
	 * Tomcat instance.
	 */
	private final Tomcat tomcat;

	/**
	 * Tomcat context.
	 */
	private volatile Context context;

	/**
	 * Build embedded tomcat with default configuration.
	 */
	public EmbeddedTomcat() {
		this(defaultConfiguration());
	}

	/**
	 * Build embedded tomcat.
	 *
	 * @param configuration Tomcat configuration.
	 */
	public EmbeddedTomcat(EmbeddedTomcatConfiguration configuration) {
		super(configuration);
		this.tomcat = initServer();
	}

	private Tomcat initServer() {
		Tomcat tomcat = new Tomcat();
		tomcat.setBaseDir(configuration.getBaseDir());
		tomcat.setPort(configuration.getPort());

		tomcat.getHost().setAutoDeploy(true);
		tomcat.getHost().setDeployOnStartup(true);

		if (configuration.isEnableNaming()) {
			tomcat.enableNaming();
		}

		return tomcat;
	}

	private Context initContext() {
		try {
			return createContext();
		}
		catch (Exception ex) {
			throw new ServerInitializationException(ex);
		}
	}

	/**
	 * Create tomcat context.
	 * May be override by subclasses.
	 *
	 * @return Tomcat context.
	 * @throws Exception Exception May be thrown by web app context initialization (will be wrapped later).
	 * @deprecated Protected visibility may be removed in a next release, please submit an issue here to discuss it.
	 */
	@Deprecated
	protected Context createContext() throws Exception {
		Context context = null;

		final String webapp = configuration.getWebapp();
		final String path = configuration.getPath();
		final String classpath = configuration.getClasspath();
		final boolean forceMetaInf = configuration.isForceMetaInf();
		final ClassLoader parentClassLoader = configuration.getParentClasspath();
		final String descriptor = configuration.getOverrideDescriptor();

		File webappDirectory = new File(webapp);
		if (webappDirectory.exists()) {
			String webappAbsolutePath = webappDirectory.getAbsolutePath();
			tomcat.getHost().setAppBase(webappAbsolutePath);
			context = tomcat.addWebapp(path, webappAbsolutePath);

			// Add additional classpath entry
			if (isNotBlank(classpath)) {
				File file = new File(classpath);
				if (file.exists()) {

					// Check that additional classpath entry contains META-INF directory
					File metaInf = new File(file, "META-INF");
					if (!metaInf.exists() && forceMetaInf) {
						metaInf.mkdir();
					}

					// == Tomcat 8
					String absolutePath = file.getAbsolutePath();
					StandardRoot root = new StandardRoot(context);
					root.createWebResourceSet(
						WebResourceRoot.ResourceSetType.PRE,
						"/WEB-INF/classes",
						absolutePath,
						null,
						path
					);

					context.setResources(root);

					// == Tomcat 8

					// == Tomcat 7
					// String s = file.toURI().toString();
					// loader.addRepository(s);
					// == Tomcat 7

					// Used to scan additional classpath directory
					// https://issues.apache.org/bugzilla/show_bug.cgi?id=52853
					((StandardJarScanner) context.getJarScanner()).setScanAllDirectories(true);
				}
			}

			// Custom parent classloader.
			final ClassLoader threadCl = Thread.currentThread().getContextClassLoader();
			final ClassLoader tomcatParentClassLoader;

			if (parentClassLoader != null) {
				tomcatParentClassLoader = new CompositeClassLoader(parentClassLoader, threadCl);
			}
			else {
				tomcatParentClassLoader = threadCl;
			}

			// Set the parent class loader that will be given
			// to the created loader.
			//
			// Setting the parent class loader here is a shortcut for (code in previous versions):
			//
			//   Loader loader = context.getLoader();
			//   if (loader == null) {
			//     loader = new WebappLoader(parentClassLoader);
			//   }
			//
			context.setParentClassLoader(tomcatParentClassLoader);

			// Override web.xml path
			if (descriptor != null) {
				context.setAltDDName(descriptor);
			}
		}

		return context;
	}

	@Override
	public Tomcat getDelegate() {
		return tomcat;
	}

	@Override
	protected void doStart() {
		try {
			context = initContext();
			tomcat.start();
		}
		catch (Exception ex) {
			throw new ServerStartException(ex);
		}
	}

	@Override
	protected void doStop() {
		try {
			tomcat.stop();

			// Do not forget to destroy context
			if (context != null) {
				context.destroy();
				context = null;
			}

			if (!configuration.isKeepBaseDir()) {
				deleteDirectory(configuration.getBaseDir());
			}
		}
		catch (Exception ex) {
			throw new ServerStopException(ex);
		}
	}

	@Override
	public String getScheme() {
		return getConnector().getScheme();
	}

	@Override
	public ServletContext getServletContext() {
		return context == null ? null : context.getServletContext();
	}

	@Override
	protected int doGetPort() {
		return getConnector().getLocalPort();
	}

	private Connector getConnector() {
		return tomcat.getConnector();
	}

	private static void deleteDirectory(String path) {
		if (path == null) {
			return;
		}

		File file = new File(path);
		if (file.exists()) {
			File[] files = file.listFiles();
			if (files != null && files.length > 0) {
				for (File f : files) {
					if (f.isDirectory()) {
						deleteDirectory(f.getAbsolutePath());
					}

					f.delete();
				}
			}

			file.delete();
		}
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.client;

import java.util.HashMap;
import java.util.Map;

import static com.github.mjeanroy.junit.servers.commons.Dates.getTime;
import static com.github.mjeanroy.junit.servers.commons.Preconditions.notBlank;

/**
 * Static cookie utilities.
 */
public final class Cookies {

	/**
	 * The cookie separator used when multiple cookies needs to be sent to a server
	 * in an HTTP request.
	 */
	private static final String SEPARATOR = "; ";

	/**
	 * The character separating the name and the value of a cookie.
	 */
	private static final String NAME_VALUE_SEPARATOR = "=";

	/**
	 * The character separating cookies fields in the {@code Set-Cookie} header.
	 */
	private static final String FIELD_SEPARATOR = ";";

	// Ensure non instantiation.
	private Cookies() {
	}

	/**
	 * Create cookie with a name and a value, all other parameters will
	 * use the default values:
	 *
	 *

	 *
The webapp path is initialized with {@code "src/main/webapp"}

	 *
The path is initialized with {@code "/"}

	 *
The port is initialized with zero.

	 *
The cookie is not secured.

	 *
The cookie is not http-only.

	 *
The cookie max-age is {@code null}.

	 *
.
	 *
	 * @param name Cookie name.
	 * @param value Cookie value.
	 * @return Cookie.
	 * @throws NullPointerException if name or value is null.
	 * @throws IllegalArgumentException if name is empty or blank.
	 */
	public static Cookie cookie(String name, String value) {
		return new Cookie.Builder(name, value).build();
	}

	/**
	 * Create cookie.
	 *
	 * @param name Cookie name.
	 * @param value Cookie value.
	 * @param domain Cookie domain.
	 * @param path Cookie path.
	 * @param expires Cookie expires value.
	 * @param maxAge Cookie max age value.
	 * @param secure Secure flag.
	 * @param httpOnly Http flag.
	 * @return Cookie.
	 * @throws NullPointerException if name or value is null.
	 * @throws IllegalArgumentException if name is empty or blank.
	 */
	public static Cookie cookie(String name, String value, String domain, String path, Long expires, Long maxAge, boolean secure, boolean httpOnly) {
		Cookie.Builder builder = new Cookie.Builder(name, value)
			.domain(domain)
			.path(path)
			.secure(secure)
			.httpOnly(httpOnly);

		if (expires != null) {
			builder.expires(expires);
		}

		if (maxAge != null) {
			builder.maxAge(maxAge);
		}

		return builder.build();
	}

	/**
	 * Create a secured cookie, it means that cookie will have
	 * secure flag and http only flag set to true.
	 *
	 * @param name Cookie name.
	 * @param value Cookie value.
	 * @param domain Cookie domain.
	 * @param path Cookie path.
	 * @param expires Cookie expires value.
	 * @param maxAge Cookie max age.
	 * @return Cookie.
	 * @throws NullPointerException if name or value is null.
	 * @throws IllegalArgumentException if name is empty or blank.
	 */
	public static Cookie secureCookie(String name, String value, String domain, String path, Long expires, Long maxAge) {
		return Cookies.cookie(name, value, domain, path, expires, maxAge, true, true);
	}

	/**
	 * Create a session cookie, it means that cookie will have
	 * secure flag and http only flag set to true and expires / max age values
	 * are set to zero.
	 *
	 * @param name Cookie name.
	 * @param value Cookie value.
	 * @param domain Cookie domain.
	 * @param path Cookie path.
	 * @return Cookie.
	 * @throws NullPointerException if name or value is null.
	 * @throws IllegalArgumentException if name is empty or blank.
	 */
	public static Cookie sessionCookie(String name, String value, String domain, String path) {
		return Cookies.cookie(name, value, domain, path, -1L, 0L, true, true);
	}

	/**
	 * Create a cookie from header value.
	 *
	 * @param rawValue Header value.
	 * @return Cookie.
	 */
	public static Cookie read(String rawValue) {
		notBlank(rawValue, "Cookie value");

		final String[] parts = rawValue.split(FIELD_SEPARATOR);

		// Extract name and value
		final String[] nameAndValue = parts[0].split(NAME_VALUE_SEPARATOR);
		if (nameAndValue.length != 2) {
			throw new IllegalArgumentException("Cookie must have a valid name and a valid value");
		}

		final String name = nameAndValue[0].trim();
		final String value = nameAndValue[1].trim();
		if (name.isEmpty()) {
			throw new IllegalArgumentException("Cookie must have a valid name");
		}

		// Extract other parts
		Map<String, String> params = new HashMap<>();
		if (parts.length > 1) {
			for (String part : parts) {
				String[] param = part.split(NAME_VALUE_SEPARATOR);
				String paramName = param[0].toLowerCase().trim();
				String paramValue = param.length == 2 ? param[1].trim() : "";
				params.put(paramName, paramValue);
			}
		}

		final String domain = params.get("domain");
		final String path = params.get("path");
		final boolean secure = params.containsKey("secure");
		final boolean httpOnly = params.containsKey("httponly");

		final String maxAgeTt = params.get("max-age");
		final String expiresDate = params.get("expires");

		Long expires;
		Long maxAge = maxAgeTt == null ? null : Long.valueOf(maxAgeTt);

		if (expiresDate != null) {
			expires = getTime(expiresDate, "EEE, d MMM yyyy HH:mm:ss Z", "EEE, d-MMM-yyyy HH:mm:ss Z", "EEE, d/MMM/yyyy HH:mm:ss Z");
		}
		else {
			expires = null;
		}

		if (maxAge == null && expires == null) {
			maxAge = 0L;
		}
		else if (maxAge == null) {
			maxAge = expires - System.currentTimeMillis();
		}

		return Cookies.cookie(name, value, domain, path, expires, maxAge, secure, httpOnly);
	}

	/**
	 * Serialize cookies as a string that is ready to be sent to a server over an HTTP
	 * request (a.k.a value of {@code Cookie} header).
	 *
	 * @param cookies Cookies to serialize.
	 * @return The final value.
	 */
	public static String serialize(Iterable<Cookie> cookies) {
		StringBuilder result = new StringBuilder();
		boolean first = true;

		for (Cookie cookie : cookies) {
			if (!first) {
				result.append(SEPARATOR);
			}

			String name = cookie.getName();
			String value = cookie.getValue();
			result.append(name).append(NAME_VALUE_SEPARATOR).append(value);
			first = false;
		}

		return result.toString();
	}
}


/**
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2019 <mickael.jeanroy@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.mjeanroy.junit.servers.client;

import com.github.mjeanroy.junit.servers.commons.CollectionUtils;
import com.github.mjeanroy.junit.servers.commons.ToStringBuilder;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

import static com.github.mjeanroy.junit.servers.commons.Preconditions.notBlank;
import static com.github.mjeanroy.junit.servers.commons.Preconditions.notEmpty;
import static com.github.mjeanroy.junit.servers.commons.Preconditions.notNull;
import static com.github.mjeanroy.junit.servers.commons.Strings.toLowerCase;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;

/**
 * Http getHeader representation.
 *
 *

 * A header is defined by:
 *

 *
 A (case insensitive) name.

 *
A value, or a list of values.

 *

 */
public class HttpHeader {

	/**
	 * The separator for header values.
	 */
	private static final String HEADER_SEPARATOR = ",";

	/**
	 * Create a header with a single value.
	 *
	 * @param name Header name, must not be blank.
	 * @param value Header value.
	 * @return Header.
	 * @throws NullPointerException If {@code name} or {@code value} are {@code null}.
	 * @throws IllegalArgumentException If {@code name} is blank.
	 */
	public static HttpHeader header(String name, String value) {
		return new HttpHeader(name, singletonList(value));
	}

	/**
	 * Create a header with multiple values.
	 *
	 * @param name Header name.
	 * @param values Header values, must not be empty.
	 * @return Header.
	 * @throws NullPointerException If {@code name} or one of {@code values} are {@code null}.
	 * @throws IllegalArgumentException If {@code name} is blank.
	 */
	public static HttpHeader header(String name, Collection<String> values) {
		return new HttpHeader(name, values);
	}

	/**
	 * Create builder for {@link HttpHeader}.
	 *
	 * @param name Header name.
	 * @return Header value.
	 */
	public static HttpHeader.Builder builder(String name) {
		return new Builder(name);
	}

	/**
	 * Header name.
	 */
	private final String name;

	/**
	 * Header values.
	 */
	private final List<String> values;

	// Use static factories
	private HttpHeader(String name, Collection<String> values) {
		this.name = notBlank(name, "name");
		this.values = new ArrayList<>(values.size());

		for (String val : notEmpty(values, "values")) {
			this.values.add(notNull(val, "value"));
		}
	}

	/**
	 * Get header name.
	 *
	 * @return Header name.
	 */
	public String getName() {
		return name;
	}

	/**
	 * Get header values (returned list is not modifiable).
	 *
	 * @return Header values.
	 */
	public List<String> getValues() {
		return unmodifiableList(values);
	}

	/**
	 * Get first value of header.
	 *
	 * @return First header value.
	 */
	public String getFirstValue() {
		return values.get(0);
	}

	/**
	 * Get last value of header.
	 * If header has only a single value, result will be the same as the
	 * result of {@link #getFirstValue()}.
	 *
	 * @return Last header value.
	 */
	public String getLastValue() {
		return values.get(values.size() - 1);
	}

	/**
	 * Serialize header values using the default separator. The result is a string value that
	 * can be added to any HTTP request.
	 *
	 * @return Header values serialized as a string.
	 */
	public String serializeValues() {
		return CollectionUtils.join(values, HEADER_SEPARATOR);
	}

	@Override
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		}

		if (o instanceof HttpHeader) {
			HttpHeader h = (HttpHeader) o;
			return Objects.equals(toLowerCase(name), toLowerCase(h.name))
				&& Objects.equals(values, h.values);
		}

		return false;
	}

	@Override
	public int hashCode() {
		return Objects.hash(toLowerCase(name), values);
	}

	@Override
	public String toString() {
		return ToStringBuilder.create(getClass())
			.append("name", name)
			.append("values", values)
			.build();
	}

	/**
	 * Builder for {@link HttpHeader}.
	 */
	public static class Builder {
		/**
		 * The header name.
		 */
		private final String name;

		/**
		 * Header values.
		 */
		private final List<String> values;

		private Builder(String name) {
			this.name = notNull(name, "Http Header name must be defined");
			this.values = new ArrayList<>();
		}

		/**
		 * Add value to http header.
		 *
		 * @param value Header value.
		 * @return The builder.
		 */
		public Builder addValue(String value) {
			this.values.add(value);
			return this;
		}

		/**
		 * Build HTTP header instance.
		 *
		 * @return The created HTTP header.
		 */
		public HttpHeader build() {
			return header(name, values);
		}
	}
}