/*******************************************************************************
* 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));
}
}