/*
 * Copyright 2009 Perforce Software Inc., All Rights Reserved.
 */
package com.perforce.p4java.impl.mapbased.rpc;

import com.perforce.p4java.Log;
import com.perforce.p4java.PropertyDefs;
import com.perforce.p4java.env.PerforceEnvironment;
import com.perforce.p4java.exception.*;
import com.perforce.p4java.impl.mapbased.rpc.connection.RpcConnection;
import com.perforce.p4java.impl.mapbased.rpc.func.client.ClientTrust;
import com.perforce.p4java.impl.mapbased.rpc.func.proto.PerformanceMonitor;
import com.perforce.p4java.impl.mapbased.rpc.func.proto.ProtocolCommand;
import com.perforce.p4java.impl.mapbased.rpc.helper.RpcUserAuthCounter;
import com.perforce.p4java.impl.mapbased.rpc.msg.RpcMessage;
import com.perforce.p4java.impl.mapbased.rpc.msg.ServerMessage;
import com.perforce.p4java.impl.mapbased.rpc.packet.helper.RpcPacketFieldRule;
import com.perforce.p4java.impl.mapbased.rpc.stream.RpcStreamConnection;
import com.perforce.p4java.impl.mapbased.server.Server;
import com.perforce.p4java.option.UsageOptions;
import com.perforce.p4java.option.server.TrustOptions;
import com.perforce.p4java.server.*;

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.*;

/**
 * RPC-based Perforce server implementation superclass.
 */

public abstract class RpcServer extends Server {

        /**
         * The implementation type of this implementation.
         */
        public static final IServerImplMetadata.ImplType IMPL_TYPE
                                                                = IServerImplMetadata.ImplType.NATIVE_RPC;

        /**
         * The default string sent to the Perforce server in the protocol
         * command defining the client's program name. This can be set with the
         * IServer interface.
         */
        public static final String DEFAULT_PROG_NAME = "p4jrpc";

        /**
         * The default string sent to the Perforce server in the protocol
         * command defining the client's program version. This can be set with the
         * IServer interface.
         */
        public static final String DEFAULT_PROG_VERSION = "Beta 1.0";

        /**
         * Default Perforce client API level; 76 represents 2014.1 capabilities.
         * Don't change this unless you know what you're doing. Note that this
         * is a default for most commands; some commands dynamically bump up
         * the level for the command's duration.
         */
        public static final int DEFAULT_CLIENT_API_LEVEL = 76;	// 2014.1
        														// p4/msgs/p4tagl.cc
																// p4/client/client.cc
																// p4/server/rhservice.h

        /**
         * Default Perforce server API level; 99999 apparently means "whatever...".
         * Don't change this unless you know what you're doing.
         */
        public static final int DEFAULT_SERVER_API_LEVEL = 99999;	// As picked off the wire for C++ API
        															// p4/client/clientmain.cc
																	// p4/server/rhservice.cc

        /**
         * Signifies whether or not we use tagged output. Don't change this unless
         * you like weird incomprehensible errors and days of debugging.
         */
        public static final boolean RPC_TAGS_USED = true;

        /**
         * Signifies whether or not the client is capable of handling streams.
         */
        public static final boolean RPC_ENABLE_STREAMS = true;

        /**
         * The system properties key for the JVM's current directory.
         */
        public static final String RPC_ENV_CWD_KEY = "user.dir";

        /**
         * The system properties key for the OS name.
         */
        public static final String RPC_ENV_OS_NAME_KEY = "os.name";

        /**
         * RPC_ENV_OS_NAME_KEY property value prefix for Windows systems.
         */
        public static final String RPC_ENV_WINDOWS_PREFIX = "windows";

        /**
         * What we use in the RPC environment packet to signal to the
         * Perforce server that we're a Windows box.
         */
        public static final String RPC_ENV_WINDOWS_SPEC = "NT"; // sic

        /**
         * What we use in the RPC environment packet to signal to the
         * Perforce server that we're a NON-Windows box.
         */

        public static final String RPC_ENV_UNIX_SPEC = "UNIX";

        /**
         * What we use in the RPC environment packet to signal to the Perforce
         * server that we don't have a client set yet or don't know what it is.
         */
        public static final String RPC_ENV_NOCLIENT_SPEC = "unknownclient";

        /**
         * What we use in the RPC environment packet to signal to the Perforce
         * server that we don't have a hostname set yet or don't know what it is.
         */
        public static final String RPC_ENV_NOHOST_SPEC = "nohost"; // as cribbed from the C++ API...

        /**
         * What we use in the RPC environment packet to signal to the Perforce
         * server that we don't have a client set yet or don't know what it is.
         */
        public static final String RPC_ENV_NOUSER_SPEC = "nouser"; // as cribbed from the C++ API...

        /**
         * What we use as a P4JTracer trace prefix for methods here.
         */
        public static final String TRACE_PREFIX = "RpcServer";

        /**
         * Used to key temporary output streams in the command environment's
         * state map for things like getFileContents(), etc., using the execStreamCmd
         * method(s).
         */
        public static final String RPC_TMP_OUTFILE_STREAM_KEY = "";

        /**
         * Use to key converter to use out of state map
         */
        public static final String RPC_TMP_CONVERTER_KEY = "RPC_TMP_CONVERTER_KEY";

        protected String localHostName = null;	// intended to be just the unqualified name...
        protected int clientApiLevel = DEFAULT_CLIENT_API_LEVEL;
        protected int serverApiLevel = DEFAULT_SERVER_API_LEVEL;
        protected String applicationName = null;	// Application name

        private static final String AUTH_FAIL_STRING_1 = "Single sign-on on client failed"; // SSO failure
        private static final String AUTH_FAIL_STRING_2 = "Password invalid";

