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

package com.navercorp.nbasearc.confmaster.server.cluster;

import static com.navercorp.nbasearc.confmaster.Constant.*;
import static com.navercorp.nbasearc.confmaster.Constant.Color.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.zookeeper.Op;
import org.apache.zookeeper.OpResult;
import org.codehaus.jackson.annotate.JsonAutoDetect;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.annotate.JsonPropertyOrder;
import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility;
import org.slf4j.helpers.MessageFormatter;
import org.springframework.context.ApplicationContext;

import com.navercorp.nbasearc.confmaster.ConfMasterException.MgmtSmrCommandException;
import com.navercorp.nbasearc.confmaster.ConfMasterException.MgmtSetquorumException;
import com.navercorp.nbasearc.confmaster.Constant.Color;
import com.navercorp.nbasearc.confmaster.config.Config;
import com.navercorp.nbasearc.confmaster.heartbeat.HBState;
import com.navercorp.nbasearc.confmaster.heartbeat.HBSession;
import com.navercorp.nbasearc.confmaster.io.BlockingSocket;
import com.navercorp.nbasearc.confmaster.io.BlockingSocketImpl;
import com.navercorp.nbasearc.confmaster.logger.Logger;
import com.navercorp.nbasearc.confmaster.server.MemoryObjectMapper;
import com.navercorp.nbasearc.confmaster.server.ZooKeeperHolder;
import com.navercorp.nbasearc.confmaster.server.workflow.WorkflowLogger;

