| /****************************************************************************** |
| * Copyright (c) 2016-2018 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.json.adapters; |
| |
| import java.io.IOException; |
| import java.lang.reflect.Type; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.function.Predicate; |
| |
| import org.eclipse.lsp4j.jsonrpc.messages.Either; |
| import org.eclipse.lsp4j.jsonrpc.messages.Either3; |
| import org.eclipse.lsp4j.jsonrpc.messages.Tuple; |
| |
| import com.google.gson.Gson; |
| import com.google.gson.JsonArray; |
| import com.google.gson.JsonElement; |
| import com.google.gson.JsonObject; |
| import com.google.gson.JsonParseException; |
| import com.google.gson.JsonParser; |
| import com.google.gson.TypeAdapter; |
| import com.google.gson.TypeAdapterFactory; |
| import com.google.gson.reflect.TypeToken; |
| import com.google.gson.stream.JsonReader; |
| import com.google.gson.stream.JsonToken; |
| import com.google.gson.stream.JsonWriter; |
| |
| /** |
| * Type adapter for {@link Either} and {@link Either3}. |
| */ |
| public class EitherTypeAdapter<L, R> extends TypeAdapter<Either<L, R>> { |
| |
| public static class Factory implements TypeAdapterFactory { |
| |
| @SuppressWarnings({ "rawtypes", "unchecked" }) |
| @Override |
| public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { |
| if (!TypeUtils.isEither(typeToken.getType())) { |
| return null; |
| } |
| return new EitherTypeAdapter(gson, typeToken); |
| } |
| |
| } |
| |
| /** |
| * A predicate that is usedful for checking alternatives in case both the left and the right type |
| * are JSON object types. |
| */ |
| public static class PropertyChecker implements Predicate<JsonElement> { |
| |
| private final String propertyName; |
| private final String expectedValue; |
| private final Class<? extends JsonElement> expectedType; |
| |
| public PropertyChecker(String propertyName) { |
| this.propertyName = propertyName; |
| this.expectedValue = null; |
| this.expectedType = null; |
| } |
| |
| public PropertyChecker(String propertyName, String expectedValue) { |
| this.propertyName = propertyName; |
| this.expectedValue = expectedValue; |
| this.expectedType = null; |
| } |
| |
| public PropertyChecker(String propertyName, Class<? extends JsonElement> expectedType) { |
| this.propertyName = propertyName; |
| this.expectedType = expectedType; |
| this.expectedValue = null; |
| } |
| |
| @Override |
| public boolean test(JsonElement element) { |
| if (element.isJsonObject()) { |
| JsonObject object = element.getAsJsonObject(); |
| JsonElement value = object.get(propertyName); |
| if (expectedValue != null) |
| return value != null && value.isJsonPrimitive() && expectedValue.equals(value.getAsString()); |
| else if (expectedType != null) |
| return expectedType.isInstance(value); |
| else |
| return value != null; |
| } |
| return false; |
| } |
| |
| } |
| |
| /** |
| * A predicate for the case that a type alternative is a list. |
| */ |
| public static class ListChecker implements Predicate<JsonElement> { |
| |
| private final Predicate<JsonElement> elementChecker; |
| private final boolean resultIfEmpty; |
| |
| public ListChecker(Predicate<JsonElement> elementChecker) { |
| this(elementChecker, false); |
| } |
| |
| public ListChecker(Predicate<JsonElement> elementChecker, boolean resultIfEmpty) { |
| this.elementChecker = elementChecker; |
| this.resultIfEmpty = resultIfEmpty; |
| } |
| |
| @Override |
| public boolean test(JsonElement t) { |
| if (elementChecker.test(t)) |
| return true; |
| if (t.isJsonArray()) { |
| JsonArray array = t.getAsJsonArray(); |
| if (array.size() == 0) |
| return resultIfEmpty; |
| for (JsonElement e : array) { |
| if (elementChecker.test(e)) |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| } |
| |
| protected final TypeToken<Either<L, R>> typeToken; |
| protected final EitherTypeArgument<L> left; |
| protected final EitherTypeArgument<R> right; |
| protected final Predicate<JsonElement> leftChecker; |
| protected final Predicate<JsonElement> rightChecker; |
| |
| public EitherTypeAdapter(Gson gson, TypeToken<Either<L, R>> typeToken) { |
| this(gson, typeToken, null, null); |
| } |
| |
| public EitherTypeAdapter(Gson gson, TypeToken<Either<L, R>> typeToken, Predicate<JsonElement> leftChecker, Predicate<JsonElement> rightChecker) { |
| this.typeToken = typeToken; |
| Type[] elementTypes = TypeUtils.getElementTypes(typeToken, Either.class); |
| this.left = new EitherTypeArgument<L>(gson, elementTypes[0]); |
| this.right = new EitherTypeArgument<R>(gson, elementTypes[1]); |
| this.leftChecker = leftChecker; |
| this.rightChecker = rightChecker; |
| } |
| |
| @Override |
| public void write(JsonWriter out, Either<L, R> value) throws IOException { |
| if (value == null) { |
| out.nullValue(); |
| } else if (value.isLeft()) { |
| left.write(out, value.getLeft()); |
| } else { |
| right.write(out, value.getRight()); |
| } |
| } |
| |
| @Override |
| public Either<L, R> read(JsonReader in) throws IOException { |
| JsonToken next = in.peek(); |
| if (next == JsonToken.NULL) { |
| in.nextNull(); |
| return null; |
| } |
| return create(next, in); |
| } |
| |
| protected Either<L, R> create(JsonToken nextToken, JsonReader in) throws IOException { |
| boolean matchesLeft = left.isAssignable(nextToken); |
| boolean matchesRight = right.isAssignable(nextToken); |
| if (matchesLeft && matchesRight) { |
| if (leftChecker != null || rightChecker != null) { |
| JsonElement element = JsonParser.parseReader(in); |
| if (leftChecker != null && leftChecker.test(element)) |
| // Parse the left alternative from the JSON element tree |
| return createLeft(left.read(element)); |
| if (rightChecker != null && rightChecker.test(element)) |
| // Parse the right alternative from the JSON element tree |
| return createRight(right.read(element)); |
| } |
| throw new JsonParseException("Ambiguous Either type: token " + nextToken + " matches both alternatives."); |
| } else if (matchesLeft) { |
| // Parse the left alternative from the JSON stream |
| return createLeft(left.read(in)); |
| } else if (matchesRight) { |
| // Parse the right alternative from the JSON stream |
| return createRight(right.read(in)); |
| } else if (leftChecker != null || rightChecker != null) { |
| // If result is not the list but directly the only item in the list |
| JsonElement element = JsonParser.parseReader(in); |
| if (leftChecker != null && leftChecker.test(element)) |
| // Parse the left alternative from the JSON element tree |
| return createLeft(left.read(element)); |
| if (rightChecker != null && rightChecker.test(element)) |
| // Parse the right alternative from the JSON element tree |
| return createRight(right.read(element)); |
| } |
| throw new JsonParseException("Unexpected token " + nextToken + ": expected " + left + " | " + right + " tokens."); |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected Either<L, R> createLeft(L obj) throws IOException { |
| if (Either3.class.isAssignableFrom(typeToken.getRawType())) |
| return (Either<L, R>) Either3.forLeft3(obj); |
| return Either.forLeft(obj); |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected Either<L, R> createRight(R obj) throws IOException { |
| if (Either3.class.isAssignableFrom(typeToken.getRawType())) |
| return (Either<L, R>) Either3.forRight3((Either<?, ?>) obj); |
| return Either.forRight(obj); |
| } |
| |
| protected static class EitherTypeArgument<T> { |
| |
| protected final TypeToken<T> typeToken; |
| protected final TypeAdapter<T> adapter; |
| protected final Collection<JsonToken> expectedTokens; |
| |
| @SuppressWarnings("unchecked") |
| public EitherTypeArgument(Gson gson, Type type) { |
| this.typeToken = (TypeToken<T>) TypeToken.get(type); |
| this.adapter = (type == Object.class) ? (TypeAdapter<T>) new JsonElementTypeAdapter(gson) : gson.getAdapter(this.typeToken); |
| this.expectedTokens = new HashSet<>(); |
| for (Type expectedType : TypeUtils.getExpectedTypes(type)) { |
| Class<?> rawType = TypeToken.get(expectedType).getRawType(); |
| JsonToken expectedToken = getExpectedToken(rawType); |
| expectedTokens.add(expectedToken); |
| } |
| } |
| |
| protected JsonToken getExpectedToken(Class<?> rawType) { |
| if (rawType.isArray() || Collection.class.isAssignableFrom(rawType) || Tuple.class.isAssignableFrom(rawType)) { |
| return JsonToken.BEGIN_ARRAY; |
| } |
| if (Boolean.class.isAssignableFrom(rawType)) { |
| return JsonToken.BOOLEAN; |
| } |
| if (Number.class.isAssignableFrom(rawType) || Enum.class.isAssignableFrom(rawType)) { |
| return JsonToken.NUMBER; |
| } |
| if (Character.class.isAssignableFrom(rawType) || String.class.isAssignableFrom(rawType)) { |
| return JsonToken.STRING; |
| } |
| return JsonToken.BEGIN_OBJECT; |
| } |
| |
| public boolean isAssignable(JsonToken jsonToken) { |
| return this.expectedTokens.contains(jsonToken); |
| } |
| |
| public void write(JsonWriter out, T value) throws IOException { |
| this.adapter.write(out, value); |
| } |
| |
| public T read(JsonReader in) throws IOException { |
| return this.adapter.read(in); |
| } |
| |
| public T read(JsonElement element) throws IOException { |
| return this.adapter.fromJsonTree(element); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder(); |
| for (JsonToken expectedToken : expectedTokens) { |
| if (builder.length() != 0) { |
| builder.append(" | "); |
| } |
| builder.append(expectedToken); |
| } |
| return builder.toString(); |
| } |
| |
| } |
| |
| } |