blob: f30a5c445304f3295b038a85dcc07b99596c2b58 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2001, 2008 Oracle 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:
* Oracle Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jst.jsf.validation.internal.strategy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jst.jsf.common.dom.AttrDOMAdapter;
import org.eclipse.jst.jsf.common.dom.AttributeIdentifier;
import org.eclipse.jst.jsf.common.dom.DOMAdapter;
import org.eclipse.jst.jsf.common.internal.types.CompositeType;
import org.eclipse.jst.jsf.common.internal.types.TypeComparator;
import org.eclipse.jst.jsf.common.internal.types.TypeComparatorDiagnosticFactory;
import org.eclipse.jst.jsf.common.internal.types.TypeConstants;
import org.eclipse.jst.jsf.common.internal.types.TypeTransformer;
import org.eclipse.jst.jsf.common.runtime.internal.model.ViewObject;
import org.eclipse.jst.jsf.common.runtime.internal.model.component.ComponentFactory;
import org.eclipse.jst.jsf.common.runtime.internal.model.component.ComponentInfo;
import org.eclipse.jst.jsf.common.runtime.internal.model.decorator.ConverterDecorator;
import org.eclipse.jst.jsf.common.runtime.internal.model.decorator.ConverterTypeInfo;
import org.eclipse.jst.jsf.context.structureddocument.IStructuredDocumentContext;
import org.eclipse.jst.jsf.context.structureddocument.IStructuredDocumentContextFactory;
import org.eclipse.jst.jsf.core.internal.JSFCorePlugin;
import org.eclipse.jst.jsf.core.internal.region.Region2AttrAdapter;
import org.eclipse.jst.jsf.core.internal.region.Region2ElementAdapter;
import org.eclipse.jst.jsf.core.jsfappconfig.internal.IJSFAppConfigManager;
import org.eclipse.jst.jsf.core.jsfappconfig.internal.JSFAppConfigManagerFactory;
import org.eclipse.jst.jsf.designtime.DTAppManagerUtil;
import org.eclipse.jst.jsf.designtime.internal.view.DTUIViewRoot;
import org.eclipse.jst.jsf.designtime.internal.view.IDTViewHandler.ViewHandlerException;
import org.eclipse.jst.jsf.designtime.internal.view.XMLViewDefnAdapter;
import org.eclipse.jst.jsf.designtime.internal.view.XMLViewDefnAdapter.DTELExpression;
import org.eclipse.jst.jsf.designtime.internal.view.XMLViewObjectMappingService;
import org.eclipse.jst.jsf.designtime.internal.view.XMLViewObjectMappingService.ElementData;
import org.eclipse.jst.jsf.facesconfig.emf.ConverterForClassType;
import org.eclipse.jst.jsf.facesconfig.emf.ConverterType;
import org.eclipse.jst.jsf.metadataprocessors.MetaDataEnabledProcessingFactory;
import org.eclipse.jst.jsf.metadataprocessors.features.ELIsNotValidException;
import org.eclipse.jst.jsf.metadataprocessors.features.IValidELValues;
import org.eclipse.jst.jsf.metadataprocessors.features.IValidValues;
import org.eclipse.jst.jsf.metadataprocessors.features.IValidationMessage;
import org.eclipse.jst.jsf.validation.internal.AbstractXMLViewValidationStrategy;
import org.eclipse.jst.jsf.validation.internal.JSFValidationContext;
import org.eclipse.jst.jsf.validation.internal.el.ELExpressionValidator;
import org.eclipse.jst.jsp.core.internal.regions.DOMJSPRegionContexts;
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.xml.core.internal.regions.DOMRegionContext;
/**
* An XML view validation strategy that validates
*
* @author cbateman
*
*/
public class AttributeValidatingStrategy extends
AbstractXMLViewValidationStrategy
{
private static final String DISABLE_ALTERATIVE_TYPES_KEY = "jsfCoreDisableConverterValidation"; //$NON-NLS-1$
private static final String ENABLE_ALTERATIVE_TYPES_KEY = "jsfCoreEnableConverterValidation"; //$NON-NLS-1$
static final boolean DEBUG;
static
{
final 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$
}
/**
* identifier
*/
public final static String ID = "org.eclipse.jst.jsf.validation.strategy.AttributeValidatingStrategy"; //$NON-NLS-1$
private final static String DISPLAY_NAME = "Attribute Validator"; //$NON-NLS-1$
private final JSFValidationContext _validationContext;
private final TypeComparator _typeComparator;
private Set<String> _conversionTypes;
/**
* Default constructor
*
* @param validationContext
*/
public AttributeValidatingStrategy(final JSFValidationContext validationContext)
{
super(ID, DISPLAY_NAME);
_validationContext = validationContext;
_typeComparator = new TypeComparator(
new TypeComparatorDiagnosticFactory(validationContext
.getPrefs().getTypeComparatorPrefs()));
}
@Override
public boolean isInteresting(final DOMAdapter domAdapter)
{
return (domAdapter instanceof AttrDOMAdapter);
}
@Override
public void validate(final DOMAdapter domAdapter)
{
if (domAdapter instanceof AttrDOMAdapter)
{
final long curTime = System.nanoTime();
final Region2AttrAdapter attrAdapter = (Region2AttrAdapter) domAdapter;
// check that this is attribute value region - 221722
if (attrAdapter.getAttributeValueRegion() != null)
{
final IStructuredDocumentContext context = IStructuredDocumentContextFactory.INSTANCE
.getContext(attrAdapter.getDocumentContext()
.getStructuredDocument(), attrAdapter
.getOwningElement().getDocumentContext()
.getDocumentPosition()
+ attrAdapter.getAttributeValueRegion()
.getStart());
validateAttributeValue(context, attrAdapter);
}
if (DEBUG)
{
System.out.println(String.format("Validation for attribute: %s took %d" //$NON-NLS-1$
, domAdapter.toString()
, Long.valueOf(System.nanoTime()-curTime)));
}
}
}
/**
* Validates the attribute value. Reports any problems to the reporter in
* the JSFValidationContext.
*
* @param context
* @param attrAdapter
*/
private void validateAttributeValue(
final IStructuredDocumentContext context,
final Region2AttrAdapter attrAdapter)
{
// so of the code in run calls out into extension code or code
// dependent on external data (meta-data)
SafeRunner.run(new ISafeRunnable()
{
public void handleException(final Throwable exception)
{
JSFCorePlugin.log(String.format(
"Error validating attribute: %s on element %s", //$NON-NLS-1$
attrAdapter.getNodeName(), attrAdapter
.getOwningElement().getNodeName()), exception);
}
public void run() throws Exception
{
final Region2ElementAdapter elementAdapter =
attrAdapter.getOwningElement();
// if there's elText then validate it
// TODO: this approach will fail with mixed expressions
if (!checkIfELAndValidate(elementAdapter, attrAdapter, context))
{
validateNonELAttributeValue(context, attrAdapter);
}
}
});
}
private boolean checkIfELAndValidate(final Region2ElementAdapter elementAdapter,
final Region2AttrAdapter attrAdapter,
final IStructuredDocumentContext context)
{
int offsetOfFirstEL = -1;
final String attrValue = attrAdapter.getValue();
// TODO: should find and validate all
offsetOfFirstEL = attrValue.indexOf('#');
if (offsetOfFirstEL != -1 && offsetOfFirstEL < attrValue.length() - 1
&& attrValue.charAt(offsetOfFirstEL + 1) == '{')
{
offsetOfFirstEL += 2;
}
else
{
offsetOfFirstEL = -1;
}
final XMLViewDefnAdapter adapter = DTAppManagerUtil
.getXMLViewDefnAdapter(context);
boolean isEL = false;
if (adapter != null && offsetOfFirstEL != -1)
{
try
{
// use the attribute's context plus the offset into the
// whole attribute value to find where we think the el
// expression starts. Add one since the attribute value
// string returned by attrAdapter will have the value quotes
// removed, but the region offsets include the quotes.
IStructuredDocumentContext elContext = IStructuredDocumentContextFactory.INSTANCE
.getContext(context.getStructuredDocument(), context
.getDocumentPosition()
+ offsetOfFirstEL + 1);
final DTELExpression elExpression = adapter
.getELExpression(elContext);
if (elExpression != null)
{
final String elText = elExpression.getText();
if (DEBUG)
{
System.out.println(addDebugSpacer(3) + "EL attrVal= " //$NON-NLS-1$
+ elText);
}
elContext = elExpression.getDocumentContext();
// EL validation is user configurable because
// it can be computationally costly.
if (_validationContext.shouldValidateEL())
{
// also, skip the validation if the expression is empty
// or only whitespace, since the parser doesn't handle
// it
// anyway.
if ("".equals(elText.trim())) //$NON-NLS-1$
{
final int offset = elContext.getDocumentPosition() - 1;
final int length = elText.length() + 2;
final Diagnostic diagnostic = _validationContext
.getDiagnosticFactory()
.create_EMPTY_EL_EXPRESSION();
// detected empty EL expression
if (_validationContext.shouldValidateEL())
{
_validationContext.getReporter().report(
diagnostic, offset, length);
}
}
else
{
final List elVals = MetaDataEnabledProcessingFactory
.getInstance()
.getAttributeValueRuntimeTypeFeatureProcessors(
IValidELValues.class,
elContext,
attrAdapter
.getAttributeIdentifier());
final String safeELText = elText.replaceAll(
"[\n\r\t]", " "); //$NON-NLS-1$ //$NON-NLS-2$
validateELExpression(context, elContext, elVals,
elementAdapter, attrAdapter, safeELText);
isEL = true;
}
} else {
isEL = true;
}
}
}
catch (final ViewHandlerException e)
{
// fall through to return false
}
}
// is el if we've already detected it or if the step 2 method
// finds it. Run the method first to avoid short-circuiting
final boolean isEL2 = checkIfELAndValidate2(attrAdapter, context);
return isEL || isEL2;
}
/**
* 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 checkIfELAndValidate2(final Region2AttrAdapter attrAdapter,
final IStructuredDocumentContext sDocContext)
{
final ITextRegion attrValueRegion = attrAdapter
.getAttributeValueRegion();
if (attrValueRegion instanceof ITextRegionCollection)
{
final ITextRegionCollection parentRegion = ((ITextRegionCollection) attrValueRegion);
if (parentRegion.getType() == DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE)
{
final ITextRegionList regionList = parentRegion.getRegions();
if (regionList.size() >= 3)
{
final ITextRegion openQuote = regionList.get(0);
final ITextRegion vblOpen = regionList.get(1);
if ((openQuote.getType() == DOMJSPRegionContexts.XML_TAG_ATTRIBUTE_VALUE_DQUOTE || openQuote
.getType() == DOMJSPRegionContexts.JSP_VBL_DQUOTE ||
openQuote.getType() == DOMJSPRegionContexts.JSP_TAG_ATTRIBUTE_VALUE_DQUOTE)
&& vblOpen.getType() == DOMJSPRegionContexts.JSP_VBL_OPEN)
{
boolean foundClosingQuote = false;
for (int i = 2; !foundClosingQuote
&& i < regionList.size(); i++)
{
final ITextRegion searchRegion = regionList.get(i);
if (searchRegion.getType() == DOMJSPRegionContexts.JSP_VBL_CLOSE)
{
foundClosingQuote = true;
}
}
if (!foundClosingQuote
&& _validationContext.shouldValidateEL())
{
final int offset = sDocContext
.getDocumentPosition() + 1;
final int length = parentRegion.getText().length();
final Diagnostic diagnostic = _validationContext
.getDiagnosticFactory()
.create_MISSING_CLOSING_EXPR_BRACKET();
_validationContext.getReporter().report(diagnostic,
offset, length);
}
return true;
}
}
}
}
return false;
}
private void validateELExpression(final IStructuredDocumentContext context,
final IStructuredDocumentContext elContext, final List elVals,
final Region2ElementAdapter elementAdapter,
final Region2AttrAdapter attrAdapter, final String elText)
{
// Call EL validator which will perform at least the syntactical
// validation
final ELExpressionValidator elValidator = new ELExpressionValidator(
elContext, elText, _validationContext
.getSymbolResolverFactory(), _validationContext
.getReporter());
elValidator.validateXMLNode();
final CompositeType exprType = elValidator.getExpressionType();
if (exprType != null)
{
// Ignore the expression whose last two segments are of types Object.
final CompositeType boxedType = TypeTransformer
.transformBoxPrimitives(exprType);
final String[] testSignatures = boxedType.getSignatures();
if (testSignatures.length > 0 && TypeConstants.TYPE_JAVAOBJECT.equals(testSignatures[0]))
{
if (elText.indexOf('.') != -1)
{
String elText2 = elText.substring(0, elText.lastIndexOf('.'));
final ELExpressionValidator elValidator2 = new ELExpressionValidator(
elContext, elText2, _validationContext
.getSymbolResolverFactory(), _validationContext
.getReporter());
elValidator2.validateXMLNode();
final CompositeType exprType2 = elValidator.getExpressionType();
final CompositeType boxedType2 = TypeTransformer.transformBoxPrimitives(exprType2);
final String[] testSignatures2 = boxedType2.getSignatures();
if (testSignatures2.length > 0 && TypeConstants.TYPE_JAVAOBJECT.equals(testSignatures2[0]))
{
return;
}
}
}
for (final Iterator it = elVals.iterator(); it.hasNext();)
{
final IValidELValues elval = (IValidELValues) it.next();
final String attributeVal = attrAdapter.getValue();
CompositeType expectedType;
Diagnostic status = null;
try
{
expectedType = elval.getExpectedRuntimeType();
if (expectedType != null)
{
expectedType = maybeAddAlternativeTypes(
expectedType, exprType, elementAdapter,
attrAdapter);
status = _typeComparator.calculateTypeCompatibility(
expectedType, exprType);
if (status.getSeverity() != Diagnostic.OK)
{
reportValidationMessage(status, context,
attributeVal);
}
}
}
catch (final ELIsNotValidException e)
{
reportValidationMessage(createValidationMessage(context,
attributeVal, IStatus.WARNING, e.getMessage(),
_validationContext.getFile()), context,
attributeVal);
}
}
}
}
private boolean disableAlternativeTypes()
{
if (hasProperty(DISABLE_ALTERATIVE_TYPES_KEY))
{
return true;
}
if (hasProperty(ENABLE_ALTERATIVE_TYPES_KEY))
{
return false;
}
// As of Helios, alternative type is disabled by default
return true;
// final IPreferenceStore prefStore = JSFCorePlugin.getDefault().getPreferenceStore();
// return prefStore.getBoolean("org.eclipse.jst.jsf.core."+DISABLE_ALTERATIVE_TYPES_KEY); //$NON-NLS-1$
}
private boolean hasProperty(final String key) {
String res = System.getProperty(key);
if (res == null) {
//check env var also
res = System.getenv(key);
}
return res != null;
}
/**
* @return true if alternative type comparison (i.e. post-conversion) passes
*/
private CompositeType maybeAddAlternativeTypes(
final CompositeType expectedType, final CompositeType exprTypes,
final Region2ElementAdapter elementAdapter,
final Region2AttrAdapter attrAdapter)
{
final long curTime = System.nanoTime();
//As of Helios, alternative type is disabled by default
//and enabled by the ENABLE_ALTERNATIVE_TYPES_KEY system/env property
if (disableAlternativeTypes())
{
return expectedType;
}
final IStructuredDocumentContext context = elementAdapter
.getDocumentContext();
final DTUIViewRoot viewRoot = _validationContext.getViewRootHandle().getCachedViewRoot();
final IAdaptable serviceAdaptable = viewRoot.getServices();
final XMLViewObjectMappingService mappingService = (XMLViewObjectMappingService) serviceAdaptable
.getAdapter(XMLViewObjectMappingService.class);
if (mappingService != null)
{
final ElementData elementData = XMLViewObjectMappingService
.createElementData(elementAdapter.getNamespace(),
elementAdapter.getLocalName(), context,
Collections.EMPTY_MAP);
final ViewObject viewObject = mappingService
.findViewObject(elementData);
// if the corresponding view object is a valueholder, then
// we need to see if you think there a valid conversion
// available
if (viewObject instanceof ComponentInfo
&& ((ComponentInfo) viewObject).getComponentTypeInfo() != null
&& ((ComponentInfo) viewObject).getComponentTypeInfo()
.isInstanceOf(
ComponentFactory.INTERFACE_VALUEHOLDER))
{
final ComponentInfo component = (ComponentInfo) viewObject;
// get the original elementData
final ElementData mappedElementData = mappingService
.findElementData(component);
final String propName = mappedElementData
.getPropertyName(attrAdapter.getLocalName());
if ("value".equals(propName)) //$NON-NLS-1$
{
// final List converters =
// component.getDecorators(ComponentFactory.CONVERTER);
// (ConverterDecorator) it.next();
final CompositeType alternativeTypes = createCompositeType(
expectedType, exprTypes, component
.getDecorators(ComponentFactory.CONVERTER));
if (DEBUG)
{
System.out.println(String.format(
"maybeAddAlternative took %d", Long.valueOf(System //$NON-NLS-1$
.nanoTime()
- curTime)));
}
return alternativeTypes;
}
}
}
if (DEBUG)
{
System.out.println(String.format("maybeAddAlternative took %d", Long //$NON-NLS-1$
.valueOf(System.nanoTime() - curTime)));
}
// don't add anything by default
return expectedType;
}
private CompositeType createCompositeType(final CompositeType initialTypes,
final CompositeType testTypes, final List<ConverterDecorator> decorators)
{
// indicates unknown converter
final Set<String> types = new HashSet(Arrays.asList(initialTypes
.getSignatures()));
// look for converters. If there's one where we don't know the type,
// simply copy over the testTypes to force validation to ignore, since
// we have no idea.
for (final ConverterDecorator decorator : decorators)
{
if (decorator.getTypeInfo() != null)
{
final ConverterTypeInfo converterTypeInfo = decorator.getTypeInfo();
if (converterTypeInfo.getForClass().length == 0)
{
types.addAll(Arrays.asList(testTypes.getSignatures()));
break;
}
types.addAll(createSignatures(converterTypeInfo.getForClass()));
}
}
types.addAll(getRegisteredConversionTypesByClass());
return new CompositeType(types.toArray(new String[0])
, initialTypes.getAssignmentTypeMask());
}
private Set<String> getRegisteredConversionTypesByClass()
{
if (_conversionTypes == null)
{
_conversionTypes = new HashSet<String>();
final IProject project = _validationContext.getFile().getProject();
final IJSFAppConfigManager appConfig = JSFAppConfigManagerFactory.getJSFAppConfigManagerInstance(project);
final List<ConverterType> converters = appConfig.getConverters();
for (final ConverterType converterType : converters)
{
final ConverterForClassType forClassType = converterType.getConverterForClass();
if (forClassType != null)
{
final String forClass = forClassType.getTextContent();
if (forClass != null)
{
String signature = forClass.trim();
try
{
// arrays are a special case where the
// [Ljava.lang.String; syntax is valid.
if (Signature.getArrayCount(signature)>0)
{
_conversionTypes.add(signature);
}
}
catch (final IllegalArgumentException e)
{
// ignore
}
try
{
signature = Signature.createTypeSignature(signature, true);
_conversionTypes.add(signature);
}
catch (final Exception e)
{
// ignore: JSFCorePlugin.log(IStatus.INFO, "Could not use registered converter for-class: "+forClass); //$NON-NLS-1$
}
}
}
}
}
return _conversionTypes;
}
private List<String> createSignatures(final String[] classNames)
{
final List<String> signatures = new ArrayList<String>();
for (final String className : classNames)
{
try
{
String signature = Signature.createTypeSignature(className, true);
signatures.add(signature);
}
catch (final Exception e)
{
JSFCorePlugin.log(e, "Trying to create signature"); //$NON-NLS-1$
}
}
return signatures;
}
/**
* 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 validateNonELAttributeValue(
final IStructuredDocumentContext context,
final Region2AttrAdapter attrAdapter)
{
final String attributeValue = attrAdapter.getValue();
// else validate as static attribute value
if (DEBUG)
{
System.out.println(addDebugSpacer(3) + "attrVal= " //$NON-NLS-1$
+ (attributeValue != null ? attributeValue : "null")); //$NON-NLS-1$
}
final AttributeIdentifier attributeId = attrAdapter
.getAttributeIdentifier();
if (attributeId.getTagIdentifier() == null
|| attributeId.getTagIdentifier().getTagName() == null
|| attributeId.getName() == null)
{
return;
}
final List vv = MetaDataEnabledProcessingFactory.getInstance()
.getAttributeValueRuntimeTypeFeatureProcessors(
IValidValues.class, context, attributeId);
if (!vv.isEmpty())
{
for (final Iterator it = vv.iterator(); it.hasNext();)
{
final IValidValues v = (IValidValues) it.next();
if (!v.isValidValue(attributeValue.trim()))
{
if (DEBUG)
{
System.out.println(addDebugSpacer(4) + "NOT VALID "); //$NON-NLS-1$
}
for (final Iterator msgs = v.getValidationMessages()
.iterator(); msgs.hasNext();)
{
final IValidationMessage msg = (IValidationMessage) msgs
.next();
reportValidationMessage(createValidationMessage(
context, attributeValue, msg.getSeverity(), msg
.getMessage(), _validationContext
.getFile()), context, attributeValue);
}
}
else if (DEBUG)
{
System.out.println(addDebugSpacer(5) + "VALID "); //$NON-NLS-1$
}
}
}
else if (DEBUG)
{
System.out.println(addDebugSpacer(4) + "NO META DATA "); //$NON-NLS-1$
}
}
private void reportValidationMessage(final Diagnostic problem,
final IStructuredDocumentContext context,
final String attributeValue)
{
final int start = context.getDocumentPosition() + 1;
final int length = attributeValue.length();
_validationContext.getReporter().report(problem, start, length);
}
private Diagnostic createValidationMessage(
final IStructuredDocumentContext context,
final String attributeValue, final int severity, final String msg,
final IFile file)
{
// TODO: need factory
final Diagnostic diagnostic = new BasicDiagnostic(severity, "", -1, //$NON-NLS-1$
msg, null);
return diagnostic;
}
private String addDebugSpacer(final int count)
{
final String TAB = "\t"; //$NON-NLS-1$
final StringBuffer ret = new StringBuffer(""); //$NON-NLS-1$
for (int i = 0; i <= count; i++)
{
ret.append(TAB);
}
return ret.toString();
}
}