        private static final String[] accessErrMsgs = {
                        CORE_AUTH_FAIL_STRING_1,
                        CORE_AUTH_FAIL_STRING_2,
                        CORE_AUTH_FAIL_STRING_3,
                        CORE_AUTH_FAIL_STRING_4,
                        AUTH_FAIL_STRING_1,
                        AUTH_FAIL_STRING_2
        };

        private static final String PASSWORD_NOT_SET_STRING = "no password set for this user";
    	
        protected long connectionStart = 0;

        protected Map<String, Object> serverProtocolMap = new HashMap<String, Object>();

        private PerformanceMonitor perfMonitor = new PerformanceMonitor();

        protected ServerStats serverStats = null;

        protected String serverId = null;

    	protected Map<String, String> secretKeys = new HashMap<String, String>();

        protected ClientTrust clientTrust = null;
        
        protected String ticketsFilePath = null;
        
        protected String trustFilePath = null;

        protected RpcUserAuthCounter authCounter = new RpcUserAuthCounter();
        
    	/**
         * The RPC command args before the function name (i.e. "tag")
         */
        protected Map<String, Object> cmdMapArgs = null;

        /**
         * If true, relax the command name validation checks done
         * in the RPC layer. This is dangerous, and any use of this
         * should only be done if you know what you're doing and you're
         * able to deal with the consequences (which won't be spelled
         * out here).
         */
        protected boolean relaxCmdNameValidationChecks = false;

        /**
         * The default init sets up things like host names, etc., and fails if
         * we can't establish some pretty basic things at connect time. Does
         * <i>not</i> attempt to actually connect to the target Perforce
         * server -- this is left for the connect() call, below.
         *
         * @see com.perforce.p4java.impl.mapbased.server.Server#init(String, int, Properties)
         */

        public ServerStatus init(String host, int port, Properties props)
                        throws ConfigException, ConnectionException {
                return init(host, port, props, null);
        }

        public ServerStatus init(String host, int port, Properties props, UsageOptions opts)
        				throws ConfigException, ConnectionException {
            return init(host, port, props, opts, false);
        }

        public ServerStatus init(String host, int port, Properties props, UsageOptions opts, boolean secure)
                                throws ConfigException, ConnectionException {
                super.init(host, port, props, opts, secure);
                try {
            			this.cmdMapArgs = new HashMap<String, Object>();
            			this.cmdMapArgs.put(ProtocolCommand.RPC_ARGNAME_PROTOCOL_ZTAGS, "");
                        this.relaxCmdNameValidationChecks = RpcPropertyDefs.getPropertyAsBoolean(props,
                                                                RpcPropertyDefs.RPC_RELAX_CMD_NAME_CHECKS_NICK, false);
                        this.applicationName = RpcPropertyDefs.getProperty(props,
                                								RpcPropertyDefs.RPC_APPLICATION_NAME_NICK, null);
                        if (this.getUsageOptions().getHostName() != null) {
                                // This had better reflect reality...
                                this.localHostName = this.getUsageOptions().getHostName();
                        } else {
                                this.localHostName = java.net.InetAddress.getLocalHost().getHostName();
                        }
                        if (this.localHostName == null) {
                                throw new NullPointerError("Null client host name in RPC connection init");
                        }
                        if (!this.useAuthMemoryStore) {
		                        // Search properties for ticket file path, fix for job035376
		                        this.ticketsFilePath = this.props.getProperty(PropertyDefs.TICKET_PATH_KEY_SHORT_FORM,
		                                                             this.props.getProperty(PropertyDefs.TICKET_PATH_KEY));
		                        // Search environment variable
		                        if (this.ticketsFilePath == null) {
		                        		this.ticketsFilePath = PerforceEnvironment.getP4Tickets();
		                        }
		                        // Search standard OS location
		                        if (this.ticketsFilePath == null) {
		                        		this.ticketsFilePath = getDefaultP4TicketsFile();
		                        }
		                        
		                        // Search properties for trust file path
		                        this.trustFilePath = this.props.getProperty(PropertyDefs.TRUST_PATH_KEY_SHORT_FORM,
		                                                           this.props.getProperty(PropertyDefs.TRUST_PATH_KEY));
		                        // Search environment variable
		                        if (this.trustFilePath == null) {
		                                this.trustFilePath = PerforceEnvironment.getP4Trust();

		                        }
		                        // Search standard OS location
		                        if (this.trustFilePath == null) {
		                                this.trustFilePath = getDefaultP4TrustFile();
		                        }
                        }
                        this.serverStats = new ServerStats();
                } catch (UnknownHostException uhe) {
                        throw new ConfigException("Unable to determine client host name: "
                                        + uhe.getLocalizedMessage());
                }

                // Initialize client trust
                clientTrust = new ClientTrust(this);
                
                return status;
        }

