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