PanelAddressBar.java

/*******************************************************************************
 * Copyhacked (H) 2012-2025.
 * This program and the accompanying materials
 * are made available under no term at all, use it like
 * you want, but share and discuss it
 * every time possible with every body.
 * 
 * Contributors:
 *      ron190 at ymail dot com - initial implementation
 ******************************************************************************/
package com.jsql.view.swing.panel;

import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.icons.FlatRadioButtonMenuItemIcon;
import com.jsql.model.injection.method.AbstractMethodInjection;
import com.jsql.util.I18nUtil;
import com.jsql.util.LogLevelUtil;
import com.jsql.util.ParameterUtil;
import com.jsql.util.StringUtil;
import com.jsql.view.swing.panel.address.ActionEnterAddressBar;
import com.jsql.view.swing.panel.address.PanelTrailingAddress;
import com.jsql.view.swing.panel.address.ModelAddressLine;
import com.jsql.view.swing.panel.util.ButtonExpandText;
import com.jsql.view.swing.text.*;
import com.jsql.view.swing.text.listener.DocumentListenerEditing;
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.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Stream;

/**
 * Create panel at the top of the window.
 * Contains textfields in a panel.
 */
public class PanelAddressBar extends JPanel {

    /**
     * Log4j logger sent to view.
     */
    private static final Logger LOGGER = LogManager.getRootLogger();

    private final AtomicReference<JTextField> atomicTextFieldAddress = new AtomicReference<>();  // atomic to build dynamically
    private final AtomicReference<JTextField> atomicTextFieldRequest = new AtomicReference<>();
    private final AtomicReference<JTextField> atomicTextFieldHeader = new AtomicReference<>();

    private final AtomicReference<JRadioButton> atomicRadioRequest = new AtomicReference<>();  // atomic to build dynamically
    private final AtomicReference<JRadioButton> atomicRadioMethod = new AtomicReference<>();
    private final AtomicReference<JRadioButton> atomicRadioHeader = new AtomicReference<>();

    private static final String KEY_ADDRESS_BAR_PLACEHOLDER = "ADDRESS_BAR_PLACEHOLDER";
    private static final String BUTTON_ADVANCED = "BUTTON_ADVANCED";

    // Current injection method
    private AbstractMethodInjection methodInjection = MediatorHelper.model().getMediatorMethod().getQuery();
    private String typeRequest = StringUtil.GET;

    private final PanelTrailingAddress panelTrailingAddress;

    private boolean isAdvanceActivated = false;
    