        /**
         * Check the fingerprint of the Perforce server SSL connection
         * 
         * @throws ConnectionException 
         */
        protected void checkFingerprint(RpcConnection rpcConnection) throws ConnectionException {
        	if (rpcConnection != null && rpcConnection.isSecure() && !rpcConnection.isTrusted()) {
	        	if (rpcConnection.getFingerprint() == null) {
	        		throw new ConnectionException("Null fingerprint for this Perforce SSL connection");
	        	}
        		// New connection
       			if (!clientTrust.fingerprintExists(rpcConnection.getServerIpPort())) {
       				throw new TrustException(TrustException.Type.NEW_CONNECTION,
       						getServerHostPort(),
       						rpcConnection.getServerIpPort(),
       						rpcConnection.getFingerprint(),
       						clientTrust.getMessages().getMessage(
       						ClientTrust.CLIENT_TRUST_WARNING_NEW_CONNECTION,
       						new Object[] { getServerHostPort(), rpcConnection.getFingerprint() }) +
       						clientTrust.getMessages().getMessage(
       						ClientTrust.CLIENT_TRUST_EXCEPTION_NEW_CONNECTION)     						
       						);
       			}
       			// New key
       			if (!clientTrust.fingerprintMatches(rpcConnection.getServerIpPort(),
       					rpcConnection.getFingerprint())) {
       				throw new TrustException(TrustException.Type.NEW_KEY,
       						getServerHostPort(),
       						rpcConnection.getServerIpPort(),
       						rpcConnection.getFingerprint(),
       						clientTrust.getMessages().getMessage(
       						ClientTrust.CLIENT_TRUST_WARNING_NEW_KEY,
       						new Object[] { getServerHostPort(), rpcConnection.getFingerprint() }) +
       						clientTrust.getMessages().getMessage(
       						ClientTrust.CLIENT_TRUST_EXCEPTION_NEW_KEY)     						
       						);
       			}
           		// Trust this connection
       			rpcConnection.setTrusted(true);
        	}
        }
        
        /**
         * Try to establish an actual RPC connection to the target Perforce server.
         * Most of the actual setup work is done in the RpcConnection and
         * RpcPacketDispatcher constructors, but associated gubbins such as
         * auto login, etc., are done in the superclass.
         *
         * @see com.perforce.p4java.impl.mapbased.server.Server#connect()
         */

        public void connect() throws ConnectionException, AccessException, RequestException, ConfigException {
        	this.connectionStart = System.currentTimeMillis();
        	super.connect();
        }

        /**
         * Try to cleanly disconnect from the Perforce server at the other end
         * of the current connection (with the emphasis on "cleanly"). This
         * should theoretically include sending a release2 message, but we
         * don't always get the chance to do that.
         *
         * @see com.perforce.p4java.impl.mapbased.server.Server#disconnect()
         */
        public void disconnect() throws ConnectionException, AccessException {
                super.disconnect();

                if (this.connectionStart != 0) {
                        Log.stats("RPC connection connected for "
                                        + (System.currentTimeMillis() - this.connectionStart)
                                        + " msec elapsed time");
                }
                this.serverStats.logStats();

                // Clear up all counts for this RPC server
    		    this.authCounter.clearCount();
        }

        /**
         * @see com.perforce.p4java.server.IServer#supportsSmartMove()
         */
        public boolean supportsSmartMove()  throws ConnectionException,
                                RequestException, AccessException {
                // Return true iff server version >= 2009.1
        		// and move command is not disabled on server

                if (this.serverVersion < 20091)
                	return false;
                IServerInfo info = getServerInfo();
                if (info == null)
                	return false;
                return !info.isMoveDisabled();
        }

        /**
         * @see com.perforce.p4java.server.IOptionsServer#getErrorOrInfoStr(Map)
         */
        public IServerMessage getErrorOrInfoStr(Map<String, Object> map) {
                return getServerMessage(map, MessageSeverityCode.E_INFO);
        }

        public boolean isInfoMessage(Map<String, Object> map) {
                if (map != null) {
                        return (RpcMessage.getSeverity((String) map.get("code0")) == MessageSeverityCode.E_INFO);
                }
                return false;
        }

        public int getSeverityCode(Map<String, Object> map) {
                // Note: only gets first severity, i.e. code0:

                if ((map != null) && map.containsKey("code0")) {
                        return RpcMessage.getSeverity((String) map.get("code0"));
                }

                return MessageSeverityCode.E_EMPTY;
        }

        protected int getGenericCode(Map<String, Object> map) {
                // Note: only gets first code, i.e. code0:

                if ((map != null) && map.containsKey("code0")) {
                        return RpcMessage.getGeneric((String) map.get("code0"));
                }

                return MessageGenericCode.EV_NONE;
        }

        // p4ic4idea change: returning an IServerMessage instead of a string.
        private IServerMessage getServerMessage(Map<String, Object> map, int minimumCode ) {
                if (map != null) {
                        int index = 0;
                        String code = (String) map.get(RpcMessage.CODE + index);

                        List<ISingleServerMessage> singleMessages = new ArrayList<ISingleServerMessage>();

                        while (code != null) {
                                singleMessages.add(new ServerMessage.SingleServerMessage(code, index, map));
                                index++;
                                code = (String) map.get(RpcMessage.CODE + index);
                        }

                        // Only return a string if at least one severity code was found
                        if (! singleMessages.isEmpty()) {
                                final ServerMessage msg = new ServerMessage(singleMessages, map);
                                if (msg.getSeverity() >= minimumCode) {
                                        return msg;
                                }
                        }
                }
                return null;
        }

        /**
         * RPC impl errors come across the wire as a map in the form usually like this:
         * <pre>
         * fmt0=Access for user '%user%' has not been enabled by 'p4 protect'.,
         * func=client-Message, user=nouser, code0=822483067
         * </pre>
         * With tags being used for non-error payloads, we can just basically
         * pick up the presence of the code0 entry; if it's there, use fmt0
         * as the format and the other args as appropriate...<p>
         *
         * <p>
         *
         * p4ic4idea changed the return code from String to IServerMessage
         *
         * @see com.perforce.p4java.server.IOptionsServer#getErrorStr(Map)
         */
        public IServerMessage getErrorStr(Map<String, Object> map) {
                return getServerMessage(map, MessageSeverityCode.E_FAILED);
        }

