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();
        }
    }
}