package de.uni_passau.fim.infosun.prophet.experimentViewer;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.WindowConstants;
import javax.swing.filechooser.FileNameExtensionFilter;
import de.uni_passau.fim.infosun.prophet.Constants;
import de.uni_passau.fim.infosun.prophet.plugin.PluginList;
import de.uni_passau.fim.infosun.prophet.util.QuestionViewPane;
import de.uni_passau.fim.infosun.prophet.util.language.UIElementNames;
import de.uni_passau.fim.infosun.prophet.util.qTree.Attribute;
import de.uni_passau.fim.infosun.prophet.util.qTree.QTreeNode;
import de.uni_passau.fim.infosun.prophet.util.qTree.handlers.QTreeXMLHandler;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import static de.uni_passau.fim.infosun.prophet.Constants.DEFAULT_FILE;
import static de.uni_passau.fim.infosun.prophet.Constants.FILE_ANSWERS;
import static de.uni_passau.fim.infosun.prophet.Constants.KEY_BACKWARD;
import static de.uni_passau.fim.infosun.prophet.Constants.KEY_DONOTSHOWCONTENT;
import static de.uni_passau.fim.infosun.prophet.Constants.KEY_EXPERIMENT_CODE;
import static de.uni_passau.fim.infosun.prophet.Constants.KEY_FORWARD;
import static de.uni_passau.fim.infosun.prophet.Constants.KEY_ONLY_SHOW_X_CHILDREN;
import static de.uni_passau.fim.infosun.prophet.Constants.KEY_RANDOMIZE_CHILDREN;
import static de.uni_passau.fim.infosun.prophet.Constants.KEY_SHOW_NUMBER_OF_CHILDREN;
import static de.uni_passau.fim.infosun.prophet.Constants.KEY_SUBJECT_CODE;
import static de.uni_passau.fim.infosun.prophet.Constants.KEY_TIMING;
import static de.uni_passau.fim.infosun.prophet.Constants.KEY_VIEWER_LANGUAGE;
import static de.uni_passau.fim.infosun.prophet.util.language.UIElementNames.getLocalized;
import static javax.swing.JOptionPane.ERROR_MESSAGE;
import static javax.swing.JOptionPane.INFORMATION_MESSAGE;
import static javax.swing.JOptionPane.YES_NO_OPTION;
import static javax.swing.JOptionPane.YES_OPTION;
import static javax.swing.JOptionPane.showConfirmDialog;
import static javax.swing.JOptionPane.showMessageDialog;
/**
* A viewer for the experiments created with the <code>ExperimentEditor</code>.
*
* @author Georg Seibt
*/
public class EViewer extends JFrame {
private static final int WIDTH = 800;
private static final int HEIGHT = 600;
private static final int LOAD_FAIL_EXIT_STATUS = 1;
private static final String NO_EXP_CODE = "NoExperimentCode";
private static final String NO_SUBJ_CODE = "NoSubjectCode";
private QTreeNode expTreeRoot;
private List<ViewNode> experiment; // the experiment tree in pre-order
private int currentIndex; // index into the 'experiment' List
private boolean visibleStopwatches;
private JPanel timePanel;
private File saveDir;
/**
* An <code>ActionListener</code> that is added to all <code>QuestionViewPane</code> instances created by the
* <code>EViewer</code>. Calls the {@link #nextNode(boolean, boolean)} or {@link #previousNode(boolean, boolean)}
* methods after there was an appropriate event.
*/
private ActionListener listener = event -> {
switch (event.getActionCommand()) {
case KEY_FORWARD:
nextNode(false, false);
break;
case KEY_BACKWARD:
previousNode(false, false);
break;
default:
System.err.println("Unrecognized action command from " + QuestionViewPane.class.getSimpleName() +
". Action command: " + event.getActionCommand());
}
};
/**
* Constructs a new <code>EViewer</code> and starts the experiment.
*/
public EViewer() {
setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
setPreferredSize(new Dimension(WIDTH, HEIGHT));
this.expTreeRoot = loadExperiment();
this.visibleStopwatches = Boolean.parseBoolean(expTreeRoot.getAttribute(KEY_TIMING).getValue());
initLanguage();
// randomize the children if this is enabled
applyToTree(expTreeRoot, node -> {
boolean enabled = Boolean.parseBoolean(node.getAttribute(KEY_RANDOMIZE_CHILDREN).getValue());
if (enabled) {
Collections.shuffle(node.getChildren());
}
});
// only retain X children of a node if this is enabled
applyToTree(expTreeRoot, node -> {
Attribute attribute = node.getAttribute(KEY_ONLY_SHOW_X_CHILDREN);
boolean enabled = Boolean.parseBoolean(attribute.getValue());
if (enabled) {
int number = Integer.parseInt(attribute.getSubAttribute(KEY_SHOW_NUMBER_OF_CHILDREN).getValue());
List<QTreeNode> children = node.getChildren();
if (children.size() > number) {
children.removeAll(children.subList(number, children.size()));
}
}
});
Function<QTreeNode, ViewNode> mapper = node -> new ViewNode(node, listener);
this.experiment = expTreeRoot.preOrder().stream().map(mapper).collect(Collectors.toList()); // rtt ArrayList
this.currentIndex = 0;
ViewNode expNode = experiment.get(currentIndex);
expNode.setEntered(true);
StopwatchLabel totalTime = new StopwatchLabel(expTreeRoot, getLocalized("STOPWATCHLABEL_TOTAL_TIME"));
totalTime.start();
expNode.setStopwatch(totalTime);
if (visibleStopwatches) {
this.timePanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10));
this.timePanel.add(totalTime);
add(timePanel, BorderLayout.SOUTH);
}
add(expNode.getViewPane(), BorderLayout.CENTER);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
String message = getLocalized("EVIEWER_EXPERIMENT_NOT_FINISHED");
int choice = showConfirmDialog(EViewer.this, message, null, YES_NO_OPTION);
if (choice == YES_OPTION) {
experiment.get(currentIndex).getViewPane().clickSubmit(false);
endExperiment();
dispose();
}
}
});
PluginList.experimentViewerRun(this);
PluginList.enterNode(expNode.getTreeNode());
pack();
setLocationRelativeTo(null);
}
/**
* Initialises the language bundle used by the <code>EViewer</code>.
*/
private void initLanguage() {
String langTag = expTreeRoot.getAttribute(KEY_VIEWER_LANGUAGE).getValue();
if (langTag.equals(Locale.GERMAN.toLanguageTag()) || langTag.equals(Locale.ENGLISH.toLanguageTag()) || langTag.equals(Constants.PORTUGUES_BR.toLanguageTag())) {
UIElementNames.setLocale(Locale.forLanguageTag(langTag));
}
}
/**
* Applies the given <code>Consumer</code> recursively to every node in the tree with root <code>node</code>.
*
* @param node the root of the tree the function is to be applied to
* @param function the function to be applied
*/
private static void applyToTree(QTreeNode node, Consumer<QTreeNode> function) {
function.accept(node);
node.getChildren().forEach(n -> applyToTree(n, function));
}
/**
* Advances from one node to the next visitable node in the given direction.
*
* @param saveAnswers whether to save answers of the current node before advancing
* @param ignoreDeny whether to ignore plugins denying exiting the current node
* @param forward true for forward, false for backwards
*/
private void advance(boolean saveAnswers, boolean ignoreDeny, boolean forward) {
QuestionViewPane currentViewNode = experiment.get(currentIndex).getViewPane();
QTreeNode currentNode = experiment.get(currentIndex).getTreeNode();
if (saveAnswers) {
currentViewNode.clickSubmit(false);
}
String message;
if (!ignoreDeny && (message = PluginList.denyNextNode(currentNode)) != null) {
showMessageDialog(this, message, null, INFORMATION_MESSAGE);
return;
}
// make sure that a subject code was entered before leaving the first node forward
if (forward && currentNode.getType() == QTreeNode.Type.EXPERIMENT && currentNode.equals(expTreeRoot)) {
String[] answers = currentNode.getAnswers(KEY_SUBJECT_CODE);
if (answers == null || answers.length < 1) {
showMessageDialog(this, getLocalized("EVIEWER_NO_SUBJECT_CODE"), null, ERROR_MESSAGE);
return;
}
}
ViewNode newViewNode;
QTreeNode newTreeNode;
boolean doNotShow;
int newIndex = currentIndex;
do {
if (forward) {
if (++newIndex >= experiment.size()) {
endExperiment();
return;
}
} else {
if (--newIndex <= 0) {
return;
}
}
newViewNode = experiment.get(newIndex);
newTreeNode = newViewNode.getTreeNode();
doNotShow = newTreeNode.containsAttribute(KEY_DONOTSHOWCONTENT);
doNotShow &= Boolean.parseBoolean(newTreeNode.getAttribute(KEY_DONOTSHOWCONTENT).getValue());
} while (doNotShow || PluginList.denyEnterNode(newTreeNode));
switchNode(newIndex);
}
/**
* Tries to advance to the next node of the experiment. If this leads to exiting the last visitable node the
* experiment will end. Since plugins may disallow exiting the current node or entering other nodes this method
* may not advance at all or skip nodes.
*
* @param saveAnswers whether to save answers of the current node before advancing
* @param ignoreDeny whether to ignore plugins denying exiting the current node
*/
public void nextNode(boolean saveAnswers, boolean ignoreDeny) {
advance(saveAnswers, ignoreDeny, true);
}
/**
* Tries to regress to the previous node of the experiment. Since plugins may disallow exiting the current node or
* entering other nodes this method may not regress at all or skip nodes. Will not regress past the first
* visitable node after the experiment root.
*
* @param saveAnswers whether to save answers of the current node before advancing
* @param ignoreDeny whether to ignore plugins denying exiting the current node
*/
public void previousNode(boolean saveAnswers, boolean ignoreDeny) {
advance(saveAnswers, ignoreDeny, false);
}
/**
* Switches from displaying the node at <code>currentIndex</code> to <code>newIndex</code>.
* This method maintains the <code>StopwatchLabel</code> states and notifies the <code>Plugin</code> instances of
* exit from <code>currentIndex</code> and entry into <code>newIndex</code>.
*
* @param newIndex the index of the new node to display
*/
private void switchNode(int newIndex) {
if (currentIndex == newIndex) {
return;
}
setEnabled(false);
ViewNode oldNode = experiment.get(currentIndex);
ViewNode newNode = experiment.get(newIndex);
updateStopwatches(oldNode, newNode);
newNode.setEntered(true);
remove(oldNode.getViewPane());
add(newNode.getViewPane(), BorderLayout.CENTER);
QTreeNode exitNode = oldNode.getTreeNode();
if (exitNode.getChildren().isEmpty()) {
PluginList.exitNode(exitNode);
while (exitNode.isLastChild()) {
PluginList.exitNode(exitNode.getParent());
exitNode = exitNode.getParent();
}
}
PluginList.enterNode(newNode.getTreeNode());
currentIndex = newIndex;
revalidate();
repaint();
setEnabled(true);
}
/**
* Stops/Starts the stopwatches of <code>oldNode</code> and <code>newNode</code>.
*
* @param oldNode
* the old selected node
* @param newNode
* the new selected node
*/
private void updateStopwatches(ViewNode oldNode, ViewNode newNode) {
// the root node counts the total time
if (currentIndex != 0) {
oldNode.getStopwatch().pause();
}
newNode.getStopwatch().start();
if (visibleStopwatches) {
if (currentIndex != 0) {
timePanel.remove(oldNode.getStopwatch());
}
timePanel.add(newNode.getStopwatch());
}
}
/**
* Ends the experiment and shows a dialog containing the <code>Plugin</code> messages.
* The window will then close.
*/
private void endExperiment() {
experiment.get(0).getStopwatch().pause();
ViewNode currentNode = experiment.get(currentIndex);
QTreeNode exitNode = currentNode.getTreeNode();
currentNode.getStopwatch().pause();
do {
PluginList.exitNode(exitNode);
exitNode = exitNode.getParent();
} while (exitNode != null);
try {
QTreeXMLHandler.saveAnswerXML(expTreeRoot, new File(getSaveDir(), FILE_ANSWERS));
} catch (IOException e) {
System.err.println("Could not save the answers.xml.");
System.err.println(e.getMessage());
}
Document doc = Document.createShell("");
Element body = doc.body();
body.appendElement("p").text(getLocalized("EVIEWER_EXPERIMENT_FINISHED"));
body.append(PluginList.finishExperiment());
setEnabled(false);
showMessageDialog(this, new JLabel(doc.outerHtml()), null, INFORMATION_MESSAGE);
dispose();
}
/**
* Locates the experiment XML file and deserializes it. If no experiment XML can be found or there is an error
* deserializing the XML file this method will terminate the JVM with exit status 1.
*
* @return the deserialized <code>QTreeNode</code>
*/
private QTreeNode loadExperiment() {
File experimentFile = new File(DEFAULT_FILE);
if (!experimentFile.exists()) {
File workingDir = new File(".");
JFileChooser fileChooser = new JFileChooser(workingDir);
fileChooser.addChoosableFileFilter(new FileNameExtensionFilter(getLocalized("EVIEWER_XML_FILES"), "*.xml"));
if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
experimentFile = fileChooser.getSelectedFile();
} else {
showMessageDialog(this, getLocalized("EVIEWER_NO_EXPERIMENT_CHOSEN"));
System.exit(LOAD_FAIL_EXIT_STATUS);
}
boolean inWorkingDir = false;
try {
inWorkingDir = Files.isSameFile(workingDir.toPath(), experimentFile.getParentFile().toPath());
} catch (IOException e) {
System.err.println("Could not check whether the chosen experiment File is in the working directory.");
System.err.println(e.getMessage());
}
if (!inWorkingDir) {
showMessageDialog(this, getLocalized("EVIEWER_EXPERIMENT_NOT_IN_WORKING_DIR"));
System.exit(LOAD_FAIL_EXIT_STATUS);
}
}
QTreeNode treeRoot = QTreeXMLHandler.loadExperimentXML(experimentFile);
if (treeRoot == null) {
showMessageDialog(this, getLocalized("MESSAGE_NO_VALID_EXPERIMENT_FILE"));
System.exit(LOAD_FAIL_EXIT_STATUS);
}
return treeRoot;
}
/**
* Returns the root <code>QTreeNode</code> of the experiment tree.
*
* @return the root node
*/
public QTreeNode getExperimentTree() {
return expTreeRoot;
}
/**
* Returns the directory in which the <code>EViewer</code> stores the date resulting from the current experiment
* run. This method will return <code>null</code> if the experimentee has not yet entered a subject code
* and progressed to the next page of the experiment or if the directory could not be created.
*
* @return the save directory or <code>null</code>
*/
public File getSaveDir() {
if (saveDir != null) {
return saveDir;
}
String[] subjectCodeAns = expTreeRoot.getAnswers(KEY_SUBJECT_CODE);
String experimentCode;
String subjectCode;
String dirName;
if (subjectCodeAns != null && subjectCodeAns.length >= 1) {
subjectCode = subjectCodeAns[0].trim();
subjectCode = (subjectCode.isEmpty()) ? NO_SUBJ_CODE : subjectCode;
if (expTreeRoot.containsAttribute(KEY_EXPERIMENT_CODE)) {
experimentCode = expTreeRoot.getAttribute(KEY_EXPERIMENT_CODE).getValue().trim();
experimentCode = (experimentCode.isEmpty()) ? NO_EXP_CODE : experimentCode;
} else {
experimentCode = NO_EXP_CODE;
}
dirName = experimentCode + '_' + subjectCode;
saveDir = new File(dirName);
if (saveDir.exists()) {
IntFunction<File> mapper = value -> new File(dirName + '_' + value);
Stream<File> dirs = IntStream.rangeClosed(1, Integer.MAX_VALUE).mapToObj(mapper);
saveDir = dirs.filter(f -> !f.exists()).findFirst().get();
}
if (!saveDir.mkdirs()) {
System.err.println("Could not create the save directory for the " + getClass().getSimpleName() + ".");
saveDir = null;
}
}
return saveDir;
}
/**
* Shows the GUI of the <code>ExperimentEditor</code>.
*
* @param args
* command line arguments, ignored by this application
*/
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | UnsupportedLookAndFeelException | InstantiationException | IllegalAccessException e) {
System.err.println("Could not set the look and feel to the system look and feel.");
System.err.println(e.getMessage());
}
new EViewer().setVisible(true);
});
}
}