    	/**
    	 * @see com.perforce.p4java.server.IOptionsServer#getInfoStr(Map)
    	 */
        public String getInfoStr(Map<String, Object> map) {
                if (map != null) {
                        String code0 = (String) map.get("code0");

                        int severity = RpcMessage.getSeverity(code0);

                        if (severity == MessageSeverityCode.E_INFO) {
                                String fmtStr = (String) map.get("fmt0");

                                if (fmtStr == null) {
                                        return "";	// a la the p4 command line version
                                }

                                if (!fmtStr.contains("%")) {
                                        return fmtStr;
                                }

                                return RpcMessage.interpolateArgs(fmtStr, map);
                        }
                }

                return null;
        }

        /**
         * @see com.perforce.p4java.impl.mapbased.server.Server#isAuthFail(IServerMessage)
         */
        @Override
        public boolean isAuthFail(IServerMessage err) {
                if (err != null) {
                        for (String str : accessErrMsgs) {
                                if (err.hasMessageFragment(str)) {
                                        return true;
                                }
                        }
                }

                return false;
        }

        /**
         * @see com.perforce.p4java.impl.mapbased.server.Server#isLoginNotRequired(String)
         */
        @Override
        public boolean isLoginNotRequired(String msgStr) {
                if (msgStr != null) {
	                    if (msgStr.contains(PASSWORD_NOT_SET_STRING)) {
	                            return true;
	                    }
                }

                return false;
        }

        public int getClientApiLevel() {
                return this.clientApiLevel;
        }

        public void setClientApiLevel(int clientApiLevel) {
                this.clientApiLevel = clientApiLevel;
        }

        public String getApplicationName() {
                return this.applicationName;
        }

        public void setApplicationName(String applicationName) {
                this.applicationName = applicationName;
        }
        
        public PerformanceMonitor getPerfMonitor() {
                return this.perfMonitor;
        }

        public void setPerfMonitor(PerformanceMonitor perfMonitor) {
                this.perfMonitor = perfMonitor;
        }

        protected String getOsTypeForEnv() {
                String osName = System.getProperty(RPC_ENV_OS_NAME_KEY);

                if ((osName != null) && osName.toLowerCase(Locale.ENGLISH).contains(RPC_ENV_WINDOWS_PREFIX)){
                        return RPC_ENV_WINDOWS_SPEC;
                }

                return RPC_ENV_UNIX_SPEC; // sic -- as seen in the C++ API...
        }

        protected String getLanguageForEnv() {
                return this.getUsageOptions().getTextLanguage();
        }

        protected String getClientNameForEnv() {
                if (getClientName() != null) {
                        return getClientName();
                } else {
                        return this.getUsageOptions().getUnsetClientName();
                }
        }

        protected String getHostForEnv() {
                if (localHostName != null) {
                        return localHostName;
                }
                return RPC_ENV_NOHOST_SPEC;
        }

        protected String getUserForEnv() {
                if (getUserName() != null) {
                        return getUserName();
                }
                return this.getUsageOptions().getUnsetUserName();
        }


        protected void processCmdCallbacks(int cmdCallBackKey, long timeTaken, List<Map<String, Object>> resultMaps) {
                this.commandCallback.completedServerCommand(cmdCallBackKey, timeTaken);
                if (resultMaps != null) {
                        for (Map<String, Object> map : resultMaps) {
                                final IServerMessage msg = getErrorOrInfoStr(map);
                                if (msg != null) {
                                        int severity = msg.getSeverity();
                                        if (severity != MessageSeverityCode.E_EMPTY) {
                                                this.commandCallback.receivedServerMessage(
                                                        cmdCallBackKey, msg.getGeneric(), severity, msg);
                                        }

                                        if (msg.getSeverity() == MessageSeverityCode.E_INFO) {
                                                this.commandCallback.receivedServerInfoLine(cmdCallBackKey, msg);
                                        } else if (severity >= MessageSeverityCode.E_FAILED) {
                                                this.commandCallback.receivedServerErrorLine(cmdCallBackKey, msg);
                                        }
                                }
                        }
                }
        }

        /**
         * Save current ticket returned from {@link #getAuthTicket()}.
         *
         * @see #saveTicket(String)
         * @throws P4JavaException
         */
        public void saveCurrentTicket() throws P4JavaException {
                saveTicket(getAuthTicket());
        }

        /**
         * Save specified auth ticket value as associate with this server's address
         * and configured user returned from {@link #getUserName()}. This will
         * attempt to write an entry to the p4tickets file either specified as the
         * P4TICKETS environment variable or at the OS specific default location. If
         * the ticket value is null then the current entry in the will be cleared.
         *
         * @param ticketValue
         * @throws ConfigException
         */
        public void saveTicket(String ticketValue) throws ConfigException {
        	saveTicket(getUserName(), ticketValue);

        }

        /**
         * Save specified auth ticket value as associate with this server's address
         * and user name from the userName parameter. This will attempt to write
         * an entry to the p4tickets file either specified as the P4TICKETS environment
         * variable or at the OS specific default location. If the ticket value
         * is null then the current entry will be cleared.
         *
         * @param userName
         * @param ticketValue
         * @throws ConfigException
         */
        public void saveTicket(String userName, String ticketValue) throws ConfigException {
        		ConfigException exception = null;

        		// Must downcase the username to find or save a ticket when
        		// connected to a case insensitive server.
        		if (!isCaseSensitive() && userName != null) {
    				userName = userName.toLowerCase();
        		}
        		
        		String serverId = getServerId();
                // Try to save the ticket by server id first if set
                if (serverId != null) {
                        try {
                                AuthTicketsHelper.saveTicket(userName, serverId, ticketValue,
                                		this.ticketsFilePath);
                        } catch (IOException e) {
                                exception = new ConfigException(e);
                        }
                }

                // If id is null try to use configured server address
                // If ticket value is null try to clear out any old values by the
                // configured server address
                if (ticketValue == null || serverId == null) {
                        // Try to save the ticket by server address
                        String server = null;
                        if (this.serverHost != null) {
                                server = this.serverHost;
                                if (this.serverPort != UNKNOWN_SERVER_PORT) {
                                        server += ":" + Integer.toString(this.serverPort);
                                }
                        } else if (this.serverPort != UNKNOWN_SERVER_PORT) {
                                server = Integer.toString(this.serverPort);
                        }
                        if (server != null) {
                                try {
                                        AuthTicketsHelper.saveTicket(userName, server, ticketValue,
                                        		this.ticketsFilePath);

                                } catch (IOException e) {
                                        // Use first exception if thrown
                                        if (exception == null) {
                                                exception = new ConfigException(e);
                                        }
                                }
                        }
                }

                //Throw the exception from either save attempt
                if (exception != null) {
                        throw exception;
                }
        }

