001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2016 ForgeRock AS.
016 */
017package org.opends.guitools.controlpanel.ui;
018
019import static org.opends.messages.AdminToolMessages.*;
020
021import java.awt.CardLayout;
022import java.awt.Component;
023import java.awt.GridBagConstraints;
024import java.awt.Insets;
025import java.awt.event.ActionEvent;
026import java.awt.event.ActionListener;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.List;
030
031import javax.swing.JButton;
032import javax.swing.JPanel;
033import javax.swing.SwingUtilities;
034import javax.swing.border.Border;
035import javax.swing.border.EmptyBorder;
036import javax.swing.tree.TreePath;
037
038import org.forgerock.i18n.LocalizableMessage;
039import org.opends.guitools.controlpanel.browser.BasicNodeError;
040import org.opends.guitools.controlpanel.browser.BrowserController;
041import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
042import org.opends.guitools.controlpanel.datamodel.CustomSearchResult;
043import org.opends.guitools.controlpanel.datamodel.ServerDescriptor;
044import org.opends.guitools.controlpanel.event.ConfigurationChangeEvent;
045import org.opends.guitools.controlpanel.event.EntryReadErrorEvent;
046import org.opends.guitools.controlpanel.event.EntryReadEvent;
047import org.opends.guitools.controlpanel.event.EntryReadListener;
048import org.opends.guitools.controlpanel.event.LDAPEntryChangedEvent;
049import org.opends.guitools.controlpanel.event.LDAPEntryChangedListener;
050import org.opends.guitools.controlpanel.task.DeleteEntryTask;
051import org.opends.guitools.controlpanel.task.ModifyEntryTask;
052import org.opends.guitools.controlpanel.task.Task;
053import org.opends.guitools.controlpanel.util.Utilities;
054import org.opends.server.config.ConfigConstants;
055import org.forgerock.opendj.ldap.DN;
056import org.opends.server.types.Entry;
057import org.opends.server.types.OpenDsException;
058import org.opends.server.util.ServerConstants;
059
060/** This is the panel that contains all the different views to display an entry. */
061public class LDAPEntryPanel extends StatusGenericPanel
062implements EntryReadListener
063{
064  private static final long serialVersionUID = -6608246173472437830L;
065  private JButton saveChanges;
066  private JButton delete;
067  private JPanel mainPanel;
068  private CardLayout cardLayout;
069
070  private ErrorSearchingEntryPanel errorSearchingPanel;
071  private LDIFViewEntryPanel ldifEntryPanel;
072  private TableViewEntryPanel tableEntryPanel;
073  private SimplifiedViewEntryPanel simplifiedEntryPanel;
074
075  private ViewEntryPanel displayedEntryPanel;
076
077  private CustomSearchResult searchResult;
078  private BrowserController controller;
079  private TreePath treePath;
080
081  private ModifyEntryTask newTask;
082
083  private final String NOTHING_SELECTED = "Nothing Selected";
084  private final String MULTIPLE_SELECTED = "Multiple Selected";
085  private final String LDIF_VIEW = "LDIF View";
086  private final String ATTRIBUTE_VIEW = "Attribute View";
087  private final String SIMPLIFIED_VIEW = "Simplified View";
088  private final String ERROR_SEARCHING = "Error Searching";
089
090  private View view = View.SIMPLIFIED_VIEW;
091
092  /** The different views that we have to display an LDAP entry. */
093  public enum View
094  {
095    /** Simplified view. */
096    SIMPLIFIED_VIEW,
097    /** Attribute view (contained in a table). */
098    ATTRIBUTE_VIEW,
099    /** LDIF view (text based). */
100    LDIF_VIEW
101  }
102
103  /** Default constructor. */
104  public LDAPEntryPanel()
105  {
106    super();
107    createLayout();
108  }
109
110  /** Creates the layout of the panel (but the contents are not populated here). */
111  private void createLayout()
112  {
113    GridBagConstraints gbc = new GridBagConstraints();
114    cardLayout = new CardLayout();
115    mainPanel = new JPanel(cardLayout);
116    mainPanel.setOpaque(false);
117    gbc.gridx = 0;
118    gbc.gridy = 0;
119    gbc.gridwidth = 2;
120    gbc.weightx = 1.0;
121    gbc.weighty = 1.0;
122    gbc.fill = GridBagConstraints.BOTH;
123    add(mainPanel, gbc);
124    gbc.gridwidth = 1;
125    gbc.anchor = GridBagConstraints.WEST;
126    gbc.insets = new Insets(5, 5, 5, 5);
127    gbc.weighty = 0.0;
128    gbc.gridy ++;
129    gbc.fill = GridBagConstraints.NONE;
130    delete = Utilities.createButton(INFO_CTRL_PANEL_DELETE_ENTRY_BUTTON.get());
131    delete.setOpaque(false);
132    add(delete, gbc);
133    delete.addActionListener(new ActionListener()
134    {
135      @Override
136      public void actionPerformed(ActionEvent ev)
137      {
138        deleteEntry();
139      }
140    });
141
142    gbc.anchor = GridBagConstraints.EAST;
143    gbc.gridx ++;
144    saveChanges =
145      Utilities.createButton(INFO_CTRL_PANEL_SAVE_CHANGES_LABEL.get());
146    saveChanges.setOpaque(false);
147    add(saveChanges, gbc);
148    saveChanges.addActionListener(new ActionListener()
149    {
150      @Override
151      public void actionPerformed(ActionEvent ev)
152      {
153        saveChanges(true);
154      }
155    });
156
157    Border border = new EmptyBorder(10, 10, 10, 10);
158
159    NoItemSelectedPanel noEntryPanel = new NoItemSelectedPanel();
160    noEntryPanel.setMessage(INFO_CTRL_PANEL_NO_ENTRY_SELECTED_LABEL.get());
161    Utilities.setBorder(noEntryPanel, border);
162    mainPanel.add(noEntryPanel, NOTHING_SELECTED);
163
164    NoItemSelectedPanel multipleEntryPanel = new NoItemSelectedPanel();
165    multipleEntryPanel.setMessage(
166        INFO_CTRL_PANEL_MULTIPLE_ENTRIES_SELECTED_LABEL.get());
167    Utilities.setBorder(multipleEntryPanel, border);
168    mainPanel.add(multipleEntryPanel, MULTIPLE_SELECTED);
169
170    errorSearchingPanel = new ErrorSearchingEntryPanel();
171    if (errorSearchingPanel.requiresBorder())
172    {
173      Utilities.setBorder(multipleEntryPanel, border);
174    }
175    mainPanel.add(errorSearchingPanel, ERROR_SEARCHING);
176
177    LDAPEntryChangedListener listener = new LDAPEntryChangedListener()
178    {
179      @Override
180      public void entryChanged(LDAPEntryChangedEvent ev)
181      {
182        boolean enable = saveChanges.isVisible() &&
183            !authenticationRequired(getInfo().getServerDescriptor());
184        if (enable)
185        {
186          if (ev.getEntry() == null)
187          {
188            // Something changed that is wrong: assume the entry has been
189            // modified, when the user tries to save we will inform of the
190            // problem
191            enable = true;
192          }
193          else
194          {
195            boolean modified =
196              !Utilities.areDnsEqual(ev.getEntry().getName().toString(),
197                  searchResult.getDN()) ||
198                  !ModifyEntryTask.getModifications(ev.getEntry(), searchResult,
199                      getInfo()).isEmpty();
200            enable = modified;
201          }
202        }
203        saveChanges.setEnabled(enable);
204      }
205    };
206
207    ldifEntryPanel = new LDIFViewEntryPanel();
208    ldifEntryPanel.addLDAPEntryChangedListener(listener);
209    if (ldifEntryPanel.requiresBorder())
210    {
211      Utilities.setBorder(ldifEntryPanel, border);
212    }
213    mainPanel.add(ldifEntryPanel, LDIF_VIEW);
214
215    tableEntryPanel = new TableViewEntryPanel();
216    tableEntryPanel.addLDAPEntryChangedListener(listener);
217    if (tableEntryPanel.requiresBorder())
218    {
219      Utilities.setBorder(tableEntryPanel, border);
220    }
221    mainPanel.add(tableEntryPanel, ATTRIBUTE_VIEW);
222
223    simplifiedEntryPanel = new SimplifiedViewEntryPanel();
224    simplifiedEntryPanel.addLDAPEntryChangedListener(listener);
225    if (simplifiedEntryPanel.requiresBorder())
226    {
227      Utilities.setBorder(simplifiedEntryPanel, border);
228    }
229    mainPanel.add(simplifiedEntryPanel, SIMPLIFIED_VIEW);
230
231    cardLayout.show(mainPanel, NOTHING_SELECTED);
232  }
233
234  @Override
235  public void okClicked()
236  {
237    // No ok button
238  }
239
240  @Override
241  public void entryRead(EntryReadEvent ev)
242  {
243    searchResult = ev.getSearchResult();
244
245    updateEntryView(searchResult, treePath);
246  }
247
248  /**
249   * Updates the panel with the provided search result.
250   * @param searchResult the search result corresponding to the selected node.
251   * @param treePath the tree path of the selected node.
252   */
253  private void updateEntryView(CustomSearchResult searchResult,
254      TreePath treePath)
255  {
256    boolean isReadOnly = isReadOnly(searchResult.getDN());
257    boolean canDelete = canDelete(searchResult.getDN());
258
259    delete.setVisible(canDelete);
260    saveChanges.setVisible(!isReadOnly);
261    String cardKey;
262    switch (view)
263    {
264    case LDIF_VIEW:
265      displayedEntryPanel = ldifEntryPanel;
266      cardKey = LDIF_VIEW;
267      break;
268    case ATTRIBUTE_VIEW:
269      displayedEntryPanel = tableEntryPanel;
270      cardKey = ATTRIBUTE_VIEW;
271      break;
272    default:
273      displayedEntryPanel = simplifiedEntryPanel;
274      cardKey = SIMPLIFIED_VIEW;
275    }
276    displayedEntryPanel.update(searchResult, isReadOnly, treePath);
277    saveChanges.setEnabled(false);
278    cardLayout.show(mainPanel, cardKey);
279  }
280
281  /**
282   * Sets the view to be displayed by this panel.
283   * @param view the view.
284   */
285  public void setView(View view)
286  {
287    if (view != this.view)
288    {
289      this.view = view;
290      if (searchResult != null)
291      {
292        updateEntryView(searchResult, treePath);
293      }
294    }
295  }
296
297  /**
298   * Displays a message informing that an error occurred reading the entry.
299   * @param ev the entry read error event.
300   */
301  @Override
302  public void entryReadError(EntryReadErrorEvent ev)
303  {
304    searchResult = null;
305
306    errorSearchingPanel.setError(ev.getDN(), ev.getError());
307
308    delete.setVisible(false);
309    saveChanges.setVisible(false);
310
311    cardLayout.show(mainPanel, ERROR_SEARCHING);
312
313    displayedEntryPanel = null;
314  }
315
316  /**
317   * Displays a message informing that an error occurred resolving a referral.
318   * @param dn the DN of the local entry.
319   * @param referrals the list of referrals defined in the entry.
320   * @param error the error that occurred resolving the referral.
321   */
322  public void referralSolveError(String dn, String[] referrals,
323      BasicNodeError error)
324  {
325    searchResult = null;
326
327    errorSearchingPanel.setReferralError(dn, referrals, error);
328
329    delete.setVisible(false);
330    saveChanges.setVisible(false);
331
332    cardLayout.show(mainPanel, ERROR_SEARCHING);
333
334    displayedEntryPanel = null;
335  }
336
337  /** Displays a panel informing that nothing is selected. */
338  public void noEntrySelected()
339  {
340    searchResult = null;
341
342    delete.setVisible(false);
343    saveChanges.setVisible(false);
344
345    cardLayout.show(mainPanel, NOTHING_SELECTED);
346
347    displayedEntryPanel = null;
348  }
349
350  /** Displays a panel informing that multiple entries are selected. */
351  public void multipleEntriesSelected()
352  {
353    searchResult = null;
354
355    delete.setVisible(false);
356    saveChanges.setVisible(false);
357
358    cardLayout.show(mainPanel, MULTIPLE_SELECTED);
359
360    displayedEntryPanel = null;
361  }
362
363  @Override
364  public GenericDialog.ButtonType getButtonType()
365  {
366    return GenericDialog.ButtonType.NO_BUTTON;
367  }
368
369  @Override
370  public LocalizableMessage getTitle()
371  {
372    return INFO_CTRL_PANEL_EDIT_LDAP_ENTRY_TITLE.get();
373  }
374
375  @Override
376  public Component getPreferredFocusComponent()
377  {
378    return saveChanges;
379  }
380
381  @Override
382  public void configurationChanged(ConfigurationChangeEvent ev)
383  {
384    final ServerDescriptor desc = ev.getNewDescriptor();
385    SwingUtilities.invokeLater(new Runnable()
386    {
387      @Override
388      public void run()
389      {
390        boolean isReadOnly = true;
391        boolean canDelete = false;
392        if (searchResult != null && desc.isAuthenticated())
393        {
394          isReadOnly = isReadOnly(searchResult.getDN());
395          canDelete = canDelete(searchResult.getDN());
396        }
397
398        delete.setVisible(canDelete);
399        saveChanges.setVisible(!isReadOnly);
400      }
401    });
402  }
403
404  @Override
405  public void setInfo(ControlPanelInfo info)
406  {
407    super.setInfo(info);
408    simplifiedEntryPanel.setInfo(info);
409    ldifEntryPanel.setInfo(info);
410    tableEntryPanel.setInfo(info);
411    errorSearchingPanel.setInfo(info);
412  }
413
414  private List<DN> parentReadOnly;
415  private List<DN> nonDeletable;
416  {
417    try
418    {
419      parentReadOnly = Arrays.asList(
420        DN.valueOf(ConfigConstants.DN_TASK_ROOT),
421        DN.valueOf(ConfigConstants.DN_MONITOR_ROOT),
422        DN.valueOf(ConfigConstants.DN_BACKUP_ROOT),
423        DN.valueOf(ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT)
424      );
425      nonDeletable = Arrays.asList(
426          DN.valueOf(ConfigConstants.DN_CONFIG_ROOT),
427          DN.valueOf(ConfigConstants.DN_DEFAULT_SCHEMA_ROOT),
428          DN.valueOf(ConfigConstants.DN_TRUST_STORE_ROOT)
429      );
430    }
431    catch (Throwable t)
432    {
433      throw new RuntimeException("Error decoding DNs: "+t, t);
434    }
435  }
436
437  /**
438   * Returns <CODE>true</CODE> if the provided DN corresponds to a read-only
439   * entry and <CODE>false</CODE> otherwise.
440   * @param sDn the DN of the entry.
441   * @return <CODE>true</CODE> if the provided DN corresponds to a read-only
442   * entry and <CODE>false</CODE> otherwise.
443   */
444  private boolean isReadOnly(String sDn)
445  {
446    boolean isReadOnly = false;
447    try
448    {
449      DN dn = DN.valueOf(sDn);
450      for (DN parentDN : parentReadOnly)
451      {
452        if (dn.isSubordinateOrEqualTo(parentDN))
453        {
454          isReadOnly = true;
455          break;
456        }
457      }
458      if (!isReadOnly)
459      {
460        isReadOnly = dn.equals(DN.rootDN());
461      }
462    }
463    catch (Throwable t)
464    {
465      throw new RuntimeException("Error decoding DNs: "+t, t);
466    }
467    return isReadOnly;
468  }
469
470  /**
471   * Returns <CODE>true</CODE> if the provided DN corresponds to an entry that
472   * can be deleted and <CODE>false</CODE> otherwise.
473   * @param sDn the DN of the entry.
474   * @return <CODE>true</CODE> if the provided DN corresponds to an entry that
475   * can be deleted and <CODE>false</CODE> otherwise.
476   */
477  public boolean canDelete(String sDn)
478  {
479    try
480    {
481      DN dn = DN.valueOf(sDn);
482      return !dn.equals(DN.rootDN())
483          && !nonDeletable.contains(dn)
484          && isDescendantOfAny(dn, parentReadOnly);
485    }
486    catch (Throwable t)
487    {
488      throw new RuntimeException("Error decoding DNs: "+t, t);
489    }
490  }
491
492  private boolean isDescendantOfAny(DN dn, List<DN> parentDNs)
493  {
494    for (DN parentDN : parentDNs)
495    {
496      if (dn.isSubordinateOrEqualTo(parentDN))
497      {
498        return false;
499      }
500    }
501    return true;
502  }
503
504  /**
505   * Saves the changes done to the entry.
506   * @param modal whether the progress dialog for the task must be modal or
507   * not.
508   */
509  private void saveChanges(boolean modal)
510  {
511    newTask = null;
512    final ArrayList<LocalizableMessage> errors = new ArrayList<>();
513    // Check that the entry is correct.
514    try
515    {
516      ProgressDialog dlg = new ProgressDialog(
517          Utilities.getFrame(this),
518          Utilities.getFrame(this),
519          INFO_CTRL_PANEL_MODIFYING_ENTRY_CHANGES_TITLE.get(), getInfo());
520      dlg.setModal(modal);
521      Entry entry = displayedEntryPanel.getEntry();
522      newTask = new ModifyEntryTask(getInfo(), dlg, entry,
523            searchResult, controller, treePath);
524      for (Task task : getInfo().getTasks())
525      {
526        task.canLaunch(newTask, errors);
527      }
528
529      if (errors.isEmpty())
530      {
531        if (newTask.hasModifications()) {
532          String dn = entry.getName().toString();
533          launchOperation(newTask,
534              INFO_CTRL_PANEL_MODIFYING_ENTRY_SUMMARY.get(dn),
535              INFO_CTRL_PANEL_MODIFYING_ENTRY_COMPLETE.get(),
536              INFO_CTRL_PANEL_MODIFYING_ENTRY_SUCCESSFUL.get(dn),
537              ERR_CTRL_PANEL_MODIFYING_ENTRY_ERROR_SUMMARY.get(),
538              ERR_CTRL_PANEL_MODIFYING_ENTRY_ERROR_DETAILS.get(dn),
539              null,
540              dlg);
541          saveChanges.setEnabled(false);
542          dlg.setVisible(true);
543        }
544        else
545        {
546          // Mark the panel as it has no changes.  This can happen because every
547          // time the user types something the saveChanges button is enabled
548          // (for performance reasons with huge entries).
549          saveChanges.setEnabled(false);
550        }
551      }
552    }
553    catch (OpenDsException ode)
554    {
555      errors.add(ERR_CTRL_PANEL_INVALID_ENTRY.get(ode.getMessageObject()));
556    }
557    if (!errors.isEmpty())
558    {
559      displayErrorDialog(errors);
560    }
561  }
562
563  private void deleteEntry()
564  {
565    final ArrayList<LocalizableMessage> errors = new ArrayList<>();
566    // Check that the entry is correct.
567    // Rely in numsubordinates and hassubordinates
568    boolean isLeaf = !BrowserController.getHasSubOrdinates(searchResult);
569
570    if (treePath != null)
571    {
572      LocalizableMessage title = isLeaf ? INFO_CTRL_PANEL_DELETING_ENTRY_TITLE.get() :
573        INFO_CTRL_PANEL_DELETING_SUBTREE_TITLE.get();
574      ProgressDialog dlg = new ProgressDialog(
575          Utilities.createFrame(),
576          Utilities.getParentDialog(this), title, getInfo());
577      DeleteEntryTask newTask = new DeleteEntryTask(getInfo(), dlg,
578          new TreePath[]{treePath}, controller);
579      for (Task task : getInfo().getTasks())
580      {
581        task.canLaunch(newTask, errors);
582      }
583      if (errors.isEmpty())
584      {
585        LocalizableMessage confirmationMessage =
586          isLeaf ? INFO_CTRL_PANEL_DELETE_ENTRY_CONFIRMATION_DETAILS.get(
587              searchResult.getDN()) :
588                INFO_CTRL_PANEL_DELETE_SUBTREE_CONFIRMATION_DETAILS.get(
589                    searchResult.getDN());
590          if (displayConfirmationDialog(
591              INFO_CTRL_PANEL_CONFIRMATION_REQUIRED_SUMMARY.get(),
592              confirmationMessage))
593          {
594            String dn = searchResult.getDN();
595            if (isLeaf)
596            {
597              launchOperation(newTask,
598                  INFO_CTRL_PANEL_DELETING_ENTRY_SUMMARY.get(dn),
599                  INFO_CTRL_PANEL_DELETING_ENTRY_COMPLETE.get(),
600                  INFO_CTRL_PANEL_DELETING_ENTRY_SUCCESSFUL.get(dn),
601                  ERR_CTRL_PANEL_DELETING_ENTRY_ERROR_SUMMARY.get(),
602                  ERR_CTRL_PANEL_DELETING_ENTRY_ERROR_DETAILS.get(dn),
603                  null,
604                  dlg);
605            }
606            else
607            {
608              launchOperation(newTask,
609                  INFO_CTRL_PANEL_DELETING_SUBTREE_SUMMARY.get(dn),
610                  INFO_CTRL_PANEL_DELETING_SUBTREE_COMPLETE.get(),
611                  INFO_CTRL_PANEL_DELETING_SUBTREE_SUCCESSFUL.get(dn),
612                  ERR_CTRL_PANEL_DELETING_SUBTREE_ERROR_SUMMARY.get(),
613                  ERR_CTRL_PANEL_DELETING_SUBTREE_ERROR_DETAILS.get(dn),
614                  null,
615                  dlg);
616            }
617            dlg.setVisible(true);
618          }
619      }
620    }
621    if (!errors.isEmpty())
622    {
623      displayErrorDialog(errors);
624    }
625  }
626
627  /**
628   * Returns the browser controller in charge of the tree.
629   * @return the browser controller in charge of the tree.
630   */
631  public BrowserController getController()
632  {
633    return controller;
634  }
635
636  /**
637   * Sets the browser controller in charge of the tree.
638   * @param controller the browser controller in charge of the tree.
639   */
640  public void setController(BrowserController controller)
641  {
642    this.controller = controller;
643  }
644
645  /**
646   * Returns the tree path associated with the node that is being displayed.
647   * @return the tree path associated with the node that is being displayed.
648   */
649  public TreePath getTreePath()
650  {
651    return treePath;
652  }
653
654  /**
655   * Sets the tree path associated with the node that is being displayed.
656   * @param treePath the tree path associated with the node that is being
657   * displayed.
658   */
659  public void setTreePath(TreePath treePath)
660  {
661    this.treePath = treePath;
662  }
663
664  /**
665   * Method used to know if there are unsaved changes or not.  It is used by
666   * the entry selection listener when the user changes the selection.
667   * @return <CODE>true</CODE> if there are unsaved changes (and so the
668   * selection of the entry should be cancelled) and <CODE>false</CODE>
669   * otherwise.
670   */
671  public boolean mustCheckUnsavedChanges()
672  {
673    return displayedEntryPanel != null &&
674        saveChanges.isVisible() && saveChanges.isEnabled();
675  }
676
677  /**
678   * Tells whether the user chose to save the changes in the panel, to not save
679   * them or simply canceled the selection change in the tree.
680   * @return the value telling whether the user chose to save the changes in the
681   * panel, to not save them or simply canceled the selection in the tree.
682   */
683  public UnsavedChangesDialog.Result checkUnsavedChanges()
684  {
685    UnsavedChangesDialog.Result result;
686    UnsavedChangesDialog unsavedChangesDlg = new UnsavedChangesDialog(
687          Utilities.getParentDialog(this), getInfo());
688    unsavedChangesDlg.setMessage(INFO_CTRL_PANEL_UNSAVED_CHANGES_SUMMARY.get(),
689       INFO_CTRL_PANEL_UNSAVED_ENTRY_CHANGES_DETAILS.get(searchResult.getDN()));
690    Utilities.centerGoldenMean(unsavedChangesDlg,
691          Utilities.getParentDialog(this));
692    unsavedChangesDlg.setVisible(true);
693    result = unsavedChangesDlg.getResult();
694    if (result == UnsavedChangesDialog.Result.SAVE)
695    {
696      saveChanges(false);
697      if (newTask == null || // The user data is not valid
698          newTask.getState() != Task.State.FINISHED_SUCCESSFULLY)
699      {
700        result = UnsavedChangesDialog.Result.CANCEL;
701      }
702    }
703
704    return result;
705  }
706}