/******************************************************************************* * Copyright (c) 2012-2013 Red Hat, Inc. * Distributed under license by Red Hat, Inc. All rights reserved. * This program is 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 * * Contributor: * Red Hat, Inc. - initial API and implementation ******************************************************************************/ package org.jboss.tools.common.validation.java; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.resources.IFile; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IMember; import org.eclipse.jdt.core.ISourceRange; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.ui.text.IJavaPartitions; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.BadPartitioningException; import org.eclipse.jface.text.DocumentRewriteSessionEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentExtension3; import org.eclipse.jface.text.IDocumentExtension4; import org.eclipse.jface.text.IDocumentRewriteSessionListener; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.reconciler.DirtyRegion; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.texteditor.IDocumentProvider; import org.eclipse.ui.texteditor.ITextEditor; import org.eclipse.wst.sse.ui.internal.reconcile.DirtyRegionProcessor; 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.validation.internal.provisional.core.IValidator; import org.jboss.tools.common.EclipseUtil; import org.jboss.tools.common.log.LogHelper; import org.jboss.tools.common.validation.AsYouTypeValidatorManager; import org.jboss.tools.common.validation.CommonValidationPlugin; import org.jboss.tools.common.validation.ITypedReporter; import org.jboss.tools.common.validation.TempMarkerManager; import org.jboss.tools.common.validation.ValidationMessage; /** * As-You-Type validation Java files * * @author Victor V. Rubezhny * */ @SuppressWarnings("restriction") final public class JavaDirtyRegionProcessor extends DirtyRegionProcessor { private ITextEditor fEditor; private IDocument fDocument; private IValidationContext fHelper; private JavaProblemReporter fReporter; private AsYouTypeValidatorManager fValidatorManager; private boolean fDocumentJustSetup = false; private boolean fIsCanceled = false; private boolean fInRewriteSession = false; private IDocumentRewriteSessionListener fDocumentRewriteSessionListener = new DocumentRewriteSessionListener(); private List<ITypedRegion> fPartitionsToProcess = new ArrayList<ITypedRegion>(); private int fStartPartitionsToProcess = -1; private int fEndPartitionsToProcess = -1; private int fStartRegionToProcess = -1; private int fEndRegionToProcess = -1; public final class JavaProblemReporter implements IReporter, ITypedReporter { private List<IMessage> messages = new ArrayList<IMessage>(); private IFile fFile; private ICompilationUnit fCompilationUnit; private IAnnotationModel fAnnotationModel; private boolean fIsCanceled = false; @Override public void removeMessageSubset(IValidator validator, Object obj, String groupName) { // Does nothing } @Override public void removeAllMessages(IValidator origin, Object object) { // Does nothing } @Override public void removeAllMessages(IValidator origin) { // Does nothing } public void removeAllMessages() { messages.clear(); } public void setCanceled(boolean set) { this.fIsCanceled = set; } @Override public boolean isCancelled() { return this.fIsCanceled; } @SuppressWarnings("rawtypes") @Override public List getMessages() { return messages; } @Override public void displaySubtask(IValidator validator, IMessage message) { // Does nothing } Set<Annotation> fAnnotations = new HashSet<Annotation>(); public void update() { clearAllAnnotations(); getAnnotationModel(); // This updates saved annotation model if needed fFile = (fEditor != null && fEditor.getEditorInput() instanceof IFileEditorInput ? ((IFileEditorInput)fEditor.getEditorInput()).getFile() : null); fCompilationUnit = (fFile != null ? EclipseUtil.getCompilationUnit(fFile) : null); } protected IAnnotationModel getAnnotationModel() { final IDocumentProvider documentProvider= fEditor.getDocumentProvider(); if (documentProvider == null) { return null; } IAnnotationModel newModel = documentProvider.getAnnotationModel(fEditor.getEditorInput()); if (fAnnotationModel != newModel) { fAnnotationModel = newModel; } return fAnnotationModel; } public ICompilationUnit getCompilationUnit() { return fCompilationUnit; } public void clearAllAnnotations() { if (fAnnotations.isEmpty()) { return; } Annotation[] annotations = fAnnotations.toArray(new Annotation[0]); for (Annotation annotation : annotations) { fAnnotations.remove(annotation); if(fAnnotationModel != null) fAnnotationModel.removeAnnotation(annotation); } } @Override public void addMessage(IValidator origin, IMessage message) { messages.add(message); } public void finishReporting() { if (isCancelled() || getAnnotationModel() == null || fCompilationUnit == null) return; IEditorInput editorInput= fEditor.getEditorInput(); if (editorInput == null) return; String editorInputName = editorInput.getName(); Collection<String> regionTypes = getTypesForRegion(); Collection<String> fileTypes = getTypesForFile(); Annotation[] annotations = fAnnotations.toArray(new Annotation[0]); List<Annotation> annotationsToRemove = new ArrayList<Annotation>(); Set<ValidationMessage> existingValidationMessages = new HashSet<ValidationMessage>(); for (Annotation annotation : annotations) { if (!(annotation instanceof TempJavaProblemAnnotation)) continue; TempJavaProblemAnnotation jpAnnotation = (TempJavaProblemAnnotation)annotation; Position position = getAnnotationModel().getPosition(jpAnnotation); Map attributes = jpAnnotation.getAttributes(); Object typeOfAnnotation = attributes == null ? null : attributes.get(TempMarkerManager.MESSAGE_TYPE_ATTRIBUTE_NAME); boolean isRegionWideAnnotationType = regionTypes.contains(typeOfAnnotation); boolean isFileWideAnnotationType = fileTypes.contains(typeOfAnnotation); IRegion regionOfAnnotation = position == null ? null : findRegion(position.getOffset()); // Find a validation message for the annotation boolean existing = false; for (IMessage m : messages) { if (!(m instanceof ValidationMessage)) continue; ValidationMessage valMessage = (ValidationMessage)m; if (position != null && position.getOffset() == valMessage.getOffset() && position.getLength() == valMessage.getLength()) { String text = valMessage.getText(); text = text == null ? "" : text; //$NON-NLS-1$ if (!text.equalsIgnoreCase(jpAnnotation.getText())) continue; Object type = valMessage.getAttribute(TempMarkerManager.MESSAGE_TYPE_ATTRIBUTE_NAME); type = type == null ? "" : type; //$NON-NLS-1$ Map jpAttributes = jpAnnotation.getAttributes(); if (jpAttributes == null) continue; if (!type.equals(jpAttributes.get(TempMarkerManager.MESSAGE_TYPE_ATTRIBUTE_NAME))) continue; // This is an annotation to keep (message is found for the annotation) existingValidationMessages.add(valMessage); existing = true; break; } } // This is an annotation to remove (no message found for the annotation) if (!existing) { if (isFileWideAnnotationType || (isRegionWideAnnotationType && regionOfAnnotation != null)) annotationsToRemove.add(annotation); } } Map<Annotation, Position> annotationsToAdd = new HashMap<Annotation, Position>(); for (IMessage message : messages) { if (!(message instanceof ValidationMessage) || existingValidationMessages.contains(message)) continue; ValidationMessage valMessage = (ValidationMessage)message; Position position = new Position(valMessage.getOffset(), valMessage.getLength()); TempJavaProblem problem = new TempJavaProblem(valMessage, editorInputName); TempJavaProblemAnnotation problemAnnotation = new TempJavaProblemAnnotation(problem, fCompilationUnit); annotationsToAdd.put(problemAnnotation, position); } getAnnotationModel(); // This is to update saved document annotation model for (Annotation a : annotationsToRemove) { fAnnotations.remove(a); fAnnotationModel.removeAnnotation(a); } for (Annotation a : annotationsToAdd.keySet()) { Position p = annotationsToAdd.get(a); fAnnotations.add(a); fAnnotationModel.addAnnotation(a, p); } removeAllMessages(); clearRegions(); } private List<String> fTypesForFileValidation = new ArrayList<String>(); private List<String> fTypesForRegionValidatoin = new ArrayList<String>(); @Override public void addTypeForFile(String type) { if (!fTypesForFileValidation.contains(type)) { fTypesForFileValidation.add(type); } } @Override public Collection<String> getTypesForFile() { return Collections.unmodifiableList(fTypesForFileValidation); } @Override public void addTypeForRegion(String type) { if (!fTypesForRegionValidatoin.contains(type)) { fTypesForRegionValidatoin.add(type); } } @Override public Collection<String> getTypesForRegion() { return Collections.unmodifiableList(fTypesForRegionValidatoin); } private List<IRegion> fRegions; public void setRegions(List<IRegion> regions) { this.fRegions = regions; } public void clearRegions() { this.fRegions = null; } private IRegion findRegion(int position) { if (fRegions != null) { for (IRegion region : fRegions) { if (region.getOffset() <= position && region.getOffset() + region.getLength() > position) return region; } } return null; } } class DocumentRewriteSessionListener implements IDocumentRewriteSessionListener { public void documentRewriteSessionChanged(DocumentRewriteSessionEvent event) { fInRewriteSession = event != null && event.getChangeType().equals(DocumentRewriteSessionEvent.SESSION_START); } } public JavaDirtyRegionProcessor(ITextEditor editor) { this.fEditor = editor; fHelper = createValidationContext(); fReporter = createProblemReporter(); } private IValidationContext createValidationContext() { return new IValidationContext() { @Override public Object loadModel(String arg0, Object[] arg1) { return null; } @Override public Object loadModel(String arg0) { return null; } @Override public String[] getURIs() { IFile file = (fEditor != null && fEditor.getEditorInput() instanceof IFileEditorInput ? ((IFileEditorInput)fEditor.getEditorInput()).getFile() : null); String URI = file == null ? null : file.getFullPath().toPortableString(); return URI == null ? new String[0] : new String[] {URI}; } }; } private JavaProblemReporter createProblemReporter() { JavaProblemReporter reporter = new JavaProblemReporter(); reporter.update(); return reporter; } @Override public synchronized void startReconciling() { super.startReconciling(); } private boolean isInRewrite() { return fInRewriteSession; } @Override public void setDocument(IDocument doc) { if (fDocument != null) { if (fDocument instanceof IDocumentExtension4) { ((IDocumentExtension4) fDocument).removeDocumentRewriteSessionListener(fDocumentRewriteSessionListener); } if (fValidatorManager != null && fDocument != null) { fValidatorManager.disconnect(fDocument); } } fDocument = doc; super.setDocument(doc); if (fDocument != null) { if (fDocument instanceof IDocumentExtension4) { ((IDocumentExtension4) fDocument).addDocumentRewriteSessionListener(fDocumentRewriteSessionListener); } if (fValidatorManager == null) { fValidatorManager = new AsYouTypeValidatorManager(); } fValidatorManager.connect(fDocument); if (fReporter != null) { fReporter.update(); } } fDocumentJustSetup = true; } @Override public void install(ITextViewer textViewer) { super.install(textViewer); } @Override public void uninstall() { fIsCanceled = true; if(fReporter != null) { fReporter.clearAllAnnotations(); fReporter.setCanceled(true); } super.uninstall(); } @Override protected void beginProcessing() { fPartitionsToProcess.clear(); fStartRegionToProcess = -1; fEndRegionToProcess = -1; fStartPartitionsToProcess = -1; fEndPartitionsToProcess = -1; } private boolean isEditorDirty() { if (fDocumentJustSetup && fEditor.isDirty()) { fDocumentJustSetup = false; } return !fDocumentJustSetup; } protected void process(DirtyRegion dirtyRegion) { IDocument doc = getDocument(); if (!isEditorDirty() || !isInstalled() || isInRewrite() || dirtyRegion == null || doc == null || fIsCanceled) { return; } int start = dirtyRegion.getOffset(); int end = DirtyRegion.REMOVE.equals(dirtyRegion.getType()) ? dirtyRegion.getOffset() : dirtyRegion.getOffset() + dirtyRegion.getLength(); // Check the document boundaries int docLen = doc.getLength(); if (docLen == 0) return; if (start > docLen) start = docLen; if (end >= docLen) end = docLen - 1; fStartRegionToProcess = (fStartRegionToProcess == -1 || fStartRegionToProcess > start) ? start : fStartRegionToProcess; fEndRegionToProcess = (fEndRegionToProcess == -1 || fEndRegionToProcess < end) ? end : fEndRegionToProcess; /* * Expand dirtyRegion to partitions boundaries */ try { ITypedRegion startPartition = (doc instanceof IDocumentExtension3) ? ((IDocumentExtension3)doc).getPartition(IJavaPartitions.JAVA_PARTITIONING, start, true) : doc.getPartition(start); if (startPartition != null && start > startPartition.getOffset()) start = startPartition.getOffset(); ITypedRegion endPartition = (doc instanceof IDocumentExtension3) ? ((IDocumentExtension3)doc).getPartition(IJavaPartitions.JAVA_PARTITIONING, end, false) : doc.getPartition(end); if (endPartition != null && end < endPartition.getOffset() + endPartition.getLength()) end = endPartition.getOffset() + endPartition.getLength(); } catch (BadLocationException e) { LogHelper.logError(CommonValidationPlugin.getDefault(), e); } catch (BadPartitioningException e) { LogHelper.logError(CommonValidationPlugin.getDefault(), e); } fStartPartitionsToProcess = (fStartPartitionsToProcess == -1 || fStartPartitionsToProcess > start) ? start : fStartPartitionsToProcess; fEndPartitionsToProcess = (fEndPartitionsToProcess == -1 || fEndPartitionsToProcess < end) ? end : fEndPartitionsToProcess; ITypedRegion[] partitions = computePartitioning(start, end - start); for (ITypedRegion partition : partitions) { if (partition != null && !fIsCanceled) { String type = partition.getType(); if ((IJavaPartitions.JAVA_STRING.equals(type) || IJavaPartitions.JAVA_CHARACTER.equals(type) || IJavaPartitions.JAVA_SINGLE_LINE_COMMENT.equals(type) || IJavaPartitions.JAVA_MULTI_LINE_COMMENT.equals(type) || IJavaPartitions.JAVA_DOC.equals(type)) && !fPartitionsToProcess.contains(partition)) { fPartitionsToProcess.add(partition); } } } } @Override protected void endProcessing() { if (fValidatorManager == null || fReporter == null || fStartPartitionsToProcess == -1 || fEndPartitionsToProcess == -1) return; fReporter.clearRegions(); if (fPartitionsToProcess != null && !fPartitionsToProcess.isEmpty()) { List<IRegion> regions = Arrays.asList(fPartitionsToProcess.toArray(new IRegion[fPartitionsToProcess.size()])); fReporter.setRegions(regions); fValidatorManager.validateString( regions, fHelper, fReporter); fReporter.finishReporting(); } else if (isJavaElementValidationRequired()) { // The 'else' is added here due to not to validate // an element in case of at lease one string is validated, // because the string validation performs the validation of an element // as well fValidatorManager.validateJavaElement( Arrays.asList( new IRegion[] { new Region(fStartRegionToProcess, fEndRegionToProcess - fStartRegionToProcess) }), fHelper, fReporter); fReporter.finishReporting(); } } private boolean isJavaElementValidationRequired() { ICompilationUnit unit = fReporter.getCompilationUnit(); if (unit == null) return false; boolean result = false; boolean atLeastOneElementIsProcessed = false; int position = fStartRegionToProcess; try { unit = unit.getWorkingCopy(null); IJavaElement element = null; while (position >= 0 && (element = unit.getElementAt(position--)) == null) ; if (position < 0) position = 0; ITypedRegion[] partitions = computePartitioning(position, fEndPartitionsToProcess - position); ITypedRegion startPartition = findPartitionByOffset(partitions, position); ITypedRegion endPartition = (startPartition != null && fEndRegionToProcess >= startPartition.getOffset() && fEndRegionToProcess < startPartition.getOffset() + startPartition.getLength()) ? startPartition : findPartitionByOffset(partitions, fEndRegionToProcess); if (startPartition != null && startPartition.equals(endPartition) && !isProcessingRequiredForPartition(startPartition)) { return false; } while (position <= fEndRegionToProcess) { ITypedRegion partition = findPartitionByOffset(partitions, position); if(!isProcessingRequiredForPartition(partition)) { position = partition.getOffset() + partition.getLength(); continue; } element = unit.getElementAt(position); if (element == null) { position++; continue; } atLeastOneElementIsProcessed = true; boolean doSkipThisElement = false; if (element instanceof IMember && element.getElementType() == IJavaElement.METHOD) { ISourceRange range = ((IMember)element).getSourceRange(); if (position >= range.getOffset()) { try { String text = fDocument.get(range.getOffset(), position - range.getOffset() + 1); if (text.indexOf('{') != -1 && !text.endsWith("}")) { //$NON-NLS-1$ doSkipThisElement = true; position = range.getOffset() + range.getLength(); } } catch (BadLocationException e) { // Ignore it and do not skip validation } position++; } } else { IJavaElement parent = element.getParent(); while (parent != null && parent.getElementType() != IJavaElement.COMPILATION_UNIT) { if (parent.getElementType() == IJavaElement.METHOD) { doSkipThisElement = true; break; } parent = parent.getParent(); } position++; } if (!doSkipThisElement) return true; } } catch (JavaModelException e) { LogHelper.logError(CommonValidationPlugin.getDefault(), e); } finally { try { unit.discardWorkingCopy(); } catch (JavaModelException e) { LogHelper.logError(CommonValidationPlugin.getDefault(), e); } } return atLeastOneElementIsProcessed ? result : true; } private ITypedRegion findPartitionByOffset(ITypedRegion[] partitions, int offset) { if (partitions == null) return null; for (ITypedRegion partition : partitions) { if (offset >= partition.getOffset() && offset < partition.getOffset() + partition.getLength()) return partition; } return null; } private boolean isProcessingRequiredForPartition(ITypedRegion partition) { if (partition == null) return false; String type = partition.getType(); return !(IJavaPartitions.JAVA_STRING.equals(type) || IJavaPartitions.JAVA_CHARACTER.equals(type) || IJavaPartitions.JAVA_SINGLE_LINE_COMMENT.equals(type) || IJavaPartitions.JAVA_MULTI_LINE_COMMENT.equals(type) || IJavaPartitions.JAVA_DOC.equals(type)); } }