        /**
         * Get the p4tickets entry value for the current user returned from
         * {@link #getUserName()} and server address based upon a search of either
         * the file found at {@link PropertyDefs#TICKET_PATH_KEY_SHORT_FORM},
         * {@link PropertyDefs#TICKET_PATH_KEY}, the P4TICKETS environment variable
         * or the standard p4tickets file location for the current OS. Will return
         * null if not found or if an error occurred attempt to lookup the value.
         *
         * @param serverId
         *
         * @return - ticket value to get used for {@link #setAuthTicket(String)} or
         *         null if not found.
         */
        public String loadTicket(String serverId) {
                String ticketValue = null;
                String name = getUserName();
                if (name != null) {
                        try {
                                ticketValue = AuthTicketsHelper.getTicketValue(name,
                                                serverId, this.ticketsFilePath);
                        } catch (IOException e) {
                                ticketValue = null;
                        }
                        if (ticketValue == null) {
                                String server = null;
                                if (this.serverHost != null) {
                                        server = this.serverHost;
                                        if (this.serverPort != UNKNOWN_SERVER_PORT) {
                                                server += ":" + Integer.toString(this.serverPort);
                                        }
                                } else if (this.serverPort != UNKNOWN_SERVER_PORT) {
                                        server = Integer.toString(this.serverPort);
                                }
                                try {
                                        ticketValue = AuthTicketsHelper.getTicketValue(name,
                                                        server, this.ticketsFilePath);
                                } catch (IOException e) {
                                        ticketValue = null;
                                }
                        }
                }
                return ticketValue;
        }

        /**
         * Save specified fingerprint value as associate with this server's address.
         * This will attempt to write an entry to the p4trust file either specified
         * as the P4TRUST environment variable or at the OS specific default location.
         * If the fingerprint value is null then the current entry will be cleared.
         *
         * @param serverIpPort
         * @param fingerprintValue
         * @throws ConfigException
         */
        public void saveFingerprint(String serverIpPort, String fingerprintValue) throws ConfigException {
                if (serverIpPort == null) {
		              return;
                }
        	
                // Save the fingerprint by server IP and port
                try {
                        FingerprintsHelper.saveFingerprint(
                        		ClientTrust.USER_NAME_PLACE_HOLDER,
                        		serverIpPort, fingerprintValue, this.trustFilePath);
                } catch (IOException e) {
                        throw new ConfigException(e);
                }
        }

        /**
         * Get the p4trust entry value for the server IP and port based upon a search
         * of either the file found at {@link PropertyDefs#TRUST_PATH_KEY_SHORT_FORM},
         * {@link PropertyDefs#TRUST_PATH_KEY}, the P4TRUST environment variable
         * or the standard p4trust file location for the current OS. Will return
         * null if not found or if an error occurred attempt to lookup the value.
         *
         * @param serverIpPort
         *
         * @return - fingerprint or null if not found.
         */
        public Fingerprint loadFingerprint(String serverIpPort) {
                if (serverIpPort == null) {
        		        return null;
                }
        	
                Fingerprint fingerprint = null;

                try {
                        fingerprint = FingerprintsHelper.getFingerprint(ClientTrust.USER_NAME_PLACE_HOLDER,
                                        serverIpPort, this.trustFilePath);
                } catch (IOException e) {
                        fingerprint = null;
                }

                return fingerprint;
        }

        /**
         * Get the p4trust entries from the file found at {@link PropertyDefs#TRUST_PATH_KEY_SHORT_FORM},
         * {@link PropertyDefs#TRUST_PATH_KEY}, the P4TRUST environment variable
         * or the standard p4trust file location for the current OS. Will return
         * null if nothing found or if an error occurred attempt to lookup the entries.
         *
         * @return - list of fingerprints or null if nothing found.
         */
        public Fingerprint[] loadFingerprints() {
                Fingerprint[] fingerprints = null;
        	
                try {
                	fingerprints = FingerprintsHelper.getFingerprints(this.trustFilePath);
                } catch (IOException e) {
                	// Suppress error
                }

                return fingerprints;
        }

        /**
    	 * @see com.perforce.p4java.impl.mapbased.server.Server#getTrust()
    	 */
    	public String getTrust() throws P4JavaException {
    		RpcConnection rpcConnection = null;
    		 
    		try {
    			rpcConnection = new RpcStreamConnection(serverHost, serverPort, props,
    					this.serverStats, this.charset, this.secure);
	
    			return rpcConnection.getFingerprint();
    			
    		} finally {
    			if (rpcConnection != null) {
    				rpcConnection.disconnect(null);
    			}
    		}
    	}

