| /******************************************************************************* |
| * Copyright (c) 2004, 2009 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.wst.html.core.internal.validate; |
| |
| import java.util.List; |
| import java.util.Locale; |
| |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionContainer; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; |
| import org.eclipse.wst.xml.core.internal.contentmodel.CMAttributeDeclaration; |
| import org.eclipse.wst.xml.core.internal.contentmodel.CMDataType; |
| import org.eclipse.wst.xml.core.internal.contentmodel.CMElementDeclaration; |
| import org.eclipse.wst.xml.core.internal.contentmodel.CMNamedNodeMap; |
| import org.eclipse.wst.xml.core.internal.contentmodel.CMNode; |
| import org.eclipse.wst.xml.core.internal.contentmodel.modelquery.ModelQuery; |
| import org.eclipse.wst.xml.core.internal.modelquery.ModelQueryUtil; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMAttr; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMElement; |
| import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| |
| public class HTMLAttributeValidator extends PrimeValidator { |
| |
| private static final int REGION_NAME = 1; |
| private static final int REGION_VALUE = 2; |
| // <<D210422 |
| private static final char SINGLE_QUOTE = '\''; |
| private static final char DOUBLE_QUOTE = '\"'; |
| |
| // D210422 |
| /** |
| * HTMLAttributeValidator constructor comment. |
| */ |
| public HTMLAttributeValidator() { |
| super(); |
| } |
| |
| /** |
| */ |
| private Segment getErrorSegment(IDOMNode errorNode, int regionType) { |
| ITextRegion rgn = null; |
| switch (regionType) { |
| case REGION_NAME : |
| rgn = errorNode.getNameRegion(); |
| break; |
| case REGION_VALUE : |
| rgn = errorNode.getValueRegion(); |
| break; |
| default : |
| // nothing to do. |
| break; |
| } |
| if (rgn != null) { |
| if (errorNode instanceof IDOMAttr) { |
| IDOMElement ownerElement = (IDOMElement) ((IDOMAttr) errorNode).getOwnerElement(); |
| if (ownerElement != null) { |
| int regionStartOffset = ownerElement.getFirstStructuredDocumentRegion().getStartOffset(rgn); |
| int regionLength = rgn.getTextLength(); |
| return new Segment(regionStartOffset, regionLength); |
| } |
| } |
| } |
| return new Segment(errorNode.getStartOffset(), 1); |
| } |
| |
| /** |
| * Allowing the INodeAdapter to compare itself against the type allows it |
| * to return true in more than one case. |
| */ |
| public boolean isAdapterForType(Object type) { |
| return ((type == HTMLAttributeValidator.class) || super.isAdapterForType(type)); |
| } |
| |
| /** |
| */ |
| public void validate(IndexedRegion node) { |
| Element target = (Element) node; |
| if (CMUtil.isForeign(target)) |
| return; |
| CMElementDeclaration edec = CMUtil.getDeclaration(target); |
| if (edec == null) |
| return; |
| CMNamedNodeMap declarations = edec.getAttributes(); |
| |
| List modelQueryNodes = null; |
| NamedNodeMap attrs = target.getAttributes(); |
| for (int i = 0; i < attrs.getLength(); i++) { |
| int rgnType = REGION_NAME; |
| int state = ErrorState.NONE_ERROR; |
| Attr a = (Attr) attrs.item(i); |
| // D203637; If the target attr has prefix, the validator should |
| // not |
| // warn about it. That is, just ignore. It is able to check |
| // whether |
| // an attr has prefix or not by calling XMLAttr#isGlobalAttr(). |
| // When a attr has prefix (not global), it returns false. |
| boolean isXMLAttr = a instanceof IDOMAttr; |
| if (isXMLAttr) { |
| IDOMAttr xmlattr = (IDOMAttr) a; |
| if (!xmlattr.isGlobalAttr()) |
| continue; // skip futher validation and begin next loop. |
| } |
| |
| CMAttributeDeclaration adec = (CMAttributeDeclaration) declarations.getNamedItem(a.getName()); |
| |
| /* Check the modelquery if nothing is declared by the element declaration */ |
| if (adec == null) { |
| if (modelQueryNodes == null) |
| modelQueryNodes = ModelQueryUtil.getModelQuery(target.getOwnerDocument()).getAvailableContent((Element) node, edec, ModelQuery.INCLUDE_ATTRIBUTES); |
| |
| String attrName = a.getName().toLowerCase(Locale.US); |
| for (int k = 0; k < modelQueryNodes.size(); k++) { |
| CMNode cmnode = (CMNode) modelQueryNodes.get(k); |
| if (cmnode.getNodeType() == CMNode.ATTRIBUTE_DECLARATION && cmnode.getNodeName().toLowerCase(Locale.US).equals(attrName)) { |
| adec = (CMAttributeDeclaration) cmnode; |
| break; |
| } |
| } |
| } |
| |
| if (adec == null) { |
| // No attr declaration was found. That is, the attr name is |
| // undefined. |
| // but not regard it as undefined name if it includes nested |
| // region |
| if (!hasNestedRegion(((IDOMNode) a).getNameRegion())) { |
| rgnType = REGION_NAME; |
| state = ErrorState.UNDEFINED_NAME_ERROR; |
| } |
| } else { |
| // The attr declaration was found. |
| // At 1st, the name should be checked. |
| if (CMUtil.isHTML(edec) && (!CMUtil.isXHTML(edec))) { |
| // If the target element is pure HTML (not XHTML), some |
| // attributes |
| // might be written in boolean format. It should be check |
| // specifically. |
| if (CMUtil.isBooleanAttr(adec) && ((IDOMAttr) a).hasNameOnly()) |
| continue; // OK, keep going. No more check is needed |
| // against this attr. |
| } else { |
| // If the target is other than pure HTML (JSP or XHTML), |
| // the name |
| // must be checked exactly (ie in case sensitive way). |
| String actual = a.getName(); |
| String desired = adec.getAttrName(); |
| if (!actual.equals(desired)) { // case mismatch |
| rgnType = REGION_NAME; |
| state = ErrorState.MISMATCHED_ERROR; |
| } |
| } |
| // Then, the value must be checked. |
| if (state == ErrorState.NONE_ERROR) { // Need more check. |
| // Now, the value should be checked, if the type is ENUM. |
| CMDataType attrType = adec.getAttrType(); |
| String actualValue = a.getValue(); |
| if (attrType.getImpliedValueKind() == CMDataType.IMPLIED_VALUE_FIXED) { |
| // Check FIXED value. |
| String validValue = attrType.getImpliedValue(); |
| if (!actualValue.equals(validValue)) { |
| rgnType = REGION_VALUE; |
| state = ErrorState.UNDEFINED_VALUE_ERROR; |
| } |
| } |
| else if (CMDataType.URI.equals(attrType.getDataTypeName())) { |
| // TODO: URI validation? |
| if (false && actualValue.indexOf('#') < 0 && actualValue.indexOf(":/") == -1 && CMUtil.isHTML(edec)) { //$NON-NLS-1$ //$NON-NLS-2$ |
| IStructuredDocumentRegion start = ((IDOMNode) node).getStartStructuredDocumentRegion(); |
| if (start != null && start.getFirstRegion().getTextLength() == 1) { |
| IPath basePath = new Path(((IDOMNode) node).getModel().getBaseLocation()); |
| if (basePath.segmentCount() > 1) { |
| IPath path = ModuleCoreSupport.resolve(basePath, actualValue); |
| IResource found = ResourcesPlugin.getWorkspace().getRoot().findMember(path); |
| if (found == null || !found.isAccessible()) { |
| rgnType = REGION_VALUE; |
| state = ErrorState.RESOURCE_NOT_FOUND; |
| } |
| } |
| } |
| } |
| } |
| else if (CMDataType.ENUM.equals(attrType.getDataTypeName())) { |
| /* |
| * Check current value is valid among a known list. |
| * There may be enumerated values provided even when |
| * the datatype is not ENUM, but we'll only validate |
| * against that list if the type matches. |
| */ |
| String[] enumeratedValues = attrType.getEnumeratedValues(); |
| // several candidates are found. |
| boolean found = false; |
| for (int j = 0; j < enumeratedValues.length; j++) { |
| // At 1st, compare ignoring case. |
| if (actualValue.equalsIgnoreCase(enumeratedValues[j])) { |
| found = true; |
| if (CMUtil.isCaseSensitive(edec) && (!actualValue.equals(enumeratedValues[j]))) { |
| rgnType = REGION_VALUE; |
| state = ErrorState.MISMATCHED_VALUE_ERROR; |
| } |
| break; // exit the loop. |
| } |
| } |
| if (!found) { |
| // retrieve and check extended values (retrieval can call extensions, which may take longer) |
| String[] modelQueryExtensionValues = ModelQueryUtil.getModelQuery(target.getOwnerDocument()).getPossibleDataTypeValues((Element) node, adec); |
| // copied loop from above |
| for (int j = 0; j < modelQueryExtensionValues.length; j++) { |
| // At 1st, compare ignoring case. |
| if (actualValue.equalsIgnoreCase(modelQueryExtensionValues[j])) { |
| found = true; |
| if (CMUtil.isCaseSensitive(edec) && (!actualValue.equals(modelQueryExtensionValues[j]))) { |
| rgnType = REGION_VALUE; |
| state = ErrorState.MISMATCHED_VALUE_ERROR; |
| } |
| break; // exit the loop. |
| } |
| } |
| // No candidate was found. That is, |
| // actualValue is invalid. |
| // but not regard it as undefined value if it |
| // includes nested region. |
| if (!hasNestedRegion(((IDOMNode) a).getValueRegion())) { |
| rgnType = REGION_VALUE; |
| state = ErrorState.UNDEFINED_VALUE_ERROR; |
| } |
| } |
| } |
| } |
| // <<D210422 |
| if (state == ErrorState.NONE_ERROR) { // Need more check. |
| if (isXMLAttr) { |
| String source = ((IDOMAttr) a).getValueRegionText(); |
| if (source != null) { |
| char firstChar = source.charAt(0); |
| char lastChar = source.charAt(source.length() - 1); |
| if (isQuote(firstChar) || isQuote(lastChar)) { |
| if (lastChar != firstChar) { |
| rgnType = REGION_VALUE; |
| state = ErrorState.UNCLOSED_ATTR_VALUE; |
| } |
| } |
| } |
| } |
| } |
| // D210422 |
| } |
| if (state != ErrorState.NONE_ERROR) { |
| Segment seg = getErrorSegment((IDOMNode) a, rgnType); |
| if (seg != null) |
| reporter.report(new ErrorInfoImpl(state, seg, a)); |
| } |
| } |
| } |
| |
| /** |
| * True if container has nested regions, meaning container is probably too |
| * complicated (like JSP regions) to validate with this validator. |
| */ |
| private boolean hasNestedRegion(ITextRegion container) { |
| if (!(container instanceof ITextRegionContainer)) |
| return false; |
| ITextRegionList regions = ((ITextRegionContainer) container).getRegions(); |
| if (regions == null) |
| return false; |
| // BUG207194: return true by default as long as container is an |
| // ITextRegionContainer with at least 1 region |
| return true; |
| } |
| |
| // <<D214022 |
| private boolean isQuote(char c) { |
| return (c == SINGLE_QUOTE) || (c == DOUBLE_QUOTE); |
| } |
| // D210422 |
| } |