PanelTrailingAddress.java

package com.jsql.view.swing.panel.address;

import com.jsql.model.injection.strategy.AbstractStrategy;
import com.jsql.model.injection.strategy.StrategyError;
import com.jsql.model.injection.engine.model.Engine;
import com.jsql.model.injection.engine.model.yaml.Method;
import com.jsql.util.CookiesUtil;
import com.jsql.util.I18nUtil;
import com.jsql.util.LogLevelUtil;
import com.jsql.util.ParameterUtil;
import com.jsql.view.swing.panel.PanelAddressBar;
import com.jsql.view.swing.text.JToolTipI18n;
import com.jsql.view.swing.util.I18nViewUtil;
import com.jsql.view.swing.util.MediatorHelper;
import com.jsql.view.swing.util.UiUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class PanelTrailingAddress extends JPanel {

    private static final Logger LOGGER = LogManager.getRootLogger();

    public static final String MENU_STRATEGY = "menuStrategy";
    public static final String ITEM_RADIO_STRATEGY_ERROR = "itemRadioStrategyError";
    public static final String MENU_VENDOR = "menuVendor";
    public static final String ITEM_RADIO_VENDOR = "itemRadioVendor";
    public static final String PARAM_AUTO = "Param auto";

    private JMenu itemRadioStrategyError;

    private final JLabel labelTarget = new JLabel(UiUtil.TARGET.getIcon(), SwingConstants.LEFT);
    private final JLabel labelEngine = new JLabel(UiUtil.ARROW_DOWN.getIcon(), SwingConstants.LEFT);
    private final JLabel labelStrategy = new JLabel(UiUtil.ARROW_DOWN.getIcon(), SwingConstants.LEFT);
    private final JPopupMenu popupMenuTargets = new JPopupMenu();
    private ButtonGroup groupRadio = new ButtonGroup();
    private final JPopupMenu popupMenuEngines = new JPopupMenu();
    private final JPopupMenu popupMenuStrategies = new JPopupMenu();

    private final ButtonGroup groupStrategy = new ButtonGroup();
    public static final String PREFIX_NAME_ERROR = "itemRadioError";
    private static final String I18N_TOOLTIP_STRATEGY = "STRATEGY_%s_TOOLTIP";

    /**
     * Loader displayed during injection.
     */
    private final JProgressBar loader;

    /**
     * Connection button.
     */
    public final ButtonStart buttonStart = new ButtonStart();
    
    public PanelTrailingAddress(PanelAddressBar panelAddressBar) {
        this.buttonStart.addActionListener(new ActionStart(panelAddressBar));
        this.setOpaque(false);
        this.setBorder(null);
        this.labelStrategy.setText("Strategy auto");
        this.labelStrategy.setName(PanelTrailingAddress.MENU_STRATEGY);

        for (final AbstractStrategy strategy: MediatorHelper.model().getMediatorStrategy().getStrategies()) {
            var nameStrategy = strategy.getName().toUpperCase(Locale.ROOT);
            JMenuItem itemRadioStrategy;
            if (strategy == MediatorHelper.model().getMediatorStrategy().getError()) {
                itemRadioStrategy = new JMenu(strategy.toString());
                this.itemRadioStrategyError = (JMenu) itemRadioStrategy;
                itemRadioStrategy.getComponent().setName(PanelTrailingAddress.ITEM_RADIO_STRATEGY_ERROR);
            } else {
                var atomicTooltip = new AtomicReference<>(new JToolTipI18n(
                    I18nUtil.valueByKey(String.format(PanelTrailingAddress.I18N_TOOLTIP_STRATEGY, nameStrategy))
                ));
                itemRadioStrategy = new JRadioButtonMenuItem(strategy.toString()) {
                    @Override
                    public JToolTip createToolTip() {
                        atomicTooltip.set(new JToolTipI18n(
                            I18nUtil.valueByKey(
                                String.format(PanelTrailingAddress.I18N_TOOLTIP_STRATEGY, nameStrategy)
                            )
                        ));
                        return atomicTooltip.get();
                    }
                };
                I18nViewUtil.addComponentForKey(
                    String.format(PanelTrailingAddress.I18N_TOOLTIP_STRATEGY, nameStrategy),
                    atomicTooltip.get()
                );
                itemRadioStrategy.getComponent().setName("itemRadioStrategy" + strategy);
                itemRadioStrategy.addActionListener(actionEvent -> {
                    this.labelStrategy.setText(strategy.toString());
                    MediatorHelper.model().getMediatorStrategy().setStrategy(strategy);
                });
                this.groupStrategy.add(itemRadioStrategy);
            }

            this.popupMenuStrategies.add(itemRadioStrategy);
            itemRadioStrategy.setToolTipText(
                I18nUtil.valueByKey(String.format(PanelTrailingAddress.I18N_TOOLTIP_STRATEGY, nameStrategy))
            );
            itemRadioStrategy.setEnabled(false);
        }

        this.labelEngine.setText(MediatorHelper.model().getMediatorEngine().getAuto().toString());
        this.labelEngine.setName(PanelTrailingAddress.MENU_VENDOR);
        this.popupMenuEngines.setLayout(UiUtil.getColumnLayout(MediatorHelper.model().getMediatorEngine().getEngines().size()));
        var groupEngine = new ButtonGroup();
        for (final Engine engine : MediatorHelper.model().getMediatorEngine().getEngines()) {
            JMenuItem itemRadioEngine = new JRadioButtonMenuItem(engine.toString(), engine == MediatorHelper.model().getMediatorEngine().getAuto());
            itemRadioEngine.setName(PanelTrailingAddress.ITEM_RADIO_VENDOR + engine);
            itemRadioEngine.addActionListener(actionEvent -> {
                this.labelEngine.setText(engine.toString());
                MediatorHelper.model().getMediatorEngine().setEngineByUser(engine);
            });
            this.popupMenuEngines.add(itemRadioEngine);
            groupEngine.add(itemRadioEngine);
        }

        this.loader = new JProgressBar();
        var dimension = UIManager.getDimension("ProgressBar.horizontalSize");
        this.loader.setPreferredSize(new Dimension(32, dimension.height));
        this.loader.setIndeterminate(true);
        this.add(this.loader);

        this.labelEngine.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                Arrays.stream(PanelTrailingAddress.this.popupMenuEngines.getComponents())
                    .map(JComponent.class::cast)
                    .forEach(JComponent::updateUI);  // required: incorrect when dark/light mode switch
                PanelTrailingAddress.this.popupMenuEngines.updateUI();  // required: incorrect when dark/light mode switch
                SwingUtilities.invokeLater(() -> {  // reduce flickering on linux
                    PanelTrailingAddress.this.popupMenuEngines.show(e.getComponent(), e.getComponent().getX(),5 + e.getComponent().getY() + e.getComponent().getHeight());
                    PanelTrailingAddress.this.popupMenuEngines.setLocation(e.getComponent().getLocationOnScreen().x,5 + e.getComponent().getLocationOnScreen().y + e.getComponent().getHeight());
                });
            }
        });
        this.labelStrategy.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                Arrays.stream(PanelTrailingAddress.this.popupMenuStrategies.getComponents())
                    .map(JComponent.class::cast)
                    .forEach(JComponent::updateUI);  // required: incorrect when dark/light mode switch
                for (var i = 0 ; i < PanelTrailingAddress.this.getMenuError().getItemCount() ; i++) {
                    PanelTrailingAddress.this.getMenuError().getItem(i).updateUI();  // required: incorrect when dark/light mode switch
                }
                PanelTrailingAddress.this.popupMenuStrategies.updateUI();  // required: incorrect when dark/light mode switch
                SwingUtilities.invokeLater(() -> {  // reduce flickering on linux
                    PanelTrailingAddress.this.popupMenuStrategies.show(e.getComponent(), e.getComponent().getX(),5 + e.getComponent().getY() + e.getComponent().getHeight());
                    PanelTrailingAddress.this.popupMenuStrategies.setLocation(e.getComponent().getLocationOnScreen().x,5 + e.getComponent().getLocationOnScreen().y + e.getComponent().getHeight());
                });
            }
        });

        this.labelTarget.setText(PanelTrailingAddress.PARAM_AUTO);
        this.labelTarget.addMouseListener(new TargetMouseAdapter(panelAddressBar));

        this.add(this.labelTarget);
        this.add(this.labelEngine);
        this.add(this.labelStrategy);
        this.add(this.buttonStart);
        this.setCursor(Cursor.getDefaultCursor());
        this.loader.setVisible(false);
    }

    public void endPreparation() {
        this.buttonStart.setToolTipText(I18nUtil.valueByKey("BUTTON_START_TOOLTIP"));
        this.buttonStart.setInjectionReady();
        this.loader.setVisible(false);
    }
    
    public void reset() {
        this.labelStrategy.setText("Strategy auto");
        if (MediatorHelper.model().getMediatorEngine().getEngineByUser() == MediatorHelper.model().getMediatorEngine().getAuto()) {
            this.labelEngine.setText(MediatorHelper.model().getMediatorEngine().getAuto().toString());
        }
        Arrays.stream(this.popupMenuStrategies.getComponents())
            .forEach(component -> component.setEnabled(false));
        this.getMenuError().removeAll();

        this.groupStrategy.clearSelection();
        Iterable<AbstractButton> iterable = () -> this.groupStrategy.getElements().asIterator();
        StreamSupport.stream(iterable.spliterator(), false)
            .filter(abstractButton -> abstractButton.getName().startsWith(PanelTrailingAddress.PREFIX_NAME_ERROR))
            .forEach(this.groupStrategy::remove);
    }
    
    public void setEngine(Engine engine) {
        this.labelEngine.setText(engine.toString());
        this.itemRadioStrategyError.removeAll();
        var indexError = 0;
        if (
            engine != MediatorHelper.model().getMediatorEngine().getAuto()
            && engine.instance().getModelYaml().getStrategy().getError() != null
        ) {
            for (Method methodError: engine.instance().getModelYaml().getStrategy().getError().getMethod()) {
                JMenuItem itemRadioError = new JRadioButtonMenuItem(methodError.getName());
                itemRadioError.setEnabled(false);
                itemRadioError.setName(PanelTrailingAddress.PREFIX_NAME_ERROR + methodError.getName());
                this.itemRadioStrategyError.add(itemRadioError);
                this.groupStrategy.add(itemRadioError);
                int indexErrorFinal = indexError;
                itemRadioError.addActionListener(actionEvent -> {
                    this.labelStrategy.setText(methodError.getName());
                    MediatorHelper.model().getMediatorStrategy().setStrategy(MediatorHelper.model().getMediatorStrategy().getError());
                    MediatorHelper.model().getMediatorStrategy().getError().setIndexErrorStrategy(indexErrorFinal);
                });
                indexError++;
            }
        }
    }
    
    public void activateStrategy(AbstractStrategy strategy) {
        if (strategy instanceof StrategyError strategyError) {
            this.labelStrategy.setText(strategyError.toString());
            int indexError = strategyError.getIndexErrorStrategy();
            String nameError = MediatorHelper.model().getMediatorEngine().getEngine().instance().getModelYaml().getStrategy().getError().getMethod().get(
                indexError
            ).getName();

            Arrays.stream(this.getMenuError().getMenuComponents())
                .map(JRadioButtonMenuItem.class::cast)
                .filter(component -> component.getText().equals(nameError))
                .forEach(jRadioButtonMenuItem -> {
                    jRadioButtonMenuItem.setSelected(true);
                    this.labelStrategy.setText(nameError);
                });
        } else {
            this.labelStrategy.setText(strategy.toString());
            Arrays.stream(this.popupMenuStrategies.getComponents())
                .map(JMenuItem.class::cast)
                .filter(jMenuItem -> jMenuItem.getText().equals(strategy.toString()))
                .forEach(jMenuItem -> jMenuItem.setSelected(true));
        }
    }

    public void markInvulnerable(AbstractStrategy strategy) {
        Arrays.stream(this.popupMenuStrategies.getComponents())
            .map(JMenuItem.class::cast)
            .filter(jMenuItem -> jMenuItem.getText().equals(strategy.toString()))
            .forEach(jMenuItem -> jMenuItem.setEnabled(false));
    }
    
    public void markInvulnerable(int indexMethodError, AbstractStrategy strategy) {
        Arrays.stream(this.popupMenuStrategies.getSubElements())
            .map(JMenuItem.class::cast)
            .filter(jMenuItem -> jMenuItem.getText().equals(strategy.toString()))
            .map(JMenu.class::cast)
            .filter(jMenuItem -> {
                var isNotNull = true;
                // Fix #36975: ArrayIndexOutOfBoundsException on getItem()
                // Fix #40352: NullPointerException on ?
                // Fix #95855: NPE on setEnabled()
                try {
                    isNotNull = jMenuItem.getItem(indexMethodError) != null;
                } catch (ArrayIndexOutOfBoundsException e) {
                    LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
                    return false;
                }
                return isNotNull;
            })
            .forEach(jMenuItem -> jMenuItem.getItem(indexMethodError).setEnabled(false));
    }

    private JMenu getMenuError() {
        var nameError = MediatorHelper.model().getMediatorStrategy().getError().getName();
        return (JMenu) Arrays.stream(this.popupMenuStrategies.getComponents())
            .map(JMenuItem.class::cast)
            .filter(jMenuItem -> jMenuItem.getText().equalsIgnoreCase(nameError))
            .findFirst()
            .orElse(new JMenuItem("Mock"));
    }

    public void markVulnerable(AbstractStrategy strategy) {
        Arrays.stream(this.popupMenuStrategies.getComponents())
            .map(JMenuItem.class::cast)
            .filter(jMenuItem -> jMenuItem.getText().equals(strategy.toString()))
            .forEach(jMenuItem -> jMenuItem.setEnabled(true));
    }

    public void markVulnerable(int indexMethodError, AbstractStrategy strategy) {
        // Fix #46578: ArrayIndexOutOfBoundsException on getItem()
        try {
            Arrays.stream(this.popupMenuStrategies.getComponents())
                .map(JMenuItem.class::cast)
                .filter(jMenuItem -> jMenuItem.getText().equals(strategy.toString()))
                .map(JMenu.class::cast)
                .filter(jMenuItem -> jMenuItem.getItem(indexMethodError) != null)
                .forEach(jMenuItem -> {
                    jMenuItem.setEnabled(true);
                    jMenuItem.getItem(indexMethodError).setEnabled(true);
                });
        } catch (ArrayIndexOutOfBoundsException e) {
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
        }
    }

    private class TargetMouseAdapter extends MouseAdapter {

        private final PanelAddressBar panelAddressBar;

        public TargetMouseAdapter(PanelAddressBar panelAddressBar) {
            this.panelAddressBar = panelAddressBar;
        }

        @Override
        public void mousePressed(MouseEvent event) {
            PanelTrailingAddress.this.popupMenuTargets.removeAll();
            JRadioButtonMenuItem menuParamAuto = new JRadioButtonMenuItem(PanelTrailingAddress.PARAM_AUTO);
            menuParamAuto.setActionCommand(PanelTrailingAddress.PARAM_AUTO);  // mock required when adding star: @ParameterUtil.controlInput
            menuParamAuto.addActionListener(actionEvent ->
                PanelTrailingAddress.this.labelTarget.setText(menuParamAuto.getText())
            );
            PanelTrailingAddress.this.popupMenuTargets.add(menuParamAuto);

            var rawQuery = this.panelAddressBar.getTextFieldAddress().getText().trim();
            var rawRequest = this.panelAddressBar.getTextFieldRequest().getText().trim();
            var rawHeader = this.panelAddressBar.getTextFieldHeader().getText().trim();

            var selection = PanelTrailingAddress.this.groupRadio.getSelection();
            String selectionCommand;  // effectively final
            if (selection != null) {
                selectionCommand = selection.getActionCommand();
            } else {
                selectionCommand = StringUtils.EMPTY;
            }
            PanelTrailingAddress.this.groupRadio = new ButtonGroup();
            PanelTrailingAddress.this.groupRadio.add(menuParamAuto);
            JMenu menuQuery = new JMenu("Query");
            if (!rawQuery.isEmpty()) {
                try {
                    rawQuery = !rawQuery.matches("(?i)^\\w+://.*") ? "http://"+ rawQuery : rawQuery;
                    var url = new URI(rawQuery).toURL();
                    if (url.getQuery() != null) {
                        this.buildMenu(url.getQuery(), ParameterUtil.PREFIX_COMMAND_QUERY, selectionCommand, menuQuery);
                    }
                } catch (IllegalArgumentException | MalformedURLException | URISyntaxException e) {
                    LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Incorrect URL: {}", e.getMessage());
                    return;
                }
            }

            JMenu menuRequest = new JMenu("Request");
            if (!rawRequest.isEmpty()) {
                this.buildMenu(rawRequest, ParameterUtil.PREFIX_COMMAND_REQUEST, selectionCommand, menuRequest);
            }

            JMenu menuHeader = new JMenu("Header");
            if (!rawHeader.isEmpty()) {
                this.buildMenuHeader(rawHeader, selectionCommand, menuHeader);
            }

            Arrays.stream(PanelTrailingAddress.this.popupMenuTargets.getComponents())
                .map(JComponent.class::cast)
                .forEach(c -> c.setEnabled(false));
            menuParamAuto.setEnabled(true);
            if (PanelTrailingAddress.this.groupRadio.getSelection() == null) {
                menuParamAuto.setSelected(true);
                PanelTrailingAddress.this.labelTarget.setText(menuParamAuto.getText());
            }
            menuQuery.setEnabled(menuQuery.getMenuComponentCount() > 0);
            menuRequest.setEnabled(menuRequest.getMenuComponentCount() > 0);
            menuHeader.setEnabled(menuHeader.getMenuComponentCount() > 0);

            if (
                menuQuery.getMenuComponentCount() > 0
                || menuRequest.getMenuComponentCount() > 0
                || menuHeader.getMenuComponentCount() > 0
            ) {
                Arrays.stream(PanelTrailingAddress.this.popupMenuTargets.getComponents())
                    .map(JComponent.class::cast)
                    .forEach(JComponent::updateUI);  // required: incorrect when dark/light mode switch
                PanelTrailingAddress.this.popupMenuTargets.updateUI();  // required: incorrect when dark/light mode switch
                SwingUtilities.invokeLater(() -> {  // reduce flickering on linux
                    PanelTrailingAddress.this.popupMenuTargets.show(event.getComponent(), event.getComponent().getX(), 5 + event.getComponent().getY() + event.getComponent().getHeight());
                    PanelTrailingAddress.this.popupMenuTargets.setLocation(event.getComponent().getLocationOnScreen().x, 5 + event.getComponent().getLocationOnScreen().y + event.getComponent().getHeight());
                });
            } else {
                LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Missing parameter to inject");
            }
        }

        private void buildMenuHeader(String rawHeader, String selectionCommand, JMenu menuHeader) {
            var listHeaders = Pattern.compile("\\\\r\\\\n")
                .splitAsStream(rawHeader)
                .map(keyValue -> Arrays.copyOf(keyValue.split(":"), 2))
                .map(keyValue -> new AbstractMap.SimpleEntry<>(
                    keyValue[0],
                    keyValue[1] == null ? StringUtils.EMPTY : keyValue[1]
                ))
                .toList();
            listHeaders.forEach(entry -> {
                JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(entry.getKey());
                menuItem.setSelected((ParameterUtil.PREFIX_COMMAND_HEADER + entry.getKey()).equals(selectionCommand));
                menuItem.setActionCommand(ParameterUtil.PREFIX_COMMAND_HEADER + entry.getKey());
                menuItem.addActionListener(actionEvent ->
                    PanelTrailingAddress.this.labelTarget.setText(entry.getKey())
                );
                groupRadio.add(menuItem);
                menuHeader.add(menuItem);
            });
            if (listHeaders.stream().anyMatch(s -> CookiesUtil.COOKIE.equalsIgnoreCase(s.getKey()))) {
                var cookies = listHeaders.stream()
                    .filter(s -> CookiesUtil.COOKIE.equalsIgnoreCase(s.getKey()))
                    .findFirst()
                    .orElse(new AbstractMap.SimpleEntry<>(CookiesUtil.COOKIE, ""));
                if (!cookies.getValue().trim().isEmpty()) {
                    JMenu menuCookie = new JMenu(CookiesUtil.COOKIE);
                    String[] cookieValues = StringUtils.split(cookies.getValue(), ";");
                    Stream.of(cookieValues).forEach(cookie -> {
                        String[] cookieEntry = StringUtils.split(cookie, "=");
                        JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(cookieEntry[0].trim());
                        menuItem.setSelected((ParameterUtil.PREFIX_COMMAND_COOKIE + cookieEntry[0].trim()).equals(selectionCommand));
                        menuItem.setActionCommand(ParameterUtil.PREFIX_COMMAND_COOKIE + cookieEntry[0].trim());
                        menuItem.addActionListener(actionEvent ->
                            PanelTrailingAddress.this.labelTarget.setText(cookieEntry[0].trim())
                        );
                        groupRadio.add(menuItem);
                        menuCookie.add(menuItem);
                    });
                    menuHeader.addSeparator();
                    menuHeader.add(menuCookie);
                }
            }
            PanelTrailingAddress.this.popupMenuTargets.add(menuHeader);
        }

        private void buildMenu(String rawParams, String prefixCommand, String selectionCommand, JMenu menu) {
            Pattern.compile("&").splitAsStream(rawParams)
            .map(keyValue -> Arrays.copyOf(keyValue.split("="), 2))
            .map(keyValue -> new AbstractMap.SimpleEntry<>(
                keyValue[0],
                keyValue[1] == null ? StringUtils.EMPTY : keyValue[1]
            ))
            .forEach(entry -> {
                JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(entry.getKey());
                menuItem.setSelected((prefixCommand + entry.getKey()).equals(selectionCommand));
                menuItem.setActionCommand(prefixCommand + entry.getKey());
                menuItem.addActionListener(actionEvent ->
                    PanelTrailingAddress.this.labelTarget.setText(entry.getKey())
                );
                PanelTrailingAddress.this.groupRadio.add(menuItem);
                menu.add(menuItem);
            });
            PanelTrailingAddress.this.popupMenuTargets.add(menu);
        }
    }
    

    // Getter and setter

    public JComponent getLoader() {
        return this.loader;
    }

    public ButtonStart getButtonStart() {
        return this.buttonStart;
    }

    public ButtonGroup getGroupRadio() {
        return this.groupRadio;
    }
}