    	/**
    	 * @see com.perforce.p4java.impl.mapbased.server.Server#addTrust(com.perforce.p4java.option.server.TrustOptions)
    	 */
    	public String addTrust(TrustOptions opts) throws P4JavaException {
    		RpcConnection rpcConnection = null;
 
    		try {
    			rpcConnection = new RpcStreamConnection(serverHost, serverPort, props,
    					this.serverStats, this.charset, this.secure);
    			
    			if (opts == null) {
	    			opts = new TrustOptions();
	    		}

    			String newConnectionWarning = clientTrust.getMessages().getMessage(
   						ClientTrust.CLIENT_TRUST_WARNING_NEW_CONNECTION,
   						new Object[] { getServerHostPort(), rpcConnection.getFingerprint() });
    			
    			String newKeyWarning = clientTrust.getMessages().getMessage(
   						ClientTrust.CLIENT_TRUST_WARNING_NEW_KEY,
   						new Object[] { getServerHostPort(), rpcConnection.getFingerprint() });
    			
    			boolean fpExist = clientTrust.fingerprintExists(rpcConnection.getServerIpPort());
    			boolean fpMatch = clientTrust.fingerprintMatches(rpcConnection.getServerIpPort(),
    					rpcConnection.getFingerprint());
    			
    			// auto refuse
    			if (opts.isAutoRefuse()) {
    				// new connection
    				if (!fpExist) {
    					return newConnectionWarning;
    				}
    				// new key 
    				if (!fpMatch) {
    					return newKeyWarning;
    				}
    			}

    			// new connection
    			if (!fpExist) {
    				// auto accept
    				if (opts.isAutoAccept()) {
        	    		// install fingerprint
        	    		clientTrust.installFingerprint(rpcConnection.getServerIpPort(),
        	    				rpcConnection.getFingerprint());
        	    		return newConnectionWarning +
        	    				clientTrust.getMessages().getMessage(ClientTrust.CLIENT_TRUST_ADDED,
        	    						new Object[] { getServerHostPort(), rpcConnection.getServerIpPort() });    						
    				}
    				// didn't accept
       				throw new TrustException(TrustException.Type.NEW_CONNECTION,
       						getServerHostPort(),
       						rpcConnection.getServerIpPort(),
       						rpcConnection.getFingerprint(),
       						newConnectionWarning +
       						clientTrust.getMessages().getMessage(ClientTrust.CLIENT_TRUST_ADD_EXCEPTION_NEW_CONNECTION)     						
       						);
    			}
    			
    			// new key
    			if (!fpMatch) {
    				// force install
    				if (opts.isForce()) {
    					// auto accept
    					if (opts.isAutoAccept()) {
            	    		// install fingerprint
            	    		clientTrust.installFingerprint(rpcConnection.getServerIpPort(),
            	    				rpcConnection.getFingerprint());
            	    		return newKeyWarning +
            	    				clientTrust.getMessages().getMessage(ClientTrust.CLIENT_TRUST_ADDED,
            	    						new Object[] { getServerHostPort(), rpcConnection.getServerIpPort() });    						
    					}
        				// didn't accept
           				throw new TrustException(TrustException.Type.NEW_KEY,
           						getServerHostPort(),
           						rpcConnection.getServerIpPort(),
           						rpcConnection.getFingerprint(),
           						newKeyWarning +
           						clientTrust.getMessages().getMessage(ClientTrust.CLIENT_TRUST_ADD_EXCEPTION_NEW_KEY)     						
           						);
    				}
    				// not force install
       				throw new TrustException(TrustException.Type.NEW_KEY,
       						getServerHostPort(),
       						rpcConnection.getServerIpPort(),
       						rpcConnection.getFingerprint(),
       						newKeyWarning +
       						clientTrust.getMessages().getMessage(ClientTrust.CLIENT_TRUST_ADD_EXCEPTION_NEW_KEY)     						
       						);
    			}

    			// trust already established
	    		return clientTrust.getMessages().getMessage(ClientTrust.CLIENT_TRUST_ALREADY_ESTABLISHED);     						
    			
    		} finally {
    			if (rpcConnection != null) {
    				rpcConnection.disconnect(null);
    			}
    		}
    	}
    	
    	/**
    	 * @see com.perforce.p4java.impl.mapbased.server.Server#addTrust(String)
    	 */
    	public String addTrust(String fingerprintValue) throws P4JavaException {
    		if (fingerprintValue == null) {
    			throw new NullPointerError("null fingerprintValue passed to addTrust");
    		}
    		
    		RpcConnection rpcConnection = null;

    		try {
    			rpcConnection = new RpcStreamConnection(serverHost, serverPort, props,
    					this.serverStats, this.charset, this.secure);

    			String message = "";
    			
	    		// new connection
	    		if (!clientTrust.fingerprintExists(rpcConnection.getServerIpPort())) {
	    			message = clientTrust.getMessages().getMessage(
       						ClientTrust.CLIENT_TRUST_WARNING_NEW_CONNECTION,
       						new Object[] { getServerHostPort(), rpcConnection.getFingerprint() });
	    		} else {
		    		// new key
	    			if (!clientTrust.fingerprintMatches(rpcConnection.getServerIpPort(),
	    					fingerprintValue)) {
	    				message = clientTrust.getMessages().getMessage(
	       						ClientTrust.CLIENT_TRUST_WARNING_NEW_KEY,
	       						new Object[] { getServerHostPort(), rpcConnection.getFingerprint() });
	    			}
	    		}
	
	    		// install the fingerprint to the trust file
	    		clientTrust.installFingerprint(rpcConnection.getServerIpPort(),
	    				fingerprintValue);
    			
        		return message + clientTrust.getMessages().getMessage(
						ClientTrust.CLIENT_TRUST_ADDED,
						new Object[] { getServerHostPort(), rpcConnection.getServerIpPort() });

    		} finally {
    			if (rpcConnection != null) {
    				rpcConnection.disconnect(null);
    			}
    		}
    	}

