/*******************************************************************************
* Copyright (c) 2012, 2015 VMware, Inc.
* 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:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.quickfix;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
import org.eclipse.core.internal.resources.ICoreConstants;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
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.ui.internal.reconcile.validator.AnnotationInfo;
import org.eclipse.wst.sse.ui.internal.reconcile.validator.ISourceValidator;
import org.eclipse.wst.sse.ui.internal.reconcile.validator.IncrementalReporter;
import org.eclipse.wst.validation.internal.core.Message;
import org.eclipse.wst.validation.internal.core.ValidationException;
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.eclipse.wst.xml.core.internal.document.TextImpl;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode;
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMText;
import org.springframework.ide.eclipse.beans.core.BeansCorePlugin;
import org.springframework.ide.eclipse.beans.core.BeansCoreUtils;
import org.springframework.ide.eclipse.beans.core.model.IBeansConfig;
import org.springframework.ide.eclipse.beans.core.model.IBeansConfigSet;
import org.springframework.ide.eclipse.beans.core.model.IBeansImport;
import org.springframework.ide.eclipse.beans.core.model.IBeansModel;
import org.springframework.ide.eclipse.beans.core.model.IBeansProject;
import org.springframework.ide.eclipse.beans.core.model.IImportedBeansConfig;
import org.springframework.ide.eclipse.core.model.IModelElement;
import org.springframework.ide.eclipse.core.model.IModelElementVisitor;
import org.springframework.ide.eclipse.core.model.IResourceModelElement;
import org.springframework.ide.eclipse.core.model.validation.ValidationProblemAttribute;
import org.springframework.ide.eclipse.quickfix.processors.BeanQuickAssistProcessor;
import org.springframework.ide.eclipse.quickfix.processors.QuickfixProcessorFactory;
import org.springframework.ide.eclipse.quickfix.validator.BeanValidatorVisitor;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* Source validator for beans XML editor.
*
* @author Terry Denney
* @author Leo Dos Santos
* @author Christian Dupuis
* @author Martin Lippert
* @since 2.0
*/
public class BeansEditorValidator implements ISourceValidator, IValidator {
private static class ContextElementVisitor implements IModelElementVisitor {
private final Set<IResourceModelElement> contextElements;
private final IResource resource;
private IBeansConfig currentConfig = null;
public ContextElementVisitor(IResource resource, Set<IResourceModelElement> contextElements) {
this.resource = resource;
this.contextElements = contextElements;
}
public boolean visit(IModelElement element, IProgressMonitor monitor) {
if (element instanceof IBeansModel) {
return true;
}
else if (element instanceof IBeansProject) {
return true;
}
else if (element instanceof IImportedBeansConfig) {
if (resource.equals(((IImportedBeansConfig) element).getElementResource())) {
contextElements.add(currentConfig);
}
return true;
}
else if (element instanceof IBeansConfig) {
this.currentConfig = (IBeansConfig) element;
return true;
}
else if (element instanceof IBeansImport) {
for (IImportedBeansConfig config : ((IBeansImport) element).getImportedBeansConfigs()) {
config.accept(this, monitor);
}
return false;
}
else if (element instanceof IBeansConfigSet) {
for (IBeansConfig config : ((IBeansConfigSet) element).getConfigs()) {
if (resource.equals(config.getElementResource())) {
contextElements.add((IBeansConfigSet) element);
break;
}
}
return false;
}
return false;
}
}
protected class LocalizedMessage extends Message {
private String _message = null;
public LocalizedMessage(int severity, String messageText) {
this(severity, messageText, null);
}
public LocalizedMessage(int severity, String messageText, IResource targetObject) {
this(severity, messageText, (Object) targetObject);
}
public LocalizedMessage(int severity, String messageText, Object targetObject) {
super(null, severity, null);
setLocalizedMessage(messageText);
setTargetObject(targetObject);
}
@Override
public boolean equals(Object obj) {
if (obj instanceof LocalizedMessage) {
LocalizedMessage m = (LocalizedMessage) obj;
return m.getText().equals(getText()) && m.getOffset() == getOffset();
}
return false;
}
public String getLocalizedMessage() {
return _message;
}
@Override
public String getText() {
return getLocalizedMessage();
}
@Override
public String getText(ClassLoader cl) {
return getLocalizedMessage();
}
@Override
public String getText(Locale l) {
return getLocalizedMessage();
}
@Override
public String getText(Locale l, ClassLoader cl) {
return getLocalizedMessage();
}
@Override
public int hashCode() {
return (getText() + getOffset()).hashCode();
}
public void setLocalizedMessage(String message) {
_message = message;
}
}
private IDocument document = null;
private IFile file = null;
private IStructuredModel model = null;
private IProject project;
// add node and all children node to checked nodes
private void addCheckedNodes(IDOMNode node, Set<IDOMNode> checkedNodes) {
checkedNodes.add(node);
NodeList childNodes = node.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
Node child = childNodes.item(i);
if (child instanceof IDOMNode) {
addCheckedNodes((IDOMNode) child, checkedNodes);
}
}
}
public void cleanup(IReporter reporter) {
reporter.removeAllMessages(this);
}
public void connect(IDocument document) {
this.document = document;
if (model == null) {
model = StructuredModelManager.getModelManager().getExistingModelForRead(document);
}
if (model != null) {
String location = model.getBaseLocation();
IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
IPath filePath = new Path(location);
if (root.getFullPath().append(filePath).segmentCount() < ICoreConstants.MINIMUM_FILE_SEGMENT_LENGTH) {
disconnect(document);
}
else {
file = root.getFile(filePath);
project = file.getProject();
}
}
}
public void createAndAddEmptyMessage(ITextRegion valueRegion, IDOMNode parentNode, String messageText,
IReporter reporter, QuickfixProcessorFactory quickfixFactory, String problemId,
ValidationProblemAttribute... problemAttributes) {
createAndAddMessage(valueRegion, parentNode, messageText, reporter, quickfixFactory, false, false,
IMessage.ALL_MESSAGES, problemId, problemAttributes);
}
private void createAndAddMessage(int offset, int length, int emptyMsgOffset, int emptyMsgLength,
boolean missingEndQuote, IDOMNode beanNode, String text, String messageText, IReporter reporter,
QuickfixProcessorFactory quickfixFactory, boolean showErrorMessage, int severity, String problemId,
ValidationProblemAttribute... problemAttributes) {
if (beanNode == null || text == null || messageText == null || document == null) {
return;
}
if (showErrorMessage) {
IMessage message = new LocalizedMessage(severity, messageText);
message.setOffset(offset);
message.setLength(length);
try {
message.setLineNo(document.getLineOfOffset(offset) + 1);
}
catch (BadLocationException e) {
message.setLineNo(-1);
}
reporter.addMessage(this, message);
}
// TODO: investigate better way to make suggestion works for the last
// position
// messageEmpty does not show up in the editor, it is only
// used for showing quick assist in the last cursor position
IMessage messageEmpty = new LocalizedMessage(IMessage.ALL_MESSAGES, messageText);
messageEmpty.setOffset(emptyMsgOffset);
messageEmpty.setLength(emptyMsgLength);
try {
messageEmpty.setLineNo(document.getLineOfOffset(offset) + 1);
}
catch (BadLocationException e) {
messageEmpty.setLineNo(-1);
}
if (reporter instanceof IncrementalReporter) {
BeanQuickAssistProcessor processor = quickfixFactory.create(offset, length, text, missingEndQuote,
beanNode, this, problemId, problemAttributes);
if (processor != null) {
messageEmpty.setAttribute(IQuickAssistProcessor.class.getName(), processor);
AnnotationInfo info = new QuickfixAnnotationInfo(messageEmpty);
IncrementalReporter incrementalReporter = (IncrementalReporter) reporter;
AnnotationInfo[] existingInfos = incrementalReporter.getAnnotationInfo();
for (AnnotationInfo existingInfo : existingInfos) {
IMessage existingMessage = existingInfo.getMessage();
if (existingMessage.getOffset() != messageEmpty.getOffset()) {
continue;
}
if (!existingMessage.getText().equals(messageEmpty.getText())) {
continue;
}
Object existingProcessor = existingMessage.getAttribute(IQuickAssistProcessor.class.getName());
if (existingProcessor != null && existingProcessor.equals(processor)) {
return;
}
}
incrementalReporter.addAnnotationInfo(this, info);
}
}
}
public void createAndAddMessage(ITextRegion valueRegion, IDOMNode parentNode, String messageText,
IReporter reporter, QuickfixProcessorFactory quickfixFactory, boolean affectsWholeBean, boolean showError,
int severity, String problemId, ValidationProblemAttribute... problemAttributes) {
// IMessage message = new LocalizedMessage(IMessage.HIGH_SEVERITY,
// messageText);
// TODO: investigate better way to make suggestion works for the last
// position
// messageEmpty does not show up in the editor, it is only
// used for showing quick assist in the last cursor position
// IMessage messageEmpty = new LocalizedMessage(IMessage.ALL_MESSAGES,
// messageText);
int offset = valueRegion.getStart() + parentNode.getStartOffset();
boolean missingEndQuote = false;
if (document == null) {
return;
}
String text = document.get().substring(offset, offset + valueRegion.getLength());
text = text.trim();
int length = text.length();
if (text.startsWith("\'") || text.startsWith("\"")) {
offset++;
length--;
text = text.substring(1);
}
if (text.endsWith("\'") || text.endsWith("\"")) {
length--;
text = text.substring(0, text.length() - 1);
}
else {
missingEndQuote = true;
}
int msgOffset, msgLength;
if (affectsWholeBean) {
msgOffset = parentNode.getStartOffset();
msgLength = parentNode.getEndOffset() - msgOffset;
offset = msgOffset;
length = msgLength;
}
else {
msgOffset = offset;
msgLength = length;
}
createAndAddMessage(offset, length, msgOffset, msgLength + 1, missingEndQuote, parentNode, text, messageText,
reporter, quickfixFactory, showError, severity, problemId, problemAttributes);
}
public void createAndAddMessage(ITextRegion valueRegion, IDOMNode parentNode, String messageText,
IReporter reporter, QuickfixProcessorFactory quickfixFactory, boolean affectsWholeBean, int severity,
String problemId, ValidationProblemAttribute... problemAttributes) {
createAndAddMessage(valueRegion, parentNode, messageText, reporter, quickfixFactory, affectsWholeBean, true,
severity, problemId, problemAttributes);
}
public void createAndAddMessage(ITextRegion valueRegion, IDOMNode parentNode, String messageText,
IReporter reporter, QuickfixProcessorFactory quickfixFactory, int severity, String problemId,
ValidationProblemAttribute... problemAttributes) {
createAndAddMessage(valueRegion, parentNode, messageText, reporter, quickfixFactory, false, true, severity,
problemId, problemAttributes);
}
public void createAndAddMessageForNode(IDOMNode node, IDOMNode beanNode, String text, String messageText,
IReporter reporter, QuickfixProcessorFactory quickfixFactory, int severity, String problemId,
ValidationProblemAttribute... problemAttributes) {
int offset = node.getStartOffset();
int length = node.getEndOffset() - offset;
boolean missingEndQuote = false;
createAndAddMessage(offset, length, offset, length + 1, missingEndQuote, beanNode, text, messageText, reporter,
quickfixFactory, true, severity, problemId, problemAttributes);
}
public void disconnect(IDocument document) {
if (this.model != null) {
model.releaseFromRead();
model = null;
}
this.document = null;
}
private final Set<IResourceModelElement> getContextElements(IBeansConfig config) {
Set<IResourceModelElement> contextElements = new LinkedHashSet<IResourceModelElement>();
BeansCorePlugin.getModel().accept(new ContextElementVisitor(config.getElementResource(), contextElements),
new NullProgressMonitor());
if (contextElements.isEmpty()) {
contextElements.add(config);
}
return contextElements;
}
public IFile getFile() {
return file;
}
private IDOMNode getNodeAt(int documentOffset, int length) {
IndexedRegion node = null;
if (model != null) {
// int lastOffset = documentOffset;
node = model.getIndexedRegion(documentOffset);
if (node == null) {
return null;
}
if (node.getStartOffset() < documentOffset || node.getStartOffset() >= documentOffset + length) {
node = null;
}
// while ((node == null) && lastOffset >= 0) {
// lastOffset--;
// node = model.getIndexedRegion(lastOffset);
// }
}
if (node instanceof IDOMNode && !(node instanceof TextImpl)) {
return getNonTextNode((IDOMNode) node);
}
return null;
}
private IDOMNode getNonTextNode(IDOMNode node) {
if (node == null || !(node instanceof IDOMText)) {
return node;
}
return getNonTextNode((IDOMNode) node.getParentNode());
}
public IProject getProject() {
return project;
}
public void validate(IRegion dirtyRegion, IValidationContext context, IReporter reporter) {
if (document == null || !BeansCoreUtils.isBeansConfig(file)) {
return;
}
if (!(document instanceof IStructuredDocument)) {
return;
}
IStructuredDocumentRegion[] regions = ((IStructuredDocument) document).getStructuredDocumentRegions(
dirtyRegion.getOffset(), dirtyRegion.getLength());
Set<IDOMNode> checkedNodes = new HashSet<IDOMNode>();
// long start = System.currentTimeMillis();
for (IStructuredDocumentRegion region : regions) {
IDOMNode node = getNodeAt(region.getStartOffset(), region.getLength());
if (node != null && !checkedNodes.contains(node)) {
validateNode(node, reporter);
addCheckedNodes(node, checkedNodes);
}
}
// System.out.println(String.format("%s, reconiling region %s:%s on %s",
// (System.currentTimeMillis() - start),
// dirtyRegion.getOffset(), dirtyRegion.getLength(),
// file.getFullPath().toString()));
}
public void validate(IValidationContext helper, IReporter reporter) throws ValidationException {
}
private void validateNode(IDOMNode node, IReporter reporter) {
IBeansModel model = BeansCorePlugin.getModel();
Set<IBeansConfig> configs = model.getConfigs(file, true);
for (IBeansConfig config : configs) {
Set<IResourceModelElement> contextElements = getContextElements(config);
for (IResourceModelElement contextElement : contextElements) {
BeanValidatorVisitor visitor = new BeanValidatorVisitor(config, contextElement, reporter, this);
if (visitor.visitNode(node, true, true)) {
return;
}
}
}
}
}