TransformHistory.java
package emissary.core;
import emissary.directory.KeyManipulator;
import emissary.place.IServiceProviderPlace;
import jakarta.annotation.Nullable;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.stream.Collectors;
import static emissary.place.IServiceProviderPlace.SPROUT_KEY;
public class TransformHistory implements Serializable {
private static final long serialVersionUID = -7252497842562281631L;
private final List<History> history;
public TransformHistory() {
history = new ArrayList<>();
}
public TransformHistory(TransformHistory history) {
this.history = new ArrayList<>(history.history);
}
/**
* Replace history with the new history
*
* @param keys of new history strings to use
*/
public void set(List<String> keys) {
clear();
addAll(keys.stream().map(History::new).collect(Collectors.toList()));
}
/**
* Replace history with the new history
*
* @param history of new history strings to use
*/
public void set(TransformHistory history) {
clear();
addAll(history.history);
}
private void addAll(List<History> history) {
this.history.addAll(history);
}
/**
* Appends the new key to the transform history. This is called by MobileAgent before moving to the new place. It
* usually adds the four-tuple of a place's key
*
* @see emissary.core.MobileAgent#agentControl
* @param key the new value to append
*/
public void append(final String key) {
append(key, false);
}
/**
* Appends the new key to the transform history. This is called by MobileAgent before moving to the new place. It
* usually adds the four-tuple of a place's key. Coordinated history keys are meant for informational purposes and have
* no bearing on the routing algorithm. It is important to list the places visited in coordination, but should not
* report as the last place visited.
*
* @see emissary.core.MobileAgent#agentControl
* @param key the new value to append
* @param coordinated true if history entry is for informational purposes only
*/
public void append(String key, boolean coordinated) {
if (coordinated) {
History last = lastVisit();
if (last != null) {
last.addCoordinated(key);
}
} else {
history.add(new History(key));
}
}
/**
* Clear the transformation history
*/
public void clear() {
history.clear();
}
/**
* For backwards compatibility, only return the history that does not contain the coordinated places
*
* @return List of places visited sans coordination places
*/
public List<String> get() {
return get(false);
}
/**
* Return the transform history, optionally including coordinated places
*
* @return List of places visited
*/
public List<String> get(boolean includeCoordinated) {
if (includeCoordinated) {
List<String> keys = new ArrayList<>();
history.forEach(k -> {
keys.add(k.getKey());
keys.addAll(k.getCoordinated());
});
return keys;
} else {
return history.stream()
.map(History::getKey)
.collect(Collectors.toList());
}
}
/**
* Return the full history object
*
* @return history object
*/
public List<History> getHistory() {
return Collections.unmodifiableList(history);
}
/**
* Get the last place visited (does not include places visited during coordination)
*
* @return last place visited
*/
@Nullable
public History lastVisit() {
if (CollectionUtils.isEmpty(history)) {
return null;
}
return history.get(history.size() - 1);
}
/**
* Get the second-to-last place visited (does not include places visited during coordination)
*
* @return second-to-last place visited
*/
@Nullable
public History penultimateVisit() {
if (CollectionUtils.isEmpty(history) || history.size() < 2) {
return null;
}
return history.get(history.size() - 2);
}
/**
* Test to see if a place has been visited (does not include places visited during coordination)
*
* @return true is place has been visited
*/
public boolean hasVisited(final String pattern) {
for (final History hist : history) {
if (KeyManipulator.gmatch(hist.getKey(), pattern)) {
return true;
}
}
return false;
}
/**
* True if this payload hasn't had any processing yet. Does not count parent processing as being for this payload.
*
* @return true if not yet started
*/
public boolean beforeStart() {
if (history.isEmpty()) {
return true;
}
final String s = history.get(history.size() - 1).getKey();
return s.contains(IServiceProviderPlace.SPROUT_KEY);
}
public int size() {
return size(false);
}
public int size(boolean includeCoordinated) {
int size = history.size();
if (includeCoordinated) {
size += (int) history.stream().mapToLong(h -> h.coordinated.size()).sum();
}
return size;
}
public Deque<String> format() {
Deque<String> formattedHistory = new ArrayDeque<>();
String prevDataAndServiceType = "";
for (final History h : this.history) {
String key = h.getKey();
String currentDataAndServiceType = "";
StringBuilder displayStrings = new StringBuilder();
if (key.contains(SPROUT_KEY)) {
displayStrings.append(StringUtils.substringBefore(formattedHistory.removeLast(), ".")).append(".")
.append(KeyManipulator.getServiceType(key)).append(": ");
while (!formattedHistory.isEmpty()) {
String last = formattedHistory.removeLast();
if (last.contains(SPROUT_KEY)) {
formattedHistory.add(last);
break;
}
}
} else {
currentDataAndServiceType = KeyManipulator.getDataType(key) + "." + KeyManipulator.getServiceType(key);
if (currentDataAndServiceType.equals(prevDataAndServiceType)) {
displayStrings.append(formattedHistory.removeLast()).append(", ");
} else {
displayStrings.append(currentDataAndServiceType).append(": ");
}
}
displayStrings.append(KeyManipulator.getServiceClassname(key));
if (CollectionUtils.isNotEmpty(h.getCoordinated())) {
displayStrings
.append(h.getCoordinated().stream().map(KeyManipulator::getServiceClassname).collect(Collectors.joining(", ", "(", ")")));
}
formattedHistory.add(displayStrings.toString());
prevDataAndServiceType = currentDataAndServiceType;
}
return formattedHistory;
}
@Override
public String toString() {
final StringBuilder myOutput = new StringBuilder();
final String ls = System.lineSeparator();
myOutput.append("transform history (").append(size(true)).append(") :").append(ls);
history.forEach(x -> myOutput.append(x.toString()).append(ls));
return myOutput.toString();
}
public static class History {
String key;
List<String> coordinated = new ArrayList<>();
/**
* Needed to support Kryo deserialization
*/
private History() {}
public History(String key) {
this.key = key;
}
public String getKey() {
return getKey(false);
}
public String getKey(boolean stripUrl) {
return stripUrl ? stripUrl(key) : key;
}
public List<String> getCoordinated() {
return getCoordinated(false);
}
public List<String> getCoordinated(boolean stripUrl) {
return stripUrl ? coordinated.stream().map(History::stripUrl).collect(Collectors.toUnmodifiableList())
: Collections.unmodifiableList(coordinated);
}
public void addCoordinated(String key) {
coordinated.add(key);
}
protected static String stripUrl(String key) {
return StringUtils.substringBefore(key, ".http");
}
@Override
public String toString() {
StringBuilder hist = new StringBuilder(" -> " + getKey());
for (String coord : coordinated) {
hist.append(System.lineSeparator()).append(" +--> ").append(coord);
}
return hist.toString();
}
}
}