
nl.rrd.wool.model.WoolNodeBody Maven / Gradle / Ivy
Show all versions of wool-core Show documentation
/*
* Copyright 2019 Roessingh Research and Development.
*
* 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 nl.rrd.wool.model;
import nl.rrd.wool.expressions.EvaluationException;
import nl.rrd.wool.model.command.*;
import nl.rrd.wool.model.nodepointer.WoolNodePointer;
import nl.rrd.wool.model.nodepointer.WoolNodePointerExternal;
import nl.rrd.wool.model.nodepointer.WoolNodePointerInternal;
import java.util.*;
/**
* A node body can occur in three different contexts inside a {@link WoolNode
* WoolNode}.
*
*
* - Directly in the node. In this case it specifies the agent statement with
* possible commands and user replies.
* - As part of a clause in a {@link WoolIfCommand WoolIfCommand}. The content
* is the same as directly in the node. The only difference is that it is
* performed conditionally.
* - As part of a {@link WoolReply WoolReply}. In this case it specifies the
* user statement with possible commands, but no replies. Note that the UI shows
* these statements as options immediately along with the agent statement. This
* {@link WoolNodeBody WoolNodeBody} does not contain commands that are to be
* performed when the reply is chosen. Such commands are specified separately in
* a {@link WoolReply WoolReply}.
*
*
* The body contains a statement as a list of segments where each segment is
* one of:
*
*
* - {@link WoolNodeBody.TextSegment TextSegment}: a {@link WoolVariableString
* WoolVariableString} with text and variables
* - {@link WoolNodeBody.CommandSegment CommandSegment}: a command (see
* below)
*
*
* The segments are always normalized so that subsequent text segments are
* automatically merged into one.
*
* The type of commands depend on the context. Directly in the node or in a
* {@link WoolIfCommand WoolIfCommand}, it can be:
*
*
* - {@link WoolActionCommand WoolActionCommand}: Actions to perform along
* with the agent's text statement.
* - {@link WoolIfCommand WoolIfCommand}: Contains clauses, each with a {@link
* WoolNodeBody WoolNodeBody} specifying conditional statements, replies and
* commands.
* - {@link WoolSetCommand WoolSetCommand}: Sets a variable value.
*
*
* As part of a reply (remember the earlier remarks about commands in a
* reply), it can be:
*
*
* - {@link WoolInputCommand WoolInputCommand}: Allow user to provide input
* other than just clicking the reply option.
*
*
* @author Dennis Hofs (RRD)
*/
public class WoolNodeBody {
private List segments = new ArrayList<>();
private List replies = new ArrayList<>();
public WoolNodeBody() {
}
public WoolNodeBody(WoolNodeBody other) {
for (Segment segment : other.segments) {
this.segments.add(segment.clone());
}
for (WoolReply reply : other.replies) {
this.replies.add(new WoolReply(reply));
}
}
/**
* Returns the segments as an unmodifiable list.
*
* @return the segments as an unmodifiable list
*/
public List getSegments() {
return Collections.unmodifiableList(segments);
}
public void addSegment(Segment segment) {
Segment lastSegment = null;
if (!segments.isEmpty())
lastSegment = segments.get(segments.size() - 1);
if (lastSegment instanceof TextSegment &&
segment instanceof TextSegment) {
TextSegment lastTextSegment = (TextSegment)lastSegment;
TextSegment textSegment = (TextSegment)segment;
WoolVariableString text = new WoolVariableString();
text.addSegments(lastTextSegment.text.getSegments());
text.addSegments(textSegment.text.getSegments());
TextSegment mergedSegment = new TextSegment(text);
segments.remove(segments.size() - 1);
segments.add(mergedSegment);
} else {
segments.add(segment);
}
}
public void clearSegments() {
segments.clear();
}
/**
* Normalizes whitespace in the text segments. It removes empty lines and
* makes sure that lines end with "\n". Within each line, it trims
* whitespace from the start and end, and it replaces any sequence of
* spaces and tabs with one space.
*
* This method should only be called if all variables in the text
* segments have been resolved.
*
* @param trimText true if trailing new lines should be trimmed, false if
* they should be preserved
*/
private void normalizeWhitespace(boolean trimText) {
TextSegment lastText = null;
String currLine = null;
Iterator it = segments.iterator();
while (it.hasNext()) {
Segment segment = it.next();
if (!(segment instanceof TextSegment))
continue;
TextSegment textSegment = (TextSegment)segment;
lastText = textSegment;
String text = textSegment.text.evaluate(null);
text = text.replaceAll("[\r\n]+", "\n")
.replaceAll("[\t ]+", " ");
StringBuilder normText = new StringBuilder();
int start = 0;
int index;
while ((index = text.indexOf('\n', start)) != -1) {
String line = text.substring(start, index).trim();
if (currLine != null && line.isEmpty()) {
normText.append("\n");
} else if (currLine != null) {
normText.append(" " + line + "\n");
} else if (!line.isEmpty()) {
normText.append(line + "\n");
}
currLine = null;
start = index + 1;
}
String line = text.substring(start).trim();
if (!line.isEmpty()) {
if (currLine != null) {
currLine += " " + line;
normText.append(" " + line);
} else {
currLine = line;
normText.append(line);
}
}
if (normText.length() == 0) {
it.remove();
} else {
textSegment.text = new WoolVariableString(normText.toString());
}
}
if (trimText && lastText != null) {
String text = lastText.text.evaluate(null).replaceAll("\\s+$", "");
lastText.text = new WoolVariableString(text);
}
}
public List getReplies() {
return replies;
}
public WoolReply findReplyById(int replyId) {
for (WoolReply reply : replies) {
if (reply.getReplyId() == replyId)
return reply;
}
for (Segment segment : segments) {
WoolReply reply = segment.findReplyById(replyId);
if (reply != null)
return reply;
}
return null;
}
public void addReply(WoolReply reply) {
replies.add(reply);
}
/**
* Retrieves all variable names that are read in this body.
*
* @return the variable names that are read in this body
*/
public List getReadVariableNames() {
Set set = new HashSet<>();
getReadVariableNames(set);
List result = new ArrayList<>(set);
Collections.sort(result);
return result;
}
/**
* Retrieves all variable names that are read in this body and adds them to
* the specified set.
*
* @param varNames the set to which the variable names are added
*/
public void getReadVariableNames(Set varNames) {
for (Segment segment : segments) {
segment.getReadVariableNames(varNames);
}
for (WoolReply reply : replies) {
reply.getReadVariableNames(varNames);
}
}
/**
* Retrieves all variable names that are written in this body.
*
* @return the variable names that are written in this body
*/
public List getWriteVariableNames() {
Set set = new HashSet<>();
getWriteVariableNames(set);
List result = new ArrayList<>(set);
Collections.sort(result);
return result;
}
/**
* Retrieves all variable names that are written in this body and adds them
* to the specified set.
*
* @param varNames the set to which the variable names are added
*/
public void getWriteVariableNames(Set varNames) {
for (Segment segment : segments) {
segment.getWriteVariableNames(varNames);
}
for (WoolReply reply : replies) {
reply.getWriteVariableNames(varNames);
}
}
public List getNodePointers() {
Set set = new HashSet<>();
getNodePointers(set);
List result = new ArrayList<>(set);
Collections.sort(result, new Comparator() {
@Override
public int compare(WoolNodePointer o1, WoolNodePointer o2) {
return compareNodePointers(o1, o2);
}
});
return result;
}
private int compareNodePointers(WoolNodePointer o1, WoolNodePointer o2) {
if (o1 instanceof WoolNodePointerInternal) {
if (o2 instanceof WoolNodePointerExternal)
return -1;
WoolNodePointerInternal p1 = (WoolNodePointerInternal)o1;
WoolNodePointerInternal p2 = (WoolNodePointerInternal)o2;
return p1.getNodeId().compareTo(p2.getNodeId());
} else {
if (o2 instanceof WoolNodePointerInternal)
return -1;
WoolNodePointerExternal p1 = (WoolNodePointerExternal)o1;
WoolNodePointerExternal p2 = (WoolNodePointerExternal)o2;
int result = p1.getDialogueId().compareTo(p2.getDialogueId());
if (result != 0)
return result;
return p1.getNodeId().compareTo(p2.getNodeId());
}
}
public void getNodePointers(Set pointers) {
for (Segment segment : segments) {
if (!(segment instanceof CommandSegment))
continue;
WoolCommand command = ((CommandSegment)segment).command;
command.getNodePointers(pointers);
}
for (WoolReply reply : replies) {
pointers.add(reply.getNodePointer());
}
}
/**
* Executes the agent statement and reply statements in this body with
* respect to the specified variable map. It executes ("if" and "set")
* commands and resolves variables. Any resulting body content that should
* be sent to the client, is added to agent and reply statements in
* "processedBody". This content can be text or client commands, with all
* variables resolved.
*
* This method also normalizes whitespace in the text segments. It
* removes empty lines and makes sure that lines end with "\n". Within each
* line, it trims whitespace from the start and end, and it replaces any
* sequence of spaces and tabs with one space.
*
* This method should only be called if all variables in the text
* segments have been resolved.
*
* @param variables the variable map
* @param trimText true if trailing new lines should be trimmed, false if
* they should be preserved. This should be set to true for the body that is
* directly in the node. If the body is in an "if" clause or in a reply, it
* should be set to false.
* @param processedBody the processed body
* @throws EvaluationException if an expression cannot be evaluated
*/
public void execute(Map variables, boolean trimText,
WoolNodeBody processedBody) throws EvaluationException {
for (Segment segment : segments) {
if (segment instanceof TextSegment) {
executeTextSegment((TextSegment)segment, variables,
processedBody);
} else {
executeCommandSegment((CommandSegment)segment, variables,
processedBody);
}
}
for (WoolReply reply : replies) {
processedBody.addReply(reply.execute(variables));
}
processedBody.normalizeWhitespace(trimText);
}
private void executeTextSegment(TextSegment segment,
Map variables, WoolNodeBody processedBody) {
TextSegment processedText = new TextSegment(
segment.text.execute(variables));
processedBody.addSegment(processedText);
}
private void executeCommandSegment(CommandSegment segment,
Map variables, WoolNodeBody processedBody)
throws EvaluationException {
segment.command.executeBodyCommand(variables, processedBody);
}
public void trimWhitespace() {
trimWhitespace(segments);
}
public static void trimWhitespace(List segments) {
removeLeadingWhitespace(segments);
removeTrailingWhitespace(segments);
}
public void removeLeadingWhitespace() {
removeLeadingWhitespace(segments);
}
public static void removeLeadingWhitespace(List segments) {
while (!segments.isEmpty()) {
Segment segment = segments.get(0);
if (!(segment instanceof TextSegment))
return;
TextSegment textSegment = (TextSegment)segment;
WoolVariableString text = textSegment.getText();
text.removeLeadingWhitespace();
if (!text.getSegments().isEmpty())
return;
segments.remove(0);
}
}
public void removeTrailingWhitespace() {
removeTrailingWhitespace(segments);
}
public static void removeTrailingWhitespace(List segments) {
while (!segments.isEmpty()) {
Segment segment = segments.get(segments.size() - 1);
if (!(segment instanceof TextSegment))
return;
TextSegment textSegment = (TextSegment)segment;
WoolVariableString text = textSegment.getText();
text.removeTrailingWhitespace();
if (!text.getSegments().isEmpty())
return;
segments.remove(segments.size() - 1);
}
}
@Override
public String toString() {
String newline = System.getProperty("line.separator");
StringBuilder builder = new StringBuilder();
for (Segment segment : segments) {
builder.append(segment.toString());
}
for (WoolReply reply : replies) {
builder.append(newline);
builder.append(reply);
}
return builder.toString();
}
public static abstract class Segment implements Cloneable {
/**
* Tries to find a reply with the specified ID within this segment. If
* no such reply is found, this method returns null.
*
* @param replyId the reply ID
* @return the reply or null
*/
public abstract WoolReply findReplyById(int replyId);
/**
* Retrieves all variable names that are read in this segment and adds
* them to the specified set.
*
* @param varNames the set to which the variable names are added
*/
public abstract void getReadVariableNames(Set varNames);
/**
* Retrieves all variable names that are written in this segment and
* adds them to the specified set.
*
* @param varNames the set to which the variable names are added
*/
public abstract void getWriteVariableNames(Set varNames);
/**
* Returns a deep copy of this segment.
*
* @return a deep copy of this segment
*/
@Override
public abstract Segment clone();
}
public static class TextSegment extends Segment {
private WoolVariableString text;
public TextSegment(WoolVariableString text) {
this.text = text;
}
public TextSegment(TextSegment other) {
this.text = new WoolVariableString(other.text);
}
public WoolVariableString getText() {
return text;
}
public void setText(WoolVariableString text) {
this.text = text;
}
@Override
public WoolReply findReplyById(int replyId) {
return null;
}
@Override
public void getReadVariableNames(Set varNames) {
text.getReadVariableNames(varNames);
}
@Override
public void getWriteVariableNames(Set varNames) {
}
@Override
public String toString() {
return text.toString();
}
@Override
public TextSegment clone() {
return new TextSegment(this);
}
}
public static class CommandSegment extends Segment {
private WoolCommand command;
public CommandSegment(WoolCommand command) {
this.command = command;
}
public CommandSegment(CommandSegment other) {
this.command = other.command.clone();
}
public WoolCommand getCommand() {
return command;
}
@Override
public WoolReply findReplyById(int replyId) {
return command.findReplyById(replyId);
}
@Override
public void getReadVariableNames(Set varNames) {
command.getReadVariableNames(varNames);
}
@Override
public void getWriteVariableNames(Set varNames) {
command.getWriteVariableNames(varNames);
}
@Override
public String toString() {
return command.toString();
}
@Override
public CommandSegment clone() {
return new CommandSegment(this);
}
}
}