public class PartitionGroupServer implements HeartbeatTarget,
        Comparable<PartitionGroupServer>, ClusterComponent {

    protected BlockingSocket connectionForCommand;

    protected HBSession hbSession;
    private UsedOpinionSet usedOpinions;

    private ZooKeeperHolder zk;
    private Config config;
    private MemoryObjectMapper mapper = new MemoryObjectMapper();
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    private Cluster cluster;

    private String path;
    private String name;
    private PartitionGroupServerData persistentData;
    private int znodeVersion;

    public PartitionGroupServer(ApplicationContext context, byte[] d,
            String clusterName, String pgsId, int znodeVersion) {
        persistentData = mapper.readValue(d, PartitionGroupServerData.class);
        init(context, clusterName, pgsId, znodeVersion);
    }

    public PartitionGroupServer(ApplicationContext context,
            PartitionGroupServerData d, String clusterName, String pgsId,
            int znodeVersion) {
        persistentData = d;
        init(context, clusterName, pgsId, znodeVersion);
    }

    public PartitionGroupServer(ApplicationContext context, String clusterName,
            String pgsId, String pgId, String pmName, String pmIp,
            int basePort, int backendPort, int znodeVersion) {
        persistentData = new PartitionGroupServerData(
                pgId, pmName, pmIp, basePort, backendPort);
    	init(context, clusterName, pgsId, znodeVersion);
    }

    private void init(ApplicationContext context, String clusterName, String pgsId, int znodeVersion) {
        this.zk = context.getBean(ZooKeeperHolder.class);
        this.config = context.getBean(Config.class);

        this.path = PathUtil.pgsPath(pgsId, clusterName);
        this.name = pgsId;
        this.cluster = context.getBean(ClusterComponentContainer.class).getCluster(clusterName);

        this.setZNodeVersion(znodeVersion);

        BlockingSocketImpl con = new BlockingSocketImpl(persistentData.pmIp,
                persistentData.smrMgmtPort, config.getClusterPgsTimeout(),
                PGS_PING, config.getDelim(), config.getCharset());
        con.setFirstHandshaker(new BlockingSocketImpl.FirstHandshaker() {
            @Override
            public void handshake(BufferedReader in, PrintWriter out, String delim)
                    throws IOException {
                String cmd = null;

                try {
                    cmd = "smrversion";
                    out.print(cmd + delim);
                    out.flush();

                    final String reply = in.readLine();
                    if (reply.equals("+OK 201") == false) {
                        return;
                    }
                } catch (IOException e) {
                    Logger.info("Singleton request to SMR fail. cmd: " + cmd);
                    throw e;
                }

                try {
                    cmd = "singleton confmaster";
                    out.print(cmd + delim);
                    out.flush();

                    final String reply = in.readLine();
                    if (reply.equals("+OK") == false) {
                        Logger.info("Singleton request to SMR fail. cmd: " + cmd
                                + ", reply: \"" + reply + "\"");
                        throw new IOException("Singleton request to SMR fail. " + this);
                    }
                } catch (IOException e) {
                    Logger.info("Singleton request to SMR fail. cmd: " + cmd);
                    throw e;
                }
            }
        });
        connectionForCommand = con;

        HBState hbRefData = new HBState();
        hbRefData.setZkData(persistentData.role, persistentData.stateTimestamp, 0)
                  .setLastState(persistentData.role)
                  .setLastStateTimestamp(persistentData.stateTimestamp)
                  .setSubmitMyOpinion(false);

        usedOpinions = new UsedOpinionSet();

        hbSession = new HBSession(context, this, persistentData.pmIp,
                persistentData.smrMgmtPort, cluster.getMode(),
                persistentData.hb, PGS_PING + "\r\n", hbRefData);
    }

    /*
     * Return true, if a master PGS and a slave PGS are in the same machine and
     * both are in the same PG. For instance, If an instance of this class and
     * Master PGS are in the same machine than this function return true,
     * otherwise return false.
     */
    public boolean isMasterInSameMachine(ClusterComponentContainer container) {
        final String machineName = persistentData.pmName;
        final PhysicalMachineCluster machineInfo = container.getPmc(machineName, getClusterName());
        final List<Integer> localPgsIdList = machineInfo.getPgsIdList();

        for (final Integer id : localPgsIdList) {
            PartitionGroupServer anotherPGS = container.getPgs(getClusterName(), String.valueOf(id));
            if (persistentData.pgId == anotherPGS.persistentData.pgId
                    && anotherPGS.persistentData.role.equals(PGS_ROLE_MASTER)
                    && machineName.equals(anotherPGS.persistentData.pmName)) {
                return true;
            }
        }
        return false;
    }

    public void propagateStateToHeartbeatSession() {
        hbSession.getHeartbeatState().setZkData(
                persistentData.role, persistentData.stateTimestamp, znodeVersion);
        hbSession.toggleHearbeat(cluster.getMode(), persistentData.hb);
    }

    @Override
    public void release() {
        try {
            hbSession.toggleHearbeat(cluster.getMode(), HB_MONITOR_NO);
            connectionForCommand.close();
        } catch (Exception e) {
            Logger.error("stop heartbeat fail. PGS:" + getName(), e);
        }

        try {
            hbSession.callbackDelete();
            turnOnUrgentHeartbeat();
        } catch (Exception e) {
            Logger.error("failed while delete pgs. " + toString(), e);
        }
    }

    public String executeQuery(String query) throws IOException {
        return connectionForCommand.execute(query);
    }

    public String executeQuery(String query, int retryCount) throws IOException {
        return connectionForCommand.execute(query, retryCount);
    }

    public long getActiveStateTimestamp() throws IOException {
        String response = executeQuery(PGS_PING);

        String[] resAry = response.split(" ");
        if (resAry.length < 3)
        {
            Logger.error("Invalid response. PGS_ID=" + getName() +
                    ", IP=" + persistentData.pmIp +
                    ", PORT=" + persistentData.smrBasePort +
                    ", RESPONSE=" + response);
            return -1;
        }

        return Long.parseLong(resAry[2]);
    }

    public String getActiveRole() throws IOException {
        String response = executeQuery(PGS_PING);
        String role = extractRole(response);
        if (role == null) {
            Logger.error("Invalid response. PGS_ID=" + getName() +
                    ", IP=" + persistentData.pmIp +
                    ", PORT=" + persistentData.smrBasePort +
                    ", RESPONSE=" + response);
            return "";
        }

        return role;
    }

    static public String extractRole(String response) {
        String[] resAry = response.split(" ");
        if (resAry.length < 3) {
            return null;
        }
        return resAry[1];
    }

    @Override
    public int getZNodeVersion() {
        return znodeVersion;
    }

    @Override
    public String getClusterName() {
        return cluster.getName();
    }

    @Override
    public String getView() {
        return this.persistentData.role;
    }

    @Override
    public void setState(String state, long state_timestamp) {
        persistentData.state = state;
        persistentData.stateTimestamp = state_timestamp;
    }

    public long getStateTimestamp() {
        return persistentData.stateTimestamp;
    }

    @Override
    public String getHeartbeat() {
        return persistentData.hb;
    }

    @Override
    public boolean isHBCResponseCorrect(String recvedLine) {
        return recvedLine.contains(PGS_RESPONSE_OK);
    }

    @Override
    public HBState getHeartbeatState() {
        return hbSession.getHeartbeatState();
    }

    @Override
    public String getIP() {
        return persistentData.pmIp;
    }

    @Override
    public int getPort() {
        return persistentData.smrMgmtPort;
    }

    @Override
    public UsedOpinionSet getUsedOpinions() {
        return usedOpinions;
    }

    public void syncLastState() {
        hbSession.getHeartbeatState().setLastState(persistentData.state)
            .setLastStateTimestamp(persistentData.stateTimestamp);
    }

    public static class RealState {
        private final boolean success;
        private final String state;
        private final String role;
        private final Long stateTimestamp;

        public RealState(boolean success, String state, String role, Long stateTimestamp) {
            this.success = success;
            this.state = state;
            this.role = role;
            this.stateTimestamp = stateTimestamp;
        }

        public String getState() {
            return state;
        }

        public String getRole() {
            return role;
        }

        public Long getStateTimestamp() {
            return stateTimestamp;
        }

        public boolean isSuccess() {
            return success;
        }

        @Override
        public String toString() {
            return "role: " + role + ", state: " + getState()
                    + ", timestamp: " + getStateTimestamp();
        }
    }

    public boolean isAvailable() {
        return (persistentData.state.equals(SERVER_STATE_NORMAL) ||
                persistentData.state.equals(SERVER_STATE_LCONN)) &&
               persistentData.hb.equals(HB_MONITOR_YES);
    }

    public RealState getRealState() {
        try {
            String cmd = "ping";
            String reply = executeQuery(cmd);
            Logger.debug("CMD=\"" + cmd +
                    "\", REPLY=\"" + reply +
                    "\", CLUSTER:" + getClusterName() +
                    ", PGS:" + getName() +
                    ", STATE:" + persistentData.state +
                    ", ROLE:" + persistentData.role);

            return convertReplyToState(reply);
        } catch (IOException e) {
            return new RealState(false, SERVER_STATE_FAILURE, PGS_ROLE_NONE,
                    persistentData.stateTimestamp);
        }
    }

    public long getNewMasterRewindCseq(LogSequence logSeq) {
        if (persistentData.oldMasterSmrVersion.equals(SMR_VERSION_101)) {
            Logger.info("new master rewind cseq: {}, om: {}",
                    logSeq.getLogCommit(), persistentData.oldMasterSmrVersion);
            return logSeq.getLogCommit();
        } else {
            Logger.info("new master rewind cseq: {}, om: {}",
                    logSeq.getLogCommit(), persistentData.oldMasterSmrVersion);
            return logSeq.getMax();
        }
    }

    public long getSlaveJoinSeq(LogSequence logSeq) {
        if (persistentData.oldMasterSmrVersion.equals(SMR_VERSION_101)) {
            if (persistentData.oldRole.equals(PGS_ROLE_MASTER)) {
                final long seq = Math.min(logSeq.getLogCommit(),
                        logSeq.getBeCommit());
                Logger.info("slave join rewind cseq: {}, om: {}, or: {}",
                        new Object[] { seq, persistentData.oldMasterSmrVersion,
                                persistentData.oldRole });
                return seq;
            }
        }

        final long seq = logSeq.getLogCommit();
        Logger.info("slave join rewind cseq: {}, om: {}, or: {}", new Object[] {
                seq, persistentData.oldMasterSmrVersion,
                persistentData.oldRole });
        return seq;
    }

    static public RealState convertReplyToState(String reply) {
        String[] tokens = reply.split(" ");
        String role;
        String state;

        if (tokens.length < 3) {
            state = SERVER_STATE_FAILURE;
            role = PGS_ROLE_NONE;
            return new RealState(false, state, role, -1L);
        }

        if (tokens[1].equals(PGS_ROLE_NONE_IN_PONG)) {
            state = SERVER_STATE_FAILURE;
            role = PGS_ROLE_NONE;
        } else if (tokens[1].equals(PGS_ROLE_LCONN_IN_PONG)) {
            state = SERVER_STATE_LCONN;
            role = PGS_ROLE_LCONN;
        } else if (tokens[1].equals(PGS_ROLE_MASTER_IN_PONG)) {
            state = SERVER_STATE_NORMAL;
            role = PGS_ROLE_MASTER;
        } else if (tokens[1].equals(PGS_ROLE_SLAVE_IN_PONG)) {
            state = SERVER_STATE_NORMAL;
            role = PGS_ROLE_SLAVE;
        } else {
            state = SERVER_STATE_FAILURE;
            role = PGS_ROLE_NONE;
        }

        final Long stateTimestamp = Long.parseLong(tokens[2]);
        return new RealState(true, state, role, stateTimestamp);
    }

    static public String roleToNumber(String role) {
        if (role.equals(PGS_ROLE_NONE)) {
            return PGS_ROLE_NONE_IN_PONG;
        } else if (role.equals(PGS_ROLE_LCONN)) {
            return PGS_ROLE_LCONN_IN_PONG;
        } else if (role.equals(PGS_ROLE_MASTER)) {
            return PGS_ROLE_MASTER_IN_PONG;
        } else if (role.equals(PGS_ROLE_SLAVE)) {
            return PGS_ROLE_SLAVE_IN_PONG;
        } else {
            throw new RuntimeException("Invalid parameter");
        }
    }

    public LogSequence getLogSeq() {
        if (!persistentData.role.equals(PGS_ROLE_LCONN) &&
                !persistentData.role.equals(PGS_ROLE_SLAVE) &&
                !persistentData.role.equals(PGS_ROLE_MASTER)) {
            Logger.error("getseq Logger fail. target pgs if not available" +
                    "CLUSTER=" + getClusterName() +
                    ", PG=" + persistentData.pgId +
                    ", PGS=" + getName() +
                    ", IP=" + persistentData.pmIp +
                    ", PORT=" + persistentData.smrBasePort);
            return null;
        }

        LogSequence logSeq = new LogSequence(this);
        try {
            logSeq.initialize();
        } catch (IOException e) {
            Logger.error("getseq Logger fail. " +
                    "CLUSTER=" + getClusterName() +
                    ", PG=" + persistentData.pgId +
                    ", PGS=" + getName() +
                    ", IP=" + persistentData.pmIp +
                    ", PORT=" + persistentData.smrBasePort, e);
            return null;
        }

        return logSeq;
    }

    public String smrVersion() throws MgmtSmrCommandException {
        final String cmd = "smrversion";

        try {
            // Send 'role master'
            String reply = executeQuery(cmd);

            String []toks = reply.split(" ");
            Logger.info("get smrversion success {}, reply: \"{}\"", this, reply);
            if (toks[0].equals(ERROR)) {
                return SMR_VERSION_101;
            } else if (toks[1].equals(SMR_VERSION_201)) {
                return SMR_VERSION_201;
            } else {
                throw new MgmtSmrCommandException("Get smrversion error.");
            }
        } catch (IOException e) {
            Logger.error("get smrversion fail. {}", this);
            throw new MgmtSmrCommandException("Get smrversion error. " + e.getMessage());
        }
    }

    private void roleMasterCmdResult(String cmd, String reply, long jobID,
            WorkflowLogger workflowLogger) throws MgmtSmrCommandException {
        // Make information for logging
        String infoFmt =
                "{\"PGS\":{},\"IP\":\"{}\",\"PORT\":\"{}\",\"CMD\":\"{}\",\"REPLY\":\"{}\"}";
        Object[] infoArgs = new Object[] { getName(), persistentData.pmIp,
                persistentData.smrBasePort, cmd, reply };

        if (!reply.equals(S2C_OK)) {
            workflowLogger.log(jobID,
                    SEVERITY_MAJOR, "RoleMaster",
                    LOG_TYPE_WORKFLOW, getClusterName(),
                    "master election fail. {}, cmd: \"{}\", reply: \"{}\"",
                    new Object[]{this, cmd, reply},
                    infoFmt, infoArgs);
            throw new MgmtSmrCommandException("Role master fail. "
                    + MessageFormatter.arrayFormat(infoFmt, infoArgs));
        } else {
            workflowLogger.log(jobID,
                    SEVERITY_MAJOR, "RoleMaster",
                    LOG_TYPE_WORKFLOW, getClusterName(),
                    "master election success. {}, cmd: \"{}\", reply: \"{}\"",
                    new Object[]{this, cmd, reply},
                    infoFmt, infoArgs);
        }
    }

    public void roleMaster(String smrVersion, PartitionGroup pg,
            LogSequence logSeq, int quorum, String quorumMembers, long jobID,
            WorkflowLogger workflowLogger) throws MgmtSmrCommandException {
        String cmd;
        if (smrVersion.equals(SMR_VERSION_201)) {
            cmd = "role master " + getName() + " " + quorum + " "
                    + getNewMasterRewindCseq(logSeq) + " " + quorumMembers;
        } else {
            cmd = "role master " + getName() + " " + quorum + " "
                    + getNewMasterRewindCseq(logSeq);
        }

        try {
            // Workflow log
            workflowLogger.log(jobID, SEVERITY_MODERATE, "RoleMaster",
                    LOG_TYPE_WORKFLOW, getClusterName(),
                    "try master election. cmd: \"" + cmd + "\"");

            // Send 'role master'
            String reply = executeQuery(cmd);

            // Check result
            roleMasterCmdResult(cmd, reply, jobID, workflowLogger);
        } catch (IOException e) {
            workflowLogger.log(jobID,
                    SEVERITY_MAJOR, "RoleMaster",
                    LOG_TYPE_WORKFLOW, getClusterName(),
                    "master election fail. {}, cmd: \"{}\", e: \"{}\"",
                    new Object[]{this, cmd, e.getMessage()});
            throw new MgmtSmrCommandException("Master election Error. " + e.getMessage());
        }
    }

	public class RoleMasterZkResult {
		public final PartitionGroup.PartitionGroupData pgM;
		public final PartitionGroupServerData pgsM;

		public RoleMasterZkResult(PartitionGroup.PartitionGroupData pgM,
				PartitionGroupServerData pgsM) {
			this.pgM = pgM;
			this.pgsM = pgsM;
		}
	}

    public RoleMasterZkResult updateZNodeAsMaster(PartitionGroup pg,
            List<PartitionGroupServer> pgsList, LogSequence logSeq, int quorum,
            String masterVersion, long jobID, WorkflowLogger workflowLogger)
            throws MgmtSmrCommandException {
        try {
        	PartitionGroup.PartitionGroupData pgM = pg.clonePersistentData();
        	pgM.addMasterGen(logSeq.getMax());

            // For backward compatibility, confmaster adds 1 to currentGen
            // since 1.2 and smaller version of confmaster follow a rule, PG.mGen + 1 = PGS.mGen.
            // Notice that it add 2 to currentGen because pg.persistentData.currentGen has not updated yet.
            // Please see below for more details.
            // +--------------------+---------+----------+
            // |                    | PG.mGen | PGS.mGen |
            // +--------------------+---------+----------+
            // | As-is              | 3       | 4        |
            // | Update Master.mGen | 3       | 5(3+2)   |
            // | Update PG.mGen     | 4(3+1)  | 5        |
            // | To-be              | 4       | 5        |
            // +--------------------+---------+----------+
            PartitionGroupServerData pgsM = (PartitionGroupServerData) persistentData.clone();
			pgsM.masterGen = pg.currentGen() + 2;
			pgsM.setRole(PGS_ROLE_MASTER);
			pgsM.oldRole = PGS_ROLE_MASTER;
			pgsM.oldMasterSmrVersion = masterVersion;
			pgsM.color = GREEN;

            List<Op> ops = new ArrayList<Op>();
            ops.add(Op.setData(getPath(), pgsM.toBytes(), -1));
            ops.add(Op.setData(pg.getPath(), pgM.toBytes(), -1));

            List<OpResult> results = zk.multi(ops);
            zk.handleResultsOfMulti(results);
            return new RoleMasterZkResult(pgM, pgsM);
        } catch (Exception e) {
            workflowLogger.log(jobID,
                    SEVERITY_MAJOR, "RoleMaster",
                    LOG_TYPE_WORKFLOW, getClusterName(),
                    "master election success but update zookeeper error. {}, e: \"{}\"",
                    this, e.getMessage());
            throw new MgmtSmrCommandException("Role master zookeeper Error, " + e.getMessage());
        }
    }

    private void roleSlaveCmdResult(String cmd, String reply, Color color, long jobID,
            WorkflowLogger workflowLogger) throws MgmtSmrCommandException {
        // Make information for logging
        String infoFmt =
                "{\"PGS\":{},\"IP\":\"{}\",\"PORT\":\"{}\",\"CMD\":\"{}\",\"REPLY\":\"{}\"}";
        Object[] infoArgs = new Object[] { getName(), persistentData.pmIp,
                persistentData.smrBasePort, cmd, reply };

        // Check result
        if (!reply.equals(S2C_OK)) {
            Logger.info("",
                    new Object[]{this, cmd, reply});
            workflowLogger.log(jobID,
                    SEVERITY_MAJOR, "RoleSlave",
                    LOG_TYPE_WORKFLOW, getClusterName(),
                    "slave join fail. {}, cmd: \"{}\", color: {}, reply: \"{}\"",
                    new Object[]{this, cmd, color, reply},
                    infoFmt, infoArgs);
            throw new MgmtSmrCommandException("Slave join Fail. " +
                    MessageFormatter.arrayFormat(infoFmt, infoArgs));
        } else {
            workflowLogger.log(jobID,
                    SEVERITY_MAJOR, "RoleSlave",
                    LOG_TYPE_WORKFLOW, getClusterName(),
                    "slave join success. {}, cmd: \"{}\", color: {}, reply: \"{}\"",
                    new Object[]{this, cmd, color, reply},
                    infoFmt, infoArgs);
        }
    }

    public void roleSlave(PartitionGroup pg, LogSequence targetLogSeq,
            PartitionGroupServer master, Color color, long jobID,
            WorkflowLogger workflowLogger) throws MgmtSmrCommandException {
        // Make 'role slave' command
        String cmd = "role slave " + getName() + " "
                + master.persistentData.pmIp + " "
                + master.persistentData.smrBasePort + " "
                + getSlaveJoinSeq(targetLogSeq);
        try {
            workflowLogger.log(jobID,
                    SEVERITY_MODERATE, "RolsSlave",
                    LOG_TYPE_WORKFLOW, getClusterName(),
                    "try slave join. cmd: \"" + cmd + "\", color: " + color);

            // Send 'role slave'
            String reply = executeQuery(cmd);

            // Check reply
            roleSlaveCmdResult(cmd, reply, color, jobID, workflowLogger);
        } catch (IOException e) {
            workflowLogger.log(jobID,
                    SEVERITY_MAJOR, "RoleSlave",
                    LOG_TYPE_WORKFLOW, getClusterName(),
                    "Slave join error. {}, cmd: \"{}\", color: {}, exception: \"{}\"",
                    new Object[]{this, cmd, color, e.getMessage()});
            throw new MgmtSmrCommandException("Slave join Error. " + e.getMessage());
        }
    }

    public PartitionGroupServerData updateZNodeAsSlave(long jobID, int mGen, Color color, String masterVersion,
            WorkflowLogger workflowLogger) throws MgmtSmrCommandException {
		PartitionGroupServerData pgsM = (PartitionGroupServerData) persistentData.clone();
		pgsM.masterGen = mGen;
		pgsM.setRole(PGS_ROLE_SLAVE);
		pgsM.oldRole = PGS_ROLE_SLAVE;
		pgsM.oldMasterSmrVersion = masterVersion;
		pgsM.color = color;
        try {
        	zk.setData(getPath(), pgsM.toBytes(), -1);
            return pgsM;
        } catch (Exception e) {
            workflowLogger.log(jobID,
                    SEVERITY_MAJOR, "RoleSlave",
                    LOG_TYPE_WORKFLOW, getClusterName(),
                    "slave join success, but update zookeeper error. {}, e: \"{}\"",
                    this, e.getMessage());
            throw new MgmtSmrCommandException("Slave join Error, " + e.getMessage());
        }
    }

    public void roleLconn() throws MgmtSmrCommandException {
        try {
            String reply = executeQuery("role lconn");
            if (!reply.equals(S2C_OK) && !reply.equals(S2C_ALREADY_LCONN)) {
                throw new MgmtSmrCommandException(
                        "role lconn fail. " + this + ", reply: \"" + reply + "\"");
            }
            Logger.info("role lconn success. {}, reply: \"{}\"", this, reply);
        } catch (IOException e) {
            Logger.warn("role lconn fail. {} {}", this, e.getMessage());
            throw new MgmtSmrCommandException("Role lconn Error. " + e.getMessage());
        }
    }

    public PartitionGroupServerData updateZNodeAsLconn(long jobID, Color color, WorkflowLogger workflowLogger)
                    throws MgmtSmrCommandException {
        try {
			PartitionGroupServerData pgsM = (PartitionGroupServerData) persistentData.clone();
			pgsM.setRole(PGS_ROLE_LCONN);
			pgsM.color = color;
			zk.setData(getPath(), pgsM.toBytes(), -1);
            return pgsM;
        } catch (Exception e) {
            workflowLogger.log(jobID,
                    SEVERITY_MODERATE, "RoleLconn",
                    LOG_TYPE_WORKFLOW, getClusterName(),
                    "role lconn sucess but update zookeeper error. {}, e: \"{}\"",
                    this, e.getMessage());
            throw new MgmtSmrCommandException("Role lconn zookeeper Error, " + e.getMessage());
        }
    }

    /*
     * return List<String> containing [quorum, nid1, nid2, ...]
     */
    public List<String> getQuorumV() throws MgmtSmrCommandException {
        final String cmd = "getquorumv";

        try {
            String reply = executeQuery(cmd);
            String []toks = reply.split(" ");
            if (!toks[0].equals(S2C_OK)) {
                final String msg = "getquorumv fail. " + this + ", cmd: " + cmd
                        + ", reply: " + reply;
                Logger.error(msg);
                throw new MgmtSmrCommandException(msg);
            }

            List<String> ret = new ArrayList<String>();
            for (int i = 1; i < toks.length; i++) {
                ret.add(toks[i]);
            }

            Logger.info("getquorumv success. {}, reply: {}", this, reply);
            return ret;
        } catch (IOException e) {
            final String msg = "getquorumv fail. " + this + ", cmd: " + cmd
                    + ", exception: " + e.getMessage();
            Logger.warn(msg);
            throw new MgmtSmrCommandException(msg);
        }
    }

    public int getQuorum() throws MgmtSmrCommandException {
        final String cmd = "getquorum";

        try {
            String reply = executeQuery(cmd);

            try {
                final int q = Integer.valueOf(reply);
                Logger.info("getquorum success. {}, reply: {}", this, reply);
                return q;
            } catch (NumberFormatException e) {
                final String msg = "getquorum fail. " + this + ", cmd: " + cmd
                        + ", reply: " + reply;
                Logger.error(msg);
                throw new MgmtSmrCommandException(msg);
            }
        } catch (IOException e) {
            final String msg = "getquorum fail. " + this + ", cmd: " + cmd
                    + ", exception: " + e.getMessage();
            Logger.warn(msg);
            throw new MgmtSmrCommandException(msg);
        }
    }

    public void setQuorum(int q, String quorumMembers)
            throws MgmtSetquorumException, MgmtSmrCommandException {
        final String v = smrVersion();
        String cmd;
        if (v.equals(SMR_VERSION_201)) {
            cmd = "setquorum " + q + " " + quorumMembers;
        } else {
            cmd = "setquorum " + q;
        }

        try {
            String reply = executeQuery(cmd);
            if (!reply.equals(S2C_OK)) {
                final String msg = "setquorum fail. " + this + ", cmd: " + cmd
                        + ", reply: " + reply;
                Logger.error(msg);
                throw new MgmtSetquorumException(msg);
            }
            Logger.info("setquorum success. {}, q: {}", this, q);
        } catch (IOException e) {
            final String msg = "setquorum fail. " + this + ", cmd: " + cmd + ", exception: " + e.getMessage();
            Logger.warn(msg);
            throw new MgmtSetquorumException(msg);
        }
    }

    @Override
    public String toString() {
        return fullName(getClusterName(), persistentData.pgId, getName());
    }

    public static String fullName(String clusterName, Integer pgId, String pgsId) {
        return  clusterName + "/pg:" + pgId + "/pgs:" + pgsId;
    }

    public static String fullName(String clusterName, String pgsId) {
        return  clusterName + "/pgs:" + pgsId;
    }

    @Override
    public String getFullName() {
        return toString();
    }

	@Override
	public String getPath() {
		return path;
	}

	@Override
	public String getName() {
		return name;
	}

	@Override
	public NodeType getNodeType() {
		return NodeType.PGS;
	}

	@Override
	public int compareTo(PartitionGroupServer pgs) {
	    return name.compareTo(pgs.name);
	}

    public Lock readLock() {
        return lock.readLock();
    }

    public Lock writeLock() {
        return lock.writeLock();
    }

    @Override
    public void setZNodeVersion(int v) {
    	this.znodeVersion = v;
    }

    public void turnOnUrgentHeartbeat() {
        hbSession.urgent();
    }

    public void setPersistentData(PartitionGroupServerData d) {
        persistentData = d;
    }

    public void setPersistentData(byte[] d) {
        persistentData = mapper.readValue(d, PartitionGroupServerData.class);
    }

    @JsonAutoDetect(
            fieldVisibility=Visibility.ANY,
            getterVisibility=Visibility.NONE,
            setterVisibility=Visibility.NONE)
    @JsonIgnoreProperties(
            ignoreUnknown=true)
    @JsonPropertyOrder(
            { "pg_ID", "pm_Name", "pm_IP", "backend_Port_Of_Redis",
            "replicator_Port_Of_SMR", "management_Port_Of_SMR", "state",
            "stateTimestamp", "hb", "smr_Role", "old_SMR_Role", "color", "master_Gen", "old_master_version" })
    public static class PartitionGroupServerData implements Cloneable {

        @JsonProperty("pg_ID")
        public int pgId;
        @JsonProperty("pm_Name")
        public String pmName;
        @JsonProperty("pm_IP")
        public String pmIp;
        @JsonProperty("backend_Port_Of_Redis")
        public int redisPort;
        @JsonProperty("replicator_Port_Of_SMR")
        public int smrBasePort;
        @JsonProperty("management_Port_Of_SMR")
        public int smrMgmtPort;
        @JsonProperty("state")
        public String state = SERVER_STATE_FAILURE;
        @JsonProperty("stateTimestamp")
        public long stateTimestamp = 0L;
        @JsonProperty("hb")
        public String hb = HB_MONITOR_NO;
        @JsonProperty("smr_Role")
        private String role = PGS_ROLE_NONE;
        @JsonProperty("old_SMR_Role")
        public String oldRole = PGS_ROLE_NONE;
        @JsonProperty("color")
        public Color color = Color.RED;
        @JsonProperty("master_Gen")
        public int masterGen = -1;
        @JsonProperty("old_master_version")
        public String oldMasterSmrVersion = SMR_VERSION_101;

        @JsonIgnore
        private final MemoryObjectMapper mapper = new MemoryObjectMapper();

        public PartitionGroupServerData() {}

        public PartitionGroupServerData(String pgId, String pmName,
                String pmIp, int basePort, int backendPort) {
            this.pgId = Integer.valueOf(pgId);
            this.pmName = pmName;
            this.pmIp = pmIp;
            this.smrBasePort = basePort;
            this.smrMgmtPort = basePort + 3;
            this.redisPort = backendPort;
        }

        public void setRole(String role) {
            this.role = role;

            if (role.equals(PGS_ROLE_NONE)) {
                this.state = SERVER_STATE_FAILURE;
            } else if (role.equals(PGS_ROLE_LCONN)) {
                this.state = SERVER_STATE_LCONN;
            } else if (role.equals(PGS_ROLE_SLAVE)) {
                this.state = SERVER_STATE_NORMAL;
            } else if (role.equals(PGS_ROLE_MASTER)) {
                this.state = SERVER_STATE_NORMAL;
            }
        }

        public String getRole() {
            return role;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (obj == this) {
                return true;
            }
            if (!(obj instanceof PartitionGroupServerData)) {
                return false;
            }

            PartitionGroupServerData rhs = (PartitionGroupServerData) obj;
            if (pgId != rhs.pgId) {
                return false;
            }
            if (!pmName.equals(rhs.pmName)) {
                return false;
            }
            if (!pmIp.equals(rhs.pmIp)) {
                return false;
            }
            if (redisPort != rhs.redisPort) {
                return false;
            }
            if (smrBasePort != rhs.smrBasePort) {
                return false;
            }
            if (smrMgmtPort != rhs.smrMgmtPort) {
                return false;
            }
            if (!state.equals(rhs.state)) {
                return false;
            }
            if (!role.equals(rhs.role)) {
                return false;
            }
            if (masterGen != rhs.masterGen) {
                return false;
            }
            if (!hb.equals(rhs.hb)) {
                return false;
            }
            if (stateTimestamp != rhs.stateTimestamp) {
                return false;
            }
            return true;
        }

        @Override
        public int hashCode() {
            assert false : "hashCode not designed";
            return 42; // any arbitrary constant will do
        }

        @Override
        public Object clone() {
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public String toString() {
            return mapper.writeValueAsString(this);
        }

        public byte[] toBytes() {
            return mapper.writeValueAsBytes(this);
        }

    }

	public int getRedisPort() {
		return persistentData.redisPort;
	}

    public Color getColor() {
    	return persistentData.color;
    }

	public String getRole() {
		return persistentData.role;
	}

	public String getState() {
		return persistentData.state;
	}

	public int getPgId() {
		return persistentData.pgId;
	}

	public String getPmIp() {
		return persistentData.pmIp;
	}

	public int getSmrBasePort() {
		return persistentData.smrBasePort;
	}

	public int getSmrMgmtPort() {
		return persistentData.smrMgmtPort;
	}

	public String getPmName() {
		return persistentData.pmName;
	}

	public byte[] persistentDataToBytes() {
		return persistentData.toBytes();
	}

	public String persistentDataToString() {
		return persistentData.toString();
	}

	public PartitionGroupServerData clonePersistentData() {
		return (PartitionGroupServerData) persistentData.clone();
	}

	public int getMasterGen() {
		return persistentData.masterGen;
	}
}


/*
 *   Copyright (C) 2009 2010 2011 Bartolomeo Sorrentino <bartolomeo.sorrentino@gmail.com>
 *
 *   This file is part of maven-annotation-plugin.
 *
 *    maven-annotation-plugin is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Lesser General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    maven-annotation-plugin is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU Lesser General Public License
 *    along with maven-annotation-plugin.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.bsc.maven.plugin.processor;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.*;
import javax.tools.Diagnostic.Kind;
import org.apache.maven.RepositoryUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.ToolchainManager;
import org.bsc.function.Consumer;
import org.codehaus.plexus.compiler.manager.CompilerManager;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.StringUtils;

// 3.0.5
/*
import org.sonatype.aether.RepositorySystem;
import org.sonatype.aether.RepositorySystemSession;
import org.sonatype.aether.artifact.ArtifactTypeRegistry;
import org.sonatype.aether.repository.RemoteRepository;
import org.sonatype.aether.resolution.ArtifactRequest;
import org.sonatype.aether.resolution.ArtifactResolutionException;
import org.sonatype.aether.resolution.ArtifactResult;
import org.sonatype.aether.util.artifact.DefaultArtifact;
*/

// 3.1.0
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.ArtifactTypeRegistry;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;

/**
 *
 * @author bsorrentino
 *
 */
public abstract class AbstractAnnotationProcessorMojo extends AbstractMojo
{
    interface ArtifactClosure {

        void execute( Artifact artifact );
    }

    private static final String SOURCE_CLASSIFIER = "sources";

    /**
     * value of -release parameter in java 9+
     *
     * @since 3.3.3
     */
    @Parameter
    private String releaseVersion;

    /**
     *
     */
    //@MojoParameter(expression = "${project}", readonly = true, required = true)
    @Component
    protected MavenProject project;

    /**
     *
     */
    //@MojoParameter(expression="${plugin.artifacts}", readonly = true )
    @Parameter(property="plugin.artifacts", readonly=true)
    private java.util.List<Artifact> pluginArtifacts;

    /**
     * Specify the directory where to place generated source files (same behaviour of -s option)
     *
     */
    //@MojoParameter(required = false, description = "Specify the directory where to place generated source files (same behaviour of -s option)")
    @Parameter
    private File outputDirectory;

    /**
     * Annotation Processor FQN (Full Qualified Name) - when processors are not specified, the default discovery mechanism will be used
     *
     */
    //@MojoParameter(required = false, description = "Annotation Processor FQN (Full Qualified Name) - when processors are not specified, the default discovery mechanism will be used")
    @Parameter
    private String[] processors;

    /**
     * Additional compiler arguments
     *
     */
    //@MojoParameter(required = false, description = "Additional compiler arguments")
    @Parameter
    private String compilerArguments;

    /**
     * Additional processor options (see javax.annotation.processing.ProcessingEnvironment#getOptions()
     *
     */
    @Parameter( alias = "options" )
    private java.util.Map<String,Object> optionMap;

    /**
     * Controls whether or not the output directory is added to compilation
     */
    //@MojoParameter(required = false, description = "Controls whether or not the output directory is added to compilation")
    @Parameter
    private Boolean addOutputDirectoryToCompilationSources;

    /**
     * Indicates whether the build will continue even if there are compilation errors; defaults to true.
     */
    //@MojoParameter(required = true, defaultValue = "true", expression = "${annotation.failOnError}", description = "Indicates whether the build will continue even if there are compilation errors; defaults to true.")
    @Parameter( defaultValue="true", required=true, property="annotation.failOnError" )
    private Boolean failOnError = true;

    /**
     * Indicates whether the compiler output should be visible, defaults to true.
     *
     */
    //@MojoParameter(required = true, defaultValue = "true", expression = "${annotation.outputDiagnostics}", description = "Indicates whether the compiler output should be visible, defaults to true.")
    @Parameter( defaultValue="true", required=true, property="annotation.outputDiagnostics" )
    private boolean outputDiagnostics = true;

    /**
     * System properties set before processor invocation.
     *
     */
    //@MojoParameter(required = false, description = "System properties set before processor invocation.")
    @Parameter
    private java.util.Map<String,String> systemProperties;

    /**
     * includes pattern
     */
    //@MojoParameter( description="includes pattern")
    @Parameter
    private String[] includes;

    /**
     * excludes pattern
     */
    //@MojoParameter( description="excludes pattern")
    @Parameter
    private String[] excludes;

    /**
     * additional source directories for the annotation processors.
     */
    @Parameter
    private java.util.List<File> additionalSourceDirectories;

    /**
     * if true add to the source directory of the annotation processor all compile source roots detected int the project
     * This is useful when we plan to use build-helper-maven-plugin
     *
     * @since 2.1.1
     */
    @Parameter(defaultValue = "false")
    private boolean addCompileSourceRoots = false;

    /**
     * append source artifacts to sources list
     *
     * @since 2.2.0
     */
    @Parameter( defaultValue = "false")
    private boolean appendSourceArtifacts = false;

    /**
     * The character set used for decoding sources
     *
     * @since 2.2.1
     */
    @Parameter(property = "project.build.sourceEncoding")
    private String encoding;

  /**
     * The entry point to Aether, i.e. the component doing all the work.
     *
     */
    @Component
    private RepositorySystem repoSystem;

    /**
     * The current repository/network configuration of Maven.
     *
     */
    @Parameter( defaultValue = "${repositorySystemSession}",readonly = true )
    private RepositorySystemSession repoSession;

    /**
     * The project's remote repositories to use for the resolution of plugins and their dependencies.
     *
     */
    @Parameter( defaultValue = "${project.remoteProjectRepositories}",readonly = true )
    private List<RemoteRepository> remoteRepos;

    /**
     * List of artifacts on which perform sources scanning
     *
     * Each artifact must be specified in the form grouId:artifactId.
     * If you need to include all artifacts belonging a groupId, specify as artifactId the character '*'
     *
     *

     * e.g.
     *

     *
     * org.bsc.maven:maven-confluence-plugin
     * org.bsc.maven:*
     *
     *

     *
     * @since 2.2.5
     */
    @Parameter()
    private java.util.List<String> processSourceArtifacts = Collections.emptyList();

    /**
     * Set this to true to skip annotation processing.
     *
     * @since 3.1.0
     */
    @Parameter(defaultValue = "false", property = "skipAnnotationProcessing")
    protected boolean skip;

    /**
     * Allows running the compiler in a separate process.
     * If false it uses the built in compiler, while if true it will use an executable.
     *
     * to set source and target use
     *

     *  maven.processor.source
     *  maven.processor.target
     *

     * @since 3.3
     */
    @Parameter(defaultValue = "false", property = "fork")
    protected boolean fork;

    /**
     * Maven Session
     *
     * @since 3.3
     */
    @Component
    protected MavenSession session;

    /**
     * Plexus compiler manager.
     *
     * @since 3.3
     */
    @Component
    protected CompilerManager compilerManager;

    /**
     *
     * @since 3.3
     */
    @Component
    private ToolchainManager toolchainManager;

    /**
     * for execution synchronization
     */
    private static final Lock syncExecutionLock = new ReentrantLock();

    /**
     *
     * @return supported source directories
     */
    protected abstract java.util.Set<File> getSourceDirectories( java.util.Set<File> result );

    /**
     *
     * @return output folder
     */
    protected abstract File getOutputClassDirectory();

    /**
     *
     * @param project
     * @param dir
     */
    protected abstract void addCompileSourceRoot(MavenProject project, String dir);

    /**
     *
     * @return
     */
    public abstract File getDefaultOutputDirectory();

    private String buildProcessor()
    {
        if (processors == null || processors.length == 0)
        {
            return null;
        }

        StringBuilder result = new StringBuilder();

        int i;

        for ( i = 0; i < processors.length - 1; ++i)
        {
            result.append(processors[i]).append(',');
        }

        result.append(processors[i]);

        return result.toString();
    }

    protected abstract java.util.Set<String> getClasspathElements( java.util.Set<String> result );

    private String buildCompileSourcepath( Consumer<String> onSuccess) {

        final java.util.List<String> roots = project.getCompileSourceRoots();

        if( roots == null || roots.isEmpty() ) {
            return null;
        }

        final String result = StringUtils.join(roots.iterator(), File.pathSeparator);

        onSuccess.accept( result );

        return result;
    }

    private String buildCompileClasspath()
    {

        java.util.Set<String> pathElements = new java.util.LinkedHashSet<String>();

        if( pluginArtifacts!=null  ) {

            for( Artifact a : pluginArtifacts ) {

                if( "compile".equalsIgnoreCase(a.getScope()) || "runtime".equalsIgnoreCase(a.getScope()) ) {

                    java.io.File f = a.getFile();

                    if( f!=null ) pathElements.add( a.getFile().getAbsolutePath() );
                }

            }
        }

        getClasspathElements(pathElements);

        final StringBuilder result = new StringBuilder();

        for( String elem : pathElements ) {
            result.append(elem).append(File.pathSeparator);
        }
        return result.toString();
    }

    /**
     *
     */
    public void execute() throws MojoExecutionException
    {
        if (skip)
        {
            getLog().info("skipped");
            return;
        }

        if ("pom".equalsIgnoreCase(project.getPackaging())) // Issue 17
        {
            return;
        }

        syncExecutionLock.lock();

        try
        {
            executeWithExceptionsHandled();
        }
        catch (Exception e1)
        {
            super.getLog().error("error on execute: use -X to have details " );
            super.getLog().debug(e1);
            if (failOnError)
            {
                throw new MojoExecutionException("Error executing", e1);
            }
        }
        finally {
          syncExecutionLock.unlock();
        }

    }

    /**
     * TODO remove the part with ToolchainManager lookup once we depend on
     * 3.0.9 (have it as prerequisite). Define as regular component field then.
     *
     * @param jdkToolchain
     */
    private Toolchain getToolchain(final Map<String, String> jdkToolchain)
    {
        Toolchain tc = null;

        if ( jdkToolchain != null && !jdkToolchain.isEmpty())
        {
            // Maven 3.3.1 has plugin execution scoped Toolchain Support
            try
            {
                final Method getToolchainsMethod =
                    toolchainManager.getClass().getMethod(  "getToolchains",
                                                            MavenSession.class,
                                                            String.class,
                                                            Map.class );

                @SuppressWarnings( "unchecked" )
                final List<Toolchain> tcs =
                    (List<Toolchain>) getToolchainsMethod.invoke(   toolchainManager,
                                                                    session,
                                                                    "jdk",
                                                                    jdkToolchain );

                if ( tcs != null && tcs.size() > 0 )
                {
                    tc = tcs.get( 0 );
                }
            }
            catch ( Exception e )
            {
                // ignore
            }
        }

        if ( tc == null )
        {
            tc = toolchainManager.getToolchainFromBuildContext( "jdk", session );
        }

        return tc;
    }

    private void executeWithExceptionsHandled() throws Exception
    {
        if (outputDirectory == null)
        {
            outputDirectory = getDefaultOutputDirectory();
        }

        ensureOutputDirectoryExists();
        addOutputToSourcesIfNeeded();

        // new Debug(project).printDebugInfo();

        final String includesString = ( includes==null || includes.length==0) ? "**/*.java" : StringUtils.join(includes, ",");
        final String excludesString = ( excludes==null || excludes.length==0) ? null : StringUtils.join(excludes, ",");

        java.util.Set<File> sourceDirs = getSourceDirectories(new java.util.HashSet<File>( 5 ));

        if( addCompileSourceRoots ) {
            final java.util.List<String> sourceRoots = project.getCompileSourceRoots();
            if( sourceRoots != null ) {

                for( String s : sourceRoots ) {
                    sourceDirs.add( new File(s) );
                }
            }

        }

        if( additionalSourceDirectories != null && !additionalSourceDirectories.isEmpty() ) {
            sourceDirs.addAll( additionalSourceDirectories );
        }

        if( sourceDirs == null ) {
            throw new IllegalStateException("getSourceDirectories is null!");
        }

        List<File> files = new java.util.ArrayList<File>();

        for( File sourceDir : sourceDirs ) {

            if( sourceDir==null ) {
                getLog().warn( "source directory is null! Processor task will be skipped!" );
                continue;
            }

            getLog().debug( String.format( "processing source directory [%s]", sourceDir.getPath()) );

            if( !sourceDir.exists() ) {
                getLog().warn( String.format("source directory [%s] doesn't exist! Processor task will be skipped!", sourceDir.getPath()));
                continue;
            }
            if( !sourceDir.isDirectory() ) {
                getLog().warn( String.format("source directory [%s] is invalid! Processor task will be skipped!", sourceDir.getPath()));
                continue;
            }

            files.addAll( FileUtils.getFiles(sourceDir, includesString, excludesString) );
        }

        final String compileClassPath = buildCompileClasspath();

        final String processor = buildProcessor();

        final List<String> options = new ArrayList<String>(10);

        options.add("-cp");
        options.add(compileClassPath);

        buildCompileSourcepath( new Consumer<String>() {
            public void accept(String sourcepath) {

                options.add("-sourcepath");
                options.add(sourcepath);
            }
        });

        options.add("-proc:only");

        addCompilerArguments(options);

        if (processor != null)
        {
            options.add("-processor");
            options.add(processor);
        }
        else
        {
            getLog().warn("No processors specified. Using default discovery mechanism.");
        }
        options.add("-d");
        options.add(getOutputClassDirectory().getPath());

        options.add("-s");
        options.add(outputDirectory.getPath());

        if( releaseVersion!=null  ) {
            options.add("--release");
            options.add(	releaseVersion );
        }

        if( getLog().isDebugEnabled() ) {
            for (String option : options) {
                getLog().debug(String.format("javac option: %s", option));
            }
        }

        final DiagnosticListener<JavaFileObject> dl = new DiagnosticListener<JavaFileObject>()
        {
            @Override
            public void report(Diagnostic< ? extends JavaFileObject> diagnostic) {

                if (!outputDiagnostics) {
                    return;
                }

                final Kind kind = diagnostic.getKind();

                if (null != kind)
                    switch (kind) {
                    case ERROR:
                        getLog().error(String.format("diagnostic: %s", diagnostic));
                        break;
                    case MANDATORY_WARNING:
                    case WARNING:
                        getLog().warn(String.format("diagnostic: %s", diagnostic));
                        break;
                    case NOTE:
                        getLog().info(String.format("diagnostic: %s", diagnostic));
                        break;
                    case OTHER:
                        getLog().info(String.format("diagnostic: %s", diagnostic));
                        break;
                    default:
                        break;
                }

            }
        };

        if (systemProperties != null)
        {
            java.util.Set< Map.Entry<String,String>> pSet = systemProperties.entrySet();

            for ( Map.Entry<String,String> e : pSet )
            {
                getLog().debug( String.format("set system property : [%s] = [%s]",  e.getKey(), e.getValue() ));
                System.setProperty(e.getKey(), e.getValue());
            }

        }

        //
        // add to allSource the files coming out from source archives
        //
        final List<JavaFileObject> allSources = new java.util.ArrayList<JavaFileObject>();

        processSourceArtifacts( new ArtifactClosure() {

            @Override
            public void execute(Artifact artifact) {
                try {

                    java.io.File f = artifact.getFile();

                    ZipFile zipFile = new ZipFile(f);
                    Enumeration<? extends ZipEntry> entries = zipFile.entries();
                    int sourceCount = 0;

                    while (entries.hasMoreElements()) {
                        ZipEntry entry = (ZipEntry) entries.nextElement();

                        if (entry.getName().endsWith(".java")) {
                            ++sourceCount;
                            allSources.add(ZipFileObject.create(zipFile, entry));

                        }
                    }

                    getLog().debug(String.format("** Discovered %d java sources in %s", sourceCount, f.getAbsolutePath()));

                } catch (Exception ex) {
                    getLog().warn(String.format("Problem reading source archive [%s]", artifact.getFile().getPath()));
                    getLog().debug(ex);
                }
            }
        });

        final java.util.Map<String,String> jdkToolchain =
                    java.util.Collections.emptyMap();

        final Toolchain tc = getToolchain(jdkToolchain);

        // If toolchain is set force fork compilation
        if( tc != null ) {
            fork = true;
        }

        if( fork ) {
            getLog().debug( "PROCESSOR COMPILER FORKED!");
        }

        //compileLock.lock();
        try {

            final JavaCompiler compiler = (fork) ?
                    AnnotationProcessorCompiler.createOutProcess(
                                                    tc,
                                                    compilerManager,
                                                    project,
                                                    session ) :
                    AnnotationProcessorCompiler.createInProcess();

            if( compiler==null ) {
                getLog().error("JVM is not suitable for processing annotation! ToolProvider.getSystemJavaCompiler() is null.");
                return;
            }

            Charset charset = null; ;

            if( encoding != null ) {
                try {
                   charset = Charset.forName(encoding);
                }
                catch( IllegalCharsetNameException ex1 ) {
                    getLog().warn( String.format("the given charset name [%s] is illegal!. default is used", encoding ));
                    charset = null;
                }
                catch( UnsupportedCharsetException ex2 ) {
                    getLog().warn( String.format("the given charset name [%s] is unsupported!. default is used", encoding ));
                    charset = null;
                }
            }

            final StandardJavaFileManager fileManager =
                    compiler.getStandardFileManager(null, null,
                                                    (charset==null) ?
                                                    Charset.defaultCharset() :
                                                    charset);

            if( files!=null && !files.isEmpty() ) {

                for( JavaFileObject f : fileManager.getJavaFileObjectsFromFiles(files) ) {

                    allSources.add(f);
                };

            }

            if( allSources.isEmpty() ) {
                getLog().warn( "no source file(s) detected! Processor task will be skipped");
                return;
            }

            final Iterable<String> classes = null;

            CompilationTask task = compiler.getTask(
                    new PrintWriter(System.out),
                    fileManager,
                    dl,
                    options,
                    classes,
                    allSources);

            /*
             * //Create a list to hold annotation processors LinkedList<Processor> processors = new
             * LinkedList<Processor>();
             *
             * //Add an annotation processor to the list processors.add(p);
             *
             * //Set the annotation processor to the compiler task task.setProcessors(processors);
             */

            // Perform the compilation task.
            if (!task.call())
            {

                throw new Exception("error during compilation");
            }
        }
        finally {
           //compileLock.unlock();
        }

    }

    private List<File> scanSourceDirectorySources(File sourceDir) throws IOException {
        if( sourceDir==null ) {
            getLog().warn( "source directory cannot be read (null returned)! Processor task will be skipped");
            return null;
        }
        if( !sourceDir.exists() ) {
            getLog().warn( "source directory doesn't exist! Processor task will be skipped");
            return null;
        }
        if( !sourceDir.isDirectory() ) {
            getLog().warn( "source directory is invalid! Processor task will be skipped");
            return null;
        }

        final String includesString = ( includes==null || includes.length==0) ? "**/*.java" : StringUtils.join(includes, ",");
        final String excludesString = ( excludes==null || excludes.length==0) ? null : StringUtils.join(excludes, ",");

        List<File> files = FileUtils.getFiles(sourceDir, includesString, excludesString);
        return files;
    }

    private void addCompilerArguments(List<String> options)
    {
        if (!StringUtils.isEmpty(compilerArguments))
        {
            for (String arg : compilerArguments.split(" "))
            {
                if (!StringUtils.isEmpty(arg))
                {
                    arg = arg.trim();
                    getLog().debug(String.format("Adding compiler arg: %s", arg));
                    options.add(arg);
                }
            }
        }
        if( optionMap!=null && !optionMap.isEmpty() ) {
            for( java.util.Map.Entry<String,Object> e : optionMap.entrySet() ) {

                if( !StringUtils.isEmpty(e.getKey()) && e.getValue()!=null ) {
                    String opt = String.format("-A%s=%s", e.getKey().trim(), e.getValue().toString().trim());
                    options.add( opt );
                    getLog().debug(String.format("Adding compiler arg: %s", opt));
                }
            }

        }
    }

    private void addOutputToSourcesIfNeeded()
    {
        final Boolean add = addOutputDirectoryToCompilationSources;
        if (add == null || add.booleanValue())
        {
            getLog().debug(String.format("Source directory: %s added", outputDirectory));
            addCompileSourceRoot(project, outputDirectory.getAbsolutePath());
        }
    }

    private void ensureOutputDirectoryExists()
    {
        final File f = outputDirectory;
        if (!f.exists())
        {
            f.mkdirs();
        }
        if( !getOutputClassDirectory().exists()) {
            getOutputClassDirectory().mkdirs();
        }
    }

    private boolean matchArtifact( Artifact dep/*, ArtifactFilter filter*/ ) {

        if(processSourceArtifacts == null || processSourceArtifacts.isEmpty()) {
            return false;
        }

        for( String a : processSourceArtifacts ) {

            if( a == null || a.isEmpty() ) {
                continue;
            }

            final String [] token = a.split(":");

            final boolean matchGroupId = dep.getGroupId().equals(token[0]);

            if( !matchGroupId ) {
                continue;
            }

            if( token.length == 1 ) {
                return true;
            }

            if( token[1].equals("*") ) {
                return true;

            }

            return dep.getArtifactId().equals(token[1]);

        }
        return false;
    }

    private Artifact resolveSourceArtifact( Artifact dep ) throws ArtifactResolutionException {

        if( !matchArtifact(dep) ) {
            return null;
        }

        final ArtifactTypeRegistry typeReg = repoSession.getArtifactTypeRegistry();

        final String extension = null;

        final DefaultArtifact artifact =
                new DefaultArtifact( dep.getGroupId(),
                                     dep.getArtifactId(),
                                      SOURCE_CLASSIFIER,
                                      extension,
                                      dep.getVersion(),
                                      typeReg.get(dep.getType()));

        final ArtifactRequest request = new ArtifactRequest();
        request.setArtifact( artifact );
        request.setRepositories(remoteRepos);

        getLog().debug( String.format("Resolving artifact %s from %s", artifact, remoteRepos ));

        final ArtifactResult result = repoSystem.resolveArtifact( repoSession, request );

        return RepositoryUtils.toArtifact(result.getArtifact());
    }

    private void processSourceArtifacts( ArtifactClosure closure ) {

        for (Artifact dep : this.project.getDependencyArtifacts()) {
            if (dep.hasClassifier() && SOURCE_CLASSIFIER.equals(dep.getClassifier()) ) {

                if( appendSourceArtifacts ) {
                    closure.execute(dep);
                }
                //getLog().debug("Append source artifact to classpath: " + dep.getGroupId() + ":" + dep.getArtifactId());
                //this.sourceArtifacts.add(dep.getFile());
            }
            else {
                try {
                    final Artifact sourcesDep = resolveSourceArtifact(dep);
                    if( sourcesDep != null ) {
                        closure.execute(sourcesDep);
                    }

                } catch (ArtifactResolutionException ex) {
                    getLog().warn( String.format(" sources for artifact [%s] not found!", dep.toString()));
                    getLog().debug(ex);

                }
            }
        }
    }

}


/*
 * Copyright (c) 2012 - 2016 Jadler contributors
 * This program is made available under the terms of the MIT License.
 */
package net.jadler.matchers;

import java.util.List;

import net.jadler.Request;
import org.apache.commons.lang.Validate;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;

/**
 * A {@link RequestMatcher} used for matching a request parameter.
 */
public class ParameterRequestMatcher extends RequestMatcher<List<String>> {

    private final String paramName;
    private final String desc;

    /**
     * Protected constructor useful only when subtyping. For creating instances of this class use
     * {@link #requestParameter(java.lang.String, org.hamcrest.Matcher)} instead.
     * @param pred a predicate to be applied on the given request parameter
     * @param paramName name of a request parameter (case sensitive)
     */
    public ParameterRequestMatcher(final Matcher<? super List<String>> pred, final String paramName) {
        super(pred);

        Validate.notEmpty(paramName, "paramName cannot be empty");
        this.paramName = paramName;

        this.desc = "parameter \"" + paramName + "\" is";
    }

    /**
     * Retrieves a parameter (defined in {@link #ParameterRequestMatcher(org.hamcrest.Matcher, java.lang.String)})
     * of the given request. The values are percent-encoded.
     * @param req request to retrieve the parameter from
     * @return the request parameter as a list of values or {@code null} if there is no such a parameter in the request
     */
    @Override
    protected List<String> retrieveValue(final Request req) {
        return req.getParameters().getValues(this.paramName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String provideDescription() {
        return this.desc;
    }

    /**
     * Factory method to create new instance of this matcher.
     * @param paramName name of a request parameter
     * @param pred a predicate to be applied on the request parameter
     * @return new instance of this matcher
     */
    @Factory
    public static ParameterRequestMatcher requestParameter(final String paramName,
            final Matcher<? super List<String>> pred) {
        return new ParameterRequestMatcher(pred, paramName);
    }
}


/*
 * Copyright (c) 2012 - 2016 Jadler contributors
 * This program is made available under the terms of the MIT License.
 */
package net.jadler;

import net.jadler.stubbing.Stubber;
import net.jadler.stubbing.server.StubHttpServerManager;
import java.nio.charset.Charset;
import java.util.ArrayList;
import net.jadler.stubbing.RequestStubbing;
import net.jadler.stubbing.StubbingFactory;
import net.jadler.stubbing.Stubbing;
import net.jadler.stubbing.StubResponse;
import net.jadler.stubbing.HttpStub;
import net.jadler.exception.JadlerException;
import net.jadler.stubbing.server.StubHttpServer;
import java.util.Collection;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import net.jadler.mocking.Mocker;
import net.jadler.mocking.VerificationException;
import net.jadler.mocking.Verifying;
import org.apache.commons.collections.MultiMap;
import org.apache.commons.collections.map.MultiValueMap;
import org.apache.commons.lang.Validate;
import org.hamcrest.Description;
import org.hamcrest.StringDescription;
import org.hamcrest.Matcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.hamcrest.Matchers.allOf;

/**
 *
This class represents the very hearth of the Jadler library. It acts as a great {@link Stubber} providing
 * a way to create new http stubs, {@link StubHttpServerManager} allowing the client to manage the state
 * of the underlying stub http server and {@link RequestManager} providing stub response definitions
 * according to a given http request.

 *
 *
An underlying stub http server instance is registered to an instance of this class during the instantiation.

 *
 *
Normally you shouldn't create instances of this on your own, use the {@link Jadler} facade instead.
 * However, if more http stub servers are needed in one execution thread (for example two http stub servers
 * listening on different ports) have no fear, go ahead and create two or more instances directly.

 *
 *
This class is stateful and thread-safe.

 */
public class JadlerMocker implements StubHttpServerManager, Stubber, RequestManager, Mocker {

    private final StubHttpServer server;
    private final StubbingFactory stubbingFactory;
    private final List<Stubbing> stubbings;
    private Deque<HttpStub> httpStubs;
    private final List<Request> receivedRequests;

    private MultiMap defaultHeaders;
    private int defaultStatus;
    private Charset defaultEncoding;
    private boolean recordRequests = true;

    private boolean started = false;
    private boolean configurable = true;

    private static final StubResponse NO_RULE_FOUND_RESPONSE;
    static {
        NO_RULE_FOUND_RESPONSE = StubResponse.builder()
                .status(404)
                .body("No stub response found for the incoming request", Charset.forName("UTF-8"))
                .header("Content-Type", "text/plain; charset=utf-8")
                .build();
    }

    private static final Logger logger = LoggerFactory.getLogger(JadlerMocker.class);

    /**
     * Creates new JadlerMocker instance bound to the given http stub server.
     *
     * @param server stub http server instance this mocker should use
     */
    public JadlerMocker(final StubHttpServer server) {
        this(server, new StubbingFactory());
    }

    /**
     * Package private constructor, for testing purposes only! Allows to define a {@link StubbingFactory} instance
     * as well.
     *
     * @param server stub http server instance this mocker should use
     * @param stubbingFactory a factory to create stubbing instances
     */
    JadlerMocker(final StubHttpServer server, final StubbingFactory stubbingFactory) {
        Validate.notNull(server, "server cannot be null");
        this.server = server;

        this.stubbings = new LinkedList<Stubbing>();
        this.defaultHeaders = new MultiValueMap();
        this.defaultStatus = 200;
        this.defaultEncoding =  Charset.forName("UTF-8");

        Validate.notNull(stubbingFactory, "stubbingFactory cannot be null");
        this.stubbingFactory = stubbingFactory;

        this.httpStubs = new LinkedList<HttpStub>();

        this.receivedRequests = new ArrayList<Request>();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void start() {
        if (this.started) {
            throw new IllegalStateException("The stub server has been started already.");
        }

        logger.debug("starting the underlying stub server...");

        this.server.registerRequestManager(this);

        try {
            server.start();
        } catch (final Exception ex) {
            throw new JadlerException("Stub http server start failure", ex);
        }
        this.started = true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() {
        if (!this.started) {
            throw new IllegalStateException("The stub server hasn't been started yet.");
        }

        logger.debug("stopping the underlying stub server...");

        try {
            server.stop();
        } catch (final Exception ex) {
            throw new JadlerException("Stub http server shutdown failure", ex);
        }
        this.started = false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isStarted() {
        return this.started;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getStubHttpServerPort() {
        if (!this.started) {
            throw new IllegalStateException("The stub http server hasn't been started yet.");
        }
        return server.getPort();
    }

    /**
     * Adds a default header to be added to every stub http response.
     * @param name header name (cannot be empty)
     * @param value header value (cannot be <tt>null</tt>)
     */
    public void addDefaultHeader(final String name, final String value) {
        Validate.notEmpty(name, "header name cannot be empty");
        Validate.notNull(value, "header value cannot be null, use an empty string instead");
        this.checkConfigurable();
        this.defaultHeaders.put(name, value);
    }

    /**
     * Defines a default status to be returned in every stub http response (if not redefined in the
     * particular stub rule)
     * @param defaultStatus status to be returned in every stub http response. Must be at least 0.
     */
    public void setDefaultStatus(final int defaultStatus) {
        Validate.isTrue(defaultStatus >= 0, "defaultStatus mustn't be negative");
        this.checkConfigurable();
        this.defaultStatus = defaultStatus;
    }

    /**
     * Defines default charset of every stub http response (if not redefined in the particular stub)
     * @param defaultEncoding default encoding of every stub http response
     */
    public void setDefaultEncoding(final Charset defaultEncoding) {
        Validate.notNull(defaultEncoding, "defaultEncoding cannot be null");
        this.checkConfigurable();
        this.defaultEncoding = defaultEncoding;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public RequestStubbing onRequest() {
        logger.debug("adding new stubbing...");
        this.checkConfigurable();

        final Stubbing stubbing = this.stubbingFactory.createStubbing(defaultEncoding, defaultStatus, defaultHeaders);
        stubbings.add(stubbing);
        return stubbing;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public StubResponse provideStubResponseFor(final Request request) {
        synchronized(this) {
            if (this.configurable) {
                this.configurable = false;
                this.httpStubs = this.createHttpStubs();
            }

            if (this.recordRequests) {
                this.receivedRequests.add(request);
            }
        }

        for (final Iterator<HttpStub> it = this.httpStubs.descendingIterator(); it.hasNext(); ) {
            final HttpStub rule = it.next();
            if (rule.matches(request)) {
                final StringBuilder sb = new StringBuilder();
                sb.append("Following rule will be applied:\n");
                sb.append(rule);
                logger.debug(sb.toString());

                return rule.nextResponse(request);
            }
        }

        final StringBuilder sb = new StringBuilder();
        sb.append("No suitable rule found. Reason:\n");
        for (final HttpStub rule: this.httpStubs) {
            sb.append("The rule '");
            sb.append(rule);
            sb.append("' cannot be applied. Mismatch:\n");
            sb.append(rule.describeMismatch(request));
            sb.append("\n");
        }
        logger.info(sb.toString());

        return NO_RULE_FOUND_RESPONSE;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Verifying verifyThatRequest() {
        checkRequestRecording();
        return new Verifying(this);
    }

    /**
     * {@inheritDoc}
     */
    @Deprecated
    @Override
    public int numberOfRequestsMatching(final Collection<Matcher<? super Request>> predicates) {
        Validate.notNull(predicates, "predicates cannot be null");
        checkRequestRecording();

        final Matcher<Request> all = allOf(predicates);

        int cnt = 0;

        synchronized(this) {
            for (final Request req: this.receivedRequests) {
                if (all.matches(req)) {
                    cnt++;
                }
            }
        }

        return cnt;
    }

    @Override
    public void evaluateVerification(final Collection<Matcher<? super Request>> requestPredicates,
            final Matcher<Integer> nrRequestsPredicate) {

        Validate.notNull(requestPredicates, "requestPredicates cannot be null");
        Validate.notNull(nrRequestsPredicate, "nrRequestsPredicate cannot be null");

        this.checkRequestRecording();

        synchronized(this) {
            final int cnt = this.numberOfRequestsMatching(requestPredicates);

            if (!nrRequestsPredicate.matches(cnt)) {
                this.logReceivedRequests(requestPredicates);
                throw new VerificationException(this.mismatchDescription(cnt, requestPredicates, nrRequestsPredicate));
            }
        }
    }

    /**
     *
Resets this mocker instance so it can be reused. This method clears all previously created stubs as well as
     * stored received requests (for mocking purpose,
     * see {@link RequestManager#numberOfRequestsMatching(java.util.Collection)}). Once this method has been called
     * new stubs can be created again using {@link #onRequest()}.

     *
     *
Please note that calling this method in a test body always signalizes a poorly written test
     * with a problem with the granularity. In this case consider writing more fine grained tests instead of using this
     * method.

     *
     *
While the standard Jadler lifecycle consists of creating new instance of this class and starting the
     * underlying stub server (using {@link #start()}) in the before section of a test and stopping
     * the server (using {@link #close()}) in the after section, in some specific scenarios it could be useful
     * to reuse one instance of this class in all tests instead.

     *
     *
When more than just one instance of this class is used in a test suite (for mocking more http servers) it
     * could take some time to start all underlying stub servers before and stop these after every test method. This is
     * a typical use case this method might come to help.

     *
     *
Here's an example code using jUnit which demonstrates usage of this method in a test lifecycle:

     *
     *

     * public class JadlerResetIntegrationTest {
     *     private static final JadlerMocker mocker = new JadlerMocker(new JettyStubHttpServer());
     *
     *     {@literal @}BeforeClass
     *     public static void beforeTests() {
     *         mocker.start();
     *     }
     *
     *     {@literal @}AfterClass
     *     public static void afterTests() {
     *         mocker.close();
     *     }
     *
     *     {@literal @}After
     *     public void reset() {
     *         mocker.reset();
     *     }
     *
     *     {@literal @}Test
     *     public void test1() {
     *         mocker.onRequest().respond().withStatus(201);
     *
     *         //do an http request here, 201 should be returned from the stub server
     *
     *         verifyThatRequest().receivedOnce();
     *     }
     *
     *     {@literal @}Test
     *     public void test2() {
     *         mocker.onRequest().respond().withStatus(400);
     *
     *         //do an http request here, 400 should be returned from the stub server
     *
     *         verifyThatRequest().receivedOnce();
     *     }
     * }
     *

     */
    public void reset() {
        synchronized(this) {
            this.stubbings.clear();
            this.httpStubs.clear();
            this.receivedRequests.clear();
            this.configurable = true;
        }
    }

    /**
     *
By default Jadler records all incoming requests (including their bodies) so it can provide mocking
     * (verification) features defined in {@link net.jadler.mocking.Mocker}.

     *
     *
In some very specific corner cases this implementation of mocking can cause troubles. For example imagine
     * a long running performance test using Jadler for stubbing some remote http service. Since such a test can issue
     * thousands or even millions of requests the memory consumption probably would affect the test results (either
     * by a performance slowdown or even crashes). In this specific scenarios you should consider disabling
     * the incoming requests recording using this method.

     *
     *
When disabled calling {@link net.jadler.mocking.Mocker#verifyThatRequest()} will result in
     * {@link java.lang.IllegalStateException}

     *
     *
Please note you should ignore this option almost every time you use Jadler unless you are really
     * convinced about it. Because premature optimization is the root of all evil, you know.

     *
     * @param recordRequests {@code true} for enabling http requests recording, {@code false} for disabling it
     */
    public void setRecordRequests(final boolean recordRequests) {
        this.checkConfigurable();
        this.recordRequests = recordRequests;
    }

    private Deque<HttpStub> createHttpStubs() {
        final Deque<HttpStub> stubs = new LinkedList<HttpStub>();
        for (final Stubbing stub : stubbings) {
            stubs.add(stub.createRule());
        }
        return stubs;
    }

    private void logReceivedRequests(final Collection<Matcher<? super Request>> requestPredicates) {
        final StringBuilder sb = new StringBuilder("Verification failed, here is a list of requests received so far:");
        this.appendNoneIfEmpty(this.receivedRequests, sb);

        int pos = 1;
        for (final Request req: this.receivedRequests) {
            sb.append("\n");
            final Collection<Matcher<? super Request>> matching = new ArrayList<Matcher<? super Request>>();
            final Collection<Matcher<? super Request>> clashing = new ArrayList<Matcher<? super Request>>();

            for (final Matcher<? super Request> pred: requestPredicates) {
                if (pred.matches(req)) {
                    matching.add(pred);
                }
                else {
                    clashing.add(pred);
                }
            }

            this.appendReason(sb, req, pos, matching, clashing);

            pos++;
        }

        logger.info(sb.toString());
    }

    private void appendReason(final StringBuilder sb, final Request req, final int position,
            final Collection<Matcher<? super Request>> matching,
            final Collection<Matcher<? super Request>> clashing) {

        sb.append("Request #");
        sb.append(position);
        sb.append(": ");
        sb.append(req);
        sb.append("\n");
        sb.append("  matching predicates:");
        this.appendNoneIfEmpty(matching, sb);
        sb.append('\n');

        for (final Matcher<? super Request> pred: matching) {
            sb.append("    ");
            final Description desc = new StringDescription(sb);
            pred.describeTo(desc);
            sb.append('\n');
        }

        sb.append("  clashing predicates:");
        this.appendNoneIfEmpty(clashing, sb);

        for (final Matcher<? super Request> pred: clashing) {
            sb.append("\n    ");

            final Description desc = new StringDescription(sb);
            pred.describeMismatch(req, desc);
        }
    }

    private void appendNoneIfEmpty(final Collection<?> coll, final StringBuilder sb) {
        if (coll.isEmpty()) {
            sb.append(" <none>");
        }
    }

    private String mismatchDescription(final int cnt, final Collection<Matcher<? super Request>> predicates,
            final Matcher<Integer> nrRequestsMatcher) {
        final Description desc = new StringDescription();

        desc.appendText("The number of http requests");
        if (!predicates.isEmpty()) {
            desc.appendText(" having");
        }
        desc.appendText(" ");

        for (final Iterator<Matcher<? super Request>> it = predicates.iterator(); it.hasNext();) {
            desc.appendDescriptionOf(it.next());

            if (it.hasNext()) {
                desc.appendText(" AND");
            }

            desc.appendText(" ");
        }

        desc.appendText("was expected to be ");
        desc.appendDescriptionOf(nrRequestsMatcher);
        desc.appendText(", but ");
        nrRequestsMatcher.describeMismatch(cnt, desc);

        return desc.toString();
    }

    private synchronized void checkConfigurable() {
        if (!this.configurable) {
            throw new IllegalStateException("Once first http request has been served, "
                    + "you can't do any stubbing anymore.");
        }
    }

    private synchronized void checkRequestRecording() {
        if (!this.recordRequests) {
            throw new IllegalStateException("Request recording is switched off, cannot do any request verification");
        }
    }
}


/*
 * Copyright (c) 2012 - 2016 Jadler contributors
 * This program is made available under the terms of the MIT License.
 */
package net.jadler.junit.rule;

import java.nio.charset.Charset;
import net.jadler.Jadler;
import net.jadler.Jadler.OngoingConfiguration;
import net.jadler.JadlerConfiguration;
import net.jadler.KeyValues;
import net.jadler.stubbing.server.StubHttpServer;
import org.junit.rules.ExternalResource;

import static org.apache.commons.lang.Validate.isTrue;
import static org.apache.commons.lang.Validate.notNull;

/**
 * JUnit rule which simplifies the creation of a Jadler instance.
 *
 * @author Christian Galsterer
 */
public class JadlerRule extends ExternalResource implements JadlerConfiguration {

    private static final int DEFAULT_PORT = -1;

    private final int port;
    private final StubHttpServer server;

    private String defaultContentType;
    private Charset defaultEncoding;
    private int defaultStatus = -1;
    private boolean skipsRequestsRecording = false;
    private KeyValues defaultHeaders = KeyValues.EMPTY;

    /**
     * Instructs Jadler to use a default stub server {@link net.jadler.stubbing.server.jetty.JettyStubHttpServer}
     * serving the http protocol listening on any free port.
     *
     * See also {@link net.jadler.Jadler#initJadler()}
     */
    public JadlerRule() {
        this.port = DEFAULT_PORT;
        this.server = null;
    }

    /**
     * Instructs Jadler to use a default stub server {@link net.jadler.stubbing.server.jetty.JettyStubHttpServer}
     * serving the http protocol listening on the given port.
     *
     * See also {@link net.jadler.Jadler#initJadlerListeningOn(int)}
     *
     * @param port port the stub server should be listening on (must be bigger than 0)
     */
    public JadlerRule(final int port) {
        isTrue(port > 0, "port must be an integer bigger than 0");

        this.port = port;
        this.server = null;
    }

    /**
     * Instructs Jadler to use use the given stub server instance.
     *
     * See also {@link net.jadler.Jadler#initJadlerUsing(net.jadler.stubbing.server.StubHttpServer)}
     *
     * @param server stub server to use
     */
    public JadlerRule(final StubHttpServer server) {
        notNull(server, "server cannot be null");

        this.port = DEFAULT_PORT;
        this.server = server;
    }

    @Override
    protected void before() {
        final OngoingConfiguration conf;

        if (port == DEFAULT_PORT) {
            if (server == null) {
                conf = Jadler.initJadler();
            }
            else {
                conf = Jadler.initJadlerUsing(server);
            }
        }
        else {
            conf = Jadler.initJadlerListeningOn(port);
        }

        if (this.defaultContentType != null) {
            conf.withDefaultResponseContentType(defaultContentType);
        }

        if (this.defaultEncoding != null) {
            conf.withDefaultResponseEncoding(defaultEncoding);
        }

        if (this.defaultStatus > -1) {
            conf.withDefaultResponseStatus(defaultStatus);
        }

        for (final String name: this.defaultHeaders.getKeys()) {
            for (final String value: this.defaultHeaders.getValues(name)) {
                conf.withDefaultResponseHeader(name, value);
            }
        }

        if (this.skipsRequestsRecording) {
            conf.withRequestsRecordingDisabled();
        }
    }

    @Override
    protected void after() {
        Jadler.closeJadler();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JadlerRule withDefaultResponseContentType(final String defaultContentType) {
        this.defaultContentType = defaultContentType;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JadlerRule withDefaultResponseEncoding(final Charset defaultEncoding) {
        this.defaultEncoding = defaultEncoding;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JadlerRule withDefaultResponseHeader(final String name, final String value) {
        this.defaultHeaders = this.defaultHeaders.add(name, value);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JadlerRule withDefaultResponseStatus(final int defaultStatus) {
        this.defaultStatus = defaultStatus;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JadlerRule withRequestsRecordingDisabled() {
        this.skipsRequestsRecording = true;
        return this;
    }
}


/*
 * Copyright (c) 2012 - 2016 Jadler contributors
 * This program is made available under the terms of the MIT License.
 */
package net.jadler.stubbing;

import net.jadler.Request;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import org.apache.commons.lang.Validate;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;

import static org.hamcrest.Matchers.allOf;

/**
 *
An http stub is a WHEN-THEN pair (when an http request with specific properties arrives,
 * then respond with a defined response).

 *
 *
The WHEN part is a list of predicates (in form of Hamcrest matchers) applicable to a request.
 * All of these matchers must be evaluated to {@code true} in order to apply the THEN part.

 *
 *
The THEN part is defined by an instance of the {@link Responder} interface.
 * This instance is capable of constructing stub http responses to be returned to the client.

 *
 *
Instances of this class are thread-safe if and only if the provided {@link Responder} instance
 * is thread-safe.

 *
 *
One should never create new instances of this class directly,see {@link net.jadler.Jadler}
 * for explanation and tutorial.

 */
public class HttpStub {

    private final Collection<Matcher<? super Request>> predicates;
    private final Responder responder;

    /**
     * @param predicates list of predicates. Cannot be {@code null}, however can be empty (which means this rule will
     * match every request)
     * @param responder an instance to provide stub http responses
     */
    public HttpStub(final Collection<Matcher<? super Request>> predicates, final Responder responder) {
        Validate.notNull(predicates, "predicates cannot be null, use an empty list instead");
        this.predicates = new ArrayList<Matcher<? super Request>>(predicates);

        Validate.notNull(responder, "responder cannot be null");
        this.responder = responder;
    }

    /**
     * @param request an http request to be checked whether it matches this stub rule.
     * @return {@code true} if and only if all predicates defined in this rule were evaluated to {@code true}
     * by the given request.
     */
    public boolean matches(final Request request) {
        return allOf(this.predicates).matches(request);
    }

    /**
     * @param request an http request the stub response will be generated for
     * @return next http stub response as produced by the {@link Responder} instance provided in
     * {@link #HttpStub(java.util.Collection, net.jadler.stubbing.Responder)}
     */
    public StubResponse nextResponse(final Request request) {
        return this.responder.nextResponse(request);
    }

    /**
     * Returns a reason why the given request doesn't match this rule. This method should be called if
     * and only if {@link  #matches(net.jadler.Request)} would return {@code false}. However, this is not checked.
     * @param request an http request to describe the mismatch for
     * @return a human readable mismatch reason
     */
    public String describeMismatch(final Request request) {
        final Description desc = new StringDescription();

        boolean first = true;
        for (final Iterator<Matcher<? super Request>> it = this.predicates.iterator(); it.hasNext();) {
            final Matcher<? super Request> m = it.next();

            if (!m.matches(request)) {
                if (!first) {
                    desc.appendText(" AND\n");
                }
                desc.appendText("  ");
                m.describeMismatch(request, desc);
                first = false;
            }
        }
        return desc.toString();
    }

    @Override
    public String toString() {
        final Description desc = new StringDescription();
        desc.appendText("WHEN request (\n");
        for(final Iterator<Matcher<? super Request>> it = this.predicates.iterator(); it.hasNext();) {
            desc.appendText("  ");
            desc.appendDescriptionOf(it.next());
            if (it.hasNext()) {
                desc.appendText(" AND\n");
            }
        }
        desc.appendText(")\n");
        desc.appendText("THEN respond with ");
        desc.appendText(this.responder.toString());

        return desc.toString();
    }
}


/*
 * Copyright (c) 2012 - 2016 Jadler contributors
 * This program is made available under the terms of the MIT License.
 */
package net.jadler.mocking;

import net.jadler.AbstractRequestMatching;
import net.jadler.RequestManager;
import org.apache.commons.lang.Validate;
import org.hamcrest.Matcher;

import static org.hamcrest.Matchers.equalTo;

/**
 * Allows defining new verification in a fluid fashion. You shouldn't create instances
 * of this class on your own, please see {@link net.jadler.Jadler#verifyThatRequest()} for more information
 * on creating instances of this class.
 */
public class Verifying extends AbstractRequestMatching<Verifying> {

    private final RequestManager requestManager;

    /**
     * @param requestManager request manager instance to assist the verification
     */
    public Verifying(final RequestManager requestManager) {
        Validate.notNull(requestManager, "requestManager cannot be null");

        this.requestManager = requestManager;
    }

    /**
     * Checks whether the number of requests described in this verifying object received so far matches the given predicate.
     * @param nrRequestsPredicate to be applied on the number of requests
     * @throws VerificationException if the number of requests described by this verifying is not matched by the given
     * predicate
     */
    public void receivedTimes(final Matcher<Integer> nrRequestsPredicate) {
        Validate.notNull(nrRequestsPredicate, "predicate cannot be null");

        this.requestManager.evaluateVerification(predicates, nrRequestsPredicate);
    }

    /**
     * Checks whether the number of requests described in this verifying object received so far matches the exact value.
     * @param count expected number of requests described by this verifying object
     * @throws VerificationException if the number of requests described in this verifying object received so far
     * is not equal to the expected value
     */
    public void receivedTimes(final int count) {
        Validate.isTrue(count >= 0, "count cannot be negative");
        this.receivedTimes(equalTo(count));
    }

    /**
     * Checks that exactly one request described in this verifying object has been received so far.
     * @throws VerificationException if the number of expected requests is not equal to one
     */
    public void receivedOnce() {
        this.receivedTimes(1);
    }

    /**
     * Checks that no request described in this verifying object has been received so far.
     * @throws VerificationException if at least one request described in this object has already been received
     */
    public void receivedNever() {
        this.receivedTimes(0);
    }
}