001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.changeset.query;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BorderLayout;
007import java.awt.Color;
008import java.awt.GridBagConstraints;
009import java.awt.GridBagLayout;
010import java.awt.Insets;
011import java.awt.event.ItemEvent;
012import java.awt.event.ItemListener;
013import java.text.DateFormat;
014import java.text.ParseException;
015import java.util.Date;
016import java.util.GregorianCalendar;
017import java.util.Locale;
018
019import javax.swing.BorderFactory;
020import javax.swing.ButtonGroup;
021import javax.swing.JCheckBox;
022import javax.swing.JLabel;
023import javax.swing.JOptionPane;
024import javax.swing.JPanel;
025import javax.swing.JRadioButton;
026import javax.swing.JScrollPane;
027import javax.swing.text.JTextComponent;
028
029import org.openstreetmap.josm.Main;
030import org.openstreetmap.josm.gui.HelpAwareOptionPane;
031import org.openstreetmap.josm.gui.JosmUserIdentityManager;
032import org.openstreetmap.josm.gui.help.HelpUtil;
033import org.openstreetmap.josm.gui.preferences.server.UserNameValidator;
034import org.openstreetmap.josm.gui.util.GuiHelper;
035import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
036import org.openstreetmap.josm.gui.widgets.BoundingBoxSelectionPanel;
037import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
038import org.openstreetmap.josm.gui.widgets.JosmTextField;
039import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
040import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
041import org.openstreetmap.josm.io.ChangesetQuery;
042import org.openstreetmap.josm.tools.CheckParameterUtil;
043
044/**
045 * This panel allows to specify a changeset query
046 * @since 2689
047 */
048public class AdvancedChangesetQueryPanel extends JPanel {
049
050    private final JCheckBox cbUserRestriction = new JCheckBox();
051    private final JCheckBox cbOpenAndCloseRestrictions = new JCheckBox();
052    private final JCheckBox cbTimeRestrictions = new JCheckBox();
053    private final JCheckBox cbBoundingBoxRestriction = new JCheckBox();
054    private final UserRestrictionPanel pnlUserRestriction = new UserRestrictionPanel();
055    private final OpenAndCloseStateRestrictionPanel pnlOpenAndCloseRestriction = new OpenAndCloseStateRestrictionPanel();
056    private final TimeRestrictionPanel pnlTimeRestriction = new TimeRestrictionPanel();
057    private final BBoxRestrictionPanel pnlBoundingBoxRestriction = new BBoxRestrictionPanel();
058
059    /**
060     * Constructs a new {@code AdvancedChangesetQueryPanel}.
061     */
062    public AdvancedChangesetQueryPanel() {
063        build();
064    }
065
066    protected JPanel buildQueryPanel() {
067        ItemListener stateChangeHandler = new RestrictionGroupStateChangeHandler();
068        JPanel pnl  = new VerticallyScrollablePanel(new GridBagLayout());
069        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
070        GridBagConstraints gc = new GridBagConstraints();
071
072        // -- select changesets by a specific user
073        //
074        gc.anchor = GridBagConstraints.NORTHWEST;
075        gc.weightx = 0.0;
076        gc.fill = GridBagConstraints.HORIZONTAL;
077        pnl.add(cbUserRestriction, gc);
078        cbUserRestriction.addItemListener(stateChangeHandler);
079
080        gc.gridx = 1;
081        gc.weightx = 1.0;
082        pnl.add(new JMultilineLabel(tr("Select changesets owned by specific users")), gc);
083
084        gc.gridy = 1;
085        gc.gridx = 1;
086        gc.weightx = 1.0;
087        pnl.add(pnlUserRestriction, gc);
088
089        // -- restricting the query to open and closed changesets
090        //
091        gc.gridy = 2;
092        gc.gridx = 0;
093        gc.anchor = GridBagConstraints.NORTHWEST;
094        gc.weightx = 0.0;
095        gc.fill = GridBagConstraints.HORIZONTAL;
096        pnl.add(cbOpenAndCloseRestrictions, gc);
097        cbOpenAndCloseRestrictions.addItemListener(stateChangeHandler);
098
099        gc.gridx = 1;
100        gc.weightx = 1.0;
101        pnl.add(new JMultilineLabel(tr("Select changesets depending on whether they are open or closed")), gc);
102
103        gc.gridy = 3;
104        gc.gridx = 1;
105        gc.weightx = 1.0;
106        pnl.add(pnlOpenAndCloseRestriction, gc);
107
108        // -- restricting the query to a specific time
109        //
110        gc.gridy = 4;
111        gc.gridx = 0;
112        gc.anchor = GridBagConstraints.NORTHWEST;
113        gc.weightx = 0.0;
114        gc.fill = GridBagConstraints.HORIZONTAL;
115        pnl.add(cbTimeRestrictions, gc);
116        cbTimeRestrictions.addItemListener(stateChangeHandler);
117
118        gc.gridx = 1;
119        gc.weightx = 1.0;
120        pnl.add(new JMultilineLabel(tr("Select changesets based on the date/time they have been created or closed")), gc);
121
122        gc.gridy = 5;
123        gc.gridx = 1;
124        gc.weightx = 1.0;
125        pnl.add(pnlTimeRestriction, gc);
126
127
128        // -- restricting the query to a specific bounding box
129        //
130        gc.gridy = 6;
131        gc.gridx = 0;
132        gc.anchor = GridBagConstraints.NORTHWEST;
133        gc.weightx = 0.0;
134        gc.fill = GridBagConstraints.HORIZONTAL;
135        pnl.add(cbBoundingBoxRestriction, gc);
136        cbBoundingBoxRestriction.addItemListener(stateChangeHandler);
137
138        gc.gridx = 1;
139        gc.weightx = 1.0;
140        pnl.add(new JMultilineLabel(tr("Select only changesets related to a specific bounding box")), gc);
141
142        gc.gridy = 7;
143        gc.gridx = 1;
144        gc.weightx = 1.0;
145        pnl.add(pnlBoundingBoxRestriction, gc);
146
147
148        gc.gridy = 8;
149        gc.gridx = 0;
150        gc.gridwidth = 2;
151        gc.fill = GridBagConstraints.BOTH;
152        gc.weightx = 1.0;
153        gc.weighty = 1.0;
154        pnl.add(new JPanel(), gc);
155
156        return pnl;
157    }
158
159    protected final void build() {
160        setLayout(new BorderLayout());
161        JScrollPane spQueryPanel = GuiHelper.embedInVerticalScrollPane(buildQueryPanel());
162        add(spQueryPanel, BorderLayout.CENTER);
163    }
164
165    public void startUserInput() {
166        restoreFromSettings();
167        pnlBoundingBoxRestriction.setVisible(cbBoundingBoxRestriction.isSelected());
168        pnlOpenAndCloseRestriction.setVisible(cbOpenAndCloseRestrictions.isSelected());
169        pnlTimeRestriction.setVisible(cbTimeRestrictions.isSelected());
170        pnlUserRestriction.setVisible(cbUserRestriction.isSelected());
171        pnlOpenAndCloseRestriction.startUserInput();
172        pnlUserRestriction.startUserInput();
173        pnlTimeRestriction.startUserInput();
174    }
175
176    public void displayMessageIfInvalid() {
177        if (cbUserRestriction.isSelected()) {
178            if (!pnlUserRestriction.isValidChangesetQuery()) {
179                pnlUserRestriction.displayMessageIfInvalid();
180            }
181        } else if (cbTimeRestrictions.isSelected()) {
182            if (!pnlTimeRestriction.isValidChangesetQuery()) {
183                pnlTimeRestriction.displayMessageIfInvalid();
184            }
185        } else if (cbBoundingBoxRestriction.isSelected()) {
186            if (!pnlBoundingBoxRestriction.isValidChangesetQuery()) {
187                pnlBoundingBoxRestriction.displayMessageIfInvalid();
188            }
189        }
190    }
191
192    /**
193     * Builds the changeset query based on the data entered in the form.
194     *
195     * @return the changeset query. null, if the data entered doesn't represent
196     * a valid changeset query.
197     */
198    public ChangesetQuery buildChangesetQuery() {
199        ChangesetQuery query = new ChangesetQuery();
200        if (cbUserRestriction.isSelected()) {
201            if (!pnlUserRestriction.isValidChangesetQuery())
202                return null;
203            pnlUserRestriction.fillInQuery(query);
204        }
205        if (cbOpenAndCloseRestrictions.isSelected()) {
206            // don't have to check whether it's valid. It always is.
207            pnlOpenAndCloseRestriction.fillInQuery(query);
208        }
209        if (cbBoundingBoxRestriction.isSelected()) {
210            if (!pnlBoundingBoxRestriction.isValidChangesetQuery())
211                return null;
212            pnlBoundingBoxRestriction.fillInQuery(query);
213        }
214        if (cbTimeRestrictions.isSelected()) {
215            if (!pnlTimeRestriction.isValidChangesetQuery())
216                return null;
217            pnlTimeRestriction.fillInQuery(query);
218        }
219        return query;
220    }
221
222    public void rememberSettings() {
223        Main.pref.put("changeset-query.advanced.user-restrictions", cbUserRestriction.isSelected());
224        Main.pref.put("changeset-query.advanced.open-restrictions", cbOpenAndCloseRestrictions.isSelected());
225        Main.pref.put("changeset-query.advanced.time-restrictions", cbTimeRestrictions.isSelected());
226        Main.pref.put("changeset-query.advanced.bbox-restrictions", cbBoundingBoxRestriction.isSelected());
227
228        pnlUserRestriction.rememberSettings();
229        pnlOpenAndCloseRestriction.rememberSettings();
230        pnlTimeRestriction.rememberSettings();
231    }
232
233    public void restoreFromSettings() {
234        cbUserRestriction.setSelected(Main.pref.getBoolean("changeset-query.advanced.user-restrictions", false));
235        cbOpenAndCloseRestrictions.setSelected(Main.pref.getBoolean("changeset-query.advanced.open-restrictions", false));
236        cbTimeRestrictions.setSelected(Main.pref.getBoolean("changeset-query.advanced.time-restrictions", false));
237        cbBoundingBoxRestriction.setSelected(Main.pref.getBoolean("changeset-query.advanced.bbox-restrictions", false));
238    }
239
240    class RestrictionGroupStateChangeHandler implements ItemListener {
241        protected void userRestrictionStateChanged() {
242            if (pnlUserRestriction == null)
243                return;
244            pnlUserRestriction.setVisible(cbUserRestriction.isSelected());
245        }
246
247        protected void openCloseRestrictionStateChanged() {
248            if (pnlOpenAndCloseRestriction == null)
249                return;
250            pnlOpenAndCloseRestriction.setVisible(cbOpenAndCloseRestrictions.isSelected());
251        }
252
253        protected void timeRestrictionsStateChanged() {
254            if (pnlTimeRestriction == null)
255                return;
256            pnlTimeRestriction.setVisible(cbTimeRestrictions.isSelected());
257        }
258
259        protected void boundingBoxRestrictionChanged() {
260            if (pnlBoundingBoxRestriction == null)
261                return;
262            pnlBoundingBoxRestriction.setVisible(cbBoundingBoxRestriction.isSelected());
263        }
264
265        @Override
266        public void itemStateChanged(ItemEvent e) {
267            if (e.getSource() == cbUserRestriction) {
268                userRestrictionStateChanged();
269            } else if (e.getSource() == cbOpenAndCloseRestrictions) {
270                openCloseRestrictionStateChanged();
271            } else if (e.getSource() == cbTimeRestrictions) {
272                timeRestrictionsStateChanged();
273            } else if (e.getSource() == cbBoundingBoxRestriction) {
274                boundingBoxRestrictionChanged();
275            }
276            validate();
277            repaint();
278        }
279    }
280
281    /**
282     * This is the panel for selecting whether the changeset query should be restricted to
283     * open or closed changesets
284     */
285    private static class OpenAndCloseStateRestrictionPanel extends JPanel {
286
287        private final JRadioButton rbOpenOnly = new JRadioButton();
288        private final JRadioButton rbClosedOnly = new JRadioButton();
289        private final JRadioButton rbBoth = new JRadioButton();
290
291        OpenAndCloseStateRestrictionPanel() {
292            build();
293        }
294
295        protected void build() {
296            setLayout(new GridBagLayout());
297            setBorder(BorderFactory.createCompoundBorder(
298                    BorderFactory.createEmptyBorder(3, 3, 3, 3),
299                    BorderFactory.createCompoundBorder(
300                            BorderFactory.createLineBorder(Color.GRAY),
301                            BorderFactory.createEmptyBorder(5, 5, 5, 5)
302                    )
303            ));
304            GridBagConstraints gc = new GridBagConstraints();
305            gc.anchor = GridBagConstraints.NORTHWEST;
306            gc.fill = GridBagConstraints.HORIZONTAL;
307            gc.weightx = 0.0;
308            add(rbOpenOnly, gc);
309
310            gc.gridx = 1;
311            gc.weightx = 1.0;
312            add(new JMultilineLabel(tr("Query open changesets only")), gc);
313
314            gc.gridy = 1;
315            gc.gridx = 0;
316            gc.weightx = 0.0;
317            add(rbClosedOnly, gc);
318
319            gc.gridx = 1;
320            gc.weightx = 1.0;
321            add(new JMultilineLabel(tr("Query closed changesets only")), gc);
322
323            gc.gridy = 2;
324            gc.gridx = 0;
325            gc.weightx = 0.0;
326            add(rbBoth, gc);
327
328            gc.gridx = 1;
329            gc.weightx = 1.0;
330            add(new JMultilineLabel(tr("Query both open and closed changesets")), gc);
331
332            ButtonGroup bgRestrictions = new ButtonGroup();
333            bgRestrictions.add(rbBoth);
334            bgRestrictions.add(rbClosedOnly);
335            bgRestrictions.add(rbOpenOnly);
336        }
337
338        public void startUserInput() {
339            restoreFromSettings();
340        }
341
342        public void fillInQuery(ChangesetQuery query) {
343            if (rbBoth.isSelected()) {
344                query.beingClosed(true);
345                query.beingOpen(true);
346            } else if (rbOpenOnly.isSelected()) {
347                query.beingOpen(true);
348            } else if (rbClosedOnly.isSelected()) {
349                query.beingClosed(true);
350            }
351        }
352
353        public void rememberSettings() {
354            String prefRoot = "changeset-query.advanced.open-restrictions";
355            if (rbBoth.isSelected()) {
356                Main.pref.put(prefRoot + ".query-type", "both");
357            } else if (rbOpenOnly.isSelected()) {
358                Main.pref.put(prefRoot + ".query-type", "open");
359            } else if (rbClosedOnly.isSelected()) {
360                Main.pref.put(prefRoot + ".query-type", "closed");
361            }
362        }
363
364        public void restoreFromSettings() {
365            String prefRoot = "changeset-query.advanced.open-restrictions";
366            String v = Main.pref.get(prefRoot + ".query-type", "open");
367            rbBoth.setSelected("both".equals(v));
368            rbOpenOnly.setSelected("open".equals(v));
369            rbClosedOnly.setSelected("closed".equals(v));
370        }
371    }
372
373    /**
374     * This is the panel for selecting whether the query should be restricted to a specific user
375     */
376    private static class UserRestrictionPanel extends JPanel {
377        private final ButtonGroup bgUserRestrictions = new ButtonGroup();
378        private final JRadioButton rbRestrictToMyself = new JRadioButton();
379        private final JRadioButton rbRestrictToUid = new JRadioButton();
380        private final JRadioButton rbRestrictToUserName = new JRadioButton();
381        private final JosmTextField tfUid = new JosmTextField(10);
382        private transient UidInputFieldValidator valUid;
383        private final JosmTextField tfUserName = new JosmTextField(10);
384        private transient UserNameValidator valUserName;
385        private final JMultilineLabel lblRestrictedToMyself = new JMultilineLabel(tr("Only changesets owned by myself"));
386
387        UserRestrictionPanel() {
388            build();
389        }
390
391        protected JPanel buildUidInputPanel() {
392            JPanel pnl = new JPanel(new GridBagLayout());
393            GridBagConstraints gc = new GridBagConstraints();
394            gc.fill = GridBagConstraints.HORIZONTAL;
395            gc.weightx = 0.0;
396            gc.insets = new Insets(0, 0, 0, 3);
397            pnl.add(new JLabel(tr("User ID:")), gc);
398
399            gc.gridx = 1;
400            pnl.add(tfUid, gc);
401            SelectAllOnFocusGainedDecorator.decorate(tfUid);
402            valUid = UidInputFieldValidator.decorate(tfUid);
403
404            // grab remaining space
405            gc.gridx = 2;
406            gc.weightx = 1.0;
407            pnl.add(new JPanel(), gc);
408            return pnl;
409        }
410
411        protected JPanel buildUserNameInputPanel() {
412            JPanel pnl = new JPanel(new GridBagLayout());
413            GridBagConstraints gc = new GridBagConstraints();
414            gc.fill = GridBagConstraints.HORIZONTAL;
415            gc.weightx = 0.0;
416            gc.insets = new Insets(0, 0, 0, 3);
417            pnl.add(new JLabel(tr("User name:")), gc);
418
419            gc.gridx = 1;
420            pnl.add(tfUserName, gc);
421            SelectAllOnFocusGainedDecorator.decorate(tfUserName);
422            valUserName = new UserNameValidator(tfUserName);
423
424            // grab remaining space
425            gc.gridx = 2;
426            gc.weightx = 1.0;
427            pnl.add(new JPanel(), gc);
428            return pnl;
429        }
430
431        protected void build() {
432            setLayout(new GridBagLayout());
433            setBorder(BorderFactory.createCompoundBorder(
434                    BorderFactory.createEmptyBorder(3, 3, 3, 3),
435                    BorderFactory.createCompoundBorder(
436                            BorderFactory.createLineBorder(Color.GRAY),
437                            BorderFactory.createEmptyBorder(5, 5, 5, 5)
438                    )
439            ));
440
441            ItemListener userRestrictionChangeHandler = new UserRestrictionChangedHandler();
442            GridBagConstraints gc = new GridBagConstraints();
443            gc.anchor = GridBagConstraints.NORTHWEST;
444            gc.gridx = 0;
445            gc.fill = GridBagConstraints.HORIZONTAL;
446            gc.weightx = 0.0;
447            add(rbRestrictToMyself, gc);
448            rbRestrictToMyself.addItemListener(userRestrictionChangeHandler);
449
450            gc.gridx = 1;
451            gc.fill =  GridBagConstraints.HORIZONTAL;
452            gc.weightx = 1.0;
453            add(lblRestrictedToMyself, gc);
454
455            gc.gridx = 0;
456            gc.gridy = 1;
457            gc.fill = GridBagConstraints.HORIZONTAL;
458            gc.weightx = 0.0;
459            add(rbRestrictToUid, gc);
460            rbRestrictToUid.addItemListener(userRestrictionChangeHandler);
461
462            gc.gridx = 1;
463            gc.fill =  GridBagConstraints.HORIZONTAL;
464            gc.weightx = 1.0;
465            add(new JMultilineLabel(tr("Only changesets owned by the user with the following user ID")), gc);
466
467            gc.gridx = 1;
468            gc.gridy = 2;
469            gc.fill =  GridBagConstraints.HORIZONTAL;
470            gc.weightx = 1.0;
471            add(buildUidInputPanel(), gc);
472
473            gc.gridx = 0;
474            gc.gridy = 3;
475            gc.fill = GridBagConstraints.HORIZONTAL;
476            gc.weightx = 0.0;
477            add(rbRestrictToUserName, gc);
478            rbRestrictToUserName.addItemListener(userRestrictionChangeHandler);
479
480            gc.gridx = 1;
481            gc.fill = GridBagConstraints.HORIZONTAL;
482            gc.weightx = 1.0;
483            add(new JMultilineLabel(tr("Only changesets owned by the user with the following user name")), gc);
484
485            gc.gridx = 1;
486            gc.gridy = 4;
487            gc.fill = GridBagConstraints.HORIZONTAL;
488            gc.weightx = 1.0;
489            add(buildUserNameInputPanel(), gc);
490
491            bgUserRestrictions.add(rbRestrictToMyself);
492            bgUserRestrictions.add(rbRestrictToUid);
493            bgUserRestrictions.add(rbRestrictToUserName);
494        }
495
496        public void startUserInput() {
497            if (JosmUserIdentityManager.getInstance().isAnonymous()) {
498                lblRestrictedToMyself.setText(tr("Only changesets owned by myself (disabled. JOSM is currently run by an anonymous user)"));
499                rbRestrictToMyself.setEnabled(false);
500                if (rbRestrictToMyself.isSelected()) {
501                    rbRestrictToUid.setSelected(true);
502                }
503            } else {
504                lblRestrictedToMyself.setText(tr("Only changesets owned by myself"));
505                rbRestrictToMyself.setEnabled(true);
506                rbRestrictToMyself.setSelected(true);
507            }
508            restoreFromSettings();
509        }
510
511        /**
512         * Sets the query restrictions on <code>query</code> for changeset owner based
513         * restrictions.
514         *
515         * @param query the query. Must not be null.
516         * @throws IllegalArgumentException if query is null
517         * @throws IllegalStateException if one of the available values for query parameters in
518         * this panel isn't valid
519         */
520        public void fillInQuery(ChangesetQuery query) {
521            CheckParameterUtil.ensureParameterNotNull(query, "query");
522            if (rbRestrictToMyself.isSelected()) {
523                JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
524                if (im.isPartiallyIdentified()) {
525                    query.forUser(im.getUserName());
526                } else if (im.isFullyIdentified()) {
527                    query.forUser(im.getUserId());
528                } else
529                    throw new IllegalStateException(
530                            tr("Cannot restrict changeset query to the current user because the current user is anonymous"));
531            } else if (rbRestrictToUid.isSelected()) {
532                int uid  = valUid.getUid();
533                if (uid > 0) {
534                    query.forUser(uid);
535                } else
536                    throw new IllegalStateException(tr("Current value ''{0}'' for user ID is not valid", tfUid.getText()));
537            } else if (rbRestrictToUserName.isSelected()) {
538                if (!valUserName.isValid())
539                    throw new IllegalStateException(
540                            tr("Cannot restrict the changeset query to the user name ''{0}''", tfUserName.getText()));
541                query.forUser(tfUserName.getText());
542            }
543        }
544
545        public boolean isValidChangesetQuery() {
546            if (rbRestrictToUid.isSelected())
547                return valUid.isValid();
548            else if (rbRestrictToUserName.isSelected())
549                return valUserName.isValid();
550            return true;
551        }
552
553        protected void alertInvalidUid() {
554            HelpAwareOptionPane.showOptionDialog(
555                    this,
556                    tr("Please enter a valid user ID"),
557                    tr("Invalid user ID"),
558                    JOptionPane.ERROR_MESSAGE,
559                    HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidUserId")
560            );
561        }
562
563        protected void alertInvalidUserName() {
564            HelpAwareOptionPane.showOptionDialog(
565                    this,
566                    tr("Please enter a non-empty user name"),
567                    tr("Invalid user name"),
568                    JOptionPane.ERROR_MESSAGE,
569                    HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidUserName")
570            );
571        }
572
573        public void displayMessageIfInvalid() {
574            if (rbRestrictToUid.isSelected()) {
575                if (!valUid.isValid()) {
576                    alertInvalidUid();
577                }
578            } else if (rbRestrictToUserName.isSelected()) {
579                if (!valUserName.isValid()) {
580                    alertInvalidUserName();
581                }
582            }
583        }
584
585        public void rememberSettings() {
586            String prefRoot = "changeset-query.advanced.user-restrictions";
587            if (rbRestrictToMyself.isSelected()) {
588                Main.pref.put(prefRoot + ".query-type", "mine");
589            } else if (rbRestrictToUid.isSelected()) {
590                Main.pref.put(prefRoot + ".query-type", "uid");
591            } else if (rbRestrictToUserName.isSelected()) {
592                Main.pref.put(prefRoot + ".query-type", "username");
593            }
594            Main.pref.put(prefRoot + ".uid", tfUid.getText());
595            Main.pref.put(prefRoot + ".username", tfUserName.getText());
596        }
597
598        public void restoreFromSettings() {
599            String prefRoot = "changeset-query.advanced.user-restrictions";
600            String v = Main.pref.get(prefRoot + ".query-type", "mine");
601            if ("mine".equals(v)) {
602                JosmUserIdentityManager im = JosmUserIdentityManager.getInstance();
603                if (im.isAnonymous()) {
604                    rbRestrictToUid.setSelected(true);
605                } else {
606                    rbRestrictToMyself.setSelected(true);
607                }
608            } else if ("uid".equals(v)) {
609                rbRestrictToUid.setSelected(true);
610            } else if ("username".equals(v)) {
611                rbRestrictToUserName.setSelected(true);
612            }
613            tfUid.setText(Main.pref.get(prefRoot + ".uid", ""));
614            if (!valUid.isValid()) {
615                tfUid.setText("");
616            }
617            tfUserName.setText(Main.pref.get(prefRoot + ".username", ""));
618        }
619
620        class UserRestrictionChangedHandler implements ItemListener {
621            @Override
622            public void itemStateChanged(ItemEvent e) {
623                tfUid.setEnabled(rbRestrictToUid.isSelected());
624                tfUserName.setEnabled(rbRestrictToUserName.isSelected());
625                if (rbRestrictToUid.isSelected()) {
626                    tfUid.requestFocusInWindow();
627                } else if (rbRestrictToUserName.isSelected()) {
628                    tfUserName.requestFocusInWindow();
629                }
630            }
631        }
632    }
633
634    /**
635     * This is the panel to apply a time restriction to the changeset query
636     */
637    private static class TimeRestrictionPanel extends JPanel {
638
639        private final JRadioButton rbClosedAfter = new JRadioButton();
640        private final JRadioButton rbClosedAfterAndCreatedBefore = new JRadioButton();
641        private final JosmTextField tfClosedAfterDate1 = new JosmTextField();
642        private transient DateValidator valClosedAfterDate1;
643        private final JosmTextField tfClosedAfterTime1 = new JosmTextField();
644        private transient TimeValidator valClosedAfterTime1;
645        private final JosmTextField tfClosedAfterDate2 = new JosmTextField();
646        private transient DateValidator valClosedAfterDate2;
647        private final JosmTextField tfClosedAfterTime2 = new JosmTextField();
648        private transient TimeValidator valClosedAfterTime2;
649        private final JosmTextField tfCreatedBeforeDate = new JosmTextField();
650        private transient DateValidator valCreatedBeforeDate;
651        private final JosmTextField tfCreatedBeforeTime = new JosmTextField();
652        private transient TimeValidator valCreatedBeforeTime;
653
654        TimeRestrictionPanel() {
655            build();
656        }
657
658        protected JPanel buildClosedAfterInputPanel() {
659            JPanel pnl = new JPanel(new GridBagLayout());
660            GridBagConstraints gc = new GridBagConstraints();
661            gc.fill = GridBagConstraints.HORIZONTAL;
662            gc.weightx = 0.0;
663            gc.insets = new Insets(0, 0, 0, 3);
664            pnl.add(new JLabel(tr("Date: ")), gc);
665
666            gc.gridx = 1;
667            gc.weightx = 0.7;
668            pnl.add(tfClosedAfterDate1, gc);
669            SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterDate1);
670            valClosedAfterDate1 = DateValidator.decorate(tfClosedAfterDate1);
671            tfClosedAfterDate1.setToolTipText(valClosedAfterDate1.getStandardTooltipTextAsHtml());
672
673            gc.gridx = 2;
674            gc.weightx = 0.0;
675            pnl.add(new JLabel(tr("Time:")), gc);
676
677            gc.gridx = 3;
678            gc.weightx = 0.3;
679            pnl.add(tfClosedAfterTime1, gc);
680            SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterTime1);
681            valClosedAfterTime1 = TimeValidator.decorate(tfClosedAfterTime1);
682            tfClosedAfterTime1.setToolTipText(valClosedAfterTime1.getStandardTooltipTextAsHtml());
683            return pnl;
684        }
685
686        protected JPanel buildClosedAfterAndCreatedBeforeInputPanel() {
687            JPanel pnl = new JPanel(new GridBagLayout());
688            GridBagConstraints gc = new GridBagConstraints();
689            gc.fill = GridBagConstraints.HORIZONTAL;
690            gc.weightx = 0.0;
691            gc.insets = new Insets(0, 0, 0, 3);
692            pnl.add(new JLabel(tr("Closed after - ")), gc);
693
694            gc.gridx = 1;
695            gc.fill = GridBagConstraints.HORIZONTAL;
696            gc.weightx = 0.0;
697            gc.insets = new Insets(0, 0, 0, 3);
698            pnl.add(new JLabel(tr("Date:")), gc);
699
700            gc.gridx = 2;
701            gc.weightx = 0.7;
702            pnl.add(tfClosedAfterDate2, gc);
703            SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterDate2);
704            valClosedAfterDate2 = DateValidator.decorate(tfClosedAfterDate2);
705            tfClosedAfterDate2.setToolTipText(valClosedAfterDate2.getStandardTooltipTextAsHtml());
706            gc.gridx = 3;
707            gc.weightx = 0.0;
708            pnl.add(new JLabel(tr("Time:")), gc);
709
710            gc.gridx = 4;
711            gc.weightx = 0.3;
712            pnl.add(tfClosedAfterTime2, gc);
713            SelectAllOnFocusGainedDecorator.decorate(tfClosedAfterTime2);
714            valClosedAfterTime2 = TimeValidator.decorate(tfClosedAfterTime2);
715            tfClosedAfterTime2.setToolTipText(valClosedAfterTime2.getStandardTooltipTextAsHtml());
716
717            gc.gridy = 1;
718            gc.gridx = 0;
719            gc.fill = GridBagConstraints.HORIZONTAL;
720            gc.weightx = 0.0;
721            gc.insets = new Insets(0, 0, 0, 3);
722            pnl.add(new JLabel(tr("Created before - ")), gc);
723
724            gc.gridx = 1;
725            gc.fill = GridBagConstraints.HORIZONTAL;
726            gc.weightx = 0.0;
727            gc.insets = new Insets(0, 0, 0, 3);
728            pnl.add(new JLabel(tr("Date:")), gc);
729
730            gc.gridx = 2;
731            gc.weightx = 0.7;
732            pnl.add(tfCreatedBeforeDate, gc);
733            SelectAllOnFocusGainedDecorator.decorate(tfCreatedBeforeDate);
734            valCreatedBeforeDate = DateValidator.decorate(tfCreatedBeforeDate);
735            tfCreatedBeforeDate.setToolTipText(valCreatedBeforeDate.getStandardTooltipTextAsHtml());
736
737            gc.gridx = 3;
738            gc.weightx = 0.0;
739            pnl.add(new JLabel(tr("Time:")), gc);
740
741            gc.gridx = 4;
742            gc.weightx = 0.3;
743            pnl.add(tfCreatedBeforeTime, gc);
744            SelectAllOnFocusGainedDecorator.decorate(tfCreatedBeforeTime);
745            valCreatedBeforeTime = TimeValidator.decorate(tfCreatedBeforeTime);
746            tfCreatedBeforeTime.setToolTipText(valCreatedBeforeDate.getStandardTooltipTextAsHtml());
747
748            return pnl;
749        }
750
751        protected void build() {
752            setLayout(new GridBagLayout());
753            setBorder(BorderFactory.createCompoundBorder(
754                    BorderFactory.createEmptyBorder(3, 3, 3, 3),
755                    BorderFactory.createCompoundBorder(
756                            BorderFactory.createLineBorder(Color.GRAY),
757                            BorderFactory.createEmptyBorder(5, 5, 5, 5)
758                    )
759            ));
760
761            // -- changesets closed after a specific date/time
762            //
763            GridBagConstraints gc = new GridBagConstraints();
764            gc.anchor = GridBagConstraints.NORTHWEST;
765            gc.gridx = 0;
766            gc.fill = GridBagConstraints.HORIZONTAL;
767            gc.weightx = 0.0;
768            add(rbClosedAfter, gc);
769
770            gc.gridx = 1;
771            gc.fill = GridBagConstraints.HORIZONTAL;
772            gc.weightx = 1.0;
773            add(new JMultilineLabel(tr("Only changesets closed after the following date/time")), gc);
774
775            gc.gridx = 1;
776            gc.gridy = 1;
777            gc.fill = GridBagConstraints.HORIZONTAL;
778            gc.weightx = 1.0;
779            add(buildClosedAfterInputPanel(), gc);
780
781            // -- changesets closed after a specific date/time and created before a specific date time
782            //
783            gc = new GridBagConstraints();
784            gc.anchor = GridBagConstraints.NORTHWEST;
785            gc.gridy = 2;
786            gc.gridx = 0;
787            gc.fill = GridBagConstraints.HORIZONTAL;
788            gc.weightx = 0.0;
789            add(rbClosedAfterAndCreatedBefore, gc);
790
791            gc.gridx = 1;
792            gc.fill = GridBagConstraints.HORIZONTAL;
793            gc.weightx = 1.0;
794            add(new JMultilineLabel(tr("Only changesets closed after and created before a specific date/time")), gc);
795
796            gc.gridx = 1;
797            gc.gridy = 3;
798            gc.fill = GridBagConstraints.HORIZONTAL;
799            gc.weightx = 1.0;
800            add(buildClosedAfterAndCreatedBeforeInputPanel(), gc);
801
802            ButtonGroup bg = new ButtonGroup();
803            bg.add(rbClosedAfter);
804            bg.add(rbClosedAfterAndCreatedBefore);
805
806            ItemListener restrictionChangeHandler = new TimeRestrictionChangedHandler();
807            rbClosedAfter.addItemListener(restrictionChangeHandler);
808            rbClosedAfterAndCreatedBefore.addItemListener(restrictionChangeHandler);
809
810            rbClosedAfter.setSelected(true);
811        }
812
813        public boolean isValidChangesetQuery() {
814            if (rbClosedAfter.isSelected())
815                return valClosedAfterDate1.isValid() && valClosedAfterTime1.isValid();
816            else if (rbClosedAfterAndCreatedBefore.isSelected())
817                return valClosedAfterDate2.isValid() && valClosedAfterTime2.isValid()
818                && valCreatedBeforeDate.isValid() && valCreatedBeforeTime.isValid();
819            // should not happen
820            return true;
821        }
822
823        class TimeRestrictionChangedHandler implements ItemListener {
824            @Override
825            public void itemStateChanged(ItemEvent e) {
826                tfClosedAfterDate1.setEnabled(rbClosedAfter.isSelected());
827                tfClosedAfterTime1.setEnabled(rbClosedAfter.isSelected());
828
829                tfClosedAfterDate2.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
830                tfClosedAfterTime2.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
831                tfCreatedBeforeDate.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
832                tfCreatedBeforeTime.setEnabled(rbClosedAfterAndCreatedBefore.isSelected());
833            }
834        }
835
836        public void startUserInput() {
837            restoreFromSettings();
838        }
839
840        public void fillInQuery(ChangesetQuery query) {
841            if (!isValidChangesetQuery())
842                throw new IllegalStateException(tr("Cannot build changeset query with time based restrictions. Input is not valid."));
843            if (rbClosedAfter.isSelected()) {
844                GregorianCalendar cal = new GregorianCalendar();
845                Date d1 = valClosedAfterDate1.getDate();
846                Date d2 = valClosedAfterTime1.getDate();
847                cal.setTimeInMillis(d1.getTime() + (d2 == null ? 0 : d2.getTime()));
848                query.closedAfter(cal.getTime());
849            } else if (rbClosedAfterAndCreatedBefore.isSelected()) {
850                GregorianCalendar cal = new GregorianCalendar();
851                Date d1 = valClosedAfterDate2.getDate();
852                Date d2 = valClosedAfterTime2.getDate();
853                cal.setTimeInMillis(d1.getTime() + (d2 == null ? 0 : d2.getTime()));
854                Date d3 = cal.getTime();
855
856                d1 = valCreatedBeforeDate.getDate();
857                d2 = valCreatedBeforeTime.getDate();
858                cal.setTimeInMillis(d1.getTime() + (d2 == null ? 0 : d2.getTime()));
859                Date d4 = cal.getTime();
860
861                query.closedAfterAndCreatedBefore(d3, d4);
862            }
863        }
864
865        public void displayMessageIfInvalid() {
866            if (isValidChangesetQuery())
867                return;
868            HelpAwareOptionPane.showOptionDialog(
869                    this,
870                    tr(
871                            "<html>Please enter valid date/time values to restrict<br>"
872                            + "the query to a specific time range.</html>"
873                    ),
874                    tr("Invalid date/time values"),
875                    JOptionPane.ERROR_MESSAGE,
876                    HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidDateTimeValues")
877            );
878        }
879
880        public void rememberSettings() {
881            String prefRoot = "changeset-query.advanced.time-restrictions";
882            if (rbClosedAfter.isSelected()) {
883                Main.pref.put(prefRoot + ".query-type", "closed-after");
884            } else if (rbClosedAfterAndCreatedBefore.isSelected()) {
885                Main.pref.put(prefRoot + ".query-type", "closed-after-created-before");
886            }
887            Main.pref.put(prefRoot + ".closed-after.date", tfClosedAfterDate1.getText());
888            Main.pref.put(prefRoot + ".closed-after.time", tfClosedAfterTime1.getText());
889            Main.pref.put(prefRoot + ".closed-created.closed.date", tfClosedAfterDate2.getText());
890            Main.pref.put(prefRoot + ".closed-created.closed.time", tfClosedAfterTime2.getText());
891            Main.pref.put(prefRoot + ".closed-created.created.date", tfCreatedBeforeDate.getText());
892            Main.pref.put(prefRoot + ".closed-created.created.time", tfCreatedBeforeTime.getText());
893        }
894
895        public void restoreFromSettings() {
896            String prefRoot = "changeset-query.advanced.open-restrictions";
897            String v = Main.pref.get(prefRoot + ".query-type", "closed-after");
898            rbClosedAfter.setSelected("closed-after".equals(v));
899            rbClosedAfterAndCreatedBefore.setSelected("closed-after-created-before".equals(v));
900            if (!rbClosedAfter.isSelected() && !rbClosedAfterAndCreatedBefore.isSelected()) {
901                rbClosedAfter.setSelected(true);
902            }
903            tfClosedAfterDate1.setText(Main.pref.get(prefRoot + ".closed-after.date", ""));
904            tfClosedAfterTime1.setText(Main.pref.get(prefRoot + ".closed-after.time", ""));
905            tfClosedAfterDate2.setText(Main.pref.get(prefRoot + ".closed-created.closed.date", ""));
906            tfClosedAfterTime2.setText(Main.pref.get(prefRoot + ".closed-created.closed.time", ""));
907            tfCreatedBeforeDate.setText(Main.pref.get(prefRoot + ".closed-created.created.date", ""));
908            tfCreatedBeforeTime.setText(Main.pref.get(prefRoot + ".closed-created.created.time", ""));
909            if (!valClosedAfterDate1.isValid()) {
910                tfClosedAfterDate1.setText("");
911            }
912            if (!valClosedAfterTime1.isValid()) {
913                tfClosedAfterTime1.setText("");
914            }
915            if (!valClosedAfterDate2.isValid()) {
916                tfClosedAfterDate2.setText("");
917            }
918            if (!valClosedAfterTime2.isValid()) {
919                tfClosedAfterTime2.setText("");
920            }
921            if (!valCreatedBeforeDate.isValid()) {
922                tfCreatedBeforeDate.setText("");
923            }
924            if (!valCreatedBeforeTime.isValid()) {
925                tfCreatedBeforeTime.setText("");
926            }
927        }
928    }
929
930    private static class BBoxRestrictionPanel extends BoundingBoxSelectionPanel {
931        BBoxRestrictionPanel() {
932            setBorder(BorderFactory.createCompoundBorder(
933                    BorderFactory.createEmptyBorder(3, 3, 3, 3),
934                    BorderFactory.createCompoundBorder(
935                            BorderFactory.createLineBorder(Color.GRAY),
936                            BorderFactory.createEmptyBorder(5, 5, 5, 5)
937                    )
938            ));
939        }
940
941        public boolean isValidChangesetQuery() {
942            return getBoundingBox() != null;
943        }
944
945        public void fillInQuery(ChangesetQuery query) {
946            if (!isValidChangesetQuery())
947                throw new IllegalStateException(tr("Cannot restrict the changeset query to a specific bounding box. The input is invalid."));
948            query.inBbox(getBoundingBox());
949        }
950
951        public void displayMessageIfInvalid() {
952            if (isValidChangesetQuery())
953                return;
954            HelpAwareOptionPane.showOptionDialog(
955                    this,
956                    tr(
957                            "<html>Please enter valid longitude/latitude values to restrict<br>" +
958                            "the changeset query to a specific bounding box.</html>"
959                    ),
960                    tr("Invalid bounding box"),
961                    JOptionPane.ERROR_MESSAGE,
962                    HelpUtil.ht("/Dialog/ChangesetQueryDialog#InvalidBoundingBox")
963            );
964        }
965    }
966
967    /**
968     * Validator for user ids entered in a {@link JTextComponent}.
969     *
970     */
971    private static class UidInputFieldValidator extends AbstractTextComponentValidator {
972        UidInputFieldValidator(JTextComponent tc) {
973            super(tc);
974        }
975
976        public static UidInputFieldValidator decorate(JTextComponent tc) {
977            return new UidInputFieldValidator(tc);
978        }
979
980        @Override
981        public boolean isValid() {
982            return getUid() > 0;
983        }
984
985        @Override
986        public void validate() {
987            String value  = getComponent().getText();
988            if (value == null || value.trim().isEmpty()) {
989                feedbackInvalid("");
990                return;
991            }
992            try {
993                int uid = Integer.parseInt(value);
994                if (uid <= 0) {
995                    feedbackInvalid(tr("The current value is not a valid user ID. Please enter an integer value > 0"));
996                    return;
997                }
998            } catch (NumberFormatException e) {
999                feedbackInvalid(tr("The current value is not a valid user ID. Please enter an integer value > 0"));
1000                return;
1001            }
1002            feedbackValid(tr("Please enter an integer value > 0"));
1003        }
1004
1005        public int getUid() {
1006            String value  = getComponent().getText();
1007            if (value == null || value.trim().isEmpty()) return 0;
1008            try {
1009                int uid = Integer.parseInt(value.trim());
1010                if (uid > 0)
1011                    return uid;
1012                return 0;
1013            } catch (NumberFormatException e) {
1014                return 0;
1015            }
1016        }
1017    }
1018
1019    /**
1020     * Validates dates entered as text in a {@link JTextComponent}. Validates the input
1021     * on the fly and gives feedback about whether the date is valid or not.
1022     *
1023     * Dates can be entered in one of four standard formats defined for the current locale.
1024     */
1025    private static class DateValidator extends AbstractTextComponentValidator {
1026        DateValidator(JTextComponent tc) {
1027            super(tc);
1028        }
1029
1030        public static DateValidator decorate(JTextComponent tc) {
1031            return new DateValidator(tc);
1032        }
1033
1034        @Override
1035        public boolean isValid() {
1036            return getDate() != null;
1037        }
1038
1039        public String getStandardTooltipTextAsHtml() {
1040            return "<html>" + getStandardTooltipText() + "</html>";
1041        }
1042
1043        public String getStandardTooltipText() {
1044            Date date = new Date();
1045            return  tr(
1046                    "Please enter a date in the usual format for your locale.<br>"
1047                    + "Example: {0}<br>"
1048                    + "Example: {1}<br>"
1049                    + "Example: {2}<br>"
1050                    + "Example: {3}<br>",
1051                    DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault()).format(date),
1052                    DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()).format(date),
1053                    DateFormat.getDateInstance(DateFormat.LONG, Locale.getDefault()).format(date),
1054                    DateFormat.getDateInstance(DateFormat.FULL, Locale.getDefault()).format(date)
1055            );
1056        }
1057
1058        @Override
1059        public void validate() {
1060            if (!isValid()) {
1061                String msg = "<html>The current value isn't a valid date.<br>" + getStandardTooltipText()+ "</html>";
1062                feedbackInvalid(msg);
1063                return;
1064            } else {
1065                String msg = "<html>" + getStandardTooltipText() + "</html>";
1066                feedbackValid(msg);
1067            }
1068        }
1069
1070        public Date getDate() {
1071            for (int format: new int[] {DateFormat.SHORT, DateFormat.MEDIUM, DateFormat.LONG, DateFormat.FULL}) {
1072                DateFormat df = DateFormat.getDateInstance(format);
1073                try {
1074                    return df.parse(getComponent().getText());
1075                } catch (ParseException e) {
1076                    // Try next format
1077                    if (Main.isTraceEnabled()) {
1078                        Main.trace(e.getMessage());
1079                    }
1080                }
1081            }
1082            return null;
1083        }
1084    }
1085
1086    /**
1087     * Validates time values entered as text in a {@link JTextComponent}. Validates the input
1088     * on the fly and gives feedback about whether the time value is valid or not.
1089     *
1090     * Time values can be entered in one of four standard formats defined for the current locale.
1091     */
1092    private static class TimeValidator extends AbstractTextComponentValidator {
1093        TimeValidator(JTextComponent tc) {
1094            super(tc);
1095        }
1096
1097        public static TimeValidator decorate(JTextComponent tc) {
1098            return new TimeValidator(tc);
1099        }
1100
1101        @Override
1102        public boolean isValid() {
1103            if (getComponent().getText().trim().isEmpty())
1104                return true;
1105            return getDate() != null;
1106        }
1107
1108        public String getStandardTooltipTextAsHtml() {
1109            return "<html>" + getStandardTooltipText() + "</html>";
1110        }
1111
1112        public String getStandardTooltipText() {
1113            Date date = new Date();
1114            return tr(
1115                    "Please enter a valid time in the usual format for your locale.<br>"
1116                    + "Example: {0}<br>"
1117                    + "Example: {1}<br>"
1118                    + "Example: {2}<br>"
1119                    + "Example: {3}<br>",
1120                    DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()).format(date),
1121                    DateFormat.getTimeInstance(DateFormat.MEDIUM, Locale.getDefault()).format(date),
1122                    DateFormat.getTimeInstance(DateFormat.LONG, Locale.getDefault()).format(date),
1123                    DateFormat.getTimeInstance(DateFormat.FULL, Locale.getDefault()).format(date)
1124            );
1125        }
1126
1127        @Override
1128        public void validate() {
1129            if (!isValid()) {
1130                String msg = "<html>The current value isn't a valid time.<br>" + getStandardTooltipText() + "</html>";
1131                feedbackInvalid(msg);
1132                return;
1133            } else {
1134                String msg = "<html>" + getStandardTooltipText() + "</html>";
1135                feedbackValid(msg);
1136            }
1137        }
1138
1139        public Date getDate() {
1140            if (getComponent().getText().trim().isEmpty())
1141                return null;
1142
1143            for (int style : new int[]{DateFormat.SHORT, DateFormat.MEDIUM, DateFormat.LONG, DateFormat.FULL}) {
1144                try {
1145                    return DateFormat.getTimeInstance(style, Locale.getDefault()).parse(getComponent().getText());
1146                } catch (ParseException e) {
1147                    continue;
1148                }
1149            }
1150            return null;
1151        }
1152    }
1153}