View Javadoc
1   /*******************************************************************************
2    * Copyhacked (H) 2012-2025.
3    * This program and the accompanying materials
4    * are made available under no term at all, use it like
5    * you want, but share and discuss it
6    * every time possible with every body.
7    * 
8    * Contributors:
9    *      ron190 at ymail dot com - initial implementation
10   ******************************************************************************/
11  package com.jsql.view.swing.panel;
12  
13  import com.formdev.flatlaf.FlatClientProperties;
14  import com.formdev.flatlaf.icons.FlatRadioButtonMenuItemIcon;
15  import com.jsql.model.injection.method.AbstractMethodInjection;
16  import com.jsql.util.I18nUtil;
17  import com.jsql.util.LogLevelUtil;
18  import com.jsql.util.ParameterUtil;
19  import com.jsql.util.StringUtil;
20  import com.jsql.view.swing.panel.address.ActionEnterAddressBar;
21  import com.jsql.view.swing.panel.address.PanelTrailingAddress;
22  import com.jsql.view.swing.panel.address.ModelAddressLine;
23  import com.jsql.view.swing.panel.util.ButtonExpandText;
24  import com.jsql.view.swing.text.*;
25  import com.jsql.view.swing.text.listener.DocumentListenerEditing;
26  import com.jsql.view.swing.util.I18nViewUtil;
27  import com.jsql.view.swing.util.MediatorHelper;
28  import com.jsql.view.swing.util.RadioItemNonClosing;
29  import com.jsql.view.swing.util.UiUtil;
30  import org.apache.commons.lang3.StringUtils;
31  import org.apache.logging.log4j.LogManager;
32  import org.apache.logging.log4j.Logger;
33  
34  import javax.swing.*;
35  import java.awt.*;
36  import java.awt.event.MouseAdapter;
37  import java.awt.event.MouseEvent;
38  import java.util.Arrays;
39  import java.util.concurrent.atomic.AtomicReference;
40  import java.util.function.Supplier;
41  import java.util.stream.Stream;
42  
43  /**
44   * Create panel at the top of the window.
45   * Contains textfields in a panel.
46   */
47  public class PanelAddressBar extends JPanel {
48  
49      private static final Logger LOGGER = LogManager.getRootLogger();
50  
51      private final AtomicReference<JTextField> atomicTextFieldAddress = new AtomicReference<>();  // atomic to build dynamically
52      private final AtomicReference<JTextField> atomicTextFieldRequest = new AtomicReference<>();
53      private final AtomicReference<JTextField> atomicTextFieldHeader = new AtomicReference<>();
54  
55      private final AtomicReference<JRadioButton> atomicRadioRequest = new AtomicReference<>();  // atomic to build dynamically
56      private final AtomicReference<JRadioButton> atomicRadioMethod = new AtomicReference<>();
57      private final AtomicReference<JRadioButton> atomicRadioHeader = new AtomicReference<>();
58  
59      private static final String KEY_ADDRESS_BAR_PLACEHOLDER = "ADDRESS_BAR_PLACEHOLDER";
60      private static final String BUTTON_ADVANCED = "BUTTON_ADVANCED";
61      public static final String ADVANCED_BUTTON = "advancedButton";
62  
63      // Current injection method
64      private AbstractMethodInjection methodInjection = MediatorHelper.model().getMediatorMethod().getQuery();
65      private String typeRequest = StringUtil.GET;
66  
67      private final PanelTrailingAddress panelTrailingAddress;
68  
69      private boolean isAdvanceActivated = false;
70      
71      public PanelAddressBar() {
72          var buttonGroup = new ButtonGroup();
73  
74          Stream.of(
75              new ModelAddressLine(
76                  "URL",
77                  MediatorHelper.model().getMediatorMethod().getQuery(),
78                  "QUERYSTRING",
79                  this.atomicRadioRequest,
80                  I18nUtil.valueByKey(PanelAddressBar.KEY_ADDRESS_BAR_PLACEHOLDER),
81                  this.atomicTextFieldAddress
82              ),
83              new ModelAddressLine(
84                  StringUtil.GET,
85                  MediatorHelper.model().getMediatorMethod().getRequest(),
86                  "REQUEST",
87                  this.atomicRadioMethod,
88                  "e.g. key=value&injectMe=",
89                  this.atomicTextFieldRequest
90              ),
91              new ModelAddressLine(
92                  "Header",
93                  MediatorHelper.model().getMediatorMethod().getHeader(),
94                  "HEADER",
95                  this.atomicRadioHeader,
96                  String.format(
97                      "e.g. key: value\\r\\nCookie: cKey1=cValue1; cKey2=cValue2\\r\\n%s: %s %s\\r\\ninjectMe:",
98                      "Authorization",
99                      "Basic",
100                     "dXNlcjpwYXNz"
101                 ),
102                 this.atomicTextFieldHeader
103             )
104         )
105         .forEach(modelLine -> {
106             var i18nTooltip = String.format("FIELD_%s_TOOLTIP", modelLine.i18n());
107             var tooltipTextfield = new AtomicReference<>(new JToolTipI18n(I18nUtil.valueByKey(i18nTooltip)));
108             modelLine.textfield().set(new JPopupTextField(new JTextFieldPlaceholder(
109                 modelLine.placeholder(),
110                 modelLine.radio() == this.atomicRadioRequest ? 18 : 0
111             ) {
112                 @Override
113                 public JToolTip createToolTip() {
114                     return tooltipTextfield.get();
115                 }
116             }).getProxy());
117             I18nViewUtil.addComponentForKey(i18nTooltip, tooltipTextfield.get());
118             modelLine.textfield().get().addActionListener(new ActionEnterAddressBar(this));
119             modelLine.textfield().get().setVisible(false);  // query will be set back to visible
120             modelLine.textfield().get().setToolTipText(I18nUtil.valueByKey(i18nTooltip));
121 
122             var i18nRadio = String.format("METHOD_%s_TOOLTIP", modelLine.i18n());
123             var tooltipRadio = new AtomicReference<>(new JToolTipI18n(I18nUtil.valueByKey(i18nRadio)));
124             modelLine.radio().set(
125                 new JRadioButton(modelLine.request()) {
126                     @Override
127                     public JToolTip createToolTip() {
128                         return tooltipRadio.get();
129                     }
130                 }
131             );
132             I18nViewUtil.addComponentForKey(i18nRadio, tooltipRadio.get());
133             modelLine.radio().get().setToolTipText(I18nUtil.valueByKey(i18nRadio));
134             modelLine.radio().get().setSelected(modelLine.radio() == this.atomicRadioRequest);
135             modelLine.radio().get().setHorizontalTextPosition(SwingConstants.LEFT);
136             modelLine.radio().get().setVisible(false);
137             modelLine.radio().get().setBorder(BorderFactory.createEmptyBorder(
138                 modelLine.radio() == this.atomicRadioRequest ? 0 : 6, 3, 0, 3
139             ));
140             modelLine.radio().get().addActionListener(e -> MediatorHelper.panelAddressBar().setMethodInjection(modelLine.method()));
141             buttonGroup.add(modelLine.radio().get());
142         });
143 
144         this.atomicTextFieldAddress.get().setFont(UiUtil.FONT_NON_MONO_BIG);
145         this.atomicTextFieldAddress.get().setName("textFieldAddress");
146         this.atomicTextFieldAddress.get().setPreferredSize(new Dimension(50, 32));  // required to set correct height
147         this.atomicTextFieldAddress.get().setVisible(true);
148         I18nViewUtil.addComponentForKey(PanelAddressBar.KEY_ADDRESS_BAR_PLACEHOLDER, this.atomicTextFieldAddress.get());  // only i18n placeholder
149 
150         this.panelTrailingAddress = new PanelTrailingAddress(this);
151         this.atomicTextFieldAddress.get().putClientProperty(FlatClientProperties.TEXT_FIELD_TRAILING_COMPONENT, this.panelTrailingAddress);
152         this.atomicTextFieldAddress.get().putClientProperty(FlatClientProperties.TEXT_FIELD_LEADING_ICON, UiUtil.GLOBE.getIcon());
153         this.atomicTextFieldRequest.get().putClientProperty(
154             FlatClientProperties.TEXT_FIELD_TRAILING_COMPONENT,
155             new ButtonExpandText(this.atomicTextFieldRequest.get())
156         );
157         this.atomicTextFieldHeader.get().putClientProperty(
158             FlatClientProperties.TEXT_FIELD_TRAILING_COMPONENT,
159             new ButtonExpandText(this.atomicTextFieldHeader.get())
160         );
161 
162         this.initLayout();
163     }
164 
165     private void initLayout() {
166         final JLabel advancedButton = this.initAdvancedButton();
167         
168         this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
169         
170         // First panel at the top, contains text components
171         var panelTextFields = new JPanel();
172         var groupLayout = new GroupLayout(panelTextFields);
173         panelTextFields.setLayout(groupLayout);
174         panelTextFields.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 0));
175         this.add(panelTextFields);
176 
177         final var popupMethods = new JPopupMenu();
178         final var buttonGroupMethods = new ButtonGroup();
179 
180         for (String method: new String[]{ "DELETE", StringUtil.GET, "HEAD", "OPTIONS", StringUtil.POST, "PUT", "TRACE" }) {
181             final JMenuItem newMenuItem = new RadioItemNonClosing(method, StringUtil.GET.equals(method));
182             newMenuItem.addActionListener(actionEvent -> {
183                 this.typeRequest = newMenuItem.getText();
184                 this.atomicRadioMethod.get().setText(this.typeRequest);
185                 this.atomicRadioMethod.get().requestFocusInWindow();  // required to set proper focus
186             });
187             popupMethods.add(newMenuItem);
188             buttonGroupMethods.add(newMenuItem);
189         }
190 
191         var tooltipMethods = new AtomicReference<>(new JToolTipI18n(I18nUtil.valueByKey("METHOD_CUSTOM_TOOLTIP")));
192         var panelCustomMethod = new JPanel(new BorderLayout()) {
193             @Override
194             public JToolTip createToolTip() {
195                 return tooltipMethods.get();
196             }
197         };
198         I18nViewUtil.addComponentForKey("METHOD_CUSTOM_TOOLTIP", tooltipMethods.get());
199         Supplier<Color> colorBackground = () -> UIManager.getColor("MenuItem.background");  // adapt to current theme
200         Supplier<Color> colorSelectionBackground = () -> UIManager.getColor("MenuItem.selectionBackground");  // adapt to current theme
201         panelCustomMethod.setBackground(colorBackground.get());  // required for correct color
202 
203         final var radioCustomMethod = new JRadioButton() {
204             @Override
205             public JToolTip createToolTip() {
206                 return tooltipMethods.get();
207             }
208         };
209         radioCustomMethod.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 0));
210         radioCustomMethod.setIcon(new FlatRadioButtonMenuItemIcon());
211         radioCustomMethod.setBackground(colorBackground.get());  // required for correct color
212         buttonGroupMethods.add(radioCustomMethod);
213 
214         final JTextField inputCustomMethod = new JPopupTextField("CUSTOM"){
215             @Override
216             public JToolTip createToolTip() {
217                 return tooltipMethods.get();
218             }
219         }.getProxy();
220         inputCustomMethod.addMouseListener(new MouseAdapter() {
221             @Override
222             public void mouseClicked(MouseEvent e) {
223                 radioCustomMethod.setSelected(!radioCustomMethod.isSelected());
224             }
225         });
226         inputCustomMethod.getDocument().addDocumentListener(new DocumentListenerEditing() {
227             @Override
228             public void process() {
229                 PanelAddressBar.this.validate(inputCustomMethod);
230             }
231         });
232         radioCustomMethod.addActionListener(actionEvent -> this.validate(inputCustomMethod));
233 
234         var tooltipCustomMethod = "<html>Set user defined HTTP method.<br/>" +
235             "A valid method is limited to chars:<br>" +
236             "!#$%&'*+-.^_`|~0123456789<br>" +
237             "abcdefghijklmnopqrstuvwxyz<br>" +
238             "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
239         "</html>";
240         MouseAdapter mouseAdapterSetBackground = new MouseAdapter() {
241             @Override
242             public void mouseEntered(MouseEvent e) {
243                 super.mouseEntered(e);
244                 panelCustomMethod.setBackground(colorSelectionBackground.get());
245                 radioCustomMethod.setBackground(colorSelectionBackground.get());
246             }
247             @Override
248             public void mouseExited(MouseEvent e) {
249                 super.mouseExited(e);
250                 panelCustomMethod.setBackground(colorBackground.get());
251                 radioCustomMethod.setBackground(colorBackground.get());
252             }
253         };
254         Arrays.asList(radioCustomMethod, inputCustomMethod, panelCustomMethod).forEach(component -> {
255             component.addMouseListener(mouseAdapterSetBackground);
256             component.setToolTipText(tooltipCustomMethod);
257         });
258 
259         panelCustomMethod.add(radioCustomMethod, BorderLayout.LINE_START);
260         panelCustomMethod.add(inputCustomMethod, BorderLayout.CENTER);
261         popupMethods.insert(panelCustomMethod, popupMethods.getComponentCount());
262 
263         this.atomicRadioMethod.get().addMouseListener(new MouseAdapter() {
264             @Override
265             public void mousePressed(MouseEvent e) {
266                 Arrays.stream(popupMethods.getComponents()).map(a -> (JComponent) a).forEach(JComponent::updateUI);  // required: incorrect when dark/light mode switch
267                 radioCustomMethod.setIcon(new FlatRadioButtonMenuItemIcon());
268                 radioCustomMethod.updateUI();  // required: incorrect when dark/light mode switch
269                 inputCustomMethod.updateUI();  // required: incorrect when dark/light mode switch
270                 popupMethods.updateUI();  // required: incorrect when dark/light mode switch
271 
272                 if (ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()))) {
273                     radioCustomMethod.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 6));
274                 } else {
275                     radioCustomMethod.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 0));
276                 }
277 
278                 popupMethods.show(
279                     e.getComponent(),
280                     ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()))
281                         ? e.getComponent().getX() - e.getComponent().getWidth() - popupMethods.getWidth()
282                         : e.getComponent().getX(),
283                     e.getComponent().getY() + e.getComponent().getHeight()
284                 );
285                 popupMethods.setLocation(  // required for proper location
286                     ComponentOrientation.RIGHT_TO_LEFT.equals(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()))
287                         ? e.getComponent().getLocationOnScreen().x + e.getComponent().getWidth() - popupMethods.getWidth()
288                         : e.getComponent().getLocationOnScreen().x,
289                     e.getComponent().getLocationOnScreen().y + e.getComponent().getHeight()
290                 );
291 
292                 // Orientation set after popup placement, Fix #96032: NullPointerException on show() when arabic
293                 popupMethods.applyComponentOrientation(ComponentOrientation.getOrientation(I18nUtil.getCurrentLocale()));
294             }
295         });
296 
297         groupLayout.setHorizontalGroup(
298             groupLayout
299             .createSequentialGroup()
300             .addGroup(
301                 groupLayout
302                 .createParallelGroup(GroupLayout.Alignment.TRAILING, false)
303                 .addComponent(this.atomicRadioRequest.get())
304                 .addComponent(this.atomicRadioMethod.get())
305                 .addComponent(this.atomicRadioHeader.get())
306             )
307             .addGroup(
308                 groupLayout
309                 .createParallelGroup()
310                 .addComponent(this.atomicTextFieldAddress.get())
311                 .addComponent(this.atomicTextFieldRequest.get())
312                 .addComponent(this.atomicTextFieldHeader.get())
313             )
314             .addGroup(
315                 groupLayout
316                 .createParallelGroup(GroupLayout.Alignment.LEADING, false)
317                 .addComponent(advancedButton)
318             )
319         );
320 
321         groupLayout.setVerticalGroup(
322             groupLayout
323             .createSequentialGroup()
324             .addGroup(
325                 groupLayout
326                 .createParallelGroup(GroupLayout.Alignment.CENTER, false)
327                 .addComponent(this.atomicRadioRequest.get())
328                 .addComponent(this.atomicTextFieldAddress.get())
329                 .addComponent(advancedButton)
330             )
331             .addGroup(
332                 groupLayout
333                 .createParallelGroup(GroupLayout.Alignment.BASELINE)
334                 .addComponent(this.atomicRadioMethod.get())
335                 .addComponent(this.atomicTextFieldRequest.get())
336             )
337             .addGroup(
338                 groupLayout
339                 .createParallelGroup(GroupLayout.Alignment.BASELINE)
340                 .addComponent(this.atomicRadioHeader.get())
341                 .addComponent(this.atomicTextFieldHeader.get())
342             )
343         );
344     }
345 
346     private void validate(JTextField inputCustomMethod) {
347         if (StringUtils.isEmpty(inputCustomMethod.getText())) {
348             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Missing custom method label");
349         } else if (ParameterUtil.isInvalidName(inputCustomMethod.getText())) {
350             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, () -> String.format("Illegal method: \"%s\"", inputCustomMethod.getText()));
351         } else {
352             this.typeRequest = inputCustomMethod.getText();
353             this.atomicRadioMethod.get().setText(this.typeRequest);
354         }
355     }
356 
357     private JLabel initAdvancedButton() {
358         var tooltip = new AtomicReference<>(new JToolTipI18n(I18nUtil.valueByKey(PanelAddressBar.BUTTON_ADVANCED)));
359         var advancedButton = new JLabel(UiUtil.ARROW_DOWN.getIcon()) {
360             @Override
361             public JToolTip createToolTip() {
362                 return tooltip.get();
363             }
364         };
365         advancedButton.setName(PanelAddressBar.ADVANCED_BUTTON);
366         advancedButton.setToolTipText(I18nUtil.valueByKey(PanelAddressBar.BUTTON_ADVANCED));
367         I18nViewUtil.addComponentForKey(PanelAddressBar.BUTTON_ADVANCED, tooltip.get());
368         advancedButton.addMouseListener(new MouseAdapter() {
369             @Override
370             public void mouseClicked(MouseEvent e) {
371                 boolean isVisible = advancedButton.getIcon() == UiUtil.ARROW_DOWN.getIcon();
372                 PanelAddressBar.this.atomicTextFieldRequest.get().setVisible(isVisible);
373                 PanelAddressBar.this.atomicTextFieldHeader.get().setVisible(isVisible);
374                 PanelAddressBar.this.atomicRadioRequest.get().setVisible(isVisible);
375                 PanelAddressBar.this.atomicRadioMethod.get().setVisible(isVisible);
376                 PanelAddressBar.this.atomicRadioHeader.get().setVisible(isVisible);
377                 PanelAddressBar.this.isAdvanceActivated = isVisible;
378                 MediatorHelper.menubar().setVisible(isVisible);
379                 advancedButton.setIcon(isVisible ? UiUtil.ARROW_UP.getIcon() : UiUtil.ARROW_DOWN.getIcon());
380             }
381         });
382         return advancedButton;
383     }
384     
385     
386     // Getter and setter
387 
388     public void setMethodInjection(AbstractMethodInjection methodInjection) {
389         this.methodInjection = methodInjection;
390     }
391 
392     public boolean isAdvanceActivated() {
393         return !this.isAdvanceActivated;
394     }
395 
396     public JTextField getTextFieldAddress() {
397         return this.atomicTextFieldAddress.get();
398     }
399 
400     public JTextField getTextFieldRequest() {
401         return this.atomicTextFieldRequest.get();
402     }
403 
404     public JTextField getTextFieldHeader() {
405         return this.atomicTextFieldHeader.get();
406     }
407 
408     public AbstractMethodInjection getMethodInjection() {
409         return this.methodInjection;
410     }
411 
412     public PanelTrailingAddress getPanelTrailingAddress() {
413         return this.panelTrailingAddress;
414     }
415 
416     public String getTypeRequest() {
417         return this.typeRequest;
418     }
419 
420     public JRadioButton getAtomicRadioRequest() {
421         return this.atomicRadioRequest.get();
422     }
423 
424     public JRadioButton getAtomicRadioMethod() {
425         return this.atomicRadioMethod.get();
426     }
427 
428     public JRadioButton getAtomicRadioHeader() {
429         return this.atomicRadioHeader.get();
430     }
431 }