    public PanelAddressBar() {
        var buttonGroup = new ButtonGroup();

        Stream.of(
            new ModelAddressLine(
                "URL",
                true,
                MediatorHelper.model().getMediatorMethod().getQuery(),
                "METHOD_QUERYSTRING_TOOLTIP",
                this.atomicRadioRequest,
                "FIELD_QUERYSTRING_TOOLTIP",
                I18nUtil.valueByKey(PanelAddressBar.KEY_ADDRESS_BAR_PLACEHOLDER),
                this.atomicTextFieldAddress,
                18
            ),
            new ModelAddressLine(
                StringUtil.GET,
                false,
                MediatorHelper.model().getMediatorMethod().getRequest(),
                "METHOD_REQUEST_TOOLTIP",
                this.atomicRadioMethod,
                "FIELD_REQUEST_TOOLTIP",
                "e.g. key=value&injectMe=",
                this.atomicTextFieldRequest,
                0
            ),
            new ModelAddressLine(
                "Header",
                false,
                MediatorHelper.model().getMediatorMethod().getHeader(),
                "METHOD_HEADER_TOOLTIP",
                this.atomicRadioHeader,
                "FIELD_HEADER_TOOLTIP",
                "e.g. key: value\\r\\nCookie: cKey1=cValue1; cKey2=cValue2\\r\\nAuthorization: Basic dXNlcjpwYXNz\\r\\ninjectMe:",
                this.atomicTextFieldHeader,
                0
            )
        )
        .forEach(modelLine -> {
            var tooltipTextfield = new AtomicReference<>(new JToolTipI18n(I18nUtil.valueByKey(modelLine.keyTooltipQuery)));
            modelLine.textfield.set(new JPopupTextField(new JTextFieldPlaceholder(modelLine.placeholder, modelLine.offset) {
                @Override
                public JToolTip createToolTip() {
                    return tooltipTextfield.get();
                }
            }).getProxy());
            I18nViewUtil.addComponentForKey(modelLine.keyTooltipQuery, tooltipTextfield.get());
            modelLine.textfield.get().addActionListener(new ActionEnterAddressBar(this));
            modelLine.textfield.get().setVisible(false);  // query will be set back to visible
            modelLine.textfield.get().setToolTipText(I18nUtil.valueByKey(modelLine.keyTooltipQuery));

            var tooltipRadio = new AtomicReference<>(new JToolTipI18n(I18nUtil.valueByKey(modelLine.i18nRadio)));
            modelLine.radio.set(
                new JRadioButton(modelLine.request) {
                    @Override
                    public JToolTip createToolTip() {
                        return tooltipRadio.get();
                    }
                }
            );
            I18nViewUtil.addComponentForKey(modelLine.i18nRadio, tooltipRadio.get());
            modelLine.radio.get().setToolTipText(I18nUtil.valueByKey(modelLine.i18nRadio));
            modelLine.radio.get().setSelected(modelLine.isSelected);
            modelLine.radio.get().setHorizontalTextPosition(SwingConstants.LEFT);
            modelLine.radio.get().setVisible(false);
            modelLine.radio.get().addActionListener(e -> MediatorHelper.panelAddressBar().setMethodInjection(modelLine.method));
            buttonGroup.add(modelLine.radio.get());
        });

        this.atomicTextFieldAddress.get().setFont(UiUtil.FONT_NON_MONO_BIG);
        this.atomicTextFieldAddress.get().setName("textFieldAddress");
        this.atomicTextFieldAddress.get().setPreferredSize(new Dimension(50, 32));  // required to set correct height
        this.atomicTextFieldAddress.get().setVisible(true);
        I18nViewUtil.addComponentForKey(PanelAddressBar.KEY_ADDRESS_BAR_PLACEHOLDER, this.atomicTextFieldAddress.get());  // only i18n placeholder

        this.atomicRadioRequest.get().setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3));
        this.atomicRadioMethod.get().setBorder(BorderFactory.createEmptyBorder(6, 3, 0, 3));
        this.atomicRadioHeader.get().setBorder(BorderFactory.createEmptyBorder(6, 3, 0, 3));

        this.panelTrailingAddress = new PanelTrailingAddress(this);
        this.atomicTextFieldAddress.get().putClientProperty(FlatClientProperties.TEXT_FIELD_TRAILING_COMPONENT, this.panelTrailingAddress);
        this.atomicTextFieldAddress.get().putClientProperty(FlatClientProperties.TEXT_FIELD_LEADING_ICON, UiUtil.GLOBE.getIcon());
        this.atomicTextFieldRequest.get().putClientProperty(
            FlatClientProperties.TEXT_FIELD_TRAILING_COMPONENT,
            new ButtonExpandText(this.atomicTextFieldRequest.get())
        );
        this.atomicTextFieldHeader.get().putClientProperty(
            FlatClientProperties.TEXT_FIELD_TRAILING_COMPONENT,
            new ButtonExpandText(this.atomicTextFieldHeader.get())
        );

        this.initLayout();
    }

    private void initLayout() {
        final JLabel advancedButton = this.initAdvancedButton();
        
        this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
        
        // First panel at the top, contains text components
        var panelTextFields = new JPanel();
        var groupLayout = new GroupLayout(panelTextFields);
        panelTextFields.setLayout(groupLayout);
        panelTextFields.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 0));
        this.add(panelTextFields);

        final var popup = new JPopupMenu();
        final var buttonGroupMethod = new ButtonGroup();

        for (String method: new String[]{"DELETE", StringUtil.GET, "HEAD", "OPTIONS", StringUtil.POST, "PUT", "TRACE"}) {
            final JMenuItem newMenuItem = new JRadioButtonMenuItem(method, StringUtil.GET.equals(method));
            newMenuItem.addActionListener(actionEvent -> {
                this.typeRequest = newMenuItem.getText();
                this.atomicRadioMethod.get().setText(this.typeRequest);
                this.atomicRadioMethod.get().requestFocusInWindow();  // required to set proper focus
            });
            popup.add(newMenuItem);
            buttonGroupMethod.add(newMenuItem);
        }

        var tooltipPanel = new AtomicReference<>(new JToolTipI18n(I18nUtil.valueByKey("METHOD_CUSTOM_TOOLTIP")));
        var panelCustomMethod = new JPanel(new BorderLayout()) {
            @Override
            public JToolTip createToolTip() {
                return tooltipPanel.get();
            }
        };
        I18nViewUtil.addComponentForKey("METHOD_CUSTOM_TOOLTIP", tooltipPanel.get());
        Supplier<Color> colorBackground = () -> UIManager.getColor("MenuItem.background");  // adapt to current theme
        Supplier<Color> colorSelectionBackground = () -> UIManager.getColor("MenuItem.selectionBackground");  // adapt to current theme
        panelCustomMethod.setBackground(colorBackground.get());  // required for correct color

        final var radioCustomMethod = new JRadioButton() {
            @Override
            public JToolTip createToolTip() {
                return tooltipPanel.get();
            }
        };
        radioCustomMethod.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 0));
        radioCustomMethod.setIcon(new FlatRadioButtonMenuItemIcon());
        radioCustomMethod.setBackground(colorBackground.get());  // required for correct color
        buttonGroupMethod.add(radioCustomMethod);

        final JTextField inputCustomMethod = new JPopupTextField("CUSTOM"){
            @Override
            public JToolTip createToolTip() {
                return tooltipPanel.get();
            }
        }.getProxy();
        inputCustomMethod.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                radioCustomMethod.setSelected(!radioCustomMethod.isSelected());
            }
        });
        inputCustomMethod.getDocument().addDocumentListener(new DocumentListenerEditing() {
            @Override
            public void process() {
                PanelAddressBar.this.validate(inputCustomMethod);
            }
        });
        radioCustomMethod.addActionListener(actionEvent -> this.validate(inputCustomMethod));

        var tooltipCustomMethod = "<html>Set user defined HTTP method.<br/>" +
            "A valid method is limited to chars:<br>" +
            "!#$%&'*+-.^_`|~0123456789<br>" +
            "abcdefghijklmnopqrstuvwxyz<br>" +
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
        "</html>";
        MouseAdapter mouseAdapterSetBackground = new MouseAdapter() {
            @Override
            public void mouseEntered(MouseEvent e) {
                super.mouseEntered(e);
                panelCustomMethod.setBackground(colorSelectionBackground.get());
                radioCustomMethod.setBackground(colorSelectionBackground.get());
            }
            @Override
            public void mouseExited(MouseEvent e) {
                super.mouseExited(e);
                panelCustomMethod.setBackground(colorBackground.get());
                radioCustomMethod.setBackground(colorBackground.get());
            }
        };
        Arrays.asList(radioCustomMethod, inputCustomMethod, panelCustomMethod).forEach(component -> {
            component.addMouseListener(mouseAdapterSetBackground);
            component.setToolTipText(tooltipCustomMethod);
        });

        panelCustomMethod.add(radioCustomMethod, BorderLayout.LINE_START);
        panelCustomMethod.add(inputCustomMethod, BorderLayout.CENTER);
        popup.insert(panelCustomMethod, popup.getComponentCount());

        this.atomicRadioMethod.get().addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                Arrays.stream(popup.getComponents()).map(a -> (JComponent) a).forEach(JComponent::updateUI);  // required: incorrect when dark/light mode switch
                radioCustomMethod.setIcon(new FlatRadioButtonMenuItemIcon());
                radioCustomMethod.updateUI();  // required: incorrect when dark/light mode switch
                inputCustomMethod.updateUI();  // required: incorrect when dark/light mode switch
                popup.updateUI();  // required: incorrect when dark/light mode switch
                popup.applyComponentOrientation(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()));

                if (ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()))) {
                    radioCustomMethod.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 6));
                } else {
                    radioCustomMethod.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 0));
                }

                popup.show(
                    e.getComponent(),
                    ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()))
                    ? e.getComponent().getX() - e.getComponent().getWidth() - popup.getWidth()
                    : e.getComponent().getX(),
                    e.getComponent().getY() + e.getComponent().getHeight()
                );
                popup.setLocation(  // required for proper location
                    ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()))
                    ? e.getComponent().getLocationOnScreen().x + e.getComponent().getWidth() - popup.getWidth()
                    : e.getComponent().getLocationOnScreen().x,
                    e.getComponent().getLocationOnScreen().y + e.getComponent().getHeight()
                );
            }
        });

        groupLayout.setHorizontalGroup(
            groupLayout
            .createSequentialGroup()
            .addGroup(
                groupLayout
                .createParallelGroup(GroupLayout.Alignment.TRAILING, false)
                .addComponent(this.atomicRadioRequest.get())
                .addComponent(this.atomicRadioMethod.get())
                .addComponent(this.atomicRadioHeader.get())
            )
            .addGroup(
                groupLayout
                .createParallelGroup()
                .addComponent(this.atomicTextFieldAddress.get())
                .addComponent(this.atomicTextFieldRequest.get())
                .addComponent(this.atomicTextFieldHeader.get())
            )
            .addGroup(
                groupLayout
                .createParallelGroup(GroupLayout.Alignment.LEADING, false)
                .addComponent(advancedButton)
            )
        );

        groupLayout.setVerticalGroup(
            groupLayout
            .createSequentialGroup()
            .addGroup(
                groupLayout
                .createParallelGroup(GroupLayout.Alignment.CENTER, false)
                .addComponent(this.atomicRadioRequest.get())
                .addComponent(this.atomicTextFieldAddress.get())
                .addComponent(advancedButton)
            )
            .addGroup(
                groupLayout
                .createParallelGroup(GroupLayout.Alignment.BASELINE)
                .addComponent(this.atomicRadioMethod.get())
                .addComponent(this.atomicTextFieldRequest.get())
            )
            .addGroup(
                groupLayout
                .createParallelGroup(GroupLayout.Alignment.BASELINE)
                .addComponent(this.atomicRadioHeader.get())
                .addComponent(this.atomicTextFieldHeader.get())
            )
        );
    }

    private void validate(JTextField inputCustomMethod) {
        if (StringUtils.isEmpty(inputCustomMethod.getText())) {
            LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Missing custom method label");
        } else if (ParameterUtil.isInvalidName(inputCustomMethod.getText())) {
            LOGGER.log(LogLevelUtil.CONSOLE_ERROR, () -> String.format("Illegal method: \"%s\"", inputCustomMethod.getText()));
        } else {
            this.typeRequest = inputCustomMethod.getText();
            this.atomicRadioMethod.get().setText(this.typeRequest);
        }
    }

    private JLabel initAdvancedButton() {
        var tooltip = new AtomicReference<>(new JToolTipI18n(I18nUtil.valueByKey(PanelAddressBar.BUTTON_ADVANCED)));
        var advancedButton = new JLabel(UiUtil.ARROW_DOWN.getIcon()) {
            @Override
            public JToolTip createToolTip() {
                return tooltip.get();
            }
        };
        advancedButton.setName("advancedButton");
        advancedButton.setToolTipText(I18nUtil.valueByKey(PanelAddressBar.BUTTON_ADVANCED));
        I18nViewUtil.addComponentForKey(PanelAddressBar.BUTTON_ADVANCED, tooltip.get());
        advancedButton.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                boolean isVisible = advancedButton.getIcon() == UiUtil.ARROW_DOWN.getIcon();
                PanelAddressBar.this.atomicTextFieldRequest.get().setVisible(isVisible);
                PanelAddressBar.this.atomicTextFieldHeader.get().setVisible(isVisible);
                PanelAddressBar.this.atomicRadioRequest.get().setVisible(isVisible);
                PanelAddressBar.this.atomicRadioMethod.get().setVisible(isVisible);
                PanelAddressBar.this.atomicRadioHeader.get().setVisible(isVisible);
                PanelAddressBar.this.isAdvanceActivated = isVisible;
                MediatorHelper.menubar().setVisible(isVisible);
                advancedButton.setIcon(isVisible ? UiUtil.ARROW_UP.getIcon() : UiUtil.ARROW_DOWN.getIcon());
            }
        });
        return advancedButton;
    }
    
    
    // Getter and setter

    public void setMethodInjection(AbstractMethodInjection methodInjection) {
        this.methodInjection = methodInjection;
    }

    public boolean isAdvanceActivated() {
        return !this.isAdvanceActivated;
    }

    public JTextField getTextFieldAddress() {
        return this.atomicTextFieldAddress.get();
    }

    public JTextField getTextFieldRequest() {
        return this.atomicTextFieldRequest.get();
    }

    public JTextField getTextFieldHeader() {
        return this.atomicTextFieldHeader.get();
    }

    public AbstractMethodInjection getMethodInjection() {
        return this.methodInjection;
    }

    public PanelTrailingAddress getPanelTrailingAddress() {
        return this.panelTrailingAddress;
    }

    public String getTypeRequest() {
        return this.typeRequest;
    }

    public JRadioButton getAtomicRadioRequest() {
        return this.atomicRadioRequest.get();
    }

    public JRadioButton getAtomicRadioMethod() {
        return this.atomicRadioMethod.get();
    }

    public JRadioButton getAtomicRadioHeader() {
        return this.atomicRadioHeader.get();
    }
}