package de.uni_passau.fim.infosun.prophet.util.qTree.handlers; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.CharsetEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import javax.swing.tree.TreePath; import de.uni_passau.fim.infosun.prophet.Constants; import de.uni_passau.fim.infosun.prophet.experimentEditor.qTree.QTreeModel; import de.uni_passau.fim.infosun.prophet.util.language.UIElementNames; import de.uni_passau.fim.infosun.prophet.util.qTree.QTreeNode; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.parser.Tag; /** * Handles HTML operations for the <code>QTree</code>. */ public final class QTreeHTMLHandler extends QTreeFormatHandler { /** * Utility class. */ private QTreeHTMLHandler() { } private static int highestID; private static final Set<String> returnedIDs = new HashSet<>(); /** * Checks the HTML contents of all nodes in the tree under <code>root</code> for elements with duplicate names. * The returned <code>Map</code> contains mappings from the names that occur multiple times to the * <code>TreePath</code>s to where they occur. * Each <code>TreePath</code> has the following structure: * Let n be be length of the TreePath. The indices of the TreePath are seperated in two parts: * TreePath[0] to TreePath[n-1] | TreePath[n] * <ul> * <li>Index 0 to n-1 is the TreePath from the root node to the QTreeNode that contains the duplicate name</li> * <li>Index n is an <code>Integer</code> index of the <input> within the QTreeNode that contains the * duplicate name. All elements in the HTML content of a QTreeNode that contain a <code>name</code> attribute are * indexed starting * by zero. The Integer at TreePath[n] gives the index within this "named" elements. * </li> * </ul> * * @param root * the root of the tree to be checked * * @return a <code>Map</code> of the described format */ public static Map<String, List<TreePath>> checkNames(QTreeNode root) { Map<String, List<TreePath>> names = new HashMap<>(); Map<String, List<TreePath>> duplicates = new HashMap<>(); List<TreePath> containingNodes; String nameAttr = "name"; String name; Document doc; Element body; int index; for (QTreeNode node : root.preOrder()) { doc = Jsoup.parseBodyFragment(node.getHtml()); body = doc.body(); index = 0; for (Element element : body.getElementsByAttribute(nameAttr)) { name = element.attr(nameAttr); containingNodes = names.getOrDefault(name, new ArrayList<>()); containingNodes.add(new TreePath(QTreeModel.buildPath(node, true)).pathByAddingChild(index)); names.putIfAbsent(name, containingNodes); index++; } } names.forEach((key, value) -> { if (value.size() > 1) { duplicates.put(key, value); } }); return duplicates; } /** * Returns a list of <code>String</code> IDs (numbers) that have not yet been used as an ID in any HTML element * of a node in the tree under <code>root</code> or returned by this <code>QTreeHTMLHandler</code>. * * @param root * the root of the tree in which the ids should be unique * @param number * the number of ids to be returned * * @return the ids as <code>String</code>s */ public static List<String> createIDs(QTreeNode root, int number) { Set<String> existingIDs = new HashSet<>(); List<String> newIDs = new ArrayList<>(); String idAttr = "id"; String stringID; Document doc; for (QTreeNode node : root.preOrder()) { doc = Jsoup.parseBodyFragment(node.getHtml()); doc.body().getElementsByAttribute(idAttr).forEach(element -> existingIDs.add(element.attr(idAttr))); } existingIDs.addAll(returnedIDs); for (int i = 0; i < number; i++) { do { highestID++; stringID = String.valueOf(highestID); } while (existingIDs.contains(stringID)); newIDs.add(stringID); } returnedIDs.addAll(newIDs); return newIDs; } /** * Saves the given <code>QTreeNode</code> (and thereby the whole tree under it) as a single HTML page to the given * file. This will overwrite <code>saveFile</code>. Neither <code>root</code> nor <code>saveFile</code> may * be <code>null</code>. <code>root</code> must be of type {@link QTreeNode.Type#EXPERIMENT}. * * @param root * the root of the tree to save * @param saveFile * the file to save the html to * * @throws IOException * if the file can not be written to * @throws IllegalArgumentException * if root is not of type {@link QTreeNode.Type#EXPERIMENT} */ public static void saveExperimentHTML(QTreeNode root, File saveFile) throws IOException { Objects.requireNonNull(root); Objects.requireNonNull(saveFile); if (root.getType() != QTreeNode.Type.EXPERIMENT) { throw new IllegalArgumentException("root must be of type EXPERIMENT"); } String divider = "<hr>"; checkParent(saveFile); String experimentName = root.getName(); String experimentCode = root.getAttribute(Constants.KEY_EXPERIMENT_CODE).getValue(); Document doc = Document.createShell(""); doc.head().appendElement("meta").attr("content", "text/html; charset=utf-8"); doc.title(String.format("%s - ExpCode %s", experimentName, experimentCode)); Element body = doc.body(); for (QTreeNode node : root.preOrder()) { body.appendElement("h" + Math.max(node.getType().ordinal() + 1, 6)).text(node.getName()); //FIXME Math.max or Math.min? body.append("<br>").append(node.getHtml()); if (node.getType() == QTreeNode.Type.EXPERIMENT) { String subjCodeDesc; if (!node.containsAttribute(Constants.KEY_SUBJECT_CODE_CAP)) { subjCodeDesc = UIElementNames.getLocalized("FOOTER_SUBJECT_CODE_CAPTION"); } else { subjCodeDesc = node.getAttribute(Constants.KEY_SUBJECT_CODE_CAP).getValue(); } body.appendChild(input("hidden", Constants.KEY_EXPERIMENT_CODE, experimentCode)); body.appendChild(table(null, new Object[] {subjCodeDesc, input(null, Constants.KEY_SUBJECT_CODE, null)})); } body.append(divider); } CharsetEncoder utf8encoder = StandardCharsets.UTF_8.newEncoder(); try (Writer writer = new OutputStreamWriter(new FileOutputStream(saveFile), utf8encoder)) { writer.write(doc.outerHtml()); } } /** * Creates a 'table' <code>Element</code> (using {@link Object#toString()} from the given data. * Any <code>null</code> values in <code>header</code> or <code>rows</code> (and its sub-arrays) will be ignored. * Any cells of the table that should be interpreted as HTML must be given as <code>Node</code> instances. Otherwise * HTML in the <code>String</code> returned by {@link Object#toString()} will be escaped. * * @param header the optional header for the table * @param rows the rows for the table * @return the 'table' <code>Element</code> */ public static Element table(Object[] header, Object[]... rows) { Element table = new Element(Tag.valueOf("table"), ""); if (header != null) { Element headerRowEl = table.appendElement("tr"); Element headerColEl; for (Object headerData : header) { if (headerData == null) { continue; } headerColEl = headerRowEl.appendElement("th"); if (headerData instanceof Node) { headerColEl.appendChild((Node) headerData); } else { headerColEl.text(headerData.toString()); } } } if (rows != null) { Element rowEl; Element colEl; for (Object[] row : rows) { if (row == null) { continue; } rowEl = table.appendElement("tr"); for (Object rowData : row) { if (rowData == null) { continue; } colEl = rowEl.appendElement("td"); if (rowData instanceof Node) { colEl.appendChild((Node) rowData); } else { colEl.text(rowData.toString()); } } } } return table; } /** * Creates an 'input' <code>Element</code> with the given (optional) attributes. * * @param type the value for the attribute 'type' * @param name the value for the attribute 'name' * @param value the value for the attribute 'value' * @return an 'input' <code>Element</code> */ public static Element input(String type, String name, String value) { Element element = new Element(Tag.valueOf("input"), ""); if (type != null) { element.attr("type", type); } if (name != null) { element.attr("name", name); } if (value != null) { element.attr("value", value); } return element; } }