Java tutorial
/******************************************************************************* * ATE, Automation Test Engine * * Copyright 2015, Montreal PROT, or individual contributors as * indicated by the @author tags or express copyright attribution * statements applied by the authors. All third-party contributions are * distributed under license by Montreal PROT. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ package utils.form; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.lang3.StringEscapeUtils; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.css.sac.InputSource; import org.w3c.dom.css.CSSRule; import org.w3c.dom.css.CSSRuleList; import org.w3c.dom.css.CSSStyleDeclaration; import org.w3c.dom.css.CSSStyleRule; import org.w3c.dom.css.CSSStyleSheet; import com.google.common.collect.Iterables; import com.steadystate.css.parser.CSSOMParser; import com.steadystate.css.parser.CssCharStream; import static org.joox.JOOX.*; // TODO: Auto-generated Javadoc /** * This class InputsCollector defines .... * * @author Peidong Hu * */ public class WebFormUserInputsCollector extends WebFormElementsCollector { final static public String[] USER_NOT_CHANGABLE_INPUT_TYPES = { "hidden" }; final static public String[] LEFT_LABELED_INPUT_TYPES = { "text", "date", "button", "datetime", "datetime-local", "email", "file", "image", "month", "number", "password", "range", "reset", "search", "submit", "tel", "time", "url", "week" }; final static public String[] RIGHT_LABELED_INPUT_TYPES = { "radio", "checkbox" }; final static public String[] USER_CHANGABLE_INPUT_TAGS = { "select", "input", "textarea" }; final private List<UserInputDom> userInputs = new ArrayList<UserInputDom>(); final private WebDriver webDriver; /** * Instantiates a new web form user inputs collector. * * @param domDoc * the dom doc * @param parentFrame * the parent frame nullable * @throws ParserConfigurationException * the parser configuration exception * @throws IOException */ public WebFormUserInputsCollector(WebDriver webDriver, Document domDoc, String xpathOfParentFrame) throws ParserConfigurationException, IOException { super(domDoc, xpathOfParentFrame); this.webDriver = webDriver; collectUserInputs(super.getCleanedDoc(), super.getDomDoc()); } private boolean isUserChangableInputType(Node node) { boolean retVal = true; String nodeTag = node.getNodeName(); if (nodeTag.equalsIgnoreCase("input")) { for (int i = 0; i < USER_NOT_CHANGABLE_INPUT_TYPES.length; i++) { if ($(node).attr("type").equalsIgnoreCase(USER_NOT_CHANGABLE_INPUT_TYPES[i])) { retVal = false; break; } } } else { retVal = true; } return retVal; } private void collectUserInputs(Document cleanedDoc, Document originalDoc) throws IOException { for (int j = 0; j < USER_CHANGABLE_INPUT_TAGS.length; j++) { NodeList htmlInputs = cleanedDoc.getElementsByTagName(USER_CHANGABLE_INPUT_TAGS[j]); for (int i = 0; i < htmlInputs.getLength(); i++) { Node coreNode = htmlInputs.item(i); List<Element> parentsUntilForm = $(coreNode).parentsUntil("form").get(); if ((parentsUntilForm.isEmpty() || !Iterables.get(parentsUntilForm, parentsUntilForm.size() - 1) .getNodeName().equalsIgnoreCase("html")) && isUserChangableInputType(coreNode) && webDriver.findElement(By.xpath($(coreNode).xpath())).isDisplayed()) { if (parentsUntilForm.isEmpty()) { userInputs .add(initUserInputDomInsideOfForm(cleanedDoc, coreNode, coreNode.getParentNode())); } else { List<Element> parents = $(coreNode).parentsUntil("form").parent().get(); userInputs.add(initUserInputDomInsideOfForm(cleanedDoc, coreNode, parents.get(parents.size() - 1))); } } else { // TODO collect input out of form element } } } } private void fillOutNonLabeledFieldLabelDomPointer(UserInputDom valueHolder, Node searchStartingNode, Node searchUpEndingNode, boolean endingNodeInclusive, boolean leftLabeled) { Node tempParent2 = searchStartingNode; if (leftLabeled) { while (tempParent2.getPreviousSibling() == null && tempParent2 != searchUpEndingNode) { tempParent2 = tempParent2.getParentNode(); } // if tempParent2 is form node, we will use the form's // previous sibling as the label node; // Or we will use the nearest input sibling node as the // label node; if (tempParent2 == searchUpEndingNode && !endingNodeInclusive) return; valueHolder.setLabelDomPointer(tempParent2.getPreviousSibling()); } else { while (tempParent2.getNextSibling() == null && tempParent2 != searchUpEndingNode) { tempParent2 = tempParent2.getParentNode(); } // if tempParent2 is form node, we will use the form's // previous sibling as the label node; // Or we will use the nearest input sibling node as the // label node; if (tempParent2 == searchUpEndingNode && !endingNodeInclusive) return; valueHolder.setLabelDomPointer(tempParent2.getNextSibling()); } } private void fillOutAddtionalInfoNode(UserInputDom valueHolder, Node searchStartingNode, Node searchUpEndingNode, boolean endingNodeInclusive, boolean nextSiblingOnly) { // fill out addtional info nodes Node tempParent2 = searchStartingNode; Node tempNode = tempParent2; List<Node> additionalInfoNodes = new ArrayList<Node>(); while (tempParent2.getNextSibling() == null && tempParent2 != searchUpEndingNode) { tempParent2 = tempParent2.getParentNode(); tempNode = tempParent2; } if (tempNode == searchUpEndingNode && !endingNodeInclusive) return; if (nextSiblingOnly) additionalInfoNodes.add(tempNode.getNextSibling()); else while (tempNode.getNextSibling() != null) { additionalInfoNodes.add(tempNode.getNextSibling()); if (tempNode.getNodeName().equalsIgnoreCase("form")) break; tempNode = tempNode.getNextSibling(); } valueHolder.setAdditionalInfoNodes(additionalInfoNodes); } private void fillOutUserViewablePreviousSibling(UserInputDom valueHolder, Node searchStartingNode, Node searchUpEndingNode, boolean endingNodeInclusive) { Node tempParent2 = searchStartingNode; while (tempParent2.getPreviousSibling() == null && tempParent2 != searchUpEndingNode) { tempParent2 = tempParent2.getParentNode(); } // if tempParent2 is form node, we will use the form's // previous sibling as the label node; // Or we will use the nearest input sibling node as the // label node; if (tempParent2 == searchUpEndingNode && !endingNodeInclusive) return; valueHolder.setPreviousUserViewableHtmlSibling(tempParent2.getPreviousSibling()); } private void fillOutUserViewableNextSibling(UserInputDom valueHolder, Node maxInputParentNoOtherChild, Node searchUpEndingNode, boolean endingNodeInclusive) { Node tempParent2 = maxInputParentNoOtherChild; while (tempParent2.getNextSibling() == null && tempParent2 != searchUpEndingNode) { tempParent2 = tempParent2.getParentNode(); } if (tempParent2 == searchUpEndingNode && !endingNodeInclusive) return; valueHolder.setNextUserViewableHtmlSibling(tempParent2.getNextSibling()); } private Node getMaxInputParentNoOtherChild(Node inputNode) { Node tempParent2 = inputNode.getParentNode(); Node maxInputParentNoOtherChild = inputNode; while ($(tempParent2).children().get().size() <= 1) { maxInputParentNoOtherChild = tempParent2; tempParent2 = tempParent2.getParentNode(); } return maxInputParentNoOtherChild; } private int getNumberOfUserChangableInputsInNode(Node node) { int retVal = 0; for (int i = 0; i < USER_CHANGABLE_INPUT_TAGS.length; i++) { retVal = $(node).find(USER_CHANGABLE_INPUT_TAGS[i]).get().size() + retVal; } return retVal; } private Node getMaxInputParentNoOtherInput(Node inputNode, boolean inputInForm) { Node tempParent = inputNode.getParentNode(); Node maxInputParentNoOtherInput = inputNode; while (getNumberOfUserChangableInputsInNode(tempParent) <= 1) { maxInputParentNoOtherInput = tempParent; if (inputInForm && tempParent.getNodeName().equalsIgnoreCase("form")) { break; } tempParent = tempParent.getParentNode(); } return maxInputParentNoOtherInput; } private boolean isLeftLabeled(Node inputNode) { Node inputType = inputNode.getAttributes().getNamedItem("type"); boolean retVal = true; if (null != inputType) { retVal = Arrays.asList(LEFT_LABELED_INPUT_TYPES).contains(inputType.getNodeValue()); } else if (inputNode.getNodeName().equalsIgnoreCase("textarea")) { retVal = true; } else if (inputNode.getNodeName().equalsIgnoreCase("select")) { retVal = true; } return retVal; } private boolean siblingHasInput(Node node) { boolean retVal = false; for (int i = 0; i < USER_CHANGABLE_INPUT_TAGS.length; i++) { boolean nextSiblingIsInput = false; if (node.getNextSibling() != null) { nextSiblingIsInput = Arrays.asList(USER_CHANGABLE_INPUT_TAGS) .contains($(node.getNextSibling()).tag().toLowerCase()) || $(node.getNextSibling()).find(USER_CHANGABLE_INPUT_TAGS[i]).isNotEmpty(); } boolean previousSiblingIsInput = false; if (node.getPreviousSibling() != null) { previousSiblingIsInput = Arrays.asList(USER_CHANGABLE_INPUT_TAGS) .contains($(node.getPreviousSibling()).tag().toLowerCase()) || $(node.getPreviousSibling()).find(USER_CHANGABLE_INPUT_TAGS[i]).isNotEmpty(); } retVal = nextSiblingIsInput || previousSiblingIsInput; if (retVal) break; } return retVal; } private boolean leftRightDirectedSiblingHasNoInput(Node node, boolean leftLabeled) { boolean retVal = true; for (int i = 0; i < USER_CHANGABLE_INPUT_TAGS.length; i++) { if (leftLabeled) { if (node.getPreviousSibling() == null) { retVal = true; } else if (Arrays.asList(USER_CHANGABLE_INPUT_TAGS) .contains(node.getPreviousSibling().getNodeName().toLowerCase())) retVal = false; else retVal = retVal && $(node.getPreviousSibling()).find(USER_CHANGABLE_INPUT_TAGS[i]).isEmpty(); } if (!leftLabeled) { if (node.getNextSibling() == null) { retVal = true; } else if (Arrays.asList(USER_CHANGABLE_INPUT_TAGS) .contains(node.getNextSibling().getNodeName().toLowerCase())) retVal = false; else retVal = retVal && $(node.getNextSibling()).find(USER_CHANGABLE_INPUT_TAGS[i]).isEmpty(); } } return retVal; } private boolean leftRightDirectedSiblingWithLabel(Node node, boolean leftLabeled) { boolean retVal = false; if (leftLabeled) { if (node.getPreviousSibling() != null) { retVal = $(node.getPreviousSibling()).find("label").isNotEmpty() || node.getPreviousSibling().getNodeName().equalsIgnoreCase("label"); } } else { if (node.getNextSibling() != null) { retVal = $(node.getNextSibling()).find("label").isNotEmpty() || node.getNextSibling().getNodeName().equalsIgnoreCase("label"); } } return retVal; } private List<Element> getLeftRightDirectedSiblingLabelInNode(Node node, boolean leftLabeled) { List<Element> labels2 = new ArrayList<Element>(); if (leftLabeled) { if (node.getPreviousSibling().getNodeName().equalsIgnoreCase("label")) labels2.add((Element) node.getPreviousSibling()); else labels2 = $(node.getPreviousSibling()).find("label").get(); } else { if (node.getNextSibling().getNodeName().equalsIgnoreCase("label")) labels2.add((Element) node.getPreviousSibling()); else labels2 = $(node.getNextSibling()).find("label").get(); } return labels2; } private UserInputDom initUserInputDomInsideOfForm(Document domDoc, Node inputNode, Node form) { // NodeList allInputNodes, Node inputNodeParentForm) { UserInputDom retVal = new UserInputDom(inputNode); boolean leftLabeled = isLeftLabeled(inputNode); retVal.setxPath($(inputNode).xpath()); retVal.setParentFormPointer(form); Node maxInputParentNoOtherInput = getMaxInputParentNoOtherInput(inputNode, true); // Node tempParent = inputNode.getParentNode(); boolean singleInputFieldForm = false; // Node maxInputParentNoOtherInput = tempParent; // while (($(tempParent).find("input").get().size() + $(tempParent) // .find("textarea").get().size()) <= 1) { // maxInputParentNoOtherInput = tempParent; // if (tempParent.getNodeName().equalsIgnoreCase("form")) { // singleFieldForm = true; // break; // } // tempParent = tempParent.getParentNode(); // } // if signlefieldform leastInputsCommonParent is the form node Node leastInputsCommonParent; if (maxInputParentNoOtherInput.getNodeName().equalsIgnoreCase("form")) { leastInputsCommonParent = maxInputParentNoOtherInput; singleInputFieldForm = true; } else leastInputsCommonParent = maxInputParentNoOtherInput.getParentNode(); boolean singleFieldFormInputHasNoSibling = false; Node maxInputParentNoOtherChild = getMaxInputParentNoOtherChild(inputNode); // Node tempParent2 = inputNode.getParentNode(); // Node maxInputParentNoOtherChild = inputNode; // while ($(tempParent2).children().get().size() <= 1) { // maxInputParentNoOtherChild = tempParent2; if (maxInputParentNoOtherChild.getNodeName().equalsIgnoreCase("form")) { singleFieldFormInputHasNoSibling = true; // new a return result in a single input and no input // sibling form List<Node> temp1 = new ArrayList<Node>(); temp1.add(maxInputParentNoOtherChild.getChildNodes().item(0)); retVal.setMachineLearningDomHtmlPointer(temp1); retVal.setLabelDomPointer(maxInputParentNoOtherChild.getPreviousSibling()); if (leftLabeled) { List<Node> temp = new ArrayList<Node>(); temp.add(maxInputParentNoOtherChild.getNextSibling()); retVal.setAdditionalInfoNodes(temp); } retVal.setPreviousUserViewableHtmlSibling(maxInputParentNoOtherChild.getPreviousSibling()); retVal.setNextUserViewableHtmlSibling(maxInputParentNoOtherChild.getNextSibling()); // break; } // tempParent2 = tempParent2.getParentNode(); // } // TODO the parent of maxInputParentNoOtherChild might have input // fields, might not have input fields Node leastNonInputSiblingsParent = maxInputParentNoOtherChild.getParentNode(); if (singleInputFieldForm && !singleFieldFormInputHasNoSibling) { List<Node> temp = new ArrayList<Node>(); for (int i = 0; i < leastInputsCommonParent.getChildNodes().getLength(); i++) { temp.add(leastInputsCommonParent.getChildNodes().item(i)); } retVal.setMachineLearningDomHtmlPointer(temp); List<Element> labels = $(leastInputsCommonParent).find("label").get(); if (labels.size() > 0) { retVal.setLabelDomPointer(labels.get(0)); } else { fillOutNonLabeledFieldLabelDomPointer(retVal, maxInputParentNoOtherChild, leastInputsCommonParent, false, leftLabeled); // tempParent2 = maxInputParentNoOtherChild; // // while (tempParent2.getPreviousSibling() == null // && !tempParent2.getNodeName().equalsIgnoreCase( // "form")) { // tempParent2 = tempParent2.getParentNode(); // } // // if tempParent2 is form node, we will use the form's // // previous sibling as the label node; // // Or we will use the nearest input sibling node as the // // label node; // retVal.setLabelDomPointer(tempParent2.getPreviousSibling()); } // fill out addtional info nodes if (leftLabeled) fillOutAddtionalInfoNode(retVal, maxInputParentNoOtherChild, leastInputsCommonParent, false, false); // tempParent2 = maxInputParentNoOtherChild; // List<Node> additionalInfoNodes = new ArrayList<Node>(); // while (tempParent2.getNextSibling() == null // && !tempParent2.getNodeName().equalsIgnoreCase("form")) { // tempParent2 = tempParent2.getParentNode(); // Node tempNode = tempParent2; // while (tempNode.getNextSibling() != null) { // additionalInfoNodes.add(tempNode.getNextSibling()); // if (tempNode.getNodeName().equalsIgnoreCase("form")) break; // tempNode = tempNode.getNextSibling(); // } // } // retVal.setAdditionalInfoNodes(additionalInfoNodes); // fill out non empty user viewable previous sibling fillOutUserViewablePreviousSibling(retVal, maxInputParentNoOtherChild, leastInputsCommonParent, false); // tempParent2 = maxInputParentNoOtherChild; // // while (tempParent2.getPreviousSibling() == null // && !tempParent2.getNodeName().equalsIgnoreCase( // "form")) { // tempParent2 = tempParent2.getParentNode(); // } // // if tempParent2 is form node, we will use the form's // // previous sibling as the label node; // // Or we will use the nearest input sibling node as the // // label node; // retVal.setPreviousUserViewableHtmlSibling(tempParent2.getPreviousSibling()); // set non empty user viewable next sibling fillOutUserViewableNextSibling(retVal, maxInputParentNoOtherChild, leastInputsCommonParent, false); // tempParent2 = maxInputParentNoOtherChild; // while (tempParent2.getNextSibling() == null // && !tempParent2.getNodeName().equalsIgnoreCase("form")) { // tempParent2 = tempParent2.getParentNode(); // } // retVal.setNextUserViewableHtmlSibling(tempParent2.getNextSibling()); } else if (!singleFieldFormInputHasNoSibling) { List<Element> labels = $(maxInputParentNoOtherInput).find("label").get(); if (labels.size() > 0) { List<Node> tempList = new ArrayList<Node>(); tempList.add(maxInputParentNoOtherInput); retVal.setMachineLearningDomHtmlPointer(tempList); retVal.setLabelDomPointer(labels.get(0)); if (leftLabeled) fillOutAddtionalInfoNode(retVal, maxInputParentNoOtherChild, leastNonInputSiblingsParent, false, false); fillOutUserViewablePreviousSibling(retVal, maxInputParentNoOtherChild, leastNonInputSiblingsParent, false); fillOutUserViewableNextSibling(retVal, maxInputParentNoOtherChild, leastNonInputSiblingsParent, false); } else { if (leftRightDirectedSiblingHasNoInput(maxInputParentNoOtherInput, leftLabeled) && leftRightDirectedSiblingWithLabel(maxInputParentNoOtherInput, leftLabeled)) { List<Element> labels2 = getLeftRightDirectedSiblingLabelInNode(maxInputParentNoOtherInput, leftLabeled); List<Node> tempList = new ArrayList<Node>(); if (leftLabeled) { tempList.add(maxInputParentNoOtherInput.getPreviousSibling()); tempList.add(maxInputParentNoOtherInput); } else { tempList.add(maxInputParentNoOtherInput); tempList.add(maxInputParentNoOtherInput.getNextSibling()); } retVal.setMachineLearningDomHtmlPointer(tempList); retVal.setLabelDomPointer(labels2.get(0)); if (leftLabeled && leftRightDirectedSiblingHasNoInput(maxInputParentNoOtherInput, false)) fillOutAddtionalInfoNode(retVal, maxInputParentNoOtherInput, maxInputParentNoOtherInput.getParentNode(), false, true); fillOutUserViewablePreviousSibling(retVal, maxInputParentNoOtherInput, maxInputParentNoOtherInput.getParentNode(), false); fillOutUserViewableNextSibling(retVal, maxInputParentNoOtherInput, maxInputParentNoOtherInput.getParentNode(), false); } else if (this.siblingHasInput(maxInputParentNoOtherInput)) { // most likely field (label equivelant information has been // inside Node // maxInputParentNoOtherInput List<Node> tempList = new ArrayList<Node>(); tempList.add(maxInputParentNoOtherInput); retVal.setMachineLearningDomHtmlPointer(tempList); fillOutNonLabeledFieldLabelDomPointer(retVal, maxInputParentNoOtherChild, maxInputParentNoOtherInput, false, leftLabeled); if (leftLabeled) fillOutAddtionalInfoNode(retVal, maxInputParentNoOtherChild, maxInputParentNoOtherInput, false, false); fillOutUserViewablePreviousSibling(retVal, maxInputParentNoOtherChild, maxInputParentNoOtherInput, false); fillOutUserViewableNextSibling(retVal, maxInputParentNoOtherChild, maxInputParentNoOtherInput, false); } else if (leftRightDirectedSiblingHasNoInput(maxInputParentNoOtherInput, leftLabeled)) { // most likely field information is out of // maxInputParentNoOtherInput in its siblings with no label // tag List<Node> tempList = new ArrayList<Node>(); if (leftLabeled) { tempList.add(maxInputParentNoOtherInput.getPreviousSibling()); tempList.add(maxInputParentNoOtherInput); } else { tempList.add(maxInputParentNoOtherInput); tempList.add(maxInputParentNoOtherInput.getNextSibling()); } retVal.setMachineLearningDomHtmlPointer(tempList); if (leftLabeled) retVal.setLabelDomPointer(maxInputParentNoOtherInput.getPreviousSibling()); else retVal.setLabelDomPointer(maxInputParentNoOtherInput.getNextSibling()); if (leftLabeled && leftRightDirectedSiblingHasNoInput(maxInputParentNoOtherInput, false)) fillOutAddtionalInfoNode(retVal, maxInputParentNoOtherInput, maxInputParentNoOtherInput.getParentNode(), false, true); fillOutUserViewablePreviousSibling(retVal, maxInputParentNoOtherInput, maxInputParentNoOtherInput.getParentNode(), false); fillOutUserViewableNextSibling(retVal, maxInputParentNoOtherInput, maxInputParentNoOtherInput.getParentNode(), false); } } // if (leastNonInputSiblingsParent != leastInputsCommonParent) { // tempParent2 = leastNonInputSiblingsParent; // while (tempParent2.getParentNode() != leastInputsCommonParent) { // tempParent2.getParentNode(); // } // Node maxNonInputSiblingsParent = tempParent2; // // } else { // List<Element> allInputSiblings = $(leastInputsCommonParent) // .children().get(); // for (int indexOfDirectChild; indexOfDirectChild < // allInputSiblings // .size(); indexOfDirectChild++) { // // if the sibling has form in it, then ignore this // // sibling since it and its children inputs are not // // related to this current processing input. // if ($(allInputSiblings.get(indexOfDirectChild)) // .find("form").get().size() > 0) { // continue; // } // // int sizeOfInputElementInSibling = $( // allInputSiblings.get(indexOfDirectChild)) // .find("input").get().size(); // if (sizeOfInputElementInSibling == 1) { // // input block which might have label and additional // // info // Node nextSibling = allInputSiblings.get( // indexOfDirectChild).getNextSibling(); // Node previousSibling = allInputSiblings.get( // indexOfDirectChild).getPreviousSibling(); // if (previousSibling == null) { // // first child in least common parent // // } else if (nextSibling == null) { // // last child in least common parent // } else { // // neither first nor last child. // if ($(nextSibling).find("input").get().size() == 1) { // // this html block most likely a label // } // } // } else if (sizeOfInputElementInSibling == 0) { // // might be label or other information of the next // // input or might be non input related infor. // continue; // // } else if (sizeOfInputElementInSibling > 1) { // // html block has more than one input, need to find // // the least parent for these inputs. // // TODO need a nest function to handle this type of // // html block // } // } // } } return retVal; } /** * @return the userInputs */ public final List<UserInputDom> getUserInputs() { return userInputs; } // private boolean has2ChildInputNodes(Node node) { // boolean retVal; // NodeList childNodes = node.getChildNodes(); // } // // private Node findLabelNodeOfInput(Node inputNode, Document domDoc) { // Node retVal; // Node previousSibling = inputNode.getPreviousSibling(); // if (previousSibling.getNodeName().equalsIgnoreCase("label")) { // retVal = previousSibling; // } else if // (inputNode.getParentNode().getPreviousSibling().getNodeName().equalsIgnoreCase("label")) // { // retVal = previousSibling; // } else { // while (inputNode.getParentNode().getChildNodes()) // } // } }