blob: af81852c0c78123899831b8b8fb67f7581937074 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006 Oracle Corporation.
* 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:
* Gerry Kessler/Oracle - initial API and implementation
*
********************************************************************************/
package org.eclipse.jst.jsf.validation.internal;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jst.jsf.common.internal.types.CompositeType;
import org.eclipse.jst.jsf.common.internal.types.TypeComparator;
import org.eclipse.jst.jsf.context.resolver.structureddocument.internal.provisional.IDOMContextResolver;
import org.eclipse.jst.jsf.context.resolver.structureddocument.internal.provisional.IStructuredDocumentContextResolverFactory;
import org.eclipse.jst.jsf.context.resolver.structureddocument.internal.provisional.ITaglibContextResolver;
import org.eclipse.jst.jsf.context.structureddocument.internal.provisional.IStructuredDocumentContext;
import org.eclipse.jst.jsf.context.structureddocument.internal.provisional.IStructuredDocumentContextFactory;
import org.eclipse.jst.jsf.core.internal.JSFCorePlugin;
import org.eclipse.jst.jsf.metadataprocessors.internal.provisional.MetaDataEnabledProcessingFactory;
import org.eclipse.jst.jsf.metadataprocessors.internal.provisional.features.ELIsNotValidException;
import org.eclipse.jst.jsf.metadataprocessors.internal.provisional.features.IValidELValues;
import org.eclipse.jst.jsf.metadataprocessors.internal.provisional.features.IValidValues;
import org.eclipse.jst.jsf.metadataprocessors.internal.provisional.features.IValidationMessage;
import org.eclipse.jst.jsf.validation.internal.el.ELExpressionValidator;
import org.eclipse.jst.jsf.validation.internal.el.diagnostics.DiagnosticFactory;
import org.eclipse.jst.jsf.validation.internal.el.diagnostics.ValidationMessageFactory;
import org.eclipse.jst.jsp.core.internal.domdocument.DOMModelForJSP;
import org.eclipse.jst.jsp.core.internal.regions.DOMJSPRegionContexts;
import org.eclipse.jst.jsp.core.internal.validation.JSPValidator;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
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.ITextRegionCollection;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.sse.ui.internal.reconcile.validator.ISourceValidator;
import org.eclipse.wst.validation.internal.provisional.core.IMessage;
import org.eclipse.wst.validation.internal.provisional.core.IReporter;
import org.eclipse.wst.validation.internal.provisional.core.IValidationContext;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;
/**
* A JSP page validator that makes use of the JSF metadata processing framework so that JSP page
* semantics can be validated.
*
* This implementation currently only validates attribute values.
* @author Gerry Kessler - Oracle
*/
public class JSPSemanticsValidator extends JSPValidator implements ISourceValidator{
// TODO: should the source validator be a separate class in jsp.ui?
// problem with simple split off is that preference must also be split off
static final boolean DEBUG;
static {
String value = Platform.getDebugOption("org.eclipse.jst.jsf.validation.internal.el/debug/jspsemanticsvalidator"); //$NON-NLS-1$
DEBUG = value != null && value.equalsIgnoreCase("true"); //$NON-NLS-1$
}
private IDocument fDocument;
/* (non-Javadoc)
* @see org.eclipse.jst.jsp.core.internal.validation.JSPValidator#validateFile(org.eclipse.core.resources.IFile, org.eclipse.wst.validation.internal.provisional.core.IReporter)
*/
protected void validateFile(IFile file, IReporter reporter) {
IStructuredModel model = null;
if (DEBUG)
System.out.println("executing JSPSemanticsValidator.validateFile");
try {
model = StructuredModelManager.getModelManager().getModelForRead(file);
DOMModelForJSP jspModel = (DOMModelForJSP) model;
IStructuredDocument structuredDoc = jspModel.getStructuredDocument();
IStructuredDocumentRegion curNode = structuredDoc.getFirstStructuredDocumentRegion();
while (null != curNode && !reporter.isCancelled()) {
if (curNode.getFirstRegion().getType() == DOMRegionContext.XML_TAG_OPEN )
{
validateTag(curNode, reporter, file, false);
}
curNode = curNode.getNext();
}
}
catch (CoreException e)
{
JSFCorePlugin.log("Error validating JSF", e);
}
catch (IOException e)
{
JSFCorePlugin.log("Error validating JSF", e);
}
finally
{
if (null != model)
model.releaseFromRead();
}
}
/* (non-Javadoc)
* @see org.eclipse.wst.sse.ui.internal.reconcile.validator.ISourceValidator#validate(org.eclipse.jface.text.IRegion, org.eclipse.wst.validation.internal.provisional.core.IValidationContext, org.eclipse.wst.validation.internal.provisional.core.IReporter)
*/
public void validate(IRegion dirtyRegion, IValidationContext helper, IReporter reporter) {
if (DEBUG)
System.out.println("exec JSPSemanticsValidator.validateRegion");
if (fDocument instanceof IStructuredDocument) {
IStructuredDocument sDoc = (IStructuredDocument) fDocument;
IStructuredDocumentRegion[] regions = sDoc.getStructuredDocumentRegions(dirtyRegion.getOffset(), dirtyRegion.getLength());
if (regions != null){
validateTag(regions[0], reporter, getFile(helper), true);
}
}
}
private IFile getFile(IValidationContext helper) {
String[] uris = helper.getURIs();
IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
if (uris.length > 0)
return wsRoot.getFile(new Path(uris[0]));
return null;
}
/**
* Validates a JSP tag.
*
* Currently only attribute values with supplied annotation meta-data is being validated.
* Also, only JSF EL is being validated and not JSP EL.
*
* This method may be extended in the future to validate tag semantics an other cross attribute
* validations.
*
* @param container
* @param reporter
* @param file
* @param isIncremental -- true if this validation is "as you type"
*
*/
private void validateTag(ITextRegionCollection container, IReporter reporter, IFile file, boolean isIncremental) {
ITextRegionCollection containerRegion = container;
Iterator regions = containerRegion.getRegions().iterator();
ITextRegion region = null;
String uri = null;
String tagName = null;
String attrName = null;
while (regions.hasNext() && !reporter.isCancelled()) {
region = (ITextRegion) regions.next();
String type = region.getType();
IDOMContextResolver resolver = null;
ITaglibContextResolver tagLibResolver = null;
if (type != null && (type == DOMRegionContext.XML_TAG_NAME || type == DOMRegionContext.XML_TAG_ATTRIBUTE_NAME || type == DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE)){
IStructuredDocumentContext context = IStructuredDocumentContextFactory.INSTANCE.getContext(((IStructuredDocumentRegion)containerRegion).getParentDocument(), containerRegion.getStartOffset() + region.getStart());
resolver = IStructuredDocumentContextResolverFactory.INSTANCE.getDOMContextResolver(context);
if (type == DOMRegionContext.XML_TAG_NAME) {
tagLibResolver = IStructuredDocumentContextResolverFactory.INSTANCE.getTaglibContextResolver(context);
tagName = resolver.getNode().getLocalName();
uri = tagLibResolver.getTagURIForNodeName(resolver.getNode());
if (DEBUG)
System.out.println(addDebugSpacer(1)+"tagName= "+ (tagName!= null ? tagName : "null") +": uri= "+(uri != null ? uri : "null") );
}
else if (type == DOMRegionContext.XML_TAG_ATTRIBUTE_NAME){
attrName = resolver.getNode().getNodeName();
if (DEBUG)
System.out.println(addDebugSpacer(2)+"attrName= "+(attrName != null ? attrName : "null" ));
if (uri != null && tagName != null)
{
// TODO: validateAttribute(context, region, uri, resolver.getNode(), file);
}
}
else if (type == DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE){
final String attributeVal =
resolver.getNode().getNodeValue();
// if there's elText then validate it
// TODO: this approach will fail with mixed expressions
if (!checkIfELAndValidate(region,
context,
uri,
tagName,
attrName,
attributeVal,
isIncremental,
reporter,
file)
)
{
// else validate as static attribute value
if (DEBUG)
System.out.println(addDebugSpacer(3)+"attrVal= "+(attributeVal != null ? attributeVal : "null") );
if (uri != null && tagName != null && attrName != null)
validateAttributeValue(context, uri, tagName, attrName, attributeVal, reporter, file);
}
}
}
}
}
/**
* Checks the region to see if it contains an EL attribute value. If it
* does, validates it
* @return true if validated EL, false otherwise
*/
private boolean checkIfELAndValidate(ITextRegion region,
IStructuredDocumentContext context,
String uri,
String tagName,
String attrName,
String attrValue,
boolean isIncremental,
IReporter reporter,
IFile file)
{
if (region instanceof ITextRegionCollection) {
ITextRegionCollection parentRegion = ((ITextRegionCollection) region);
if (parentRegion.getType() == DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE)
{
// look for attribute pattern "#{}"
// TODO: need to generalize this for RValue concatenation
final ITextRegionList regionList = parentRegion.getRegions();
if (regionList.size() >= 4)
{
ITextRegion openQuote = regionList.get(0);
ITextRegion openVBLQuote = regionList.get(1);
if ( (openQuote.getType() == DOMJSPRegionContexts.XML_TAG_ATTRIBUTE_VALUE_DQUOTE
|| openQuote.getType() == DOMJSPRegionContexts.XML_TAG_ATTRIBUTE_VALUE_SQUOTE)
&& (openVBLQuote.getType() == DOMJSPRegionContexts.JSP_VBL_OPEN))
{
// we appear to be inside "#{", so next should be a VBL_CONTENT if there's anything
// here to validate
final ITextRegion content = regionList.get(2);
if (content.getType() == DOMJSPRegionContexts.JSP_VBL_CONTENT)
{
final int contentStart =
parentRegion.getStartOffset(content);
final IStructuredDocumentContext elContext =
IStructuredDocumentContextFactory.INSTANCE.
getContext(context.getStructuredDocument(),
contentStart);
final String elText = parentRegion.getText(content);
if (DEBUG)
System.out.println(addDebugSpacer(3)+"EL attrVal= "+elText);
// EL validation is user configurable because
// it can be computationally costly.
if (checkShouldValidateEL(isIncremental))
{
List elVals = MetaDataEnabledProcessingFactory.getInstance().getAttributeValueRuntimeTypeFeatureProcessors(IValidELValues.class, elContext, uri, tagName, attrName);
validateELExpression(context,
elContext,
elVals,
attrValue,
elText,
reporter,
file);
}
}
else if (content.getType() == DOMJSPRegionContexts.JSP_VBL_CLOSE)
{
final int offset = parentRegion.getStartOffset(openVBLQuote)+1;
final int length = 2;
// detected empty EL expression
reporter.addMessage(this,
ValidationMessageFactory.createFromDiagnostic(
DiagnosticFactory.create_EMPTY_EL_EXPRESSION(),
offset, length, file));
}
boolean foundClosingQuote = false;
for (int i = 2; !foundClosingQuote && i < regionList.size(); i++)
{
ITextRegion searchRegion = regionList.get(i);
if (searchRegion.getType() == DOMJSPRegionContexts.JSP_VBL_CLOSE)
{
foundClosingQuote = true;
}
}
if (!foundClosingQuote)
{
int offset = context.getDocumentPosition()+1;
int length = parentRegion.getText().length();
reporter.addMessage(this,
ValidationMessageFactory.
createFromDiagnostic(
DiagnosticFactory.create_MISSING_CLOSING_EXPR_BRACKET(),
offset, length, file));
}
return true;
}
}
}
}
return false;
}
private void validateELExpression(IStructuredDocumentContext context,
IStructuredDocumentContext elContext,
List elVals,
String attributeVal,
String elText,
IReporter reporter,
IFile file)
{
//Call EL validator which will perform at least the syntactical validation
final ELExpressionValidator elValidator =
new ELExpressionValidator(elContext, elText,file);
elValidator.validateXMLNode();
elValidator.reportFindings(this, reporter);
CompositeType exprType = elValidator.getExpressionType();
if (exprType != null)
{
for (Iterator it=elVals.iterator();it.hasNext();){
IValidELValues elval = (IValidELValues)it.next();
CompositeType expectedType;
IMessage message = null;
try {
expectedType = elval.getExpectedRuntimeType();
if (expectedType != null)
{
Diagnostic status = TypeComparator.calculateTypeCompatibility
(expectedType, exprType);
if (status.getSeverity() != Diagnostic.OK){
message = createValidationMessage(context, attributeVal, getSeverity(status.getSeverity()), status.getMessage(), file);
}
}
} catch (ELIsNotValidException e) {
message = createValidationMessage(context, attributeVal, IMessage.NORMAL_SEVERITY, e.getMessage(), file);
}
if (message != null) {
reporter.addMessage(this, message);
}
}
}
}
// private void validateAttribute(IStructuredDocumentContext context, ITextRegion region, String uri, Node attr, IFile file) {
//Not doing anything until the resolver can help me
//validate that attribute can be part of the tag
// ITaglibContextResolver tagLibResolver = IStructuredDocumentContextResolverFactory.INSTANCE.getTaglibContextResolver(context);
// if (tagLibResolver.getTagURIForNodeName(attr) == null){
// System.out.println("not ok: "+attr.getNodeName());
// }
// else
// System.out.println("ok");
// }
/**
* Validates an attribute value in context using the JSF metadata processing framework
*
* @param context
* @param region
* @param uri
* @param tagName
* @param attrName
* @param attributeVal
* @param reporter
* @param file
*/
private void validateAttributeValue(IStructuredDocumentContext context, String uri, String tagName, String attrName, String attributeVal, IReporter reporter, IFile file) {
List vv = MetaDataEnabledProcessingFactory.getInstance().getAttributeValueRuntimeTypeFeatureProcessors(IValidValues.class, context, uri, tagName, attrName);
if (!vv.isEmpty()){
for (Iterator it = vv.iterator();it.hasNext();){
IValidValues v = (IValidValues)it.next();
if (!v.isValidValue(attributeVal.trim())){
if (DEBUG)
System.out.println(addDebugSpacer(4)+"NOT VALID ");
for (Iterator msgs = v.getValidationMessages().iterator();msgs.hasNext();){
IValidationMessage msg = (IValidationMessage)msgs.next();
IMessage message = createValidationMessage(context, attributeVal, getSeverity(msg.getSeverity()), msg.getMessage(), file);
if (message != null) {
reporter.addMessage(this, message);
}
}
}
else
if (DEBUG)
System.out.println(addDebugSpacer(5) + "VALID ");
}
}
else if (DEBUG)
System.out.println(addDebugSpacer(4)+"NO META DATA ");
}
private IMessage createValidationMessage(IStructuredDocumentContext context, String attributeValue, int severity, String msg, IFile file){
IMessage message = new LocalizedMessage(severity, msg, file);
if (message != null) {
final int start = context.getDocumentPosition() + 1;
final int length = attributeValue.length();
int lineNo = 0;
try {
lineNo = context.getStructuredDocument().getLineOfOffset(start);
} catch (BadLocationException e) {
// TODO: C.B why need line number? Length and offset should be
// sufficient
}
message.setLineNo(lineNo);
message.setOffset(start);
message.setLength(length);
}
return message;
}
/**
* Maps IStatus codes to IMessage severity
* @param IStatus codesseverity
* @return IMessage severity
*/
private int getSeverity(int severity) {
switch (severity){
case IStatus.ERROR:
return IMessage.HIGH_SEVERITY;
case IStatus.WARNING:
return IMessage.NORMAL_SEVERITY;
case IStatus.INFO:
return IMessage.LOW_SEVERITY;
}
return IMessage.NORMAL_SEVERITY;
}
/* (non-Javadoc)
* @see org.eclipse.wst.sse.ui.internal.reconcile.validator.ISourceValidator#connect(org.eclipse.jface.text.IDocument)
*/
public void connect(IDocument document) {
fDocument = document;
}
/* (non-Javadoc)
* @see org.eclipse.wst.sse.ui.internal.reconcile.validator.ISourceValidator#disconnect(org.eclipse.jface.text.IDocument)
*/
public void disconnect(IDocument document) {
// do nothing; no disconnect logic
}
private String addDebugSpacer(int count){
String TAB = "\t";
StringBuffer ret = new StringBuffer("");
for(int i=0;i<=count;i++){
ret.append(TAB);
}
return ret.toString();
}
/**
* @param isIncremental -- true if this is "as-you-type" validation, false
* if this is "Build" or "Run Validation" validation
* @return true if user preferences say we should do EL validation,
* false otherwise
*/
private boolean checkShouldValidateEL(boolean isIncremental)
{
final ValidationPreferences prefs = new ValidationPreferences();
prefs.load();
if (isIncremental)
{
return prefs.getElPrefs().isEnableIncrementalValidation();
}
return prefs.getElPrefs().isEnableBuildValidation();
}
}