    	/**
    	 * @see com.perforce.p4java.impl.mapbased.server.Server#removeTrust()
    	 */
    	public String removeTrust() throws P4JavaException {
    		RpcConnection rpcConnection = null;

    		try {
    			rpcConnection = new RpcStreamConnection(serverHost, serverPort, props,
    					this.serverStats, this.charset, this.secure);

    			String message = clientTrust.getMessages().getMessage(
    					ClientTrust.CLIENT_TRUST_REMOVED,
						new Object[] { getServerHostPort(), rpcConnection.getServerIpPort() });

    			// remove the fingerprint from the trust file
	    		clientTrust.removeFingerprint(rpcConnection.getServerIpPort());
    			
	    		// new connection
	    		if (!clientTrust.fingerprintExists(rpcConnection.getServerIpPort())) {
        			return clientTrust.getMessages().getMessage(
       						ClientTrust.CLIENT_TRUST_WARNING_NEW_CONNECTION,
       						new Object[] { getServerHostPort(), rpcConnection.getFingerprint() }) + message;
	    		}

	    		// new key
    			if (!clientTrust.fingerprintMatches(rpcConnection.getServerIpPort(),
    					rpcConnection.getFingerprint())) {
    				return clientTrust.getMessages().getMessage(
       						ClientTrust.CLIENT_TRUST_WARNING_NEW_KEY,
       						new Object[] { getServerHostPort(), rpcConnection.getFingerprint() }) + message;
    			}
	
        		return message;

    		} finally {
    			if (rpcConnection != null) {
    				rpcConnection.disconnect(null);
    			}
    		}
    	}

    	/**
    	 * @see com.perforce.p4java.impl.mapbased.server.Server#getTrusts()
    	 */
    	public List<Fingerprint> getTrusts() throws P4JavaException {
    		
    		Fingerprint[] fingerprints = loadFingerprints();
    		if (fingerprints != null) {
    			return Arrays.asList(fingerprints);
    		}
			return null;
    	}
        
        /**
         * Set the server's id field used for storing authentication tickets. The id
         * specified here will be used when saving ticket values to a p4 tickets
         * file. This field should only be set to the server id returned as part of
         * a server message.
         *
         * @param serverId
         */
        public void setServerId(String serverId) {
                this.serverId = serverId;
        }

        /**
         * Get the server's id field used for storing authentication tickets. This
         * id should only be used as a server address portion for entries in a p4
         * tickets file.
         *
         * @return - possibly null server id
         */
        public String getServerId() {
                return this.serverId;
        }

        /**
         * Return true iff we should be performing server -> client
         * file write I/O operations in place for this command.<p>
         *
         * See PropertyDefs.WRITE_IN_PLACE_KEY javadoc for the semantics
         * of this.
         *
         * @param cmdName non-null command command name string
         * @return true iff we should do a sync in place
         */
        protected boolean writeInPlace(String cmdName) {
                return cmdName.equalsIgnoreCase(CmdSpec.SYNC.toString())
                                        &&	new Boolean(System.getProperty(
                                                        PropertyDefs.WRITE_IN_PLACE_KEY,
                                                                props.getProperty(
                                                                        PropertyDefs.WRITE_IN_PLACE_SHORT_FORM, "false")));
        }

        public String getSecretKey() {
            	return getSecretKey(this.userName);
        }

        public void setSecretKey(String secretKey) {
			setSecretKey(this.userName, secretKey);
	    }
    
        public String getSecretKey(String userName) {
        	if (userName != null) {
                return secretKeys.get(userName);
        	}
        	return null;
        }

        public void setSecretKey(String userName, String secretKey) {
        	if (userName != null) {
        		if (secretKey == null) {
        			this.secretKeys.remove(userName);
        		} else {
        			this.secretKeys.put(userName, secretKey);
        		}
        	}
        }

        protected boolean isRelaxCmdNameValidationChecks() {
                return relaxCmdNameValidationChecks;
        }

        protected void setRelaxCmdNameValidationChecks(
                        boolean relaxCmdNameValidationChecks) {
                this.relaxCmdNameValidationChecks = relaxCmdNameValidationChecks;
        }

        /**
    	 * Get the RPC packet field rule for skipping the charset conversion of
    	 * a range of RPC packet fields; leave the values as bytes. <p>
    	 * 
    	 * Note: currently only supporting the "export" command.
    	 */
        protected RpcPacketFieldRule getRpcPacketFieldRule(Map<String, Object> inMap, CmdSpec cmdSpec) {
    		if (inMap != null && cmdSpec != null) {
				if (cmdSpec == CmdSpec.EXPORT) {
	    			if (inMap.containsKey(cmdSpec.toString())) {
	    				// The map for this command spec
	    				if (inMap.get(cmdSpec.toString()) instanceof Map<?,?>) {
		    				@SuppressWarnings("unchecked")
							Map<String, Object> cmdMap = (Map<String, Object>)inMap.remove(cmdSpec.toString());
		    				if (cmdMap != null) {
								return RpcPacketFieldRule.getInstance(cmdMap);
							}
	    				}
	    			}
				}
    		}
    		return null;
    	}

    	/**
    	 * @see com.perforce.p4java.impl.mapbased.server.Server#setAuthTicket(String, String)
    	 */
    	public void setAuthTicket(String userName, String authTicket) {
    		if (userName == null) {
    			throw new IllegalArgumentException("Null userName passed to the setAuthTicket method.");
    		}
    		// Must downcase the username to find or save a ticket when
    		// connected to a case insensitive server.
    		if (!isCaseSensitive() && userName != null) {
				userName = userName.toLowerCase();
    		}
    		String serverAddress = getServerId() != null ? getServerId() : getServerAddress();
            if (serverAddress == null) {
       			throw new IllegalStateException("Null serverAddress in the setAuthTicket method.");
            }
            if (authTicket == null) {
            	this.authTickets.remove(composeAuthTicketEntryKey(userName, serverAddress));
            } else {
            	this.authTickets.put(composeAuthTicketEntryKey(userName, serverAddress), authTicket);
            }
    	}
    	
