All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.talend.sdk.component.intellij.service.SuggestionServiceImpl Maven / Gradle / Ivy

There is a newer version: 10.57.0
Show newest version
/**
 * Copyright (C) 2006-2023 Talend Inc. - www.talend.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.talend.sdk.component.intellij.service;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Locale.ROOT;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Stream.of;
import static org.talend.sdk.component.intellij.completion.properties.Suggestion.DISPLAY_NAME;
import static org.talend.sdk.component.intellij.completion.properties.Suggestion.PLACEHOLDER;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;

import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.lang.jvm.JvmModifier;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.JavaRecursiveElementWalkingVisitor;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnnotationMemberValue;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiEnumConstant;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.PsiPackage;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiType;
import com.intellij.psi.impl.source.PsiClassReferenceType;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;

import org.talend.sdk.component.intellij.completion.properties.Suggestion;

public class SuggestionServiceImpl implements SuggestionService {

    private static final String COMPONENTS = "org.talend.sdk.component.api.component.Components";

    private static final String OPTION = "org.talend.sdk.component.api.configuration.Option";

    private static final String PROCESSOR = "org.talend.sdk.component.api.processor.Processor";

    private static final String PARTITION_MAPPER = "org.talend.sdk.component.api.input.PartitionMapper";

    private static final String EMITTER = "org.talend.sdk.component.api.input.Emitter";

    private static final String DATA_STORE = "org.talend.sdk.component.api.configuration.type.DataStore";

    private static final String DATA_SET = "org.talend.sdk.component.api.configuration.type.DataSet";

    private static final String ACTION_TYPE = "org.talend.sdk.component.api.service.ActionType";

    private static final String ACTION_ACTION = "org.talend.sdk.component.api.service.Action";

    private static final String ACTION_HEALTHCHECK = "org.talend.sdk.component.api.service.healthcheck.HealthCheck";

    private static final String ACTION_ASYNCVALIDATION =
            "org.talend.sdk.component.api.service.asyncvalidation.AsyncValidation";

    private static final String ACTION_DYNAMICVALUES = "org.talend.sdk.component.api.service.completion.DynamicValues";

    private static final String ACTION_SUGGESTIONS = "org.talend.sdk.component.api.service.completion.Suggestions";

    private static final String ACTION_DISCOVERSCHEMA = "org.talend.sdk.component.api.service.schema.DiscoverSchema";

    private static final String ACTION_UPDATE = "org.talend.sdk.component.api.service.update.Update";

    private static final List ACTIONS = new ArrayList() {

        {
            add(ACTION_ACTION);
            add(ACTION_HEALTHCHECK);
            add(ACTION_ASYNCVALIDATION);
            add(ACTION_DYNAMICVALUES);
            add(ACTION_SUGGESTIONS);
            add(ACTION_DISCOVERSCHEMA);
            add(ACTION_UPDATE);
        }
    };

    private static final Predicate IS_STATIC;
    static {
        Predicate predicate = c -> false; // better to ignore if idea is not compatible, will not break end
                                                    // user
        try {
            final Method getModifiers = PsiClass.class.getMethod("getModifiers");
            predicate = clazz -> {
                try {
                    return Stream
                            .of(JvmModifier[].class.cast(getModifiers.invoke(clazz)))
                            .anyMatch(m -> JvmModifier.STATIC == m);
                } catch (final IllegalAccessException | InvocationTargetException e) {
                    return false;
                }
            };
        } catch (final NoSuchMethodException nsme) {
            try {
                final Method hasModifier = PsiClass.class.getMethod("hasModifier", JvmModifier.class);
                predicate = clazz -> {
                    try {
                        return Boolean.class.cast(hasModifier.invoke(clazz, JvmModifier.STATIC));
                    } catch (final IllegalAccessException | InvocationTargetException e) {
                        return false;
                    }
                };
            } catch (final NoSuchMethodException nsme2) {
                // no-op
            }
        }
        IS_STATIC = predicate;
    }

    @Override
    public boolean isSupported(final CompletionParameters completionParameters) {
        final String name = completionParameters.getOriginalFile().getName();
        return "Messages.properties".equals(name);
    }

    @Override
    public List computeValueSuggestions(final String text) {
        final String[] segments = text.split("\\.");
        switch (segments.length) {
        case 2:
            return singletonList(new Suggestion(toHumanString(segments[0]), null)
                    .newLookupElement(withPriority(Suggestion.Type.Family)));
        case 3:
            return singletonList(new Suggestion(toHumanString(segments[1]), null)
                    .newLookupElement(withPriority(Suggestion.Type.Configuration)));
        case 5: // action format
            return singletonList(new Suggestion(toHumanString(segments[3]), null)
                    .newLookupElement(withPriority(Suggestion.Type.Action)));
        default:
            return emptyList();
        }
    }

    @Override
    public List computeKeySuggestions(final Project project, final Module module,
            final String packageName, final List containerElements, final String query) {
        final JavaPsiFacade javaPsiFacade = JavaPsiFacade.getInstance(project);
        final PsiPackage pkg = javaPsiFacade.findPackage(packageName);
        if (pkg == null) {
            return Collections.emptyList();
        }

        final String defaultFamily = getFamilyFromPackageInfo(pkg, module);
        return Stream
                .concat(Stream
                        .concat(of(pkg.getClasses())
                                .flatMap(this::unwrapInnerClasses)
                                .filter(c -> AnnotationUtil
                                        .findAnnotation(c, PARTITION_MAPPER, PROCESSOR, EMITTER) != null)
                                .flatMap(clazz -> fromComponent(clazz, defaultFamily)),
                                of(pkg.getClasses())
                                        .flatMap(this::unwrapInnerClasses)
                                        .filter(c -> of(c.getAllFields())
                                                .anyMatch(f -> AnnotationUtil.findAnnotation(f, OPTION) != null))
                                        .flatMap(c -> fromConfiguration(defaultFamily, c.getName(), c))),
                        of(pkg.getClasses())
                                .flatMap(this::unwrapInnerClasses)
                                .flatMap(c -> of(c.getMethods())
                                        .filter(m -> ACTIONS
                                                .stream()
                                                .anyMatch(action -> AnnotationUtil.findAnnotation(m, action) != null)))
                                .map(m -> ACTIONS
                                        .stream()
                                        .map(action -> AnnotationUtil.findAnnotation(m, action))
                                        .filter(Objects::nonNull)
                                        .findFirst()
                                        .get())
                                .map(action -> fromAction(action, defaultFamily))
                                .filter(Optional::isPresent)
                                .map(Optional::get))
                .filter(s -> containerElements.isEmpty() || !containerElements.contains(s.getKey()))
                .filter(s -> query == null || query.isEmpty() || s.getKey().startsWith(query))
                .map(s -> s.newLookupElement(withPriority(s.getType())))
                .collect(toList());
    }

    private Optional fromAction(final PsiAnnotation action, final String defaultFamily) {

        final String actionType = ofNullable(action.getNameReferenceElement())
                .map(PsiReference::resolve)
                .filter(PsiClass.class::isInstance)
                .map(e -> (PsiClass) e)
                .map(e -> AnnotationUtil.findAnnotation(e, true, ACTION_TYPE))
                .map(e -> e.findAttributeValue("value"))
                .map(v -> removeQuotes(v.getText()))
                .orElse("");

        return ofNullable(action.findAttributeValue("value"))
                .map(t -> removeQuotes(t.getText()))
                .map(actionId -> new Suggestion(
                        defaultFamily + ".actions." + actionType + "." + actionId + "." + DISPLAY_NAME,
                        Suggestion.Type.Action));
    }

    private String toHumanString(final String segment) {
        final StringBuilder builder = new StringBuilder();
        for (int i = 0; i < segment.length(); i++) {
            final char ch = segment.charAt(i);
            if (i == 0) {
                builder.append(Character.toUpperCase(ch));
            } else {
                if (Character.isUpperCase(ch)) {
                    builder.append(" ");
                }
                builder.append(ch);
            }
        }
        return builder.toString();
    }

    private Stream unwrapInnerClasses(final PsiClass c) {
        return Stream
                .concat(Stream.of(c),
                        Stream.of(c.getAllInnerClasses()).filter(IS_STATIC::test).flatMap(this::unwrapInnerClasses));
    }

    private int withPriority(final Suggestion.Type type) {
        if (Suggestion.Type.Action.equals(type)) {
            return 5;
        }
        if (Suggestion.Type.Family.equals(type)) {
            return 3;
        }
        if (Suggestion.Type.Component.equals(type)) {
            return 2;
        }
        return 1;
    }

    private Stream fromComponent(final PsiClass clazz, final String defaultFamily) {
        final PsiAnnotation componentAnnotation =
                AnnotationUtil.findAnnotation(clazz, PARTITION_MAPPER, PROCESSOR, EMITTER);
        final PsiAnnotationMemberValue name = componentAnnotation.findAttributeValue("name");
        if (name == null || "\"\"".equals(name.getText())) {
            return Stream.empty();
        }

        final PsiAnnotationMemberValue familyValue = componentAnnotation.findAttributeValue("family");
        final String componentFamily = (familyValue == null || removeQuotes(familyValue.getText()).isEmpty()) ? null
                : removeQuotes(familyValue.getText());

        final String family = ofNullable(componentFamily).orElseGet(() -> ofNullable(defaultFamily).orElse(null));
        if (family == null) {
            return Stream.empty();
        }

        return Stream
                .of(new Suggestion(family + "." + DISPLAY_NAME, Suggestion.Type.Family), new Suggestion(
                        family + "." + removeQuotes(name.getText()) + "." + DISPLAY_NAME, Suggestion.Type.Component));
    }

    private Stream extractConfigTypes(final String family, final PsiClass configClass) {
        return Stream
                .of(DATA_STORE, DATA_SET)
                .map(a -> AnnotationUtil.findAnnotation(configClass, a))
                .filter(Objects::nonNull)
                .map(a -> new Suggestion(family + '.'
                        + a.getQualifiedName().substring(a.getQualifiedName().lastIndexOf('.') + 1).toLowerCase(ROOT)
                        + '.' + removeQuotes(a.findAttributeValue("value").getText()) + '.' + DISPLAY_NAME,
                        Suggestion.Type.Configuration));
    }

    private Stream fromConfiguration(final String family, final String configurationName,
            final PsiClass configClazz) {
        return Stream
                .concat(of(configClazz.getAllFields())
                        .filter(field -> AnnotationUtil.findAnnotation(field, OPTION) != null)
                        .flatMap(field -> {
                            final PsiAnnotation fOption = AnnotationUtil.findAnnotation(field, OPTION);
                            final PsiAnnotationMemberValue fOptionValue = fOption.findAttributeValue("value");

                            String fieldName = removeQuotes(fOptionValue.getText());
                            if (fieldName.isEmpty()) {
                                fieldName = field.getName();
                            }

                            final Suggestion displayNameSuggestion =
                                    new Suggestion(configurationName + "." + fieldName + "." + DISPLAY_NAME,
                                            Suggestion.Type.Configuration);
                            final PsiType type = field.getType();
                            if ("java.lang.String".equals(type.getCanonicalText())) {
                                return Stream
                                        .of(displayNameSuggestion,
                                                new Suggestion(configurationName + "." + fieldName + "." + PLACEHOLDER,
                                                        Suggestion.Type.Configuration));
                            }
                            final PsiClass clazz = findClass(type);
                            if (clazz != null && clazz.isEnum()) {
                                return Stream
                                        .concat(Stream.of(displayNameSuggestion),
                                                Stream
                                                        .of(clazz.getFields())
                                                        .filter(PsiEnumConstant.class::isInstance)
                                                        .map(f -> clazz
                                                                .getName()
                                                                .substring(clazz.getName().lastIndexOf('.') + 1) + '.'
                                                                + f.getName() + "._displayName")
                                                        .map(v -> new Suggestion(v, Suggestion.Type.Configuration)));
                            }
                            return Stream.of(displayNameSuggestion);
                        }), extractConfigTypes(family, configClazz));
    }

    private PsiClass findClass(final PsiType type) {
        if (PsiClass.class.isInstance(type)) {
            return PsiClass.class.cast(type);
        }
        if (PsiClassReferenceType.class.isInstance(type)) {
            return PsiClassReferenceType.class.cast(type).resolve();
        }
        return null;
    }

    private String getFamilyFromPackageInfo(final PsiPackage psiPackage, final Module module) {
        return of(FilenameIndex
                .getFilesByName(psiPackage.getProject(), "package-info.java", GlobalSearchScope.moduleScope(module)))
                        .map(psiFile -> {
                            if (!PsiJavaFile.class
                                    .cast(psiFile)
                                    .getPackageName()
                                    .equals(psiPackage.getQualifiedName())) {
                                return null;
                            }
                            final String[] family = { null };
                            PsiJavaFile.class.cast(psiFile).accept(new JavaRecursiveElementWalkingVisitor() {

                                @Override
                                public void visitAnnotation(final PsiAnnotation annotation) {
                                    super.visitAnnotation(annotation);
                                    if (!COMPONENTS.equals(annotation.getQualifiedName())) {
                                        return;
                                    }
                                    final PsiAnnotationMemberValue familyAttribute =
                                            annotation.findAttributeValue("family");
                                    if (familyAttribute == null) {
                                        return;
                                    }
                                    family[0] = removeQuotes(familyAttribute.getText());
                                }
                            });
                            return family[0];
                        })
                        .filter(Objects::nonNull)
                        .findFirst()
                        .orElseGet(() -> {
                            final PsiPackage parent = psiPackage.getParentPackage();
                            if (parent == null) {
                                return null;
                            }
                            return getFamilyFromPackageInfo(parent, module);
                        });
    }

    private String removeQuotes(final String s) {
        if (s == null || s.isEmpty()) {
            return s;
        }
        return s.replaceAll("^\"|\"$", "");
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy