/******************************************************************************* * 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.IAssignable; 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.jsf.validation.internal.el.ELValidationUtil; 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); if (ELValidationUtil.isProjectEL22(_validationContext.getFile())) { expectedType = addEL22Alternatives(expectedType); } 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 CompositeType addEL22Alternatives(final CompositeType expectedType) { CompositeType type = expectedType; if (expectedType != null) { final int assignmentTypeMask = expectedType.getAssignmentTypeMask(); if ((assignmentTypeMask & IAssignable.ASSIGNMENT_TYPE_RHS) != 0 || assignmentTypeMask == IAssignable.ASSIGNMENT_TYPE_NONE) { int assignmentType = IAssignable.ASSIGNMENT_TYPE_NONE; if ((assignmentTypeMask & IAssignable.ASSIGNMENT_TYPE_RHS) != 0) { assignmentType = IAssignable.ASSIGNMENT_TYPE_RHS; } final List<String> signatures = new ArrayList<String>(); for (final String signature: expectedType.getSignatures()) { signatures.add(signature); final List<String> methodSignatures = new ArrayList<String>(); for (int i = 0; i < 20; i++) { final String methodSignature = Signature.createMethodSignature( methodSignatures.toArray(new String[i]), Signature.getElementType(signature)); signatures.add(methodSignature); methodSignatures.add("Ljava.lang.String;"); //$NON-NLS-1$ } } type = new CompositeType( signatures.toArray(new String[signatures.size()]), assignmentType); } } return type; } 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(); } }