    	/**
    	 * @see com.perforce.p4java.impl.mapbased.server.Server#getAuthTicket(String)
    	 */
    	public String getAuthTicket(String userName) {
    		// Must downcase the username to find or save a ticket when
    		// connected to a case insensitive server.
    		if (!isCaseSensitive() && userName != null) {
				userName = userName.toLowerCase();
    		}
            String serverAddress = getServerId() != null ? getServerId() : getServerAddress();
            if (userName != null && serverAddress != null) {
        		return this.authTickets.get(composeAuthTicketEntryKey(userName, serverAddress));
        	}
    		return null;
    	}

    	/**
    	 * @see com.perforce.p4java.impl.mapbased.server.Server#setTicketsFilePath(String)
    	 */
    	public void setTicketsFilePath(String ticketsFilePath) {
    		if (ticketsFilePath == null) {
    			throw new IllegalArgumentException("Null ticketsFilePath passed to the setTicketsFilePath method.");
    		}
           	this.ticketsFilePath = ticketsFilePath;
    	}
    	
    	/**
    	 * @see com.perforce.p4java.impl.mapbased.server.Server#getTicketsFilePath()
    	 */
    	public String getTicketsFilePath() {
            return this.ticketsFilePath;
    	}
    	
    	/**
    	 * @see com.perforce.p4java.impl.mapbased.server.Server#setTrustFilePath(String)
    	 */
    	public void setTrustFilePath(String trustFilePath) {
    		if (ticketsFilePath == null) {
    			throw new IllegalArgumentException("Null trustFilePath passed to the setTrustFilePath method.");
    		}
           	this.trustFilePath = trustFilePath;
    	}
    	
    	/**
    	 * @see com.perforce.p4java.impl.mapbased.server.Server#getTrustFilePath()
    	 */
    	public String getTrustFilePath() {
            return this.trustFilePath;
    	}
    	
    	/**
    	 * Compose the key for an auth ticket entry
    	 */
    	protected String composeAuthTicketEntryKey(String userName, String serverAddress) {
    		if (userName == null) {
    			throw new IllegalArgumentException("Null userName passed to the composeAuthTicketEntryKey method.");
    		}
            if (serverAddress == null) {
       			throw new IllegalArgumentException("Null serverAddress in the composeAuthTicketEntryKey method.");
            }
			if (serverAddress.indexOf(':') == -1) {
				serverAddress += "localhost:" + serverAddress;
			}
			return (serverAddress + "=" + userName);
    	}
    	
        /**
         * Get the server's address field used for storing authentication tickets.
         *
         * @return - possibly null server address
         */
    	public String getServerAddress() {
            String serverAddress = this.serverAddress;
            // If id is null try to use configured server address
            if (serverAddress == null) {
                    if (this.serverHost != null) {
                    	serverAddress = this.serverHost;
                        if (this.serverPort != UNKNOWN_SERVER_PORT) {
                        	serverAddress += ":" + Integer.toString(this.serverPort);
                        }
                    } else if (this.serverPort != UNKNOWN_SERVER_PORT) {
                    	serverAddress = Integer.toString(this.serverPort);
                    }
            }
            return serverAddress;
    	}

        /**
         * Get the server's host and port used for the RPC connection.
         *
         * @return - possibly null server host and port
         */
    	public String getServerHostPort() {
            String serverHostPort = null;
            if (this.serverHost != null) {
            	serverHostPort = this.serverHost;
                if (this.serverPort != UNKNOWN_SERVER_PORT) {
                	serverHostPort += ":" + Integer.toString(this.serverPort);
                }
            } else if (this.serverPort != UNKNOWN_SERVER_PORT) {
            	serverHostPort = Integer.toString(this.serverPort);
            }
            return serverHostPort;
    	}

    	/**
    	 * Allow for per-command use of tags or not. Currently has limited use
    	 * (only a few commands are anomalous as far as we can tell), but may
    	 * find more uses generally with experience.<p>.
    	 * 
    	 * This is normally used on a per-command (OneShot RPC server) basis.
    	 * In order to use this on a per-session (NTS RPC server) implementation
    	 * you must resend the RPC protocol, if the 'useTags' state has changed,
    	 * prior to sending the command.
    	 */
    	protected boolean useTags(String cmdName, String[] cmdArgs, Map<String, Object> inMap, boolean isStreamCmd) {
    		CmdSpec cmdSpec = CmdSpec.getValidP4JCmdSpec(cmdName);
    		if (cmdSpec != null) {
    			if (cmdSpec == CmdSpec.LOGIN) {
    				return false;
    			}
				if (isStreamCmd) {
    				switch (cmdSpec) {
					case DESCRIBE:
					case DIFF2:
					case PRINT:
					case PROTECT:
						return false;
					default:
						break;
					}
    			}
    			// Check the inMap for any tagged/non-tagged override
    			if (inMap != null) {
    				if (inMap.containsKey(IN_MAP_USE_TAGS_KEY)) {
    					return new Boolean((String)inMap.remove(IN_MAP_USE_TAGS_KEY));
    				}
    			}
    		}
    		return RPC_TAGS_USED;
    	}

    	public RpcUserAuthCounter getAuthCounter() {
    		return this.authCounter;
    	}
}
