package com.jetbrains.jsonSchema.impl;
import com.intellij.json.psi.JsonObject;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.SmartPsiElementPointer;
import com.intellij.util.Consumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Convertor;
import com.jetbrains.jsonSchema.JsonSchemaFileType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Irina.Chernushina on 10/22/2015.
*
* We will be using following simplification:
* for all-of conditions,
* 1) we will only check that we can move forward through one of the links
* and will be selecting first applicable link
* 2) it will be totally checked only on last level
*
* this would break the following scheme:
* "all-of" [{"properties" : {
* "inner": {"type": "string"}
* }},
* {"properties": {
* "inner": {"enum": ["one", "two"]}
* }}]
*
* but this construct seems not very realistic for human writing...
* seems better using "$ref"
*
* This will significantly simplify code, since we will not need to take the list of possible variants of children and grandchildren;
* we will be able to iterate variants, not collections of variants
*/
public class JsonSchemaWalker {
public static final JsonOriginalPsiWalker JSON_ORIGINAL_PSI_WALKER = new JsonOriginalPsiWalker();
public interface CompletionSchemesConsumer {
void consume(boolean isName,
@NotNull JsonSchemaObject schema,
@NotNull VirtualFile schemaFile,
@NotNull List<Step> steps);
void oneOf(boolean isName,
@NotNull List<JsonSchemaObject> list,
@NotNull VirtualFile schemaFile,
@NotNull List<Step> steps);
void anyOf(boolean isName,
@NotNull List<JsonSchemaObject> list,
@NotNull VirtualFile schemaFile,
@NotNull List<Step> steps);
}
public static void findSchemasForAnnotation(@NotNull final PsiElement element,
@NotNull JsonLikePsiWalker walker,
@NotNull final CompletionSchemesConsumer consumer,
@NotNull final JsonSchemaObject rootSchema,
@NotNull VirtualFile schemaFile) {
final List<Step> position = walker.findPosition(element, false, true);
if (position == null || position.isEmpty()) return;
// but this does not validate definitions section against general schema --> should be done separately
final Step firstStep = position.get(0);
if (JsonSchemaFileType.INSTANCE.equals(element.getContainingFile().getFileType()) &&
firstStep != null && firstStep.getTransition() instanceof PropertyTransition &&
"definitions".equals(((PropertyTransition)firstStep.getTransition()).getName())) return;
extractSchemaVariants(element.getProject(), consumer, schemaFile, rootSchema, false, position, true);
}
public static void findSchemasForCompletion(@NotNull final PsiElement element,
@NotNull JsonLikePsiWalker walker,
@NotNull final CompletionSchemesConsumer consumer,
@NotNull final JsonSchemaObject rootSchema,
@NotNull VirtualFile schemaFile) {
final PsiElement checkable = walker.goUpToCheckable(element);
if (checkable == null) return;
final boolean isName = walker.isName(checkable);
final List<Step> position = walker.findPosition(checkable, isName, !isName);
if (position == null || position.isEmpty()) {
if (isName) consumer.consume(true, rootSchema, schemaFile, Collections.emptyList());
return;
}
extractSchemaVariants(element.getProject(), consumer, schemaFile, rootSchema, isName, position, false);
}
public static void findSchemasForDocumentation(@NotNull final PsiElement element,
@NotNull JsonLikePsiWalker walker,
@NotNull final CompletionSchemesConsumer consumer,
@NotNull final JsonSchemaObject rootSchema,
@NotNull VirtualFile schemaFile) {
final PsiElement checkable = walker.goUpToCheckable(element);
if (checkable == null) return;
final List<Step> position = walker.findPosition(checkable, true, true);
if (position == null || position.isEmpty()) {
consumer.consume(true, rootSchema, schemaFile, Collections.emptyList());
return;
}
extractSchemaVariants(element.getProject(), consumer, schemaFile, rootSchema, true, position, false);
}
public static Pair<List<Step>, String> buildSteps(@NotNull String nameInSchema) {
final List<String> chain = StringUtil.split(JsonSchemaExportedDefinitions.normalizeId(nameInSchema).replace("\\", "/"), "/");
final List<Step> steps = chain.stream().filter(s -> !s.isEmpty()).map(item -> new Step(StateType._unknown, new PropertyTransition(item)))
.collect(Collectors.toList());
if (steps.isEmpty()) return Pair.create(Collections.emptyList(), nameInSchema);
return Pair.create(steps, chain.get(chain.size() - 1));
}
protected static class DefinitionsResolver {
@NotNull private final List<Step> myPosition;
final List<Pair<JsonSchemaObject, List<Step>>> myVariants;
private List<JsonSchemaObject> mySchemaObjects = new ArrayList<>();
private boolean myOneOf;
public DefinitionsResolver(@NotNull List<Step> position) {
myPosition = position;
myVariants = new ArrayList<>();
}
public void consumeResult(@NotNull JsonSchemaObject schema) {
mySchemaObjects.add(schema);
}
public void consumeSmallStep(@NotNull JsonSchemaObject schema, int idx) {
final List<JsonSchemaObject> list = gatherSchemas(schema);
for (JsonSchemaObject object : list) {
if (!StringUtil.isEmptyOrSpaces(object.getDefinitionAddress())) {
myVariants.add(Pair.create(object, myPosition.subList(idx + 1, myPosition.size())));
}
}
}
public boolean isFound() {
return !mySchemaObjects.isEmpty();
}
public List<JsonSchemaObject> getSchemaObjects() {
return mySchemaObjects;
}
public boolean isOneOf() {
return myOneOf;
}
public List<Pair<JsonSchemaObject, List<Step>>> getVariants() {
return myVariants;
}
public void setOneOf(boolean oneOf) {
myOneOf = oneOf;
}
}
public static void extractSchemaVariants(@NotNull final Project project,
@NotNull final CompletionSchemesConsumer consumer,
@NotNull VirtualFile rootSchemaFile,
@NotNull JsonSchemaObject rootSchema,
boolean isName,
List<Step> position,
boolean acceptAdditionalPropertiesSchemas) {
final Set<Trinity<JsonSchemaObject, VirtualFile, List<Step>>> control = new HashSet<>();
final JsonSchemaServiceEx serviceEx = JsonSchemaServiceEx.Impl.getEx(project);
final ArrayDeque<Trinity<JsonSchemaObject, VirtualFile, List<Step>>> queue = new ArrayDeque<>();
queue.add(Trinity.create(rootSchema, rootSchemaFile, position));
while (!queue.isEmpty()) {
final Trinity<JsonSchemaObject, VirtualFile, List<Step>> trinity = queue.removeFirst();
if (!control.add(trinity)) break;
final JsonSchemaObject object = trinity.getFirst();
final VirtualFile schemaFile = trinity.getSecond();
final List<Step> path = trinity.getThird();
if (path.isEmpty()) {
consumer.consume(isName, object, schemaFile, path);
continue;
}
final DefinitionsResolver definitionsResolver = new DefinitionsResolver(path);
extractSchemaVariants(definitionsResolver, object, path, acceptAdditionalPropertiesSchemas);
if (definitionsResolver.isFound()) {
final List<JsonSchemaObject> matchedSchemas = definitionsResolver.getSchemaObjects();
matchedSchemas.forEach(matchedSchema -> {
final List<JsonSchemaObject> list = gatherSchemas(matchedSchema);
for (JsonSchemaObject schemaObject : list) {
if (schemaObject.getDefinitionAddress() != null && !schemaObject.getDefinitionAddress().startsWith("#/")) {
final List<Step> steps = new ArrayList<>();
// add value step if needed
if (!isName) steps.add(new Step(StateType._value, null));
visitSchemaByDefinitionAddress(serviceEx, queue, schemaFile, schemaObject.getDefinitionAddress(), steps);
}
}
});
if (matchedSchemas.size() == 1) consumer.consume(isName, matchedSchemas.get(0), schemaFile, path);
else {
if (definitionsResolver.isOneOf()) {
consumer.oneOf(isName, matchedSchemas, schemaFile, path);
} else {
consumer.anyOf(isName, matchedSchemas, schemaFile, path);
}
}
} else {
final List<Pair<JsonSchemaObject, List<Step>>> variants = definitionsResolver.getVariants();
for (Pair<JsonSchemaObject, List<Step>> variant : variants) {
if (variant.getFirst().getDefinitionAddress() == null) continue;
visitSchemaByDefinitionAddress(serviceEx, queue, schemaFile, variant.getFirst().getDefinitionAddress(), variant.getSecond());
}
}
}
}
private static void visitSchemaByDefinitionAddress(JsonSchemaServiceEx serviceEx,
ArrayDeque<Trinity<JsonSchemaObject, VirtualFile, List<Step>>> queue,
VirtualFile schemaFile, @NotNull final String definitionAddress, final List<Step> steps) {
// we can have also non-absolute transfers here, because allOf and others can not be put in-place into schema
final JsonSchemaReader.SchemaUrlSplitter splitter = new JsonSchemaReader.SchemaUrlSplitter(definitionAddress);
//noinspection ConstantConditions
final VirtualFile variantSchemaFile = splitter.isAbsolute() ? serviceEx.getSchemaFileById(splitter.getSchemaId(), schemaFile) :
schemaFile;
if (variantSchemaFile == null) return;
serviceEx.visitSchemaObject(variantSchemaFile,
variantObject -> {
List<Step> variantSteps = buildSteps(splitter.getRelativePath()).getFirst();
// empty list might be not modifiable
if (variantSteps.isEmpty()) variantSteps = steps;
else variantSteps.addAll(steps);
queue.add(Trinity.create(variantObject, variantSchemaFile, variantSteps));
return true;
});
}
private static void extractSchemaVariants(@NotNull DefinitionsResolver consumer,
@NotNull JsonSchemaObject rootSchema,
@NotNull List<Step> position,
boolean acceptAdditionalPropertiesSchemas) {
final ArrayDeque<Pair<JsonSchemaObject, Integer>> queue = new ArrayDeque<>();
queue.add(Pair.create(rootSchema, 0));
while (!queue.isEmpty()) {
final Pair<JsonSchemaObject, Integer> pair = queue.removeFirst();
final JsonSchemaObject schema = pair.getFirst();
final Integer level = pair.getSecond();
if (position.size() <= level) {
return;
}
final Step step = position.get(level);
if (step.getTransition() == null) {
consumer.consumeResult(schema);
continue;
}
if (step.getTransition() != null && !StateType._unknown.equals(step.getType())
&& !step.getTransition().possibleFromState(step.getType())) {
continue;
}
final Condition<JsonSchemaObject> byTypeFilter = object -> byStateType(step.getType(), object);
// not??
boolean isOneOf = schema.getOneOf() != null;
List<JsonSchemaObject> list = gatherSchemas(schema);
list = ContainerUtil.filter(list, byTypeFilter);
final Consumer<JsonSchemaObject> reporter = object -> {
if ((level + 1) >= position.size()) {
consumer.setOneOf(isOneOf);
consumer.consumeResult(object);
}
else {
consumer.consumeSmallStep(object, level);
queue.add(Pair.create(object, level + 1));
}
};
TransitionResultConsumer transitionResultConsumer;
for (JsonSchemaObject object : list) {
transitionResultConsumer = new TransitionResultConsumer();
step.getTransition().step(object, transitionResultConsumer, acceptAdditionalPropertiesSchemas);
if (transitionResultConsumer.isNothing()) continue;
if (transitionResultConsumer.getSchema() != null) {
reporter.consume(transitionResultConsumer.getSchema());
}
}
}
}
private static List<JsonSchemaObject> gatherSchemas(JsonSchemaObject schema) {
if (schema.getAllOf() != null) {
return gatherSchemas(mergeAll(schema));
} else {
if (schema.getAnyOf() != null) {
return mergeList(schema.getAnyOf(), schema, false);
}
if (schema.getOneOf() != null) {
return mergeList(schema.getOneOf(), schema, true);
}
}
return Collections.singletonList(schema);
}
@NotNull
public static JsonSchemaObject mergeAll(@NotNull JsonSchemaObject schema) {
JsonSchemaObject currentBase = copySchema(schema);
currentBase.setAllOf(null);
for (JsonSchemaObject object : schema.getAllOf()) {
currentBase = merge(currentBase, object);
}
return currentBase;
}
@NotNull
private static JsonSchemaObject copySchema(@NotNull JsonSchemaObject schema) {
JsonSchemaObject currentBase = new JsonSchemaObject(schema.getPeerPointer());
currentBase.mergeValues(schema);
currentBase.setDefinitionsPointer(schema.getDefinitionsPointer());
return currentBase;
}
public static List<JsonSchemaObject> mergeList(@NotNull final Collection<JsonSchemaObject> coll,
@NotNull JsonSchemaObject schema, boolean oneOf) {
final JsonSchemaObject copy = copySchema(schema);
if (oneOf) copy.setOneOf(null); else copy.setAnyOf(null);
return coll.stream().map(s -> merge(copy, s)).collect(Collectors.toList());
}
public static JsonSchemaObject merge(@NotNull JsonSchemaObject base, @NotNull JsonSchemaObject other) {
final JsonSchemaObject object = new JsonSchemaObject(base.getPeerPointer());
object.mergeValues(other);
object.mergeValues(base);
object.setDefinitionsPointer(base.getDefinitionsPointer());
return object;
}
private static boolean byStateType(@NotNull final StateType type, @NotNull final JsonSchemaObject schema) {
if (StateType._unknown.equals(type)) return true;
final JsonSchemaType requiredType = type.getCorrespondingJsonType();
if (requiredType == null) return true;
if (schema.getType() != null) {
return requiredType.equals(schema.getType());
}
if (schema.getTypeVariants() != null) {
for (JsonSchemaType schemaType : schema.getTypeVariants()) {
if (requiredType.equals(schemaType)) return true;
}
return false;
}
return true;
}
public static JsonLikePsiWalker getWalker(@NotNull final PsiElement element, JsonSchemaObject schemaObject) {
return getJsonLikeThing(element, walker -> walker, schemaObject);
}
@Nullable
private static <T> T getJsonLikeThing(@NotNull final PsiElement element,
@NotNull Convertor<JsonLikePsiWalker, T> convertor,
JsonSchemaObject schemaObject) {
final List<JsonLikePsiWalker> list = new ArrayList<>();
list.add(JSON_ORIGINAL_PSI_WALKER);
final JsonLikePsiWalkerFactory[] extensions = Extensions.getExtensions(JsonLikePsiWalkerFactory.EXTENSION_POINT_NAME);
list.addAll(Arrays.stream(extensions).map(extension -> extension.create(schemaObject)).collect(Collectors.toList()));
for (JsonLikePsiWalker walker : list) {
if (walker.handles(element)) return convertor.convert(walker);
}
return null;
}
public static class Step {
private final StateType myType;
@Nullable
private final Transition myTransition;
public Step(StateType type, @Nullable Transition transition) {
myType = type;
myTransition = transition;
}
public StateType getType() {
return myType;
}
@Nullable
public Transition getTransition() {
return myTransition;
}
}
public static class PropertyTransition implements Transition {
@NotNull private final String myName;
public PropertyTransition(@NotNull String name) {
myName = name;
}
@Override
public boolean possibleFromState(@NotNull StateType stateType) {
return StateType._object.equals(stateType);
}
@Override
public void step(@NotNull JsonSchemaObject parent,
@NotNull TransitionResultConsumer resultConsumer,
boolean acceptAdditionalPropertiesSchemas) {
if ("definitions".equals(myName)) {
if (parent.getDefinitions() != null) {
final SmartPsiElementPointer<JsonObject> pointer = parent.getDefinitionsPointer();
final JsonSchemaObject object = new JsonSchemaObject(pointer);
object.setProperties(parent.getDefinitions());
resultConsumer.setSchema(object);
return;
}
}
final JsonSchemaObject child = parent.getProperties().get(myName);
if (child != null) {
resultConsumer.setSchema(child);
} else {
final JsonSchemaObject schema = parent.getMatchingPatternPropertySchema(myName);
if (schema != null) {
resultConsumer.setSchema(schema);
return;
}
if (parent.getAdditionalPropertiesSchema() != null) {
if (acceptAdditionalPropertiesSchemas) {
resultConsumer.setSchema(parent.getAdditionalPropertiesSchema());
}
} else {
if (!Boolean.FALSE.equals(parent.getAdditionalPropertiesAllowed())) {
resultConsumer.anything();
} else {
resultConsumer.nothing();
}
}
}
}
@NotNull
public String getName() {
return myName;
}
}
public static class ArrayTransition implements Transition {
private final int myIdx;
public ArrayTransition(int idx) {
myIdx = idx;
}
@Override
public boolean possibleFromState(@NotNull StateType stateType) {
return StateType._array.equals(stateType);
}
@Override
public void step(@NotNull JsonSchemaObject parent,
@NotNull TransitionResultConsumer resultConsumer,
boolean acceptAdditionalPropertiesSchemas) {
if (parent.getItemsSchema() != null) {
resultConsumer.setSchema(parent.getItemsSchema());
} else if (parent.getItemsSchemaList() != null) {
final List<JsonSchemaObject> list = parent.getItemsSchemaList();
if (myIdx >= 0 && myIdx < list.size()) {
resultConsumer.setSchema(list.get(myIdx));
} else if (parent.getAdditionalItemsSchema() != null) {
resultConsumer.setSchema(parent.getAdditionalItemsSchema());
} else if (!Boolean.FALSE.equals(parent.getAdditionalItemsAllowed())) {
resultConsumer.anything();
} else {
resultConsumer.nothing();
}
}
}
}
private interface Transition {
boolean possibleFromState(@NotNull StateType stateType);
void step(@NotNull JsonSchemaObject parent, @NotNull TransitionResultConsumer resultConsumer, boolean acceptAdditionalPropertiesSchemas);
}
public enum StateType {
_object(JsonSchemaType._object), _array(JsonSchemaType._array), _value(null), _unknown(null);
@Nullable
private final JsonSchemaType myCorrespondingJsonType;
StateType(@Nullable JsonSchemaType correspondingJsonType) {
myCorrespondingJsonType = correspondingJsonType;
}
@Nullable
public JsonSchemaType getCorrespondingJsonType() {
return myCorrespondingJsonType;
}
}
private static class TransitionResultConsumer {
@Nullable private JsonSchemaObject mySchema;
private boolean myAny;
private boolean myNothing;
private boolean myOneOf;
public TransitionResultConsumer() {
myNothing = true;
}
@Nullable
public JsonSchemaObject getSchema() {
return mySchema;
}
public void setSchema(@Nullable JsonSchemaObject schema) {
mySchema = schema;
myNothing = schema == null;
}
public boolean isAny() {
return myAny;
}
public void anything() {
myAny = true;
myNothing = false;
}
public boolean isNothing() {
return myNothing;
}
public void nothing() {
myNothing = true;
myAny = false;
}
public void oneOf() {
myOneOf = true;
}
public boolean isOneOf() {
return myOneOf;
}
}
}