se.kth.iss.ug2.Ug2Client Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of standalone-ugclient Show documentation
Show all versions of standalone-ugclient Show documentation
A stand-alone Maven UgClient package separated from the UG server.
/*
* MIT License
*
* Copyright (c) 2017 Kungliga Tekniska högskolan
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package se.kth.iss.ug2;
import static se.kth.iss.ug2.Ug2Protocol.ATTRIBUTE;
import static se.kth.iss.ug2.Ug2Protocol.CHANGE_LOG_ATTRIBUTE;
import static se.kth.iss.ug2.Ug2Protocol.CHANGE_LOG_CLASS;
import static se.kth.iss.ug2.Ug2Protocol.CHANGE_LOG_FACILITY;
import static se.kth.iss.ug2.Ug2Protocol.CHANGE_LOG_KTHID;
import static se.kth.iss.ug2.Ug2Protocol.CHANGE_LOG_OPERATOR;
import static se.kth.iss.ug2.Ug2Protocol.CHANGE_LOG_REQUEST;
import static se.kth.iss.ug2.Ug2Protocol.CHANGE_LOG_SESSION;
import static se.kth.iss.ug2.Ug2Protocol.CHANGE_LOG_VALUE;
import static se.kth.iss.ug2.Ug2Protocol.CLASS;
import static se.kth.iss.ug2.Ug2Protocol.CODE;
import static se.kth.iss.ug2.Ug2Protocol.DETAILS;
import static se.kth.iss.ug2.Ug2Protocol.EXCLUDE_DIRECT;
import static se.kth.iss.ug2.Ug2Protocol.EXCLUDE_INDIRECT;
import static se.kth.iss.ug2.Ug2Protocol.GROUP;
import static se.kth.iss.ug2.Ug2Protocol.KEY;
import static se.kth.iss.ug2.Ug2Protocol.KTHID;
import static se.kth.iss.ug2.Ug2Protocol.LENGTH;
import static se.kth.iss.ug2.Ug2Protocol.LOOKUPATTR;
import static se.kth.iss.ug2.Ug2Protocol.LOOKUPVALUE;
import static se.kth.iss.ug2.Ug2Protocol.MAX_TIME;
import static se.kth.iss.ug2.Ug2Protocol.MAX_VERSION;
import static se.kth.iss.ug2.Ug2Protocol.MIN_TIME;
import static se.kth.iss.ug2.Ug2Protocol.MIN_VERSION;
import static se.kth.iss.ug2.Ug2Protocol.NUMLOOKUPVALS;
import static se.kth.iss.ug2.Ug2Protocol.OBJECT;
import static se.kth.iss.ug2.Ug2Protocol.OBJECTSTATUS;
import static se.kth.iss.ug2.Ug2Protocol.OPERATION;
import static se.kth.iss.ug2.Ug2Protocol.OP_ACCUMULATE_GROUP_DATA;
import static se.kth.iss.ug2.Ug2Protocol.OP_ALL_OBJECTS_HAVING;
import static se.kth.iss.ug2.Ug2Protocol.OP_CREATE_SESSION;
import static se.kth.iss.ug2.Ug2Protocol.OP_CURRENT_VERSION;
import static se.kth.iss.ug2.Ug2Protocol.OP_FIND_OBJECTS;
import static se.kth.iss.ug2.Ug2Protocol.OP_GET_CHANGE_LOG_ENTRIES;
import static se.kth.iss.ug2.Ug2Protocol.OP_GET_DATA;
import static se.kth.iss.ug2.Ug2Protocol.OP_GET_SCHEMA;
import static se.kth.iss.ug2.Ug2Protocol.OP_MEMBERSHIP;
import static se.kth.iss.ug2.Ug2Protocol.OP_OBJECTS_MATCHING;
import static se.kth.iss.ug2.Ug2Protocol.OP_PING;
import static se.kth.iss.ug2.Ug2Protocol.OP_PRE_PING;
import static se.kth.iss.ug2.Ug2Protocol.OP_SET_DATA;
import static se.kth.iss.ug2.Ug2Protocol.OP_TERMINATE_SESSION;
import static se.kth.iss.ug2.Ug2Protocol.PASSWORD;
import static se.kth.iss.ug2.Ug2Protocol.PROTOCOL_VERSION_MAJOR;
import static se.kth.iss.ug2.Ug2Protocol.PROTOCOL_VERSION_TAG;
import static se.kth.iss.ug2.Ug2Protocol.REQUESTID;
import static se.kth.iss.ug2.Ug2Protocol.SERVER_ID;
import static se.kth.iss.ug2.Ug2Protocol.SESSIONID;
import static se.kth.iss.ug2.Ug2Protocol.SESSION_KEY_AGE_MAX;
import static se.kth.iss.ug2.Ug2Protocol.SESSION_KEY_VERSION;
import static se.kth.iss.ug2.Ug2Protocol.STATUS;
import static se.kth.iss.ug2.Ug2Protocol.STATUS_AUTHFAIL;
import static se.kth.iss.ug2.Ug2Protocol.STATUS_EXISTS;
import static se.kth.iss.ug2.Ug2Protocol.STATUS_FAIL;
import static se.kth.iss.ug2.Ug2Protocol.STATUS_ILLEGALDATA;
import static se.kth.iss.ug2.Ug2Protocol.STATUS_NOTFOUND;
import static se.kth.iss.ug2.Ug2Protocol.STATUS_NOTPERMITTED;
import static se.kth.iss.ug2.Ug2Protocol.STATUS_NOTUNIQUE;
import static se.kth.iss.ug2.Ug2Protocol.STATUS_OK;
import static se.kth.iss.ug2.Ug2Protocol.STATUS_SERVERERROR;
import static se.kth.iss.ug2.Ug2Protocol.STATUS_WARNING;
import static se.kth.iss.ug2.Ug2Protocol.SYSTEM;
import static se.kth.iss.ug2.Ug2Protocol.TIME_OFFSET;
import static se.kth.iss.ug2.Ug2Protocol.USER;
import static se.kth.iss.ug2.Ug2Protocol.VALUE;
import static se.kth.iss.ug2.Ug2Protocol.VERSION;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.squareup.okhttp.OkHttpClient;
/**
* Connection to (a session at) a UG server
*/
public class Ug2Client {
public static final byte NOT_STARTED = -1;
public static final byte NORMAL = 0;
public static final byte TERMINATED = 1;
private static final ScheduledExecutorService keepAliveService = Executors.newSingleThreadScheduledExecutor();
private static final Object keepAliveLock = new Object();
private static final Logger log = LoggerFactory.getLogger(Ug2Client.class);
private static final String MISSING = "";
private static Ug2Client theInstance;
private static KeepAlive keepAlive = null;
private static long maxUnusedTime = 60;
private final Properties configuration = new Properties();
private final AtomicInteger state = new AtomicInteger(NOT_STARTED);
private Transport channel; // Connection that we use to talk to the UG2 server through.
private Instant lastAccessed = Instant.EPOCH;
private String serverSessionId = null;
private String requestIdBase;
private Long sessionId;
private String system;
private byte[] systemKey;
private byte[] _sessionKey;
private long _sessionKeyCreationTime;
private long _sessionKeyVersion;
private long sessionKeyAgeMax;
private long serverTimeAdjust = 0;
private long keepAliveInterval = 30;
private void clearSession() {
sessionId = null;
_sessionKey = null;
_sessionKeyVersion = -1;
}
/**
* Clears out old session information and creates a new session. The supplied
* {@code system} name & {@code password} will be used for authentication with UG.
* The {@link Transport} will be used for communication with UG and the {@code replyId}
* will be used to identify requests.
*
* @param system the system name that will be used for authentication.
* @param password the password that will be used for authentication.
* @param channel the Ug2Channel that will be used for communicating with UG.
* @param requestId the request id that will be used for identifying requests, ignored if {@code null}.
* @throws Ug2Exception if a new session can't be created.
*/
private void createCleanSession(String system, String password, Transport channel, String requestId)
throws Ug2Exception {
clearSession();
createSession(system, password, requestId, channel);
log.debug("New Ug2Client created. Session id: " + sessionId());
}
/**
* Loads the Ug2Client configuration. First tries to load the configuration
* from the file system, and if that fails from the classpath. If the
* supplied {@code configurationName} doesn't end with ".properties", it is
* appended to the name.
*
* @param configurationName the name of the configuration to load, either
* the path to a file or a resource on the classpath.
* @throws Ug2Exception if the configuration can't be found or if it can't
* be read.
*/
private void loadConfiguration(String configurationName) throws Ug2Exception {
String originalConfigurationName = configurationName;
if (!configurationName.endsWith(".properties")) {
configurationName = configurationName + ".properties";
}
File configurationFile = new File(configurationName);
InputStream inputStream;
if (configurationFile.canRead()) {
try {
inputStream = new FileInputStream(configurationName);
} catch (FileNotFoundException ex) {
throw new Ug2Exception("Configuration file " + configurationName + " not found", ex);
}
} else {
inputStream = getClass().getResourceAsStream(configurationName);
if (inputStream == null) {
inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(configurationName);
}
if (inputStream == null) {
ResourceBundle conf = ResourceBundle.getBundle(originalConfigurationName);
for (String key : Collections.list(conf.getKeys())) {
String value = conf.getString(key);
configuration.setProperty(key, value);
}
return;
}
}
try {
configuration.load(inputStream);
} catch (IOException ex) {
throw new Ug2Exception("Can't load configuration properties from resource " + configurationName, ex);
} finally {
try {
inputStream.close();
} catch (Exception err) {
log.warn("Failed to close config inputstream " + configurationName + ": " + err);
}
}
}
/**
* Initializes the Ug2Client instance
*
* @param requestId the request id to use for the session.
* @throws Ug2Exception if the {@code ugclient3.serverURL} is incorrect or a new session can't be created.
*/
private void init(String requestId) throws Ug2Exception {
URL serverURL;
String url = configuration.getProperty("ugclient3.serverURL");
try {
serverURL = new URL(url);
} catch (MalformedURLException ignored) {
throw new Ug2Exception("Bad server URL: " + url, Ug2Exception.CONFIGERROR);
}
String system = configuration.getProperty("ugclient3.system");
String password = configuration.getProperty("ugclient3.password");
int socketTimeOutInSeconds = Integer.parseInt(configuration.getProperty("ugclient3.socketTimeOutInSeconds"));
keepAliveInterval = Integer.parseInt(configuration.getProperty("ugclient3.sessionKeepAliveIntervalInSeconds"));
maxUnusedTime = Integer.parseInt(configuration.getProperty("ugclient3.clientMaxUnusedTimeInSeconds"));
if (keepAliveInterval > maxUnusedTime) {
log.warn("Keep alive interval " + keepAliveInterval + " is greater than max unused time " + maxUnusedTime
+ ", setting keep alive interval = max unused time");
keepAliveInterval = maxUnusedTime;
}
OkHttpClient client = new OkHttpClient();
client.setConnectTimeout(socketTimeOutInSeconds, TimeUnit.SECONDS);
client.setWriteTimeout(socketTimeOutInSeconds * 1000, TimeUnit.SECONDS);
HttpTransport channel = new HttpTransport(serverURL, client);
createCleanSession(system, password, channel, requestId);
if (keepAliveInterval > 0) {
startKeepAlivePinging();
}
}
/**
* This constructor takes principal authentication data together with the
* Ug2Channel reference to use for the new client. It tries to create a
* session object in the UG2 server with the noOfForks token series
* referring to it.
*
* @param system the system name
* @param password the password for the system
* @param channel transport channel
* @param requestId the request ID or null.
* @throws Ug2Exception on errors.
*/
public Ug2Client(String system, String password, Transport channel, String requestId)
throws Ug2Exception {
createCleanSession(system, password, channel, requestId);
}
/**
* Constructs a new Ug2Client instance using the supplied {@code configurationName} and {@code requestId}.
*
* @param configurationName the name of the configuration to use.
* @param requestId the request id to use.
* @throws Ug2Exception if the configuration can't be loaded or an error occurs during initialization.
*/
public Ug2Client(String configurationName, String requestId) throws Ug2Exception {
loadConfiguration(configurationName);
init(requestId);
}
/**
* Return a Ug2Client, creating a new instance if one doesn't exist, using the supplied {@code configurationName}.
*
* @param configurationName the name of the configuration to use. If null, "ugclient3" will be used.
* @return a new Ug2Client.
* @throws Ug2Exception if a new instance can't be created.
*/
public static synchronized Ug2Client getInstance(String configurationName) throws Ug2Exception {
if (configurationName == null) {
configurationName = "ugclient3";
}
if (theInstance == null) {
theInstance = new Ug2Client(configurationName, "INIT");
} else {
try {
theInstance.ping("TEST");
} catch (Ug2Exception ex) {
log.warn("Failed to reuse Ug2Client instance", ex);
theInstance.stopKeepAlivePinging();
theInstance.setState(TERMINATED);
theInstance = new Ug2Client(configurationName, "RECONNECT");
}
}
return theInstance;
}
/**
* Create a new Ug2Client, using null as the configuration name.
*
* @return a new Ug2Client.
* @throws Ug2Exception if a new instance can't be created.
*/
public static Ug2Client getInstance() throws Ug2Exception {
return getInstance(null);
}
/**
* This function grabs a connection to the UG2 server, sends the message and
* return the received reply. All connection handling (grabbing and
* returning) is dealth with so that the caller doesn't have to bother.
*
* @param request The Ug2Msg to send to the server.
* @return The Ug2Msg received as a reply from the server.
*/
private Ug2Msg doIt(Ug2Msg request) throws Ug2Exception {
setDefaultRequestIdBaseIfBaseMissing(request);
String logPrefix = "ReqId: " + request.getId() + ": ";
StringBuilder logBuf = new StringBuilder();
Ug2Msg reply;
String status;
String op = request.readStringMandatory(OPERATION);
String requestId = request.readStringMandatory(REQUESTID);
long reqTime;
long respTime;
if (op == null)
op = MISSING;
boolean prePing = (op.equals(OP_PRE_PING));
if (requestId == null)
requestId = MISSING;
logBuf.append(logPrefix).append(" . Operation to send: ").append(op)
.append(". Thread: ").append(Thread.currentThread().getName());
log.debug(logPrefix + logBuf.toString());
if (state.get() == TERMINATED) {
/*
* The session is terminated. Report this to the calling application.
*/
throw new Ug2Exception("Session terminated (" + sessionId() + ")", Ug2Exception.TERMINATED);
}
if (!prePing && !op.equals(OP_CREATE_SESSION))
request.addStringNoCheck(SESSIONID, sessionId.toString());
if (serverSessionId == null) {
if (!prePing)
prePing(null, null, requestId);
}
if (serverSessionId != null)
request.addString(SERVER_ID, this.serverSessionId);
byte[] sessionKey;
long sessionKeyVersion;
long sessionKeyCreationTime;
synchronized (this) {
sessionKey = _sessionKey;
sessionKeyVersion = _sessionKeyVersion;
sessionKeyCreationTime = _sessionKeyCreationTime;
}
byte[] key;
long sessionKeyAge = System.currentTimeMillis() - sessionKeyCreationTime;
if ((sessionKey != null) && (sessionKeyAge < sessionKeyAgeMax / 3)) {
key = sessionKey;
request.addLong(SESSION_KEY_VERSION, sessionKeyVersion);
} else {
revertToSystemKey();
key = systemKey;
}
try {
boolean timeFailure;
int nTimeFailures = 0;
long timeAdjust = serverTimeAdjust;
boolean ageFailure;
int nAgeFailures = 0;
boolean serverSessionFailure;
int nServerSessionFailures = 0;
boolean retry;
do {
timeFailure = false;
ageFailure = false;
serverSessionFailure = false;
retry = false;
request.addTime(timeAdjust);
request.addMessageDigest();
request.addAuthenticator(key);
reqTime = (new Date()).getTime();
reply = channel.rpc(request);
respTime = (new Date()).getTime();
if (reply == null)
throw new Ug2Exception("Ug2Channel object unexpectedly returned null.");
long timeOffset = reply.readLongNoThrow(TIME_OFFSET);
if (timeOffset != -1) {
updateServerTimeAdjustment(timeAdjust + timeOffset);
timeAdjust = (nTimeFailures * timeAdjust + timeOffset) / (nTimeFailures + 1);
log.info(logPrefix + "Offset " + timeOffset + ", changing local adjustment to " + timeAdjust);
timeFailure = true;
nTimeFailures++;
}
long keyAgeMax = reply.readLongNoThrow(SESSION_KEY_AGE_MAX);
if (keyAgeMax != -1) {
this.sessionKeyAgeMax = keyAgeMax;
if (!op.equals(OP_CREATE_SESSION)) {
ageFailure = true;
nAgeFailures++;
request.addString(SYSTEM, system);
revertToSystemKey();
key = systemKey;
request.remove(SESSION_KEY_VERSION);
}
}
String serverSession = reply.readStringNoThrow(SERVER_ID);
if (serverSession != null) {
this.serverSessionId = serverSession;
serverSessionFailure = true;
nServerSessionFailures++;
request.addString(SERVER_ID, this.serverSessionId);
request.addString(SYSTEM, system);
revertToSystemKey();
key = systemKey;
request.remove(SESSION_KEY_VERSION);
}
if (timeFailure && (nTimeFailures < 8))
retry = true;
if (ageFailure && (nAgeFailures < 2))
retry = true;
if (serverSessionFailure && (nServerSessionFailures < 4))
retry = !prePing;
}
while (retry);
if (timeFailure)
throw new Ug2Exception("Failed to meet servers time demands (system time wrong?)", Ug2Exception.CONNECTIONERROR);
if (ageFailure)
throw new Ug2Exception("Server claims session key is too old, even when using system key (should not happen)",
Ug2Exception.CONNECTIONERROR);
if (serverSessionFailure && !prePing)
throw new Ug2Exception("Too many server session changes (server session ID reject even after "
+ nServerSessionFailures + " retries)", Ug2Exception.CONNECTIONERROR);
} catch (Ug2Exception e) {
logBuf.setLength(0);
logBuf.append(". Operation: ").append(op);
logBuf.append(". Ug2Exception caught. Status: ").append(e.statusCode()).append(". Msg: ").append(e.getMessage());
logBuf.append(". Thread: ").append(Thread.currentThread().getName());
if (Ug2Exception.RPCFAILED.equals(e.statusCode()) || Ug2Exception.SERVERERROR.equals(e.statusCode())) {
log.warn(logPrefix + logBuf.toString());
} else if (Ug2Exception.SESSIONLOST.equals(e.statusCode()) || Ug2Exception.CONNECTIONERROR.equals(e.statusCode())) {
log.error(logPrefix + logBuf.toString());
} else
log.debug(logPrefix + logBuf.toString());
throw e;
}
if (respTime == 0)
respTime = (new Date()).getTime();
logBuf.setLength(0);
logBuf.append(". Operation: ").append(op);
logBuf.append(". Duration: ");
logBuf.append((double) ((respTime - reqTime)) / 1000).append(" seconds.");
this.lastAccessed = Instant.ofEpochMilli(reqTime);
String protocolVersion = reply.readStringNoThrow(PROTOCOL_VERSION_TAG);
if (protocolVersion == null)
throw new Ug2Exception("No protocol version supplied from server (perhaps server is pre major version 3)",
Ug2Exception.INTERNALERROR);
String[] protocolVersionParts = protocolVersion.split("\\.", 2);
if (protocolVersionParts.length != 2)
throw new Ug2Exception("Bad protocol version format received from server: " + protocolVersion,
Ug2Exception.INTERNALERROR);
String protocolVersionMajor = protocolVersionParts[0];
if (!protocolVersionMajor.equals(PROTOCOL_VERSION_MAJOR))
throw new Ug2Exception("Server has another major protocol (" + protocolVersionMajor + ") version than client ("
+ PROTOCOL_VERSION_MAJOR + ")", Ug2Exception.INTERNALERROR);
status = reply.readStringNoThrow(STATUS);
if (status == null)
status = MISSING;
logBuf.append(" Status: ").append(status);
if (!status.equals(STATUS_OK)) {
String details = reply.readStringNoThrow(DETAILS);
Ug2Exception e;
logBuf.append(". Msg: ");
switch (status) {
case STATUS_NOTPERMITTED:
logBuf.append("Operation not permitted. ");
if (details != null) {
logBuf.append(" Server message: ").append(details);
e = new Ug2Exception("Operation not permitted: " + details, status);
} else {
e = new Ug2Exception("Operation not permitted.", status);
}
break;
case STATUS_NOTFOUND:
logBuf.append("Object not found.");
if (details != null) {
logBuf.append(" Server message: ").append(details);
e = new Ug2Exception("Object not found: " + details, status);
} else {
e = new Ug2Exception("Object not found.", status);
}
break;
case STATUS_NOTUNIQUE:
logBuf.append("Object specification is not unique.");
if (details != null) {
logBuf.append(" Server message: ").append(details);
e = new Ug2Exception("Object specification is not unique: " + details, status);
} else {
e = new Ug2Exception("Object specification is not unique", status);
}
break;
case STATUS_EXISTS:
logBuf.append("Object already exists.");
if (details != null) {
logBuf.append(" Server message: ").append(details);
e = new Ug2Exception("Object already exists: " + details, status);
} else {
e = new Ug2Exception("Object already exists.", status);
}
break;
case STATUS_AUTHFAIL:
logBuf.append("Authentication failed.");
if (details != null) {
logBuf.append(" Server message: ").append(details);
e = new Ug2Exception("Authentication failed: " + details, status);
} else {
e = new Ug2Exception("Authentication failed.", status);
}
break;
case STATUS_SERVERERROR:
logBuf.append("Internal server error.");
if (details != null) {
logBuf.append(" Server message: ").append(details);
e = new Ug2Exception("Internal server error: " + details, status);
} else {
e = new Ug2Exception("Internal server error.", status);
}
break;
case STATUS_FAIL:
logBuf.append("Operation failed.");
if (details != null) {
logBuf.append(" Server message: ").append(details);
e = new Ug2Exception("Operation failed: " + details, status);
} else {
e = new Ug2Exception("Operation failed.", status);
}
break;
case STATUS_WARNING:
logBuf.append("Operation OK but server warning issued.");
e = new Ug2Exception("Operation OK but server warning issued.", status, reply.readStringNoThrow(CODE));
break;
case STATUS_ILLEGALDATA:
logBuf.append("Data provided in the rpc call was incorrect.");
if (details != null) {
logBuf.append(" Server message: ").append(details);
e = new Ug2Exception("Illegal data: " + details, status);
} else {
e = new Ug2Exception("Illegal data.", status);
}
break;
default:
logBuf.append("Unknown status code received from server.");
if (details != null) {
logBuf.append(" Server message: ").append(details);
e = new Ug2Exception("Unknown status code received from server: " + details, status);
} else {
e = new Ug2Exception("Unknown status code received from server.", status);
}
break;
}
log.warn(logPrefix + logBuf.toString());
throw e;
}
log.debug(logPrefix + logBuf.toString());
reply.checkAuthenticator(key);
if (request.challenge() + 1 != reply.challenge())
throw new Ug2Exception("Server failed client challenge.");
byte[] newSessionKey = reply.readSessionKey(key);
if (newSessionKey != null) {
long newSessionKeyVersion = reply.readLongNoThrow(SESSION_KEY_VERSION);
synchronized (this) {
if (newSessionKeyVersion > _sessionKeyVersion) {
_sessionKey = newSessionKey;
_sessionKeyVersion = newSessionKeyVersion;
_sessionKeyCreationTime = System.currentTimeMillis();
}
}
}
return reply;
}
private void setDefaultRequestIdBaseIfBaseMissing(Ug2Msg request) {
if (request.getRequestIdBase() == null && getRequestIdBase() != null) {
request.setRequestId(getRequestIdBase(), request.getId());
}
}
/**
* This method spins of a new keepAlive thread if no such thread is
* already associated with this client object.
*/
public void startKeepAlivePinging() {
synchronized (keepAliveLock) {
if (keepAlive == null) {
keepAlive = new KeepAlive(keepAliveInterval, maxUnusedTime);
keepAlive.startKeepAlive(keepAliveService);
}
}
keepAlive.addClient(this);
}
public void stopKeepAlivePinging() {
if (keepAlive != null) {
keepAlive.removeClient(this);
}
}
/**
* Wrapper for setData() to use when changing values for one object and one
* attribute.
*
* @param className "user" or "group"
* @param key attribute to use as key to identify object.
* @param object value for key
* @param attribute attribute to set
* @param values array of values to set attribute to
* @param requestId request ID for this request.
* @throws Ug2Exception on errors.
*/
public void setData(String className, String key, String object, String attribute, String[] values, String requestId)
throws Ug2Exception {
String[] objCast = new String[1];
String[] attrCast = new String[1];
String[][][] valCast = new String[1][1][];
objCast[0] = object;
attrCast[0] = attribute;
valCast[0][0] = values;
setData(className, key, objCast, attrCast, valCast, requestId);
}
/**
* For each given object and each given attribute the values specified will
* be the ones valid after the operation is completed. I.e. attribute values
* not listed in the values[][][] array but assigned to the object will be
* removed. Values not currently assigned to the object but listed in the
* values[][][] array will be added.
*
* @param className "user" or "group"
* @param key attribute to use as key to identify object.
* @param objects array of object identifiers for key
* @param attributes array of attributes to set
* @param values multi-dimensional array of values to set attributes to
* @param requestId request ID for this request.
* @throws Ug2Exception on errors.
*/
public void setData(String className, String key, String[] objects, String[] attributes, String[][][] values, String requestId)
throws Ug2Exception {
Ug2Msg request = new Ug2Msg(OP_SET_DATA, requestId);
request.addString(CLASS, className);
request.addString(KEY, key);
request.addArray(objects, OBJECT);
request.addArray(attributes, ATTRIBUTE);
if (values != null) {
for (int obj = 0; obj < values.length; obj++) {
for (int attr = 0; attr < values[obj].length; attr++)
request.addArray(values[obj][attr], VALUE + "-" + obj + "-" + attr);
}
}
doIt(request);
}
public Ug2DataResult getData(String className, String keyAttribute, String[] objects, String[] attributes, String requestId)
throws Ug2Exception {
/*
* Marshal the request
*/
Ug2Msg request = new Ug2Msg(OP_GET_DATA, requestId);
request.addString(CLASS, className);
request.addString(KEY, keyAttribute);
request.addArray(objects, OBJECT);
request.addArray(attributes, ATTRIBUTE);
/*
* Call the server
*/
Ug2Msg reply = doIt(request);
/*
* Unmarshal the reply
*/
String[] objectStatus = reply.readArrayMandatory(OBJECTSTATUS);
String[][][] values = new String[attributes.length][][];
for (int iAttr = 0; iAttr < attributes.length; iAttr++) {
values[iAttr] = new String[objects.length][];
ByteArrayInputStream bais = new ByteArrayInputStream(reply.readDataResult(iAttr));
DataInputStream dis = new DataInputStream(bais);
for (int iObj = 0; iObj < objects.length; iObj++) {
if (!objectStatus[iObj].equals(STATUS_OK))
values[iAttr][iObj] = null;
else {
try {
int nValues = dis.readInt();
values[iAttr][iObj] = new String[nValues];
for (int iVal = 0; iVal < nValues; iVal++)
values[iAttr][iObj][iVal] = dis.readUTF();
} catch (IOException e) {
throw new Ug2Exception("Failed to read compressed data result", e);
}
}
}
}
return new Ug2DataResult(objects, attributes, objectStatus, values, true);
}
public String[][] getData(String className, String keyAttribute, String[] objects, String attribute, String requestId)
throws Ug2Exception {
Ug2DataResult data = this.getData(className, keyAttribute, objects, new String[]{attribute}, requestId);
int n = objects.length;
String[][] result = new String[n][];
for (int i = 0; i < n; i++) {
if (!data.status(i).equals("OK"))
throw new Ug2Exception("Can't retrieve data for " + className + "/" + keyAttribute + "=" + objects[i], data.status(0));
result[i] = data.values(i, 0);
}
return result;
}
/**
* This method uses the values provided for the lookupAttribute in order to
* find objects in the UG database. The object references are returned
* expressed as values of the returnKeyAttribute, which MUST be a true key
* attribute (i.e. unique, single valued and always non-null).
*
* @param className Object references are wanted for this class
* (user|group|system)
* @param lookupAttribute The search values provided are for this object attribute
* @param lookupValues Values of the looupAttribute to return objects for.
* @param returnKeyAttribute The object references returned are expressed in values of this
* attribute NOTE: This attribute MUST be a true key, i.e.
* unique, single valued and not-null!
* @param requestId Value to tag this request with. May be null.
* @return The object arrays returned (inner array) are mapped to the values
* that they matched through the outer array, in which the index
* matches the index of the corresponding value in the lookupValues
* array.
* @throws Ug2Exception on errors.
*/
public String[][] findObjects(String className, String lookupAttribute, String[] lookupValues, String returnKeyAttribute,
String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(OP_FIND_OBJECTS, requestId);
request.addString(CLASS, className);
request.addString(LOOKUPATTR, lookupAttribute);
request.addString(KEY, returnKeyAttribute);
request.addArray(lookupValues, LOOKUPVALUE);
Ug2Msg reply = doIt(request);
String[][] retval = null;
/*
* Now we parse the received reply. Each provided lookup value in the
* request generates an array of object id Strings in the reply.
*/
StringBuilder objArrTag = new StringBuilder(OBJECT).append("-");
int tagLen = objArrTag.length();
int numObjArr = reply.readIntMandatory(NUMLOOKUPVALS);
int i;
if (numObjArr >= 0) {
retval = new String[numObjArr][];
for (i = 0; i < numObjArr; i++, objArrTag.setLength(tagLen)) {
objArrTag.append(i);
retval[i] = reply.readArrayMandatory(objArrTag.toString());
}
}
return retval;
}
/**
* This method uses the values provided for the lookupAttribute in order to
* find objects in the UG database. The object references are returned
* expressed as values of the returnKeyAttribute, which MUST be a true key
* attribute (i.e. unique, single valued and always non-null).
*
* @param className Object class (user|group|system)
* @param lookupAttribute The search value provided are for this object attribute
* @param lookupValue Value of the looupAttribute to return objects for.
* @param returnKeyAttribute The object references returned are expressed in values of this
* attribute NOTE: This attribute MUST be a true key, i.e.
* unique, single valued and not-null!
* @param requestId Value to tag this request with. May be null.
* @return array of key attributes of objects matching lookup attribute/value pair.
* @throws Ug2Exception on errors.
*/
public String[] findObjects(String className, String lookupAttribute, String lookupValue, String returnKeyAttribute,
String requestId) throws Ug2Exception {
String[][] result = findObjects(className, lookupAttribute, new String[]{lookupValue}, returnKeyAttribute, requestId);
return result[0];
}
/**
* This method uses the value provided for the lookupAttribute in order to
* find a unique object in the UG database. The object references are returned
* expressed as values of the returnKeyAttribute, which MUST be a true key
* attribute (i.e. unique, single valued and always non-null).
*
* @param className Object class (user|group|system)
* @param lookupAttribute The search value provided are for this object attribute
* @param lookupValue Value of the looupAttribute to return object for.
* @param returnKeyAttribute The object references returned are expressed in values of this
* attribute NOTE: This attribute MUST be a true key, i.e.
* unique, single valued and not-null!
* @param requestId Value to tag this request with. May be null.
* @return the key attribute of the object matching lookup attribute/value pair.
* @throws Ug2Exception on errors, including finding more than one matching object.
*/
public String findObject(String className, String lookupAttribute, String lookupValue, String returnKeyAttribute,
String requestId) throws Ug2Exception {
String[][] result = findObjects(className, lookupAttribute, new String[]{lookupValue}, returnKeyAttribute, requestId);
if (result[0].length == 0) {
return null;
} else if (result[0].length != 1) {
throw new Ug2Exception("The lookup value is not unique, there are " + result[0].length + " " + className + "objects "
+ "with the value \"" + lookupValue + "\" for attribute " + lookupAttribute);
} else {
return result[0][0];
}
}
public List groupsMatching(String value, String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(Ug2Protocol.OP_OBJECTS_MATCHING, requestId);
request.addString(Ug2Protocol.CLASS, Ug2Protocol.GROUP);
request.addString(Ug2Protocol.ATTRIBUTE, Ug2Protocol.UG1NAME);
request.addString(Ug2Protocol.VALUE, value);
Ug2Msg reply = doIt(request);
return reply.readList(Ug2Protocol.OBJECT);
}
public String[] objectsMatching(String className, String attribute, String value, String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(OP_OBJECTS_MATCHING, requestId);
request.addString(CLASS, className);
request.addString(ATTRIBUTE, attribute);
request.addString(VALUE, value);
Ug2Msg reply = doIt(request);
return reply.readArrayMandatory(OBJECT);
}
public Ug2Schema getSchema(String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(OP_GET_SCHEMA, requestId);
Ug2Msg reply = doIt(request);
return reply.readSchema();
}
/**
* Ping server
* @param requestId Value to tag this request with. May be null.
* @throws Ug2Exception on errors.
*/
public void ping(String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(OP_PING, requestId);
doIt(request).toMap();
}
void sendKeepAlive() {
try {
ping("KeepAlive-" + sessionId);
} catch (Throwable e) {
log.warn("Keep-alive ping failed for " + sessionId + ", will stop pinging", e);
stopKeepAlivePinging();
}
}
public void prePing(String system, String password, String requestId) throws Ug2Exception {
Ug2Msg request = Ug2Msg.makeEmptyMsg();
request.addStringNoCheck(OPERATION, OP_PRE_PING);
request.addStringNoCheck(REQUESTID, requestId + "_prePing");
if (system != null)
request.addStringNoCheck(SYSTEM, system);
try {
doIt(request);
return;
} catch (Ug2Exception e) {
String code = e.statusCode();
if (code == null || !code.equals(STATUS_NOTFOUND))
throw e;
String msg = e.getMessage();
if (msg == null || !msg.endsWith("No system key, provide system password!"))
throw e;
if (system == null || password == null)
throw e;
}
request.addStringNoCheck(PASSWORD, password);
doIt(request);
}
public long currentVersion(String requestId)
throws Ug2Exception {
Ug2Msg request = new Ug2Msg(OP_CURRENT_VERSION, requestId);
Ug2Msg reply = doIt(request);
return reply.readLongMandatory(VERSION);
}
/**
* Terminates the session with the Ug2Server in a controlled way by killing
* off local maintenance threads and by removing the session and ALL its
* token serie associations from the server.
*
* @param requestId Id string provided by the calling client. Provides tracability
* of requests from the client all the way through the server. If
* this parameter is non-null in the client side of the API, the
* value is included in the requestId, visible in logs for
* tracing purposes. Truncated to Ug2Msg.MAX_CLIENT_REQ_ID_LEN.
* Ignored if null.
*/
public void terminateSession(String requestId) {
if (state.get() == NORMAL) {
log.info("Terminating session " + sessionId);
Ug2Msg request = new Ug2Msg(OP_TERMINATE_SESSION, requestId);
try {
doIt(request);
} catch (Throwable e) {
log.warn("Ug2Client: terminateSession (): ReqId: " + request.getId() + ". SessId: " + sessionId()
+ ". Caught " + e.getClass().getName() + " during termination.", e);
}
stopKeepAlivePinging();
state.set(TERMINATED);
}
}
public String[] allObjectsHaving(String className, String attribute, String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(OP_ALL_OBJECTS_HAVING, requestId);
request.addString(CLASS, className);
request.addString(ATTRIBUTE, attribute);
Ug2Msg reply = doIt(request);
return reply.readArrayMandatory(OBJECT);
}
public List allUsers(String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(Ug2Protocol.OP_ALL_OBJECTS_HAVING, requestId);
request.addString(Ug2Protocol.CLASS, Ug2Protocol.USER);
request.addString(Ug2Protocol.ATTRIBUTE, Ug2Protocol.KTHID);
Ug2Msg reply = doIt(request);
return reply.readList(Ug2Protocol.OBJECT);
}
public String[][] accumulateGroupData(String groupKthid, String[] attributes, boolean excludeDirect,
String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(OP_ACCUMULATE_GROUP_DATA, requestId);
request.addString(KTHID, groupKthid);
request.addArray(attributes, ATTRIBUTE);
request.addBool(EXCLUDE_DIRECT, excludeDirect);
Ug2Msg reply = doIt(request);
String[][] result = new String[attributes.length][];
for (int i = 0; i < attributes.length; i++)
result[i] = reply.readArrayMandatory(VALUE + "-" + i);
return result;
}
public String[] accumulateGroupData(String groupKthid, String attribute, String requestId) throws Ug2Exception {
return accumulateGroupData(groupKthid, new String[]{attribute}, false, requestId)[0];
}
public String[] membership(String ug2class, String kthid, boolean excludeDirect, boolean excludeIndirect,
String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(OP_MEMBERSHIP, requestId);
request.addString(CLASS, ug2class);
request.addString(KTHID, kthid);
request.addBool(EXCLUDE_DIRECT, excludeDirect);
request.addBool(EXCLUDE_INDIRECT, excludeIndirect);
Ug2Msg reply = doIt(request);
return reply.readArrayMandatory(GROUP);
}
public Ug2ChangeLogEntry[] getChangeLogEntries(long maxEntries, long minVersion, long maxVersion, long minTime,
long maxTime, String ug2class, String kthid, String attribute,
String value, String operator, String facility, String sessionId,
String logRequestId, String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(OP_GET_CHANGE_LOG_ENTRIES, requestId);
request.addLong(LENGTH, maxEntries);
request.addLong(MIN_VERSION, minVersion);
request.addLong(MAX_VERSION, maxVersion);
request.addLong(MIN_TIME, minTime);
request.addLong(MAX_TIME, maxTime);
request.addString(CHANGE_LOG_CLASS, ug2class);
request.addString(CHANGE_LOG_KTHID, kthid);
request.addString(CHANGE_LOG_ATTRIBUTE, attribute);
request.addString(CHANGE_LOG_VALUE, value);
request.addString(CHANGE_LOG_OPERATOR, operator);
request.addString(CHANGE_LOG_FACILITY, facility);
request.addString(CHANGE_LOG_SESSION, sessionId);
request.addString(CHANGE_LOG_REQUEST, logRequestId);
Ug2Msg reply = doIt(request);
return reply.readChangeLogEntryArray(VALUE);
}
public void setState(byte state) {
this.state.set(state);
}
/**
* ************************************************************************
* Private utility method section. *
* ************************************************************************
*/
private void createSession(String system, String password, String requestId, Transport conn) throws Ug2Exception {
String logPrefix = "Ug2Client: createSession (): ";
if (system == null) {
throw new Ug2Exception(logPrefix + "No system principal name supplied.");
}
if (password == null) {
throw new Ug2Exception(logPrefix + "No system principal credentials supplied.");
}
this.channel = conn;
this._sessionKey = null;
this.systemKey = stringToKey(password);
this.system = system;
prePing(system, password, requestId);
Ug2Msg request = new Ug2Msg(OP_CREATE_SESSION, requestId);
request.addString(SYSTEM, system);
Ug2Msg reply = doIt(request);
sessionId = reply.readLongMandatory(SESSIONID);
state.set(NORMAL);
}
public static byte[] stringToKey(String password) throws Ug2Exception {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(password.getBytes(StandardCharsets.UTF_8));
return md.digest();
} catch (NoSuchAlgorithmException e) {
throw new Ug2Exception("Failed to generate message digest (MD5)", e);
}
}
/**
* Returns the session id at the server side that this client is associated
* to.
* @return sessionId or "unknown".
*/
public String sessionId() {
return sessionId == null ? "unknown" : sessionId.toString();
}
private synchronized void revertToSystemKey() {
_sessionKey = null;
_sessionKeyVersion = -1;
_sessionKeyCreationTime = -1;
}
private static final int taMax = 16;
private int taN = 0;
private final long taData[] = new long[taMax];
private int taNext = 0;
private synchronized void updateServerTimeAdjustment(long timeOffset) {
taData[taNext] = timeOffset;
taNext = (taNext + 1) % taMax;
if (taN < taMax)
taN++;
long sum = 0;
for (int i = 0; i < taN; i++)
sum += taData[i];
serverTimeAdjust = sum / taN;
log.info("Offset " + timeOffset + " received, changing global adjustment to " + serverTimeAdjust);
}
public String getRequestIdBase() {
return requestIdBase;
}
public void setRequestIdBase(String requestId) {
this.requestIdBase = requestId;
}
public List getUsers(List kthids, List attributeNames, String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(Ug2Protocol.OP_GET_DATA, requestId);
request.addString(Ug2Protocol.CLASS, USER);
request.addString(Ug2Protocol.KEY, KTHID);
request.addArray(kthids.toArray(new String[kthids.size()]), Ug2Protocol.OBJECT);
request.addArray(attributeNames.toArray(new String[attributeNames.size()]), Ug2Protocol.ATTRIBUTE);
Ug2Msg reply = doIt(request);
String[] objectStatus = reply.readArrayMandatory(OBJECTSTATUS);
List users = new ArrayList<>(kthids.size());
users.addAll(kthids.stream().map(Ug2User::new).collect(Collectors.toList()));
int i = 0;
for (String attributeName : attributeNames) {
ByteArrayInputStream bais = new ByteArrayInputStream(reply.readDataResult(i));
DataInputStream dis = new DataInputStream(bais);
for (int j = 0; j < kthids.size(); j++) {
if (objectStatus[j].equals(STATUS_OK)) {
try {
int nValues = dis.readInt();
List values = new ArrayList<>(nValues);
for (int k = 0; k < nValues; k++) {
values.add(dis.readUTF());
}
users.get(j).addAttribute(attributeName, values);
} catch (IOException e) {
throw new Ug2Exception("Failed to read compressed data result", e);
}
} else {
users.get(j).addEmptyAttribute(attributeName);
}
}
i++;
}
return users;
}
public List getGroups(List kthids, List attributeNames, String requestId) throws Ug2Exception {
Ug2Msg request = new Ug2Msg(Ug2Protocol.OP_GET_DATA, requestId);
request.addString(Ug2Protocol.CLASS, GROUP);
request.addString(Ug2Protocol.KEY, KTHID);
request.addArray(kthids.toArray(new String[kthids.size()]), Ug2Protocol.OBJECT);
request.addArray(attributeNames.toArray(new String[attributeNames.size()]), Ug2Protocol.ATTRIBUTE);
Ug2Msg reply = doIt(request);
String[] objectStatus = reply.readArrayMandatory(OBJECTSTATUS);
List groups = new ArrayList<>(kthids.size());
groups.addAll(kthids.stream().map(Ug2Group::new).collect(Collectors.toList()));
int i = 0;
for (String attributeName : attributeNames) {
ByteArrayInputStream bais = new ByteArrayInputStream(reply.readDataResult(i));
DataInputStream dis = new DataInputStream(bais);
for (int j = 0; j < kthids.size(); j++) {
if (objectStatus[j].equals(STATUS_OK)) {
try {
int nValues = dis.readInt();
List values = new ArrayList<>(nValues);
for (int k = 0; k < nValues; k++) {
values.add(dis.readUTF());
}
groups.get(j).addAttribute(attributeName, values);
} catch (IOException e) {
throw new Ug2Exception("Failed to read compressed data result", e);
}
} else {
groups.get(j).addEmptyAttribute(attributeName);
}
}
i++;
}
return groups;
}
public Instant getLastAccessed() {
return lastAccessed;
}
public void closeSession(String requestId) {
if (state.get() == NORMAL) {
Ug2Msg request = new Ug2Msg(OP_TERMINATE_SESSION, requestId);
try {
doIt(request);
state.set(TERMINATED);
} catch (Throwable e) {
log.warn("Failed to terminate session " + sessionId() + ", request " + request.getId() + " failed", e);
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy