| package org.robolectric.res.builder; |
| |
| import android.content.res.Resources; |
| import android.content.res.XmlResourceParser; |
| import com.android.internal.util.XmlUtils; |
| import org.robolectric.res.AttributeResource; |
| import org.robolectric.res.ResName; |
| import org.robolectric.res.ResourceLoader; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.Reader; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| /** |
| * Concrete implementation of the {@link XmlResourceParser}. |
| * |
| * <p>Clients expects a pull parser while the resource loader |
| * initialise this object with a {@link Document}. |
| * This implementation navigates the dom and emulates a pull |
| * parser by raising all the opportune events. |
| * |
| * <p>Note that the original android implementation is based on |
| * a set of native methods calls. Here those methods are |
| * re-implemented in java when possible. |
| */ |
| public class XmlResourceParserImpl implements XmlResourceParser { |
| |
| /** |
| * All the parser features currently supported by Android. |
| */ |
| static final String[] AVAILABLE_FEATURES = { |
| XmlResourceParser.FEATURE_PROCESS_NAMESPACES, |
| XmlResourceParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES |
| }; |
| /** |
| * All the parser features currently NOT supported by Android. |
| */ |
| static final String[] UNAVAILABLE_FEATURES = { |
| XmlResourceParser.FEATURE_PROCESS_DOCDECL, |
| XmlResourceParser.FEATURE_VALIDATION |
| }; |
| |
| private final Document document; |
| private final String fileName; |
| private final String packageName; |
| private final ResourceLoader resourceLoader; |
| private final String applicationNamespace; |
| |
| private Node currentNode; |
| |
| private boolean mStarted = false; |
| private boolean mDecNextDepth = false; |
| private int mDepth = 0; |
| private int mEventType = START_DOCUMENT; |
| |
| public XmlResourceParserImpl(Document document, String fileName, String packageName, |
| String applicationPackageName, ResourceLoader resourceLoader) { |
| this.document = document; |
| this.fileName = fileName; |
| this.packageName = packageName; |
| this.resourceLoader = resourceLoader; |
| this.applicationNamespace = AttributeResource.ANDROID_RES_NS_PREFIX + applicationPackageName; |
| } |
| |
| @Override |
| public void setFeature(String name, boolean state) |
| throws XmlPullParserException { |
| if (isAndroidSupportedFeature(name) && state) { |
| return; |
| } |
| throw new XmlPullParserException("Unsupported feature: " + name); |
| } |
| |
| @Override |
| public boolean getFeature(String name) { |
| return isAndroidSupportedFeature(name); |
| } |
| |
| @Override |
| public void setProperty(String name, Object value) |
| throws XmlPullParserException { |
| throw new XmlPullParserException("setProperty() not supported"); |
| } |
| |
| @Override |
| public Object getProperty(String name) { |
| // Properties are not supported. Android returns null |
| // instead of throwing an XmlPullParserException. |
| return null; |
| } |
| |
| @Override |
| public void setInput(Reader in) throws XmlPullParserException { |
| throw new XmlPullParserException("setInput() not supported"); |
| } |
| |
| @Override |
| public void setInput(InputStream inputStream, String inputEncoding) |
| throws XmlPullParserException { |
| throw new XmlPullParserException("setInput() not supported"); |
| } |
| |
| @Override |
| public void defineEntityReplacementText( |
| String entityName, String replacementText) |
| throws XmlPullParserException { |
| throw new XmlPullParserException( |
| "defineEntityReplacementText() not supported"); |
| } |
| |
| @Override |
| public String getNamespacePrefix(int pos) |
| throws XmlPullParserException { |
| throw new XmlPullParserException( |
| "getNamespacePrefix() not supported"); |
| } |
| |
| @Override |
| public String getInputEncoding() { |
| return null; |
| } |
| |
| @Override |
| public String getNamespace(String prefix) { |
| throw new RuntimeException( |
| "getNamespaceCount() not supported"); |
| } |
| |
| @Override |
| public int getNamespaceCount(int depth) |
| throws XmlPullParserException { |
| throw new XmlPullParserException( |
| "getNamespaceCount() not supported"); |
| } |
| |
| @Override |
| public String getPositionDescription() { |
| return "XML file " + fileName + " line #" + getLineNumber() + " (sorry, not yet implemented)"; |
| } |
| |
| @Override |
| public String getNamespaceUri(int pos) |
| throws XmlPullParserException { |
| throw new XmlPullParserException( |
| "getNamespaceUri() not supported"); |
| } |
| |
| @Override |
| public int getColumnNumber() { |
| // Android always returns -1 |
| return -1; |
| } |
| |
| @Override |
| public int getDepth() { |
| return mDepth; |
| } |
| |
| @Override |
| public String getText() { |
| if (currentNode == null) { |
| return ""; |
| } |
| return currentNode.getTextContent(); |
| } |
| |
| @Override |
| public int getLineNumber() { |
| // TODO(msama): The current implementation is |
| // unable to return line numbers. |
| return -1; |
| } |
| |
| @Override |
| public int getEventType() |
| throws XmlPullParserException { |
| return mEventType; |
| } |
| |
| /*package*/ |
| public boolean isWhitespace(String text) |
| throws XmlPullParserException { |
| if (text == null) { |
| return false; |
| } |
| return text.split("\\s").length == 0; |
| } |
| |
| @Override |
| public boolean isWhitespace() |
| throws XmlPullParserException { |
| // Note: in android whitespaces are automatically stripped. |
| // Here we have to skip them manually |
| return isWhitespace(getText()); |
| } |
| |
| @Override |
| public String getPrefix() { |
| throw new RuntimeException("getPrefix not supported"); |
| } |
| |
| @Override |
| public char[] getTextCharacters(int[] holderForStartAndLength) { |
| String txt = getText(); |
| char[] chars = null; |
| if (txt != null) { |
| holderForStartAndLength[0] = 0; |
| holderForStartAndLength[1] = txt.length(); |
| chars = new char[txt.length()]; |
| txt.getChars(0, txt.length(), chars, 0); |
| } |
| return chars; |
| } |
| |
| @Override |
| public String getNamespace() { |
| String namespace = currentNode != null ? currentNode.getNamespaceURI() : null; |
| if (namespace == null) { |
| return ""; |
| } |
| |
| return maybeReplaceNamespace(namespace); |
| } |
| |
| @Override |
| public String getName() { |
| if (currentNode == null) { |
| return ""; |
| } |
| return currentNode.getNodeName(); |
| } |
| |
| Node getAttributeAt(int index) { |
| if (currentNode == null) { |
| throw new IndexOutOfBoundsException(String.valueOf(index)); |
| } |
| NamedNodeMap map = currentNode.getAttributes(); |
| if (index >= map.getLength()) { |
| throw new IndexOutOfBoundsException(String.valueOf(index)); |
| } |
| return map.item(index); |
| } |
| |
| String getAttribute(String namespace, String name) { |
| if (currentNode == null) { |
| return null; |
| } |
| |
| Element element = (Element) currentNode; |
| if (element.hasAttributeNS(namespace, name)) { |
| return element.getAttributeNS(namespace, name).trim(); |
| } else if (applicationNamespace.equals(namespace) |
| && element.hasAttributeNS(AttributeResource.RES_AUTO_NS_URI, name)) { |
| return element.getAttributeNS(AttributeResource.RES_AUTO_NS_URI, name).trim(); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public String getAttributeNamespace(int index) { |
| Node attr = getAttributeAt(index); |
| if (attr == null) { |
| return null; |
| } |
| return maybeReplaceNamespace(attr.getNamespaceURI()); |
| } |
| |
| private String maybeReplaceNamespace(String namespace) { |
| if (AttributeResource.RES_AUTO_NS_URI.equals(namespace)) { |
| return applicationNamespace; |
| } else { |
| return namespace; |
| } |
| } |
| |
| @Override |
| public String getAttributeName(int index) { |
| try { |
| Node attr = getAttributeAt(index); |
| String namespace = maybeReplaceNamespace(attr.getNamespaceURI()); |
| return applicationNamespace.equals(namespace) ? |
| attr.getLocalName() : |
| attr.getNodeName(); |
| } catch (IndexOutOfBoundsException ex) { |
| return null; |
| } |
| } |
| |
| @Override |
| public String getAttributePrefix(int index) { |
| throw new RuntimeException("getAttributePrefix not supported"); |
| } |
| |
| @Override |
| public boolean isEmptyElementTag() throws XmlPullParserException { |
| // In Android this method is left unimplemented. |
| // This implementation is mirroring that. |
| return false; |
| } |
| |
| @Override |
| public int getAttributeCount() { |
| if (currentNode == null) { |
| return -1; |
| } |
| return currentNode.getAttributes().getLength(); |
| } |
| |
| @Override |
| public String getAttributeValue(int index) { |
| return qualify(getAttributeAt(index).getNodeValue()); |
| } |
| |
| private String qualify(String value) { |
| if (value == null) return null; |
| if (AttributeResource.isResourceReference(value)) { |
| return "@" + ResName.qualifyResourceName(value.substring(1).replace("+", ""), packageName, "attr"); |
| } else if (AttributeResource.isStyleReference(value)) { |
| return "?" + ResName.qualifyResourceName(value.substring(1), packageName, "attr"); |
| } else { |
| return value; |
| } |
| } |
| |
| @Override |
| public String getAttributeType(int index) { |
| // Android always returns CDATA even if the |
| // node has no attribute. |
| return "CDATA"; |
| } |
| |
| @Override |
| public boolean isAttributeDefault(int index) { |
| // The android implementation always returns false |
| return false; |
| } |
| |
| @Override |
| public int nextToken() throws XmlPullParserException, IOException { |
| return next(); |
| } |
| |
| @Override |
| public String getAttributeValue(String namespace, String name) { |
| return qualify(getAttribute(namespace, name)); |
| } |
| |
| @Override |
| public int next() throws XmlPullParserException, IOException { |
| if (!mStarted) { |
| mStarted = true; |
| return START_DOCUMENT; |
| } |
| if (mEventType == END_DOCUMENT) { |
| return END_DOCUMENT; |
| } |
| int ev = nativeNext(); |
| if (mDecNextDepth) { |
| mDepth--; |
| mDecNextDepth = false; |
| } |
| switch (ev) { |
| case START_TAG: |
| mDepth++; |
| break; |
| case END_TAG: |
| mDecNextDepth = true; |
| break; |
| } |
| mEventType = ev; |
| if (ev == END_DOCUMENT) { |
| // Automatically close the parse when we reach the end of |
| // a document, since the standard XmlPullParser interface |
| // doesn't have such an API so most clients will leave us |
| // dangling. |
| close(); |
| } |
| return ev; |
| } |
| |
| /** |
| * A twin implementation of the native android nativeNext(status) |
| * |
| * @throws XmlPullParserException |
| */ |
| private int nativeNext() throws XmlPullParserException { |
| switch (mEventType) { |
| case (CDSECT): { |
| throw new IllegalArgumentException( |
| "CDSECT is not handled by Android"); |
| } |
| case (COMMENT): { |
| throw new IllegalArgumentException( |
| "COMMENT is not handled by Android"); |
| } |
| case (DOCDECL): { |
| throw new IllegalArgumentException( |
| "DOCDECL is not handled by Android"); |
| } |
| case (ENTITY_REF): { |
| throw new IllegalArgumentException( |
| "ENTITY_REF is not handled by Android"); |
| } |
| case (END_DOCUMENT): { |
| // The end document event should have been filtered |
| // from the invoker. This should never happen. |
| throw new IllegalArgumentException( |
| "END_DOCUMENT should not be found here."); |
| } |
| case (END_TAG): { |
| return navigateToNextNode(currentNode); |
| } |
| case (IGNORABLE_WHITESPACE): { |
| throw new IllegalArgumentException( |
| "IGNORABLE_WHITESPACE"); |
| } |
| case (PROCESSING_INSTRUCTION): { |
| throw new IllegalArgumentException( |
| "PROCESSING_INSTRUCTION"); |
| } |
| case (START_DOCUMENT): { |
| currentNode = document.getDocumentElement(); |
| return START_TAG; |
| } |
| case (START_TAG): { |
| if (currentNode.hasChildNodes()) { |
| // The node has children, navigate down |
| return processNextNodeType( |
| currentNode.getFirstChild()); |
| } else { |
| // The node has no children |
| return END_TAG; |
| } |
| } |
| case (TEXT): { |
| return navigateToNextNode(currentNode); |
| } |
| default: { |
| // This can only happen if mEventType is |
| // assigned with an unmapped integer. |
| throw new RuntimeException( |
| "Robolectric-> Uknown XML event type: " + mEventType); |
| } |
| } |
| |
| } |
| |
| /*protected*/ int processNextNodeType(Node node) |
| throws XmlPullParserException { |
| switch (node.getNodeType()) { |
| case (Node.ATTRIBUTE_NODE): { |
| throw new IllegalArgumentException("ATTRIBUTE_NODE"); |
| } |
| case (Node.CDATA_SECTION_NODE): { |
| return navigateToNextNode(node); |
| } |
| case (Node.COMMENT_NODE): { |
| return navigateToNextNode(node); |
| } |
| case (Node.DOCUMENT_FRAGMENT_NODE): { |
| throw new IllegalArgumentException("DOCUMENT_FRAGMENT_NODE"); |
| } |
| case (Node.DOCUMENT_NODE): { |
| throw new IllegalArgumentException("DOCUMENT_NODE"); |
| } |
| case (Node.DOCUMENT_TYPE_NODE): { |
| throw new IllegalArgumentException("DOCUMENT_TYPE_NODE"); |
| } |
| case (Node.ELEMENT_NODE): { |
| currentNode = node; |
| return START_TAG; |
| } |
| case (Node.ENTITY_NODE): { |
| throw new IllegalArgumentException("ENTITY_NODE"); |
| } |
| case (Node.ENTITY_REFERENCE_NODE): { |
| throw new IllegalArgumentException("ENTITY_REFERENCE_NODE"); |
| } |
| case (Node.NOTATION_NODE): { |
| throw new IllegalArgumentException("DOCUMENT_TYPE_NODE"); |
| } |
| case (Node.PROCESSING_INSTRUCTION_NODE): { |
| throw new IllegalArgumentException("DOCUMENT_TYPE_NODE"); |
| } |
| case (Node.TEXT_NODE): { |
| if (isWhitespace(node.getNodeValue())) { |
| // Skip whitespaces |
| return navigateToNextNode(node); |
| } else { |
| currentNode = node; |
| return TEXT; |
| } |
| } |
| default: { |
| throw new RuntimeException( |
| "Robolectric -> Unknown node type: " + |
| node.getNodeType() + "."); |
| } |
| } |
| } |
| |
| /** |
| * Navigate to the next node after a node and all of his |
| * children have been explored. |
| * |
| * <p>If the node has unexplored siblings navigate to the |
| * next sibling. Otherwise return to its parent. |
| * |
| * @param node the node which was just explored. |
| * @return {@link XmlPullParserException#START_TAG} if the given |
| * node has siblings, {@link XmlPullParserException#END_TAG} |
| * if the node has no unexplored siblings or |
| * {@link XmlPullParserException#END_DOCUMENT} if the explored |
| * was the root document. |
| * @throws XmlPullParserException if the parser fails to |
| * parse the next node. |
| */ |
| int navigateToNextNode(Node node) |
| throws XmlPullParserException { |
| Node nextNode = node.getNextSibling(); |
| if (nextNode != null) { |
| // Move to the next siblings |
| return processNextNodeType(nextNode); |
| } else { |
| // Goes back to the parent |
| if (document.getDocumentElement().equals(node)) { |
| currentNode = null; |
| return END_DOCUMENT; |
| } |
| currentNode = node.getParentNode(); |
| return END_TAG; |
| } |
| } |
| |
| @Override |
| public void require(int type, String namespace, String name) |
| throws XmlPullParserException, IOException { |
| if (type != getEventType() |
| || (namespace != null && !namespace.equals(getNamespace())) |
| || (name != null && !name.equals(getName()))) { |
| throw new XmlPullParserException( |
| "expected " + TYPES[type] + getPositionDescription()); |
| } |
| } |
| |
| @Override |
| public String nextText() throws XmlPullParserException, IOException { |
| if (getEventType() != START_TAG) { |
| throw new XmlPullParserException( |
| getPositionDescription() |
| + ": parser must be on START_TAG to read next text", this, null); |
| } |
| int eventType = next(); |
| if (eventType == TEXT) { |
| String result = getText(); |
| eventType = next(); |
| if (eventType != END_TAG) { |
| throw new XmlPullParserException( |
| getPositionDescription() |
| + ": event TEXT it must be immediately followed by END_TAG", this, null); |
| } |
| return result; |
| } else if (eventType == END_TAG) { |
| return ""; |
| } else { |
| throw new XmlPullParserException( |
| getPositionDescription() |
| + ": parser must be on START_TAG or TEXT to read text", this, null); |
| } |
| } |
| |
| @Override |
| public int nextTag() throws XmlPullParserException, IOException { |
| int eventType = next(); |
| if (eventType == TEXT && isWhitespace()) { // skip whitespace |
| eventType = next(); |
| } |
| if (eventType != START_TAG && eventType != END_TAG) { |
| throw new XmlPullParserException( |
| "Expected start or end tag. Found: " + eventType, this, null); |
| } |
| return eventType; |
| } |
| |
| @Override |
| public int getAttributeNameResource(int index) { |
| return getResourceId(getAttributeName(index), packageName, "attr"); |
| } |
| |
| @Override |
| public int getAttributeListValue(String namespace, String attribute, |
| String[] options, int defaultValue) { |
| String attr = getAttribute(namespace, attribute); |
| if (attr == null) { |
| return 0; |
| } |
| List<String> optList = Arrays.asList(options); |
| int index = optList.indexOf(attr); |
| if (index == -1) { |
| return defaultValue; |
| } |
| return index; |
| } |
| |
| @Override |
| public boolean getAttributeBooleanValue(String namespace, String attribute, |
| boolean defaultValue) { |
| String attr = getAttribute(namespace, attribute); |
| if (attr == null) { |
| return defaultValue; |
| } |
| return Boolean.parseBoolean(attr); |
| } |
| |
| @Override |
| public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { |
| String attr = getAttribute(namespace, attribute); |
| if (attr != null && attr.startsWith("@") && !AttributeResource.isNull(attr)) { |
| return getResourceId(attr, packageName, null); |
| } |
| return defaultValue; |
| } |
| |
| @Override |
| public int getAttributeIntValue(String namespace, String attribute, int defaultValue) { |
| return XmlUtils.convertValueToInt(this.getAttributeValue(namespace, attribute), defaultValue); |
| } |
| |
| @Override |
| public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) { |
| int value = getAttributeIntValue(namespace, attribute, defaultValue); |
| if (value < 0) { |
| return defaultValue; |
| } |
| return value; |
| } |
| |
| @Override |
| public float getAttributeFloatValue(String namespace, String attribute, |
| float defaultValue) { |
| String attr = getAttribute(namespace, attribute); |
| if (attr == null) { |
| return defaultValue; |
| } |
| try { |
| return Float.parseFloat(attr); |
| } catch (NumberFormatException ex) { |
| return defaultValue; |
| } |
| } |
| |
| @Override |
| public int getAttributeListValue( |
| int idx, String[] options, int defaultValue) { |
| try { |
| String value = getAttributeValue(idx); |
| List<String> optList = Arrays.asList(options); |
| int index = optList.indexOf(value); |
| if (index == -1) { |
| return defaultValue; |
| } |
| return index; |
| } catch (IndexOutOfBoundsException ex) { |
| return defaultValue; |
| } |
| } |
| |
| @Override |
| public boolean getAttributeBooleanValue( |
| int idx, boolean defaultValue) { |
| try { |
| return Boolean.parseBoolean(getAttributeValue(idx)); |
| } catch (IndexOutOfBoundsException ex) { |
| return defaultValue; |
| } |
| } |
| |
| @Override |
| public int getAttributeResourceValue(int idx, int defaultValue) { |
| String attributeValue = getAttributeValue(idx); |
| if (attributeValue != null && attributeValue.startsWith("@")) { |
| int resourceId = getResourceId(attributeValue.substring(1), packageName, null); |
| if (resourceId != 0) { |
| return resourceId; |
| } |
| } |
| return defaultValue; |
| } |
| |
| @Override |
| public int getAttributeIntValue(int idx, int defaultValue) { |
| try { |
| return Integer.parseInt(getAttributeValue(idx)); |
| } catch (NumberFormatException ex) { |
| return defaultValue; |
| } catch (IndexOutOfBoundsException ex) { |
| return defaultValue; |
| } |
| } |
| |
| @Override |
| public int getAttributeUnsignedIntValue(int idx, int defaultValue) { |
| int value = getAttributeIntValue(idx, defaultValue); |
| if (value < 0) { |
| return defaultValue; |
| } |
| return value; |
| } |
| |
| @Override |
| public float getAttributeFloatValue(int idx, float defaultValue) { |
| try { |
| return Float.parseFloat(getAttributeValue(idx)); |
| } catch (NumberFormatException ex) { |
| return defaultValue; |
| } catch (IndexOutOfBoundsException ex) { |
| return defaultValue; |
| } |
| } |
| |
| @Override |
| public String getIdAttribute() { |
| return getAttribute(null, "id"); |
| } |
| |
| @Override |
| public String getClassAttribute() { |
| return getAttribute(null, "class"); |
| } |
| |
| @Override |
| public int getIdAttributeResourceValue(int defaultValue) { |
| String id = getIdAttribute(); |
| if (id == null) { |
| return defaultValue; |
| } |
| try { |
| return Integer.parseInt(id); |
| } catch (NumberFormatException ex) { |
| return defaultValue; |
| } |
| } |
| |
| @Override |
| public int getStyleAttribute() { |
| String attr = getAttribute(null, "style"); |
| if (attr == null) { |
| return 0; |
| } |
| return getResourceId(attr, packageName, "style"); |
| } |
| |
| @Override |
| public void close() { |
| // Nothing to do |
| } |
| |
| @Override |
| protected void finalize() throws Throwable { |
| close(); |
| } |
| |
| private int getResourceId(String possiblyQualifiedResourceName, String defaultPackageName, String defaultType) { |
| |
| if (AttributeResource.isNull(possiblyQualifiedResourceName)) return 0; |
| |
| if (AttributeResource.isStyleReference(possiblyQualifiedResourceName)) { |
| ResName styleReference = AttributeResource.getStyleReference(possiblyQualifiedResourceName, defaultPackageName, defaultType); |
| Integer resourceId = resourceLoader.getResourceIndex().getResourceId(styleReference); |
| if (resourceId == null) { |
| throw new Resources.NotFoundException(styleReference.getFullyQualifiedName()); |
| } |
| return resourceId; |
| } |
| |
| if (AttributeResource.isResourceReference(possiblyQualifiedResourceName)) { |
| ResName resourceReference = AttributeResource.getResourceReference(possiblyQualifiedResourceName, defaultPackageName, defaultType); |
| Integer resourceId = resourceLoader.getResourceIndex().getResourceId(resourceReference); |
| if (resourceId == null) { |
| throw new Resources.NotFoundException(resourceReference.getFullyQualifiedName()); |
| } |
| return resourceId; |
| } |
| possiblyQualifiedResourceName = removeLeadingSpecialCharsIfAny(possiblyQualifiedResourceName); |
| ResName resName = ResName.qualifyResName(possiblyQualifiedResourceName, defaultPackageName, defaultType); |
| Integer resourceId = resourceLoader.getResourceIndex().getResourceId(resName); |
| return resourceId == null ? 0 : resourceId; |
| } |
| |
| private static String removeLeadingSpecialCharsIfAny(String name){ |
| if (name.startsWith("@+")) { |
| return name.substring(2); |
| } |
| if (name.startsWith("@")) { |
| return name.substring(1); |
| } |
| return name; |
| } |
| |
| public ResourceLoader getResourceLoader() { |
| return resourceLoader; |
| } |
| |
| /** |
| * Tell is a given feature is supported by android. |
| * |
| * @param name Feature name. |
| * @return True if the feature is supported. |
| */ |
| private static boolean isAndroidSupportedFeature(String name) { |
| if (name == null) { |
| return false; |
| } |
| for (String feature : AVAILABLE_FEATURES) { |
| if (feature.equals(name)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |