/*******************************************************************************
* Copyright (c) 2013, Perforce Software
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package com.perforce.p4java.extension.server.internal;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.apache.log4j.Logger;
import com.perforce.p4java.PropertyDefs;
import com.perforce.p4java.client.IClient;
import com.perforce.p4java.core.IUser;
import com.perforce.p4java.exception.AccessException;
import com.perforce.p4java.exception.ConfigException;
import com.perforce.p4java.exception.ConnectionException;
import com.perforce.p4java.exception.NoSuchObjectException;
import com.perforce.p4java.exception.NullPointerError;
import com.perforce.p4java.exception.P4JavaException;
import com.perforce.p4java.exception.RequestException;
import com.perforce.p4java.exception.ResourceException;
import com.perforce.p4java.exception.TrustException;
import com.perforce.p4java.extension.command.PerforceDomain;
import com.perforce.p4java.extension.command.Request;
import com.perforce.p4java.extension.exception.ServerAccessException;
import com.perforce.p4java.extension.exception.ServerConnectionException;
import com.perforce.p4java.extension.exception.ServerErrorException;
import com.perforce.p4java.extension.server.LoginStatus;
import com.perforce.p4java.extension.server.PrincipalCredential;
import com.perforce.p4java.extension.server.Server;
import com.perforce.p4java.extension.server.ServerLocation;
import com.perforce.p4java.extension.utility.ErrorIds;
import com.perforce.p4java.extension.utility.ServerUriUtility;
import com.perforce.p4java.extension.utility.Validate;
import com.perforce.p4java.impl.mapbased.rpc.RpcServer;
import com.perforce.p4java.option.server.LoginOptions;
import com.perforce.p4java.server.IOptionsServer;
import com.perforce.p4java.server.PerforceCharsets;
import com.perforce.p4java.server.ServerFactory;
/**
*
*/
public class ServerImpl implements Server {
private final IOptionsServer p4d;
private final ProgramInfo programInfo;
private final ServerLocation serverLocation;
private final Properties properties;
private PrincipalCredential userCredential;
private final static Logger log = Logger.getLogger(ServerImpl.class);
public static class ProgramInfo {
private static final String DEFAULT_PROG_NAME = "GenericYetiBasedApplication";
private static final String DEFAULT_PROG_VERSION = "2012.1";
private final String progName;
private final String progVersion;
public ProgramInfo() {
this.progName = DEFAULT_PROG_NAME;
this.progVersion = DEFAULT_PROG_VERSION;
}
public ProgramInfo(String progName, String progVersion) {
this.progName = progName == null ? DEFAULT_PROG_NAME : progName;
this.progVersion = progVersion == null ? DEFAULT_PROG_VERSION
: progVersion;
}
public String getProgName() {
return progName;
}
public String getProgVersion() {
return progVersion;
}
}
public ServerImpl(ServerLocation serverLocation) {
this(serverLocation, null, null);
}
public ServerImpl(ServerLocation serverLocation, ProgramInfo programInfo) {
this(serverLocation, programInfo, null);
}
public ServerImpl(ServerLocation serverLocation, ProgramInfo programInfo,
Properties properties) {
Validate.notNull(serverLocation,
"serverLocation must not be null to ServerImpl");
this.serverLocation = serverLocation;
this.programInfo = programInfo == null ? new ProgramInfo()
: programInfo;
this.properties = properties;
// log.debug("serverLocation.getCharset()=" +
// serverLocation.getCharset());
p4d = getOptionsServer();
}
private IOptionsServer getOptionsServer() {
try {
IOptionsServer ret = ServerFactory.getOptionsServer(
ServerUriUtility.toP4JavaUri(serverLocation),
getProperties());
return ret;
}
// All possible exceptions thrown from ServerFactory.getOptionsServer
// appear to be due to unrecoverable conditions. For this reason,
// re-throw as an unchecked exception.
catch (NoSuchObjectException e) {
throw new RuntimeException(e);
} catch (ConfigException e) {
throw new RuntimeException(e);
} catch (ResourceException e) {
throw new RuntimeException(e);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(
"This P4Java URI was unabled to be parsed: "
+ ServerUriUtility.toP4JavaUri(serverLocation), e);
} catch (ConnectionException e) {
// ServerFactory.getOptionsServer should not attempt to make a
// connection. This should never be thrown.
throw new RuntimeException(e);
}
}
private Properties getProperties() {
Properties properties = new Properties();
properties.put("relaxCmdNameChecks", "true");
properties.put(PropertyDefs.PROG_NAME_KEY, programInfo.getProgName());
properties.put(PropertyDefs.PROG_VERSION_KEY,
programInfo.getProgVersion());
properties.put(PropertyDefs.PROG_NAME_KEY_SHORTFORM,
programInfo.getProgName());
properties.put(PropertyDefs.PROG_VERSION_KEY_SHORTFORM,
programInfo.getProgVersion());
properties.put("applicationName", programInfo.getProgName());
if (this.properties != null)
properties.putAll(this.properties);
return properties;
}
private Map<String, Object>[] executeNoLogin(Request command)
throws P4JavaException, ServerConnectionException,
ServerAccessException, ServerErrorException {
Map<String, Object>[] res = null;
String currentCharset = p4d.getCharsetName();
// set the charset if we have a command with a charset
if (this.isUnicode() && command.getCharset() != null) {
this.setServerCharset(command.getCharset());
}
if (command.getInputMap() != null) {
res = p4d.execMapCmd(command.getCommand(),
command.getCommandArgs(), command.getInputMap());
} else {
res = p4d.execInputStringMapCmd(command.getCommand(),
command.getCommandArgs(), command.getInputString());
}
// reset the charset to whatever it was if necessary
if (this.isUnicode() && currentCharset != null) {
p4d.setCharsetName(currentCharset);
}
return res;
}
private Map<String, Object>[] executeWithLogin(Request command)
throws P4JavaException, ServerConnectionException,
ServerAccessException, ServerErrorException {
if (command.getInputMap() != null) {
print(command.getCommand(), command.getCommandArgs(),
command.getInputMap());
} else {
print(command.getCommand(), command.getCommandArgs(),
command.getInputString());
}
final Map<String, Object>[] result = executeNoLogin(command);
if (userCredential == null || result.length == 0
|| !result[0].containsKey("code0"))
return result;
// is the code0 the "bad password" result?
if (!ErrorIds.MsgServer_BadPassword.match((String) result[0]
.get("code0"))
&& !ErrorIds.MsgServer_LoginExpired.match((String) result[0]
.get("code0"))
&& !ErrorIds.MsgServer_LoggedOut.match((String) result[0]
.get("code0")))
return result;
login(userCredential); // login will throw if it fails
// try again
return executeNoLogin(command);
}
private void setCurrentClientWithLogin(IClient client)
throws ConnectionException, RequestException, AccessException,
ServerConnectionException, ServerAccessException,
ServerErrorException {
try {
p4d.setCurrentClient(client);
} catch (AccessException e) {
// log in and try again
login(this.userCredential);
p4d.setCurrentClient(client);
}
}
private void setCurrentClientWithLogin(String client)
throws ConnectionException, RequestException, AccessException,
ServerConnectionException, ServerAccessException,
ServerErrorException {
try {
p4d.setCurrentClient(p4d.getClient(client));
} catch (AccessException e) {
// log in and try again
login(this.userCredential);
p4d.setCurrentClient(p4d.getClient(client));
}
}
/**
* @see com.perforce.p4java.extension.server.Server#execute(com.perforce.p4java.extension.command.Request)
*/
public Map<String, Object>[] execute(Request command)
throws ServerConnectionException, ServerAccessException,
ServerErrorException {
try {
if (userCredential == null) {
log.debug("attempting to execute with a null userCredential");
}
if (p4d.getCurrentClient() == null && command.getClient() != null)
setCurrentClientWithLogin(command.getClient());
if (command.getCommand().equals(PerforceDomain.PASSWD.getCommand())) {
// p4java workaround: the server caches a key for too long,
// resulting in scrambled passwords
((RpcServer) p4d).setSecretKey(p4d.getUserName(), null);
}
return executeWithLogin(command);
} catch (P4JavaException e) {
print(command.getCommand(), null, (String) null);
log.debug("failed");
if (e instanceof AccessException)
throw new ServerAccessException(e.getLocalizedMessage(), e);
else if (e instanceof ConnectionException)
throw new ServerConnectionException(e.getLocalizedMessage(), e);
else
throw new ServerErrorException(e.getLocalizedMessage(), e);
}
}
// Debug only
private String print(String command, String[] commandArgs,
String inputString) {
// Debug only, local change only
StringBuilder sb = new StringBuilder();
sb.append("EXEC >>> p4 ");
sb.append(command + " ");
if (command.equals(PerforceDomain.PASSWD.getCommand()))
sb.append("***password arguments supressed***");
else {
if (commandArgs != null) {
for (String arg : commandArgs) {
sb.append(arg + " ");
}
}
if (inputString != null && !inputString.trim().isEmpty())
sb.append("< " + inputString);
}
String msg = sb.toString();
log.debug(msg);
return msg;
}
// Debug only
private String print(String command, String[] commandArgs,
Map<String, Object> inputMap) {
StringBuilder sb = new StringBuilder();
if (command.equals(PerforceDomain.PASSWD.getCommand()))
sb.append("***password arguments supressed***");
else if (inputMap != null) {
for (String key : inputMap.keySet()) {
sb.append(key + "=" + inputMap.get(key) + ",");
}
if (sb.length() > 0)
sb.deleteCharAt(sb.length() - 1);
}
return print(command, commandArgs, sb.toString());
}
private static final HashMap<String, String> javaCharsetToP4Charset = new HashMap<String, String>();
static {
String[] knownCharsets = PerforceCharsets.getKnownCharsets();
for (String charset : knownCharsets) {
javaCharsetToP4Charset.put(
PerforceCharsets.getJavaCharsetName(charset), charset);
}
javaCharsetToP4Charset.put("MacRoman", "macosroman");
javaCharsetToP4Charset.put("P4ShiftJIS", "shiftjis");
javaCharsetToP4Charset.put("Shift_JIS", "shiftjis");
}
private void setServerCharset(Charset charset) throws ConnectionException,
AccessException, RequestException {
// only do this if we're in unicode mode
if (this.isUnicode() && charset != null) {
String cName = javaCharsetToP4Charset.get(charset.name());
if (cName == null) {
return;
}
p4d.setCharsetName(cName);
try {
if (!p4d.isConnected()) {
p4d.connect();
}
} catch (ConfigException cfe) {
throw new ConnectionException(cfe);
}
}
}
/**
* @see com.perforce.p4java.extension.server.Server#executeStreamCommand(com.perforce.p4java.extension.command.Request)
*/
public InputStream executeStreamCommand(Request command)
throws ServerConnectionException, ServerAccessException,
ServerErrorException {
String currentCharset = p4d.getCharsetName();
try {
InputStream res = null;
// set the charset if we have a command with a charset
if (this.isUnicode() && command.getCharset() != null) {
this.setServerCharset(command.getCharset());
}
if (command.getClient() != null) {
this.setCurrentClientWithLogin(command.getClient());
log.debug("CLNT >>> " + command.getClient());
}
print(command.getCommand(), command.getCommandArgs(), "");
// might have to log in
try {
res = p4d.execQuietStreamCmd(command.getCommand(),
command.getCommandArgs());
} catch (AccessException e) {
// log in and try again
login(this.userCredential);
res = p4d.execQuietStreamCmd(command.getCommand(),
command.getCommandArgs());
}
// reset the charset to whatever it was if necessary
if (this.isUnicode() && currentCharset != null) {
p4d.setCharsetName(currentCharset);
}
return res;
} catch (AccessException e) {
throw new ServerAccessException(e.getLocalizedMessage(), e);
} catch (ConnectionException e) {
throw new ServerConnectionException(e.getLocalizedMessage(), e);
} catch (RequestException e) {
throw new ServerErrorException(e);
} finally {
}
}
/**
* @see com.perforce.p4java.extension.server.Server#getServerLocation()
*/
public ServerLocation getServerLocation() {
return serverLocation;
}
/**
* @see com.perforce.p4java.extension.server.Server#connect()
*/
public void connect() throws ServerConnectionException {
if (isConnected())
return;
try {
p4d.connect();
if (serverLocation.getCharset() != null
&& serverLocation.getCharset().length() > 0)
p4d.setCharsetName(serverLocation.getCharset());
} catch (ConnectionException e) {
try {
handleConnectionException(e, p4d);
} catch (ServerAccessException sae) {
throw new RuntimeException(sae);
}
}
// The following should never occur, because we are not logging in with
// connect.
catch (AccessException e) {
throw new RuntimeException(e);
} catch (RequestException e) {
throw new RuntimeException(e);
} catch (ConfigException e) {
throw new RuntimeException(e);
}
}
/**
* @see com.perforce.p4java.extension.server.Server#connectAs()
*/
public void connectAs(String user) throws ServerConnectionException {
try {
// if the user name is not the same, make sure to disconnect first
// or you could end up using a different user to connect
if (p4d.isConnected() && !user.equals(p4d.getUserName()))
p4d.disconnect();
p4d.setUserName(user);
if (!p4d.isConnected())
p4d.connect();
if (serverLocation.getCharset() != null
&& serverLocation.getCharset().length() > 0)
p4d.setCharsetName(serverLocation.getCharset());
} catch (ConnectionException e) {
try {
handleConnectionException(e, p4d);
} catch (ServerAccessException sae) {
throw new RuntimeException(sae);
}
}
// The following should never occur, because we are not logging in with
// connect.
catch (AccessException e) {
throw new RuntimeException(e);
} catch (RequestException e) {
throw new RuntimeException(e);
} catch (ConfigException e) {
throw new RuntimeException(e);
}
}
/**
* @see com.perforce.p4java.extension.server.Server#connectAs()
*/
public void connectAs(PrincipalCredential user)
throws ServerConnectionException {
try {
// if the user name is not the same, make sure to disconnect first
// or you could end up using a different user to connect
if (p4d.isConnected() && !user.equals(p4d.getUserName()))
p4d.disconnect();
p4d.setUserName(user.getPrincipal());
if (!p4d.isConnected())
p4d.connect();
userCredential = user; // save this for later if we need to
// (re)login
if (serverLocation.getCharset() != null
&& serverLocation.getCharset().length() > 0)
p4d.setCharsetName(serverLocation.getCharset());
} catch (ConnectionException e) {
try {
handleConnectionException(e, p4d);
} catch (ServerAccessException sae) {
throw new RuntimeException(sae);
}
}
// The following should never occur, because we are not logging in with
// connect.
catch (AccessException e) {
throw new RuntimeException(e);
} catch (RequestException e) {
throw new RuntimeException(e);
} catch (ConfigException e) {
throw new RuntimeException(e);
}
}
private void handleConnectionException(ConnectionException e,
IOptionsServer server) throws ServerConnectionException,
ServerAccessException {
if (e.getCause() instanceof NullPointerError) {
throw new ServerAccessException("Invalid password");
}
log.debug("Yeti is handling a ConnectionException from P4Java.", e);
if (e.getCause() instanceof TrustException) {
TrustException c = (TrustException) e.getCause();
// Add the key (new connection only) automatically
if (c.getType() == TrustException.Type.NEW_CONNECTION) {
try {
server.addTrust(c.getFingerprint());
} catch (P4JavaException e2) {
throw new ServerConnectionException(
e2.getLocalizedMessage());
}
} else {
throw new ServerConnectionException(e.getLocalizedMessage());
}
} else
throw new ServerConnectionException(e.getLocalizedMessage());
}
/**
* @throws ServerErrorException
* @see com.perforce.p4java.extension.server.Server#login(com.perforce.p4java.extension.server.PrincipalCredential)
*/
public void login(PrincipalCredential user)
throws ServerConnectionException, ServerAccessException,
ServerErrorException {
connect();
p4d.setUserName(user.getPrincipal());
try {
// in p4java 12.2 and later, an exception is thrown when you
// try to login with a password when none is required
log.debug(String.format("logging in as %s [%08X]",
user.getPrincipal(), p4d.hashCode()));
p4d.login(user.getCredential().length() == 0 ? null : user
.getCredential());
this.userCredential = user;
} catch (ConnectionException e) {
handleConnectionException(e, p4d);
} catch (AccessException e) {
throw new com.perforce.p4java.extension.exception.ServerAccessException(
e);
} catch (RequestException e) {
throw new ServerErrorException(e);
} catch (ConfigException e) {
}
}
@Override
public void loginAs(PrincipalCredential user) {
// in p4java 12.2 and later, an exception is thrown when you
// try to login with a password when none is required
log.debug(String.format("setting auth ticket for %s [%s]",
user.getPrincipal(), user.getCredential()));
p4d.setAuthTicket(user.getPrincipal(), user.getCredential());
p4d.setUserName(user.getPrincipal());
}
/**
* @see com.perforce.p4java.extension.server.Server#clean()
*/
public void clean() {
try {
clearClient();
userCredential = null;
} catch (ServerConnectionException e) {
} catch (ServerErrorException e) {
} catch (ServerAccessException e) {
}
}
/**
* @see com.perforce.p4java.extension.server.Server#logout()
*/
public void logout() {
// logout is expensive, we only logout when absolutely necessary
try {
p4d.logout();
} catch (ConfigException e) {
throw new RuntimeException(e);
} catch (ConnectionException e) {
} catch (AccessException e) {
} catch (RequestException e) {
}
}
/**
* @see com.perforce.p4java.extension.server.Server#disconnect()
*/
public void disconnect() {
try {
p4d.disconnect();
} catch (ConnectionException e) {
} catch (AccessException e) {
}
}
/**
* @see com.perforce.p4java.extension.server.Server#isConnected()
*/
public boolean isConnected() {
return p4d.isConnected();
}
/**
* @see com.perforce.p4java.extension.server.Server#getLoginStatus()
*/
public LoginStatus getLoginStatus() {
try {
return LoginStatus.fromServerString(p4d.getLoginStatus());
} catch (P4JavaException e) {
return null;
}
}
/**
* @see com.perforce.p4java.extension.server.Server#getLoginName()
*/
public String getLoginName() {
return p4d.getUserName();
}
public void setIClient(IClient client) throws ServerConnectionException,
ServerErrorException, ServerAccessException {
try {
log.debug("CLNT >>> " + (client == null ? null : client.getName()));
setCurrentClientWithLogin(client);
} catch (ConnectionException e) {
throw new com.perforce.p4java.extension.exception.ServerConnectionException(
e);
} catch (RequestException e) {
throw new ServerErrorException(e);
} catch (AccessException e) {
throw new com.perforce.p4java.extension.exception.ServerAccessException(
e);
}
}
public IClient getIClient() {
return p4d.getCurrentClient();
}
public boolean isConfiguredWithClient() {
return p4d.getCurrentClient() != null;
}
public void clearClient() throws ServerConnectionException,
ServerErrorException, ServerAccessException {
try {
setCurrentClientWithLogin((IClient) null);
} catch (ConnectionException e) {
throw new com.perforce.p4java.extension.exception.ServerConnectionException(
e);
} catch (RequestException e) {
throw new ServerErrorException(e);
} catch (AccessException e) {
throw new com.perforce.p4java.extension.exception.ServerAccessException(
e);
}
}
public String getAuthTicket() {
return this.p4d.getAuthTicket();
}
public IOptionsServer getIOptionsServer() {
return this.p4d;
}
@Override
public boolean isUnicode() {
return this.serverLocation.getCharset() != null;
}
@Override
public void logoutAs(String user) throws ServerConnectionException,
ServerAccessException, ServerErrorException {
try {
p4d.setUserName(user);
p4d.logout();
p4d.setUserName(this.userCredential.getPrincipal());
} catch (ConnectionException e) {
throw new com.perforce.p4java.extension.exception.ServerConnectionException(
e);
} catch (RequestException e) {
throw new ServerErrorException(e);
} catch (AccessException e) {
throw new com.perforce.p4java.extension.exception.ServerAccessException(
e);
} catch (ConfigException e) {
throw new ServerErrorException(e);
}
}
@Override
public String getAuthTicket(String hostIP) throws ServerErrorException,
ServerAccessException {
StringBuffer ticket = new StringBuffer();
try {
log.debug("Getting auth ticket for "
+ this.userCredential.getPrincipal() + "@" + hostIP);
p4d.login(this.userCredential.getCredential(), ticket,
new LoginOptions(false, true, hostIP));
log.debug("Obtained auth ticket: " + ticket.toString());
} catch (AccessException e) {
// assume that they don't need a ticket if the credential is empty
if (this.userCredential.getCredential().isEmpty()) {
return "";
} else {
log.error("Password is set but login failed");
throw new ServerAccessException(e);
}
} catch (P4JavaException e) {
log.error("Generic P4JavaException");
throw new ServerErrorException(e);
}
return ticket.toString();
}
@Override
public void loginAs(String user) throws ServerConnectionException,
ServerAccessException, ServerErrorException {
try {
// this connection must already be logged in as a super
IUser userModel = p4d.getUser(user);
// in p4java 12.2 and later, an exception is thrown when you
// try to login with a password when none is required
log.debug(String.format("logging in as %s [%08X]", user,
p4d.hashCode()));
p4d.login(userModel, null, null);
p4d.setUserName(user);
} catch (ConnectionException e) {
handleConnectionException(e, p4d);
} catch (AccessException e) {
throw new com.perforce.p4java.extension.exception.ServerAccessException(
e);
} catch (RequestException e) {
throw new ServerErrorException(e);
} catch (ConfigException e) {
} catch (P4JavaException e) {
throw new ServerErrorException(e);
}
}
}