blob: 965c81135ab0bcd157da887848ea5ac36235a659 [file] [log] [blame]
/******************************************************************************
* Copyright (c) 2016 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
******************************************************************************/
package org.eclipse.lsp4j.jsonrpc.validation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.lsp4j.jsonrpc.JsonRpcException;
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
import org.eclipse.lsp4j.jsonrpc.MessageIssueException;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.lsp4j.jsonrpc.messages.Message;
import org.eclipse.lsp4j.jsonrpc.messages.MessageIssue;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import com.google.gson.JsonElement;
/**
* Validates messages and forwards them to other message consumers. In case an issue is found,
* a {@link MessageIssueException} is thrown.
*/
public class ReflectiveMessageValidator implements MessageConsumer {
private static final Logger LOG = Logger.getLogger(ReflectiveMessageValidator.class.getName());
private final MessageConsumer delegate;
/**
* When created with this constructor, the validator acts as a message sink.
*/
public ReflectiveMessageValidator() {
this.delegate = null;
}
/**
* Forward messages to the given consumer unless an issue is found.
*/
public ReflectiveMessageValidator(MessageConsumer delegate) {
this.delegate = delegate;
}
@Override
public void consume(Message message) throws MessageIssueException, JsonRpcException {
List<MessageIssue> issues = validate(message);
if (!issues.isEmpty()) {
// Sort the messages in order to get a stable order (otherwise it depends on the JVM's reflection implementation)
Collections.sort(issues, (issue1, issue2) -> issue1.getText().compareTo(issue2.getText()));
throw new MessageIssueException(message, issues);
} else if (delegate != null) {
delegate.consume(message);
}
}
/**
* Check whether the given object is valid. If it is not valid, its issues are not reported.
*/
public boolean isValid(Object object) {
List<MessageIssue> issues = validate(object);
return issues.isEmpty();
}
protected List<MessageIssue> validate(Object object) {
List<MessageIssue> result = new ArrayList<>();
try {
validate(object, result, new LinkedList<>(), new LinkedList<>());
} catch (Exception e) {
LOG.log(Level.SEVERE, "Error during message validation: " + e.getMessage(), e);
result.add(new MessageIssue("Message validation failed, please check the logs of the remote endpoint.",
ResponseErrorCode.InvalidParams.getValue()));
}
return result;
}
/**
* Validate all fields of the given object.
*/
protected void validate(Object object, List<MessageIssue> issues, Deque<Object> objectStack, Deque<Object> accessorStack) throws Exception {
if (object == null
|| object instanceof Enum<?>
|| object instanceof String
|| object instanceof Number
|| object instanceof Boolean
|| object instanceof JsonElement
|| object instanceof Throwable) {
return;
}
if (objectStack.contains(object)) {
issues.add(new MessageIssue("An element of the message has a direct or indirect reference to itself."
+ " Path: " + createPathString(accessorStack),
ResponseErrorCode.InvalidParams.getValue()));
return;
}
objectStack.push(object);
if (object instanceof List<?>) {
ListIterator<?> iter = ((List<?>) object).listIterator();
while (iter.hasNext()) {
accessorStack.push(iter.nextIndex());
Object element = iter.next();
if (element == null) {
issues.add(new MessageIssue("Lists must not contain null references."
+ " Path: " + createPathString(accessorStack),
ResponseErrorCode.InvalidParams.getValue()));
}
validate(element, issues, objectStack, accessorStack);
accessorStack.pop();
}
} else if (object instanceof Either<?, ?>) {
Either<?, ?> either = (Either<?, ?>) object;
if (either.isLeft()) {
validate(either.getLeft(), issues, objectStack, accessorStack);
} else if (either.isRight()) {
validate(either.getRight(), issues, objectStack, accessorStack);
} else {
issues.add(new MessageIssue("An Either instance must not be empty."
+ " Path: " + createPathString(accessorStack),
ResponseErrorCode.InvalidParams.getValue()));
}
} else {
for (Method method : object.getClass().getMethods()) {
if (isGetter(method)) {
accessorStack.push(method);
Object value = method.invoke(object);
if (value == null && method.getAnnotation(NonNull.class) != null) {
issues.add(new MessageIssue("The accessor '" + method.getDeclaringClass().getSimpleName()
+ "." + method.getName() + "()' must return a non-null value."
+ " Path: " + createPathString(accessorStack),
ResponseErrorCode.InvalidParams.getValue()));
}
validate(value, issues, objectStack, accessorStack);
accessorStack.pop();
}
}
}
objectStack.pop();
}
protected String createPathString(Deque<Object> accessorStack) {
StringBuilder result = new StringBuilder("$");
Iterator<Object> resultIter = accessorStack.descendingIterator();
while(resultIter.hasNext()) {
Object accessor = resultIter.next();
if (accessor instanceof Method)
result.append('.').append(getPropertyName((Method) accessor));
else if (accessor instanceof Integer)
result.append('[').append(accessor).append(']');
else
result.append(accessor);
}
return result.toString();
}
protected boolean isGetter(Method method) {
return method.getParameterCount() == 0 && method.getName().startsWith("get")
&& method.getDeclaringClass() != Object.class
&& Modifier.isPublic(method.getModifiers())
&& !Modifier.isStatic(method.getModifiers());
}
protected String getPropertyName(Method method) {
String methodName = method.getName();
if (methodName.startsWith("get") && methodName.length() > 3)
return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
else
return methodName;
}
}