com.rtoth.boilerplate.GetTestMethodsDialog.java Source code

Java tutorial

Introduction

Here is the source code for com.rtoth.boilerplate.GetTestMethodsDialog.java

Source

/*
 * Copyright (c) 2016 Robert Toth
 * <p>
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * <p>
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * <p>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.rtoth.boilerplate;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiPrimitiveType;
import com.intellij.psi.PsiType;
import com.intellij.psi.util.PsiUtil;
import com.rtoth.boilerplate.parameters.IntegerParameterRule;
import com.rtoth.boilerplate.parameters.ObjectParameterRule;
import com.rtoth.boilerplate.parameters.ParameterRule;
import com.rtoth.boilerplate.parameters.StringParameterRule;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.awt.CardLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.Map;

import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSplitPane;

/**
 * Dialog box which prompts the user for input on which methods to test and how to test them.
 */
class GetTestMethodsDialog extends DialogWrapper {
    /** Description for this dialog box. */
    private static final String DIALOG_DESCRIPTION = "Select Methods to Test";

    /** ID for the empty card display. */
    private static final String EMPTY_CARD_ID = "EMPTY_CARD";

    /** Message to display when a method has no configurable parameters. */
    private static final JLabel NO_CONFIGURABLE_PARAMETERS = new JLabel(
            "No configurable parameters right now.\nThe developers are working on this :)");

    /** Methods available for the user to configure mapped by a check box indicating whether they have been selected. */
    private final ImmutableMap<JCheckBox, PsiMethod> availableMethods;

    /** Configurable parameter rules for each of the {@code availableMethods}. */
    private final ImmutableMap<PsiMethod, ImmutableList<ParameterRule>> parameterRules;

    /** ID of the currently selected card display. */
    private String selectedCardId = EMPTY_CARD_ID;

    /**
     * Create a new {@link GetTestMethodsDialog}.
     *
     * @param sourceClass {@link PsiClass} for which this dialog is configuring test methods. Cannot be {@code null}.
     *
     * @throws NullPointerException if {@code sourceClass} is {@code null}.
     */
    GetTestMethodsDialog(@NotNull PsiClass sourceClass) {
        super(Preconditions.checkNotNull(sourceClass, "sourceClass cannot be null.").getProject());

        ImmutableMap.Builder<JCheckBox, PsiMethod> availableMethodsBuilder = ImmutableMap.builder();
        ImmutableMap.Builder<PsiMethod, ImmutableList<ParameterRule>> parameterRulesBuilder = ImmutableMap
                .builder();
        for (PsiMethod method : sourceClass.getMethods()) {
            // TODO: Update this once non-constructor methods are handled.
            if (PsiUtil.getAccessLevel(method.getModifierList()) != PsiUtil.ACCESS_LEVEL_PRIVATE
                    && method.isConstructor() && method.getParameterList().getParametersCount() > 0) {
                JCheckBox checkBox = new JCheckBox(getPresentableMethodSignature(method));
                checkBox.setSelected(false);
                availableMethodsBuilder.put(checkBox, method);
                parameterRulesBuilder.put(method, buildDefaultParameterRules(method));
            }
        }
        this.availableMethods = availableMethodsBuilder.build();
        this.parameterRules = parameterRulesBuilder.build();

        init();
        setTitle(DIALOG_DESCRIPTION);
    }

