org.yamcs.client.Command Maven / Gradle / Ivy
package org.yamcs.client;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.yamcs.client.utils.WellKnownTypes;
import org.yamcs.protobuf.Commanding.CommandAssignment;
import org.yamcs.protobuf.Commanding.CommandHistoryAttribute;
import org.yamcs.protobuf.Commanding.CommandHistoryEntry;
import org.yamcs.protobuf.IssueCommandResponse;
public class Command implements Comparable {
private static final String ATTR_BINARY = "binary";
private static final String ATTR_QUEUE = "queue";
private static final String ATTR_UNPROCESSED_BINARY = "unprocessedBinary";
private static final String ATTR_USERNAME = "username";
private static final String ATTR_COMMENT = "comment";
private static final String LEGACY_ATTR_SOURCE = "source";
private static final String[] STANDARD_ATTRIBUTES = new String[] {
ATTR_BINARY,
ATTR_COMMENT,
ATTR_QUEUE,
ATTR_UNPROCESSED_BINARY,
ATTR_USERNAME,
LEGACY_ATTR_SOURCE,
};
private static final String PREFIX_COMMAND_COMPLETE = "CommandComplete";
private static final String PREFIX_TRANSMISSION_CONSTRAINTS = "TransmissionConstraints";
private static final String[] STANDARD_ATTRIBUTE_PREFIXES = new String[] {
PREFIX_COMMAND_COMPLETE,
PREFIX_TRANSMISSION_CONSTRAINTS,
};
private static final String SUFFIX_TIME = "_Time";
private static final String SUFFIX_MESSAGE = "_Message";
private static final String SUFFIX_STATUS = "_Status";
private static final String[] STANDARD_ATTRIBUTE_SUFFIXES = new String[] {
SUFFIX_TIME,
SUFFIX_MESSAGE,
SUFFIX_STATUS,
};
// Entries that come from a cascading server are prefixed with one or more
// patterns of the kind: yamcs_
private static final Pattern CASCADED_PREFIX = Pattern.compile("^(yamcs<[^>]+>_)+(.+)");
private final String id;
private final String name;
private final Map aliases = new HashMap<>();
private final List assignments;
private final String origin;
private final int sequenceNumber;
private final Instant generationTime;
private final String source;
private Map attributes = Collections.synchronizedMap(new LinkedHashMap<>());
// Command info that was relayed from an upstream (cascaded) server.
private Map cascadedRecordsByPrefix = new LinkedHashMap<>();
public Command(String id, String name, Map aliases, List assignments,
String origin, int sequenceNumber,
Instant generationTime) {
this.id = id;
this.name = name;
this.aliases.putAll(aliases);
this.assignments = assignments;
this.origin = origin;
this.sequenceNumber = sequenceNumber;
this.generationTime = generationTime;
this.source = buildSource(name, assignments);
}
public Command(IssueCommandResponse response) {
this.id = response.getId();
this.name = response.getCommandName();
this.aliases.putAll(response.getAliasesMap());
this.assignments = response.getAssignmentsList();
this.origin = response.getOrigin();
this.sequenceNumber = response.getSequenceNumber();
this.generationTime = Helpers.toInstant(response.getGenerationTime());
this.source = buildSource(name, response.getAssignmentsList());
if (response.hasUnprocessedBinary()) {
attributes.put(ATTR_UNPROCESSED_BINARY, response.getUnprocessedBinary().toByteArray());
}
if (response.hasBinary()) {
attributes.put(ATTR_BINARY, response.getBinary().toByteArray());
}
if (response.hasQueue()) {
attributes.put(ATTR_QUEUE, response.getQueue());
}
if (response.hasUsername()) {
attributes.put(ATTR_USERNAME, response.getUsername());
}
}
public Command(CommandHistoryEntry entry) {
this(entry.getId(), entry.getCommandName(), entry.getAliasesMap(), entry.getAssignmentsList(),
entry.getOrigin(), entry.getSequenceNumber(), Helpers.toInstant(entry.getGenerationTime()));
merge(entry);
}
private static String buildSource(String name, List assignments) {
StringBuilder buf = new StringBuilder(name).append("(");
buf.append(assignments.stream()
.filter(CommandAssignment::getUserInput)
.map(assignment -> {
Object value = Helpers.parseValue(assignment.getValue());
if (value instanceof String) {
return assignment.getName() + ": \"" + value + "\"";
} else if (value instanceof byte[]) {
return assignment.getName() + ": 0x" + WellKnownTypes.toHex((byte[]) value);
} else {
return assignment.getName() + ": " + value;
}
}).collect(Collectors.joining(", ")));
return buf.append(")").toString();
}
public String getId() {
return id;
}
public Instant getGenerationTime() {
return generationTime;
}
/**
* Fully qualified command name
*/
public String getName() {
return name;
}
/**
* Alias under the specified namespace. Returns {@code null} if this command has no such alias.
*/
public String getName(String namespace) {
return aliases.get(namespace);
}
public List getAssignments() {
return Collections.unmodifiableList(assignments);
}
public String getOrigin() {
return origin;
}
public int getSequenceNumber() {
return sequenceNumber;
}
/**
* Username of the issuer
*/
public String getUsername() {
return (String) attributes.get(ATTR_USERNAME);
}
/**
* The assigned command queue
*/
public String getQueue() {
return (String) attributes.get(ATTR_QUEUE);
}
/**
* String representation of the command
*/
public String getSource() {
return source;
}
/**
* Unprocessed binary representation of the command (prior to postprocessing).
*/
public byte[] getUnprocessedBinary() {
return (byte[]) attributes.get(ATTR_UNPROCESSED_BINARY);
}
/**
* Binary representation of the command
*/
public byte[] getBinary() {
return (byte[]) attributes.get(ATTR_BINARY);
}
@SuppressWarnings("unchecked")
public T getAttribute(String key) {
return (T) attributes.get(key);
}
/**
* Returns whether this command is complete. A command can be complete, yet still failed.
*/
public boolean isComplete() {
Acknowledgment ack = getAcknowledgment(PREFIX_COMMAND_COMPLETE);
return ack != null && (ack.getStatus().equals("OK") || ack.getStatus().equals("NOK"));
}
/**
* Returns true if this command has completed successfully
*/
public boolean isSuccess() {
Acknowledgment ack = getAcknowledgment(PREFIX_COMMAND_COMPLETE);
return ack != null && ack.getStatus().equals("OK");
}
/**
* Returns true if this command failed
*/
public boolean isFailure() {
Acknowledgment ack = getAcknowledgment(PREFIX_COMMAND_COMPLETE);
return ack != null && ack.getStatus().equals("NOK");
}
/**
* Error message in case this command failed
*/
public String getError() {
Acknowledgment ack = getAcknowledgment(PREFIX_COMMAND_COMPLETE);
if (ack != null && ack.getStatus().equals("NOK")) {
return ack.getMessage();
}
return null;
}
public String getComment() {
return (String) attributes.get(ATTR_COMMENT);
}
/**
* Returns all attributes of this commands.
*/
public Map getAttributes() {
synchronized (attributes) {
return new LinkedHashMap<>(attributes);
}
}
public void merge(CommandHistoryEntry entry) {
for (CommandHistoryAttribute attr : entry.getAttrList()) {
var matcher = CASCADED_PREFIX.matcher(attr.getName());
if (matcher.matches()) {
var prefix = matcher.group(1);
var cascadedCommand = cascadedRecordsByPrefix.get(prefix);
if (cascadedCommand == null) {
cascadedCommand = new Command(id, name, entry.getAliasesMap(), assignments, origin, sequenceNumber,
generationTime);
cascadedRecordsByPrefix.put(prefix, cascadedCommand);
}
var truncatedName = matcher.group(2);
cascadedCommand.attributes.put(truncatedName, Helpers.parseValue(attr.getValue()));
} else {
attributes.put(attr.getName(), Helpers.parseValue(attr.getValue()));
}
}
}
public void merge(Command other) {
synchronized (other.attributes) {
attributes.putAll(other.attributes);
}
for (var entry : other.cascadedRecordsByPrefix.entrySet()) {
var prefix = entry.getKey();
var cascadedRecord = entry.getValue();
var existing = cascadedRecordsByPrefix.get(prefix);
if (existing == null) {
cascadedRecordsByPrefix.put(prefix, cascadedRecord);
} else {
synchronized (cascadedRecord.attributes) {
existing.attributes.putAll(cascadedRecord.attributes);
}
}
}
}
/**
* Returns non-standard attributes
*/
public LinkedHashMap getExtraAttributes() {
var extra = new LinkedHashMap();
synchronized (attributes) {
for (var attr : attributes.entrySet()) {
String name = attr.getKey();
if (isExtraAttribute(name)) {
extra.put(name, attr.getValue());
}
}
}
return extra;
}
/**
* All acknowledgments by name
*/
public LinkedHashMap getAcknowledgments() {
var acknowledgments = new LinkedHashMap();
synchronized (attributes) {
for (var attr : attributes.entrySet()) {
String name = attr.getKey();
if (isAcknowledgmentStatusAttribute(name)) {
var ack = getAcknowledgment(name.substring(0, name.length() - 7));
if (ack != null) {
acknowledgments.put(ack.getName(), ack);
}
}
}
}
return acknowledgments;
}
private boolean isExtraAttribute(String attributeName) {
for (String suffix : STANDARD_ATTRIBUTE_SUFFIXES) {
if (attributeName.endsWith(suffix)) {
return false;
}
}
for (String prefix : STANDARD_ATTRIBUTE_PREFIXES) {
if (attributeName.startsWith(prefix)) {
return false;
}
}
return Arrays.binarySearch(STANDARD_ATTRIBUTES, attributeName) >= 0;
}
private boolean isAcknowledgmentStatusAttribute(String attributeName) {
if (!attributeName.endsWith(SUFFIX_STATUS)) {
return false;
}
for (String prefix : STANDARD_ATTRIBUTE_PREFIXES) {
if (attributeName.startsWith(prefix)) {
return false;
}
}
return true;
}
public Acknowledgment getQueuedAcknowledgment() {
return getAcknowledgment(Acknowledgment.QUEUED);
}
public Acknowledgment getReleasedAcknowledgment() {
return getAcknowledgment(Acknowledgment.RELEASED);
}
public Acknowledgment getSentAcknowledgment() {
return getAcknowledgment(Acknowledgment.SENT);
}
public Acknowledgment getAcknowledgment(String name) {
Instant time = (Instant) attributes.get(name + SUFFIX_TIME);
String status = (String) attributes.get(name + SUFFIX_STATUS);
String message = (String) attributes.get(name + SUFFIX_MESSAGE);
if (time != null && status != null) {
return new Acknowledgment(name, time, status, message);
}
return null;
}
/**
* Returns command records that capture state of upstream (cascaded) servers.
*
* The returned map is keyed by the cascading prefix, for example: {@code yamcs_} when the information is
* cascaded from SERVER1, or {@code yamcs_yamcs_} when the information is cascaded from SERVER 1
* over SERVER2.
*/
public Map getCascadedRecords() {
return Collections.unmodifiableMap(cascadedRecordsByPrefix);
}
@Override
public int compareTo(Command other) {
return id.compareTo(other.id);
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Command)) {
return false;
}
Command other = (Command) obj;
return id.equals(other.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return getId();
}
}