/*-
 * #%L
 * AEM Rules for SonarQube
 * %%
 * Copyright (C) 2015-2019 Cognifide Limited
 * %%
 * 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.
 * #L%
 */
package com.cognifide.aemrules.java.checks;

import com.cognifide.aemrules.metadata.Metadata;
import com.cognifide.aemrules.tag.Tags;
import com.cognifide.aemrules.version.AemVersion;
import java.util.Set;
import org.sonar.check.Priority;
import org.sonar.check.Rule;
import org.sonar.plugins.java.api.JavaFileScanner;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.tree.AnnotationTree;
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.Tree.Kind;
import org.sonar.plugins.java.api.tree.TypeTree;
import org.sonar.plugins.java.api.tree.VariableTree;

@Rule(
    key = ThreadSafeFieldCheck.RULE_KEY,
    name = ThreadSafeFieldCheck.RULE_NAME,
    priority = Priority.CRITICAL,
    tags = {Tags.BUG, Tags.AEM}
)
@AemVersion(
    all = true
)
@Metadata(
    technicalDebt = "1h"
)
public class ThreadSafeFieldCheck extends BaseTreeVisitor implements JavaFileScanner {

    public static final String RULE_NAME = "Non-thread safe object used as a field of Servlet / Filter etc.";

    public static final String RULE_KEY = "AEM-3";

    public static final String RULE_MESSAGE = "Usage of %s as a field is not thread safe.";

    private static final Set<String> VULNERABLE_CLASSES = Set.of(
        // empty for now
    );

    private static final Set<String> VULNERABLE_INTERFACES = Set.of(
        "javax.servlet.Servlet",
        "javax.servlet.Filter",
        "org.osgi.service.event.EventHandler"
    );

    private static final Set<String> VULNERABLE_ANNOTATIONS = Set.of(
        "org.apache.felix.scr.annotations.Component",
        "org.osgi.service.component.annotations.Component",
        "org.apache.felix.scr.annotations.sling.SlingServlet", // this is possibly duplicative, but that shouldn't be a problem.
        "org.apache.felix.scr.annotations.sling.SlingFilter" // this is possibly duplicative, but that shouldn't be a problem.
    );

    private static final Set<String> NON_THREAD_SAFE_TYPES = Set.of(
        "org.apache.sling.api.resource.ResourceResolver",
        "javax.jcr.Session",
        "com.day.cq.wcm.api.PageManager",
        "com.day.cq.wcm.api.components.ComponentManager",
        "com.day.cq.wcm.api.designer.Designer",
        "com.day.cq.dam.api.AssetManager",
        "com.day.cq.tagging.TagManager",
        "com.day.cq.security.UserManager",
        "org.apache.jackrabbit.api.security.user.Authorizable",
        "org.apache.jackrabbit.api.security.user.User",
        "org.apache.jackrabbit.api.security.user.UserManager");

    private JavaFileScannerContext context;

    @Override
    public void scanFile(JavaFileScannerContext javaFileScannerContext) {
        context = javaFileScannerContext;
        scan(context.getTree());
    }

    @Override
    public void visitClass(ClassTree classTree) {
        boolean extendsClass = extendsVulnerableClass(classTree);
        boolean implementsInterface = implementsVulnerableInterface(classTree);
        boolean hasAnnotation = hasAnnotation(classTree);
        boolean vulnerable = extendsClass || implementsInterface || hasAnnotation;
        if (vulnerable) {
            checkMembers(classTree);
        }
        super.visitClass(classTree);
    }

    private void checkMembers(ClassTree classTree) {
        for (Tree member : classTree.members()) {
            checkMember(member);
        }
    }

    private void checkMember(Tree member) {
        boolean isVariableField = member.is(Kind.VARIABLE);
        if (isVariableField) {
            VariableTree variableField = (VariableTree) member;
            String name = variableField.type().symbolType().fullyQualifiedName();
            if (NON_THREAD_SAFE_TYPES.contains(name)) {
                context.reportIssue(this, member, String.format(RULE_MESSAGE, name));
            }
        }
    }

    private boolean hasAnnotation(ClassTree clazz) {
        boolean hasAnnotation = false;
        for (AnnotationTree annotationTree : clazz.modifiers().annotations()) {
            String name = annotationTree.annotationType().symbolType().fullyQualifiedName();
            hasAnnotation |= VULNERABLE_ANNOTATIONS.contains(name);
        }
        return hasAnnotation;
    }

    private boolean extendsVulnerableClass(ClassTree clazz) {
        boolean extendsClass = false;
        TypeTree type = clazz.superClass();
        if (type != null) {
            String name = type.symbolType().fullyQualifiedName();
            extendsClass = VULNERABLE_CLASSES.contains(name);
        }
        return extendsClass;
    }

    private boolean implementsVulnerableInterface(ClassTree clazz) {
        boolean implementsInterface = false;
        for (TypeTree typeTree : clazz.superInterfaces()) {
            String name = typeTree.symbolType().fullyQualifiedName();
            implementsInterface |= VULNERABLE_INTERFACES.contains(name);
        }
        return implementsInterface;
    }

}