    @Nullable
    @Override
    protected JComponent createCenterPanel() {
        // inf rows, 1 column
        // TODO: Lately the cards are showing up squished..
        final JPanel methodSelection = new JPanel(new GridBagLayout());
        final JPanel parameterRuleCards = new JPanel(new CardLayout());
        parameterRuleCards.add(new JLabel("Select a method to configure."), EMPTY_CARD_ID);

        int rowIndex = 0;
        for (Map.Entry<JCheckBox, PsiMethod> methodEntry : availableMethods.entrySet()) {
            final PsiMethod method = methodEntry.getValue();
            final String methodSignature = getPresentableMethodSignature(method);
            final JCheckBox methodCheckBox = methodEntry.getKey();
            final JButton methodConfigureButton = new JButton("->");
            methodConfigureButton.setEnabled(false);

            GridBagConstraints constraints = new GridBagConstraints();
            constraints.gridx = 0;
            constraints.gridy = rowIndex;
            constraints.anchor = GridBagConstraints.LINE_START;
            constraints.fill = GridBagConstraints.HORIZONTAL;
            methodSelection.add(methodCheckBox, constraints);
            constraints.gridx = 1;
            constraints.gridy = rowIndex;
            constraints.anchor = GridBagConstraints.LINE_END;
            constraints.fill = GridBagConstraints.HORIZONTAL;
            methodSelection.add(methodConfigureButton, constraints);

            // TODO: Better layout here.
            JPanel card = new JPanel(new GridLayout(0, 1));
            ImmutableList<ParameterRule> rules = parameterRules.get(method);
            if (!rules.isEmpty()) {
                for (ParameterRule rule : rules) {
                    card.add(rule.getUiComponent());
                }
            } else {
                card.add(NO_CONFIGURABLE_PARAMETERS);
            }
            parameterRuleCards.add(card, methodSignature);

            methodCheckBox.addChangeListener(e -> {
                // Enable/disable the parameter configuration button based on whether this method
                // is selected (checked)
                methodConfigureButton.setEnabled(methodCheckBox.isSelected());

                // If this method is now selected, and the card view is empty, show the current method's
                // card.
                if (methodCheckBox.isSelected() && selectedCardId.equals(EMPTY_CARD_ID)) {
                    CardLayout cardLayout = (CardLayout) parameterRuleCards.getLayout();
                    cardLayout.show(parameterRuleCards, methodSignature);
                    selectedCardId = methodSignature;
                }
                // If this method is no longer selected, and this method's card was displayed, make sure
                // we set the card view back to empty
                else if (!methodCheckBox.isSelected() && selectedCardId.equals(methodSignature)) {
                    CardLayout cardLayout = (CardLayout) parameterRuleCards.getLayout();
                    cardLayout.show(parameterRuleCards, EMPTY_CARD_ID);
                    selectedCardId = EMPTY_CARD_ID;
                }
            });

            methodConfigureButton.addFocusListener(new FocusListener() {
                @Override
                public void focusGained(FocusEvent e) {
                    CardLayout cardLayout = (CardLayout) parameterRuleCards.getLayout();
                    cardLayout.show(parameterRuleCards, methodSignature);
                    selectedCardId = methodSignature;
                }

                @Override
                public void focusLost(FocusEvent e) {
                    // Do nothing
                }
            });
            rowIndex++;
        }

        // TODO: Add "show card" method
        CardLayout cardLayout = (CardLayout) parameterRuleCards.getLayout();
        cardLayout.show(parameterRuleCards, EMPTY_CARD_ID);
        selectedCardId = EMPTY_CARD_ID;

        return new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, methodSelection, parameterRuleCards);
    }

    @Override
    protected ValidationInfo doValidate() {
        boolean valid = true;
        boolean anySelected = false;
        for (Map.Entry<JCheckBox, PsiMethod> entry : availableMethods.entrySet()) {
            if (entry.getKey().isSelected()) {
                anySelected = true;
                ImmutableList<ParameterRule> rules = parameterRules.get(entry.getValue());
                if (rules != null) {
                    for (ParameterRule rule : rules) {
                        if (!rule.isValid()) {
                            valid = false;
                            break;
                        }
                    }
                    if (!valid) {
                        break;
                    }
                }
            }
        }

        if (!anySelected) {
            return new ValidationInfo("Must select at least one method!");
        } else if (!valid) {
            return new ValidationInfo("One or more methods have invalid parameter rules configured!");
        }
        return null;
    }

    /**
     * Get the method rules configured by the user.
     * <p>
     * Note: This method should only be called after this dialog has be displayed and submitted successfully. i.e.
     *       it should only be called after invoking {@link #showAndGet()} returns {@code true}. Failing to do so will
     *       result in this method returning un-initialized data.
     *
     * @return Mapping of {@link PsiMethod}s to test in the source file to {@link ParameterRule}s which indicate how
     *         each of the method's parameters should be tested. Never {@code null} but may be empty. The list of
     *         {@link ParameterRule}s for each method is also guaranteed to contain exactly the number of parameters
     *         defined for the method in the correct order.
     */
    @NotNull
    ImmutableMap<PsiMethod, ImmutableList<ParameterRule>> getSelectedMethodRules() {
        ImmutableMap.Builder<PsiMethod, ImmutableList<ParameterRule>> builder = ImmutableMap.builder();
        availableMethods.entrySet().stream().filter(entry -> entry.getKey().isSelected())
                .forEach(entry -> builder.put(entry.getValue(), parameterRules.get(entry.getValue())));

        return builder.build();
    }

    /**
     * Build the list of default {@link ParameterRule}s for the provided method.
     *
     * @param method {@link PsiMethod} for which to build the default parameter rules. Cannot be {@code null}.
     * @return An {@link ImmutableList} of default {@link ParameterRule}s for {@code method}. Never {@code null}, and
     *         will always contain exactly the number of parameters defined for the method in the correct order.
     *
     * @throws NullPointerException if {@code method} is {@code null}.
     */
    @NotNull
    private ImmutableList<ParameterRule> buildDefaultParameterRules(@NotNull PsiMethod method) {
        Preconditions.checkNotNull(method, "method cannot be null.");

        ImmutableList.Builder<ParameterRule> rulesBuilder = ImmutableList.builder();
        for (PsiParameter parameter : method.getParameterList().getParameters()) {
            PsiType type = parameter.getType();
            String name = parameter.getName();
            if (name != null) {
                if (type instanceof PsiPrimitiveType) {
                    // TODO: Make this handle more primitives!
                    if (type.equals(PsiType.INT)) {
                        rulesBuilder.add(new IntegerParameterRule(name));
                    } else {
                        throw new IllegalStateException("This functionality cannot currently be used on a class "
                                + "containing an unsupported parameter type: " + type.getPresentableText());
                    }
                } else if (type.getCanonicalText().equals("java.lang.String")) {
                    rulesBuilder.add(new StringParameterRule(type, name));
                } else {
                    // TODO: What if it's an array? or something else?
                    rulesBuilder.add(new ObjectParameterRule(type, name));
                }
            } else {
                throw new IllegalStateException("Unexpected error retrieving method parameter information. "
                        + "Parameter of type " + type.getPresentableText() + " on method " + method.getName()
                        + " has a null name.");
            }
        }

        return rulesBuilder.build();
    }

    /**
     * Get the presentable method signature for the provided {@link PsiMethod}.
     * <p>
     * The format returned for constructors is (depending on whether there are any parameters):
     *      Constructor: (Type1 name1, Type2 name2, ...)
     *      or
     *      Constructor: (no parameters)
     *
     * The format returned for methods is (depending on whether there are any parameters):
     *      returnType methodName: (Type1 name1, Type2 name2, ...)
     *      or
     *      returnType methodName: (no parameters)
     *
     * @param method Method for which to get the presentable method signature. Cannot be {@code null}.
     * @return The presentable method signature for {@code method}. Never {@code null}.
     *
     * @throws NullPointerException if {@code method} is {@code null}.
     */
    @NotNull
    private String getPresentableMethodSignature(@NotNull PsiMethod method) {
        Preconditions.checkNotNull(method, "method cannot be null.");

        StringBuilder signatureBuilder;
        if (method.isConstructor()) {
            signatureBuilder = new StringBuilder("Constructor: (");
        } else {
            PsiType returnType = method.getReturnType();
            if (returnType != null) {
                signatureBuilder = new StringBuilder(
                        returnType.getPresentableText() + " " + method.getName() + ": (");
            } else {
                throw new IllegalStateException("Unexpected error retrieving method descriptions. Non-constructor "
                        + "Method " + method.getName() + " has null return type.");
            }
        }

        PsiParameter[] parameters = method.getParameterList().getParameters();
        if (parameters.length > 0) {
            boolean first = true;
            for (PsiParameter parameter : method.getParameterList().getParameters()) {
                if (!first) {
                    signatureBuilder.append(", ");
                }
                signatureBuilder.append(parameter.getType().getPresentableText());
                signatureBuilder.append(" ");
                signatureBuilder.append(parameter.getName());
                first = false;
            }
        } else {
            signatureBuilder.append("no parameters");
        }
        signatureBuilder.append(")");

        return signatureBuilder.toString();
    }
}